Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/open-loop-rename-rules-to-loops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@gemstack/ai-autopilot': minor
---

Rename the loop engine's `rules` vocabulary to `loops` (Open Loop, #241).

A loop is a meta prompt, so that is the user-facing unit even though rule logic powers it. `defineLoop` / `defaultLoops` / `Loop` / `LoopSpec` replace `defineRule` / `defaultLoopRules` / `LoopRule` / `LoopRuleSpec`; the engine class is now `LoopEngine` (was `Loop`), created via `createLoopEngine` with `LoopEngineOptions`; and its option key is `loops` (was `rules`). Vocabulary only, no behavior change.
14 changes: 7 additions & 7 deletions examples/bootstrap-quickstart/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
loopImprove,
agentDeploy,
cloudflareTarget,
Loop,
LoopEngine,
definePrompt,
defineRule,
defineLoop,
personaInstructions,
personaTools,
builtinPresetRegistry,
Expand Down Expand Up @@ -128,16 +128,16 @@ function presetWorkers(session: RunnerSession, personas: ReturnType<typeof prese
}

/** The full-fledged loop: the checklist blocks once, then clears after the fix. */
function buildLoop(): Loop {
function buildLoop(): LoopEngine {
const verdicts = [
'```json\n{ "blockers": ["No authentication on the orders page yet"] }\n```',
'```json\n{ "blockers": [] }\n```',
]
let pass = 0
return new Loop({
rules: [
defineRule({ on: 'production-check', run: ['production-grade'] }),
defineRule({ on: 'major-change', run: ['address-blockers'] }),
return new LoopEngine({
loops: [
defineLoop({ on: 'production-check', run: ['production-grade'] }),
defineLoop({ on: 'major-change', run: ['address-blockers'] }),
],
prompts: [
definePrompt({ id: 'production-grade', run: () => verdicts[Math.min(pass++, verdicts.length - 1)]! }),
Expand Down
14 changes: 7 additions & 7 deletions examples/bootstrap-quickstart/src/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
loopImprove,
agentDeploy,
cloudflareTarget,
Loop,
LoopEngine,
definePrompt,
defineRule,
defineLoop,
personaInstructions,
personaTools,
builtinPresetRegistry,
Expand Down Expand Up @@ -78,16 +78,16 @@ function presetWorkers(session: RunnerSession, personas: ReturnType<typeof prese
}

/** The full-fledged loop: the checklist blocks once, then clears after the fix (scripted). */
function buildLoop(): Loop {
function buildLoop(): LoopEngine {
const verdicts = [
'```json\n{ "blockers": ["No authentication on the orders page yet"] }\n```',
'```json\n{ "blockers": [] }\n```',
]
let pass = 0
return new Loop({
rules: [
defineRule({ on: 'production-check', run: ['production-grade'] }),
defineRule({ on: 'major-change', run: ['address-blockers'] }),
return new LoopEngine({
loops: [
defineLoop({ on: 'production-check', run: ['production-grade'] }),
defineLoop({ on: 'major-change', run: ['address-blockers'] }),
],
prompts: [
definePrompt({ id: 'production-grade', run: () => verdicts[Math.min(pass++, verdicts.length - 1)]! }),
Expand Down
22 changes: 11 additions & 11 deletions packages/ai-autopilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,14 @@ runs QA + UX. Semantic (a *kind* of change picks a *set* of prompts), not
command-driven and not run-on-every-PR.

```ts
import { Loop, definePrompt, defaultLoopRules } from '@gemstack/ai-autopilot'
import { LoopEngine, definePrompt, defaultLoops } from '@gemstack/ai-autopilot'

const loop = new Loop({
rules: defaultLoopRules(), // major-change → [review, code-quality, security]; ui-flow → [qa, ux]
const loop = new LoopEngine({
loops: defaultLoops(), // major-change → [review, code-quality, security]; ui-flow → [qa, ux]
prompts: [
definePrompt({ id: 'review', passes: 2, run: ctx => runReview(ctx.event) }),
definePrompt({ id: 'code-quality', run: ctx => runQuality(ctx.event) }),
// ...register a prompt per id the rules reference
// ...register a prompt per id the loops reference
],
ledger, // optional: exposed to each prompt via ctx.ledger (#112)
onEvent: e => log(e), // observe match / pass / done (observer-isolated)
Expand All @@ -260,10 +260,10 @@ Design choices for the two open questions:

Each prompt runs for its `passes` with **fresh context every pass** (Rom's
finding: re-running the same prompt with a reset context improves the result), so
`run` is expected to build a new agent per call. `defaultLoopRules()` is the
built-in policy as data; extend it by concatenating your own `defineRule` results.
`run` is expected to build a new agent per call. `defaultLoops()` is the
built-in policy as data; extend it by concatenating your own `defineLoop` results.
The prompt bodies themselves are the prompts library (#111), registered under the
ids the rules reference.
ids the loops reference.

## Built-in prompts — the loop's bodies, shipped as data

Expand All @@ -273,16 +273,16 @@ They ship ready to use and already know the stack (Vike + universal-orm): review
template. Each is a markdown file under `prompts/`, so a contributor improves the
prompt by editing prose, not code.

`loopPromptsFor` materializes a library into loop prompts, so `defaultLoopRules()`
`loopPromptsFor` materializes a library into loop prompts, so `defaultLoops()`
ids resolve to real bodies — this is the turnkey wire:

```ts
import { agent } from '@gemstack/ai-sdk'
import { Loop, defaultLoopRules, builtinLibrary, loopPromptsFor, runnerTools } from '@gemstack/ai-autopilot'
import { LoopEngine, defaultLoops, builtinLibrary, loopPromptsFor, runnerTools } from '@gemstack/ai-autopilot'

const library = await builtinLibrary() // the shipped bodies
const loop = new Loop({
rules: defaultLoopRules(), // review / code-quality / security / qa / ux ids
const loop = new LoopEngine({
loops: defaultLoops(), // review / code-quality / security / qa / ux ids
prompts: loopPromptsFor(library, ctx => // a FRESH agent per pass
agent({ instructions: ctx.instructions, tools: runnerTools(session) })),
ledger, // ctx.instructions already includes the decisions briefing
Expand Down
24 changes: 12 additions & 12 deletions packages/ai-autopilot/src/bootstrap/steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { agentArchitect, supervisorBuild, loopChecklist, loopImprove } from './s
import { agentDeploy, FakeDeployTarget } from './deploy.js'
import { Bootstrap } from './bootstrap.js'
import { DecisionLedger } from '../decisions/ledger.js'
import { Loop } from '../loop/loop.js'
import { definePrompt, defineRule } from '../loop/define.js'
import { LoopEngine } from '../loop/loop.js'
import { definePrompt, defineLoop } from '../loop/define.js'
import type { BootstrapEvent } from './types.js'
import type { SupervisorEvent } from '../types.js'
import type { ArchitectContext, BuildContext, LoopPassContext } from './types.js'
Expand Down Expand Up @@ -139,17 +139,17 @@ describe('loopChecklist / loopImprove (default full-fledged loop steps)', () =>
})

it('reads the { blockers } verdict the production-grade prompt returns', async () => {
const loop = new Loop({
rules: [defineRule({ on: 'production-check', run: ['production-grade'] })],
const loop = new LoopEngine({
loops: [defineLoop({ on: 'production-check', run: ['production-grade'] })],
prompts: [definePrompt({ id: 'production-grade', run: () => '```json\n{ "blockers": ["no auth"] }\n```' })],
})
const verdict = await loopChecklist({ loop })(passCtx())
assert.deepEqual(verdict.blockers, ['no auth'])
})

it('treats a missing verdict as a blocker', async () => {
const loop = new Loop({
rules: [defineRule({ on: 'production-check', run: ['production-grade'] })],
const loop = new LoopEngine({
loops: [defineLoop({ on: 'production-check', run: ['production-grade'] })],
prompts: [definePrompt({ id: 'production-grade', run: () => 'no verdict here' })],
})
const verdict = await loopChecklist({ loop })(passCtx())
Expand All @@ -159,8 +159,8 @@ describe('loopChecklist / loopImprove (default full-fledged loop steps)', () =>

it('fires the change events so the review chain runs', async () => {
let reviewRan = 0
const loop = new Loop({
rules: [defineRule({ on: 'major-change', run: ['review'] })],
const loop = new LoopEngine({
loops: [defineLoop({ on: 'major-change', run: ['review'] })],
prompts: [definePrompt({ id: 'review', run: () => { reviewRan++; return 'reviewed' } })],
})
await loopImprove({ loop })(passCtx({ blockers: ['no auth'] }))
Expand Down Expand Up @@ -189,10 +189,10 @@ describe('Bootstrap end-to-end with the default steps (offline)', () => {
const verdicts = ['```json\n{ "blockers": ["no auth"] }\n```', '```json\n{ "blockers": [] }\n```']
let checked = 0
let improved = 0
const loop = new Loop({
rules: [
defineRule({ on: 'production-check', run: ['production-grade'] }),
defineRule({ on: 'major-change', run: ['fix'] }),
const loop = new LoopEngine({
loops: [
defineLoop({ on: 'production-check', run: ['production-grade'] }),
defineLoop({ on: 'major-change', run: ['fix'] }),
],
prompts: [
definePrompt({ id: 'production-grade', run: () => verdicts[checked++] ?? '```json\n{ "blockers": [] }\n```' }),
Expand Down
6 changes: 3 additions & 3 deletions packages/ai-autopilot/src/bootstrap/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import type { Agent } from '@gemstack/ai-sdk'
import { z } from 'zod'
import { Supervisor } from '../supervisor.js'
import { decisionBriefing } from '../decisions/tools.js'
import { Loop } from '../loop/loop.js'
import { LoopEngine } from '../loop/loop.js'
import { LOOP_EVENTS, LOOP_PROMPTS } from '../loop/policy.js'
import type { Planner, Synthesizer, SupervisorOptions } from '../types.js'
import type { Verdict } from '../loop/verdict.js'
import type { BootstrapSteps, BuildContext } from './types.js'

/**
* Default wirings of the four bootstrap steps onto the real primitives —
* Supervisor, personas, and the Loop. Each is thin (it constructs a primitive
* Supervisor, personas, and the LoopEngine. Each is thin (it constructs a primitive
* and adapts its I/O to the step contract), so the same orchestrator runs
* against these in production or against stubs in a test. The model + runner stay
* injected: you pass the architect agent, the planner/workers, and the loop.
Expand Down Expand Up @@ -137,7 +137,7 @@ export function supervisorBuild(opts: SupervisorBuildOptions): BootstrapSteps['b
/** Options for {@link loopChecklist} and {@link loopImprove}. */
export interface LoopStepOptions {
/** The loop that resolves the prompt ids to bodies (via `loopPromptsFor`). */
loop: Loop
loop: LoopEngine
}

/** Options for {@link loopChecklist}. */
Expand Down
20 changes: 10 additions & 10 deletions packages/ai-autopilot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@
* change (a {@link LoopEvent}) and the right follow-up prompts fire — a major
* change runs review + code-quality + security, a new UI flow runs QA + UX.
*
* - {@link Loop} — match an event to a prompt chain and run it (N fresh passes)
* - {@link definePrompt} / {@link defineRule} — author prompts and policy rules
* - {@link defaultLoopRules} — the built-in web-app policy
* - {@link LoopEngine} — match an event to a prompt chain and run it (N fresh passes)
* - {@link definePrompt} / {@link defineLoop} — author prompts and policy loops
* - {@link defaultLoops} — the built-in web-app policy
* - {@link parseVerdict} / {@link isPassing} — the `{ blockers }` verdict the loop
* gates on, so it stops on a review's *outcome*, not just execution
*
Expand Down Expand Up @@ -188,23 +188,23 @@ export {
} from './decisions/index.js'
export {
definePrompt,
defineRule,
defineLoop,
LoopError,
Loop,
createLoop,
defaultLoopRules,
LoopEngine,
createLoopEngine,
defaultLoops,
LOOP_EVENTS,
LOOP_PROMPTS,
parseVerdict,
isPassing,
type Verdict,
type LoopOptions,
type LoopEngineOptions,
type LoopEvent,
type LoopContext,
type LoopPrompt,
type LoopPromptSpec,
type LoopRule,
type LoopRuleSpec,
type Loop,
type LoopSpec,
type PassResult,
type PromptOutcome,
type LoopRunResult,
Expand Down
12 changes: 6 additions & 6 deletions packages/ai-autopilot/src/loop/define.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { definePrompt, defineRule, LoopError } from './define.js'
import { definePrompt, defineLoop, LoopError } from './define.js'

describe('definePrompt', () => {
it('defaults passes to 1 and freezes', () => {
Expand All @@ -17,17 +17,17 @@ describe('definePrompt', () => {
})
})

describe('defineRule', () => {
describe('defineLoop', () => {
it('normalizes a single `on` to a de-duped array', () => {
const r = defineRule({ on: 'major-change', run: ['review', 'security'] })
const r = defineLoop({ on: 'major-change', run: ['review', 'security'] })
assert.deepEqual(r.on, ['major-change'])
assert.deepEqual(r.run, ['review', 'security'])
assert.ok(Object.isFrozen(r))
})

it('de-dupes kinds and requires non-empty on/run', () => {
assert.deepEqual(defineRule({ on: ['a', 'a', 'b'], run: ['x'] }).on, ['a', 'b'])
assert.throws(() => defineRule({ on: [], run: ['x'] }), LoopError)
assert.throws(() => defineRule({ on: 'a', run: [] }), LoopError)
assert.deepEqual(defineLoop({ on: ['a', 'a', 'b'], run: ['x'] }).on, ['a', 'b'])
assert.throws(() => defineLoop({ on: [], run: ['x'] }), LoopError)
assert.throws(() => defineLoop({ on: 'a', run: [] }), LoopError)
})
})
12 changes: 6 additions & 6 deletions packages/ai-autopilot/src/loop/define.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { LoopPrompt, LoopPromptSpec, LoopRule, LoopRuleSpec } from './types.js'
import type { LoopPrompt, LoopPromptSpec, Loop, LoopSpec } from './types.js'

const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/

/** Thrown when a loop prompt or rule is malformed. Fails fast at definition. */
/** Thrown when a prompt or loop is malformed. Fails fast at definition. */
export class LoopError extends Error {
constructor(message: string) {
super(`[ai-autopilot] ${message}`)
Expand All @@ -29,15 +29,15 @@ export function definePrompt(spec: LoopPromptSpec): LoopPrompt {
}

/**
* Validate a {@link LoopRuleSpec} and return a frozen {@link LoopRule}. `on` is
* Validate a {@link LoopSpec} and return a frozen {@link Loop}. `on` is
* normalized to a de-duped list of event kinds; `run` is the ordered prompt ids.
*/
export function defineRule(spec: LoopRuleSpec): LoopRule {
export function defineLoop(spec: LoopSpec): Loop {
const kinds = (Array.isArray(spec.on) ? spec.on : [spec.on]).map(k => k?.trim()).filter(Boolean)
if (kinds.length === 0) throw new LoopError('rule `on` needs at least one event kind')
if (kinds.length === 0) throw new LoopError('loop `on` needs at least one event kind')

const run = (spec.run ?? []).map(p => p?.trim()).filter(Boolean)
if (run.length === 0) throw new LoopError(`rule on [${kinds.join(', ')}] needs at least one prompt in \`run\``)
if (run.length === 0) throw new LoopError(`loop on [${kinds.join(', ')}] needs at least one prompt in \`run\``)

return Object.freeze({
on: Object.freeze([...new Set(kinds)]),
Expand Down
16 changes: 8 additions & 8 deletions packages/ai-autopilot/src/loop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
* The loop — the event-to-prompt-chain policy of `@gemstack/ai-autopilot`.
*
* The agent declares a {@link LoopEvent} (a semantic change kind); a
* {@link LoopRule} maps that kind to an ordered chain of {@link LoopPrompt}s;
* the {@link Loop} runs them (N fresh-context passes each) and consults the
* decisions ledger. {@link defaultLoopRules} is the built-in web-app policy.
* {@link Loop} maps that kind to an ordered chain of {@link LoopPrompt}s;
* the {@link LoopEngine} runs them (N fresh-context passes each) and consults the
* decisions ledger. {@link defaultLoops} is the built-in web-app policy.
*/
export { definePrompt, defineRule, LoopError } from './define.js'
export { Loop, createLoop, type LoopOptions } from './loop.js'
export { defaultLoopRules, LOOP_EVENTS, LOOP_PROMPTS } from './policy.js'
export { definePrompt, defineLoop, LoopError } from './define.js'
export { LoopEngine, createLoopEngine, type LoopEngineOptions } from './loop.js'
export { defaultLoops, LOOP_EVENTS, LOOP_PROMPTS } from './policy.js'
export { parseVerdict, isPassing, type Verdict } from './verdict.js'
export type {
LoopEvent,
LoopContext,
LoopPrompt,
LoopPromptSpec,
LoopRule,
LoopRuleSpec,
Loop,
LoopSpec,
PassResult,
PromptOutcome,
LoopRunResult,
Expand Down
Loading
Loading