From 3a047c2cd22e4b2da86deb6cce76d548b96aba2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 05:39:36 +0000 Subject: [PATCH 1/4] Initial plan From 67b8079abc092d50406b379f8308b2924af919e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 05:50:10 +0000 Subject: [PATCH 2/4] Clarify repair and steering addon usage Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- skills/rig/SKILL.md | 6 +++--- skills/rig/addons.ts | 13 +++++++++++++ skills/rig/references/agent-api.md | 2 ++ skills/rig/references/composition.md | 8 ++++---- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/skills/rig/SKILL.md b/skills/rig/SKILL.md index 78e3482..49252e6 100644 --- a/skills/rig/SKILL.md +++ b/skills/rig/SKILL.md @@ -93,11 +93,11 @@ Do not replace file intents with `cat` commands or large in-memory strings. `p.w ## Tools, composition, and reliability -- Define tools with `defineTool(name, { description, parameters: s.object(...), handler })`; schema-based handler arguments are inferred and tools default to `skipPermission: true`. +- Define tools with `defineTool(name, { description, parameters: s.object(...), handler })`; schema-based handler arguments are inferred and tools default to `skipPermission: true`. Destructure only handler fields you use. - `agents` is a named object such as `agents: { extractor }`, never an array. Attach every declared subagent to the exported root's graph. - There is no chain or loop primitive; give the coordinator explicit delegation instructions and require one combined output. -- Automatic parse/schema repair requires `repair` from `rig/addons`; `maxTurns` alone only sets the total turn budget. -- Put `steering()` after `repair` when adding a final-turn warning. Use `oncePerAgent()` for one registration callback per runtime agent. +- Automatic parse/schema repair requires the bare `repair` addon from `rig/addons`; use `addons: repair`, never `repair(...)`, and put `maxTurns` on the agent spec. +- For a final-turn warning, use `addons: [steering(), repair]`. Custom text uses `steering({ message: "..." })`, not a positional string. Use `oncePerAgent()` for one registration callback per runtime agent. ## Runnable markdown diff --git a/skills/rig/addons.ts b/skills/rig/addons.ts index 76e6bf7..2d721fd 100644 --- a/skills/rig/addons.ts +++ b/skills/rig/addons.ts @@ -4,6 +4,7 @@ import type { Agent, AgentAddon, AgentAddonContext } from "./rig.ts"; const DEFAULT_STEERING_WARNING = "You are running out of turns. This is your final attempt before reaching the turn limit. Please correct your output now."; export type SteeringOptions = { + /** Warning appended to the final retry prompt. */ message?: string; }; @@ -16,6 +17,12 @@ export type AgentRegistration = ( context: AgentAddonContext, ) => void | Promise; +/** + * Appends a final-attempt warning to the retry prompt produced by an inner addon. + * + * Place this before `repair`, for example + * `addons: [steering({ message: "Return valid JSON now." }), repair]`. + */ export function steering(options: SteeringOptions = {}): AgentAddon { const message = options.message ?? DEFAULT_STEERING_WARNING; return async (context, next) => { @@ -26,6 +33,12 @@ export function steering(options: SteeringOptions = {}): AgentAddon { }; } +/** + * Parses and validates responses, retrying failures within the agent's turn budget. + * + * This is an addon value, not a factory. Use `addons: repair` and configure + * `maxTurns` on the agent spec. + */ export const repair: AgentAddon = async (context, next) => { await next(); if (context.completed || context.error !== undefined || context.nextPrompt !== undefined) { diff --git a/skills/rig/references/agent-api.md b/skills/rig/references/agent-api.md index 9bb1e66..3a065d0 100644 --- a/skills/rig/references/agent-api.md +++ b/skills/rig/references/agent-api.md @@ -137,6 +137,8 @@ export default triage; For plain JSON Schema parameters, provide a generic such as `defineTool<{ issue: string }>(...)`. A handler may return a string or any JSON-serializable value; Rig serializes non-string values, so do not call `JSON.stringify` in the handler. Tools default to `skipPermission: true`. +Strict TypeScript compilation reports unused handler bindings. Destructure only the keys the handler uses, or rename an unavoidable binding with a leading underscore, such as `{ filename: _filename, content }`. + ## Call-time options Use call-time options only for per-run changes: diff --git a/skills/rig/references/composition.md b/skills/rig/references/composition.md index 7b45a8f..aa6ac4b 100644 --- a/skills/rig/references/composition.md +++ b/skills/rig/references/composition.md @@ -116,11 +116,11 @@ const summarize = agent({ export default summarize; ``` -The budget includes the initial attempt and all retries. +The budget includes the initial attempt and all retries. `repair` is an addon value, not a factory: use `addons: repair`, never `repair(...)`, and configure `maxTurns` on the agent spec. ## Final-turn steering -`steering()` appends a last-chance warning to the final retry prompt produced by `repair`. Put it after `repair`: +`steering()` appends a last-chance warning to the final retry prompt produced by `repair`. Put it before `repair` so it can observe the repair prompt as the addon chain unwinds: ```ts import { agent } from "rig"; @@ -130,13 +130,13 @@ import { repair, steering } from "rig/addons"; const summarize = agent({ model: "mini", maxTurns: 3, - addons: [repair, steering()], + addons: [steering(), repair], }); export default summarize; ``` -Use `repair` alone when the validation error is enough guidance. Do not use `steering()` without `repair`, because it only augments prompts generated by repair. +Use `repair` alone when the validation error is enough guidance. Pass custom warning text in an options object, as in `steering({ message: "Return valid JSON now." })`; a positional string is invalid. Do not use `steering()` without `repair`, because it only augments prompts generated by repair. ## One-time runtime registration From 0d637665799a6b411c19376e9254dd1bfbff5cd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:08:55 +0000 Subject: [PATCH 3/4] Make repair an options-based addon Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- README.md | 3 +- .../rig-syntax-copilot-pi-agent-comparison.md | 4 +- skills/rig/SKILL.md | 7 ++- skills/rig/addons.ts | 57 +++++++++++-------- skills/rig/references/agent-api.md | 2 +- skills/rig/references/composition.md | 14 ++--- skills/rig/rig.ts | 31 +++++++--- skills/rig/samples/66-ci-workflow-health.md | 4 +- src/addons.ts | 2 +- src/rig.test.ts | 50 ++++++++++++---- 10 files changed, 109 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 7c01566..e89b73f 100644 --- a/README.md +++ b/README.md @@ -220,8 +220,7 @@ For the common retry flow with last-turn steering or stable default timeouts, op ```ts const review = agent({ - maxTurns: 3, - addons: [timeout({ timeout: 30_000 }), steering(), repair], + addons: [timeout({ timeout: 30_000 }), steering(), repair({ maxTurns: 3 })], }); ``` diff --git a/docs/rig-syntax-copilot-pi-agent-comparison.md b/docs/rig-syntax-copilot-pi-agent-comparison.md index 5df8c5e..f2affa3 100644 --- a/docs/rig-syntax-copilot-pi-agent-comparison.md +++ b/docs/rig-syntax-copilot-pi-agent-comparison.md @@ -15,7 +15,7 @@ The focus is generation reliability: what an agent can produce quickly with low | `s.object(...)`, `s.enum(...)`, `s.array(...)` | Explicit JSON schema or prompt-constrained JSON validated in app code | Same pattern: schema-constrained JSON validated by the harness/app | | `p.read(...)`, `p.bash(...)` | Tool/context calls orchestrated by the host app before/within turns | Tool/context calls via pi-agent tool integration/orchestration | | `agents: { subagent }` | Multiple sessions/roles coordinated in app orchestration | Multi-agent graph/delegation orchestration | -| `maxTurns`, optional `rig/addons` repair addon | Explicit retry + repair loop in app logic | Retry/repair policies in agent workflow/harness | +| Optional `repair({ maxTurns })` addon | Explicit retry + repair loop in app logic | Retry/repair policies in agent workflow/harness | | `permissions` | Host-side policy gates around shell/write operations | Host-side tool permission policies | ## 2) Top 10 scenarios: Copilot SDK APIs (ranked easiest → hardest) @@ -30,7 +30,7 @@ The focus is generation reliability: what an agent can produce quickly with low | 6 | PR triage recommendation | Requires prioritization judgment and policy interpretation. | One/two turns with constrained triage schema and confidence fields. | | 7 | README draft generation | Creative synthesis adds style and completeness ambiguity. | Multi-section structured output with post-parse checks. | | 8 | Release notes generation | Requires grouping/dedup across many commits. | Batched commit input + grouped typed output contract. | -| 9 | Schema-repairing extractor | Needs robust retry when output is invalid or partial. | `maxTurns` plus optional `rig/addons` repair addon maps to explicit app-level validation/repair loop. | +| 9 | Schema-repairing extractor | Needs robust retry when output is invalid or partial. | Optional `repair({ maxTurns })` addon maps to an explicit app-level validation/repair loop. | | 10 | Multi-agent orchestrator | Highest coordination overhead across roles and merges. | `agents` maps to multi-session orchestration and aggregation logic. | ## 3) Top 10 scenarios: pi-agent SDK (ranked easiest → hardest) diff --git a/skills/rig/SKILL.md b/skills/rig/SKILL.md index 49252e6..9e45458 100644 --- a/skills/rig/SKILL.md +++ b/skills/rig/SKILL.md @@ -47,7 +47,8 @@ export default reviewDiff; | Concern | Location | |---------|----------| -| `name`, `instructions`, `input`, `output`, tools, stable `model`/`maxTurns` | `agent({ ... })` | +| `name`, `instructions`, `input`, `output`, tools, stable `model`/general turn cap | `agent({ ... })` | +| Parse/schema repair and its turn budget | `repair({ maxTurns })` in `addons` | | Per-run `model`, `maxTurns`, `timeout`, `signal` | `myAgent(input, { ... })` | | Stable addons | `addons` in the spec | | Additional addons | `agent.use(addon)` | @@ -96,8 +97,8 @@ Do not replace file intents with `cat` commands or large in-memory strings. `p.w - Define tools with `defineTool(name, { description, parameters: s.object(...), handler })`; schema-based handler arguments are inferred and tools default to `skipPermission: true`. Destructure only handler fields you use. - `agents` is a named object such as `agents: { extractor }`, never an array. Attach every declared subagent to the exported root's graph. - There is no chain or loop primitive; give the coordinator explicit delegation instructions and require one combined output. -- Automatic parse/schema repair requires the bare `repair` addon from `rig/addons`; use `addons: repair`, never `repair(...)`, and put `maxTurns` on the agent spec. -- For a final-turn warning, use `addons: [steering(), repair]`. Custom text uses `steering({ message: "..." })`, not a positional string. Use `oncePerAgent()` for one registration callback per runtime agent. +- Automatic parse/schema repair requires `repair({ maxTurns: 3 })` from `rig/addons`; the budget includes the initial attempt and retries. +- For a final-turn warning, use `addons: [steering(), repair({ maxTurns: 3 })]`. Custom text uses `steering({ message: "..." })`, not a positional string. Use `oncePerAgent()` for one registration callback per runtime agent. ## Runnable markdown diff --git a/skills/rig/addons.ts b/skills/rig/addons.ts index 2d721fd..2efc4df 100644 --- a/skills/rig/addons.ts +++ b/skills/rig/addons.ts @@ -8,6 +8,11 @@ export type SteeringOptions = { message?: string; }; +export type RepairOptions = { + /** Maximum total turns, including the initial attempt and all repair retries. */ + maxTurns: number; +}; + export type TimeoutOptions = { timeout: number; }; @@ -20,8 +25,8 @@ export type AgentRegistration = ( /** * Appends a final-attempt warning to the retry prompt produced by an inner addon. * - * Place this before `repair`, for example - * `addons: [steering({ message: "Return valid JSON now." }), repair]`. + * Place this before `repair()`, for example + * `addons: [steering({ message: "Return valid JSON now." }), repair({ maxTurns: 3 })]`. */ export function steering(options: SteeringOptions = {}): AgentAddon { const message = options.message ?? DEFAULT_STEERING_WARNING; @@ -34,31 +39,33 @@ export function steering(options: SteeringOptions = {}): AgentAddon { } /** - * Parses and validates responses, retrying failures within the agent's turn budget. + * Parses and validates responses, retrying failures within the configured turn budget. * - * This is an addon value, not a factory. Use `addons: repair` and configure - * `maxTurns` on the agent spec. + * Agent-spec and call-time `maxTurns` values override this default. */ -export const repair: AgentAddon = async (context, next) => { - await next(); - if (context.completed || context.error !== undefined || context.nextPrompt !== undefined) { - return; - } - if (context.response === undefined) { - return; - } - const analysis = analyzeResponse(context.response, context.outputSchema, context.spec.name, context.turn); - if (analysis.ok) { - context.completed = true; - context.output = analysis.output; - return; - } - if (context.turn >= context.maxTurns) { - context.error = analysis.error; - return; - } - context.nextPrompt = defaultRepairPrompt(context.spec, analysis.error); -}; +export function repair(options: RepairOptions): AgentAddon { + const addon: AgentAddon = async (context, next) => { + await next(); + if (context.completed || context.error !== undefined || context.nextPrompt !== undefined) { + return; + } + if (context.response === undefined) { + return; + } + const analysis = analyzeResponse(context.response, context.outputSchema, context.spec.name, context.turn); + if (analysis.ok) { + context.completed = true; + context.output = analysis.output; + return; + } + if (context.turn >= context.maxTurns) { + context.error = analysis.error; + return; + } + context.nextPrompt = defaultRepairPrompt(context.spec, analysis.error); + }; + return Object.assign(addon, { maxTurns: options.maxTurns }); +} export function timeout(options: TimeoutOptions): AgentAddon { return async (context, next) => { diff --git a/skills/rig/references/agent-api.md b/skills/rig/references/agent-api.md index 3a065d0..b79ba8e 100644 --- a/skills/rig/references/agent-api.md +++ b/skills/rig/references/agent-api.md @@ -163,6 +163,6 @@ Use only the current API: - `agent({ name, ... })` - `p.*` and ``p`...` `` from `rig` - `s.*` for explicit schemas -- `oncePerAgent`, `repair`, `steering`, and `timeout` from `rig/addons` +- `oncePerAgent`, `repair({ maxTurns })`, `steering`, and `timeout` from `rig/addons` Do not add deprecated hooks, alternate schema syntaxes, or compatibility bridges. diff --git a/skills/rig/references/composition.md b/skills/rig/references/composition.md index aa6ac4b..f762596 100644 --- a/skills/rig/references/composition.md +++ b/skills/rig/references/composition.md @@ -100,7 +100,7 @@ When a task asks for runnable markdown: ## Repair -Rig starts with no addons. `maxTurns` is only the total budget; automatic parse/schema correction requires `repair`: +Rig starts with no addons. Automatic parse/schema correction requires `repair({ maxTurns })`: ```ts import { agent } from "rig"; @@ -109,14 +109,13 @@ import { repair } from "rig/addons"; // Agent role: return a valid concise summary. const summarize = agent({ model: "mini", - maxTurns: 3, - addons: repair, + addons: repair({ maxTurns: 3 }), }); export default summarize; ``` -The budget includes the initial attempt and all retries. `repair` is an addon value, not a factory: use `addons: repair`, never `repair(...)`, and configure `maxTurns` on the agent spec. +The budget includes the initial attempt and all retries. An explicit agent-spec or call-time `maxTurns` overrides the addon's default. ## Final-turn steering @@ -129,14 +128,13 @@ import { repair, steering } from "rig/addons"; // Agent role: return a valid concise summary with final-turn steering. const summarize = agent({ model: "mini", - maxTurns: 3, - addons: [steering(), repair], + addons: [steering(), repair({ maxTurns: 3 })], }); export default summarize; ``` -Use `repair` alone when the validation error is enough guidance. Pass custom warning text in an options object, as in `steering({ message: "Return valid JSON now." })`; a positional string is invalid. Do not use `steering()` without `repair`, because it only augments prompts generated by repair. +Use `repair({ maxTurns })` alone when the validation error is enough guidance. Pass custom warning text in an options object, as in `steering({ message: "Return valid JSON now." })`; a positional string is invalid. Do not use `steering()` without `repair`, because it only augments prompts generated by repair. ## One-time runtime registration @@ -155,7 +153,7 @@ const qa = agent({ oncePerAgent((runtimeAgent) => { initializedAgents.add(runtimeAgent); }), - repair, + repair({ maxTurns: 4 }), ], }); diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index 2db72e2..9d7cde7 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -553,10 +553,14 @@ export type AgentAddonContext = { * } * }; */ -export type AgentAddon = ( - context: AgentAddonContext, - next: () => Promise, -) => void | Promise; +export type AgentAddon = { + ( + context: AgentAddonContext, + next: () => Promise, + ): void | Promise; + /** Default total turn budget used unless the agent spec or call overrides it. */ + readonly maxTurns?: number; +}; export type ToolHandler = (args: TArgs) => unknown | Promise; export type ToolParameters = Schema | Record; export type Tool = ToolConfig & { name: string }; @@ -593,9 +597,9 @@ export type AgentSpec>; @@ -2039,16 +2043,27 @@ function resolveCallRuntime(spec: NormalizedAgentSpec, options: CallOp systemMessage: unknown; tools: Tool[] | undefined; } { + const addons = normalizeAddons(spec.addons); return { model: options.model ?? spec.model ?? "small", - maxTurns: options.maxTurns ?? spec.maxTurns ?? 4, + maxTurns: options.maxTurns ?? spec.maxTurns ?? resolveAddonMaxTurns(addons) ?? 4, signal: timeoutSignal(options.signal, options.timeout), - addons: normalizeAddons(spec.addons), + addons, systemMessage: spec.systemMessage, tools: spec.tools, }; } +function resolveAddonMaxTurns(addons: AgentAddon[]): number | undefined { + for (let i = addons.length - 1; i >= 0; i -= 1) { + const maxTurns = addons[i]?.maxTurns; + if (maxTurns !== undefined) { + return maxTurns; + } + } + return undefined; +} + function normalizeAddons(addons?: AgentAddon | AgentAddon[]): AgentAddon[] { if (!addons) { return []; diff --git a/skills/rig/samples/66-ci-workflow-health.md b/skills/rig/samples/66-ci-workflow-health.md index 0a7a402..d392ebb 100644 --- a/skills/rig/samples/66-ci-workflow-health.md +++ b/skills/rig/samples/66-ci-workflow-health.md @@ -20,8 +20,7 @@ const parseYamlSteps = defineTool("parse_yaml_steps", { // Agent role: analyze CI workflow files and return a list of health issues. const ciWorkflowAnalyzer = agent({ model: "mini", - maxTurns: 3, - addons: repair, + addons: repair({ maxTurns: 3 }), instructions: p`Scan ${p.glob(".github/workflows/*.yml")} and ${p.bashRaw`find .github/workflows -name '*.yaml' 2>/dev/null`} for workflow health issues. Use parse_yaml_steps to inspect individual files.`, output: s.object({ issues: s.array(s.object({ workflow: s.nonEmptyString, severity: s.enum("info", "warning", "error"), message: s.string, fix: s.optional(s.string) })), @@ -32,4 +31,3 @@ const ciWorkflowAnalyzer = agent({ export default ciWorkflowAnalyzer; ``` - diff --git a/src/addons.ts b/src/addons.ts index faa8a63..42d1c3f 100644 --- a/src/addons.ts +++ b/src/addons.ts @@ -1,2 +1,2 @@ export { addons, oncePerAgent, repair, steering, timeout } from "../skills/rig/addons.ts"; -export type { AgentRegistration, SteeringOptions, TimeoutOptions } from "../skills/rig/addons.ts"; +export type { AgentRegistration, RepairOptions, SteeringOptions, TimeoutOptions } from "../skills/rig/addons.ts"; diff --git a/src/rig.test.ts b/src/rig.test.ts index c3fd09e..a15cff2 100644 --- a/src/rig.test.ts +++ b/src/rig.test.ts @@ -399,8 +399,7 @@ describe("agent invocation", () => { const repairable = agent({ name: "repairable", - addons: repair, - maxTurns: 2, + addons: repair({ maxTurns: 2 }), }); await expect(repairable("go")).resolves.toBe("repaired"); @@ -409,6 +408,38 @@ describe("agent invocation", () => { expect(prompts[1]).toContain("invalid JSON"); }); + it("uses the repair addon turn budget", async () => { + let calls = 0; + mocks.setSendAndWaitImpl(async () => { + calls += 1; + return "not json"; + }); + + const repairable = agent({ + name: "repairable", + addons: repair({ maxTurns: 2 }), + }); + + await expect(repairable("go")).rejects.toMatchObject({ kind: "parse", turn: 2 }); + expect(calls).toBe(2); + }); + + it("allows call-time maxTurns to override the repair addon default", async () => { + let calls = 0; + mocks.setSendAndWaitImpl(async () => { + calls += 1; + return calls === 1 ? "not json" : JSON.stringify("repaired"); + }); + + const repairable = agent({ + name: "repairable", + addons: repair({ maxTurns: 1 }), + }); + + await expect(repairable("go", { maxTurns: 2 })).resolves.toBe("repaired"); + expect(calls).toBe(2); + }); + it("parses JSON wrapped in a fenced markdown block", async () => { mocks.setSendAndWaitImpl(async () => "```json\n\"hello\"\n```"); @@ -458,9 +489,8 @@ describe("agent invocation", () => { context.nextPrompt = `please fix: ${context.nextPrompt}`; } }, - repair, + repair({ maxTurns: 2 }), ], - maxTurns: 2, }); await expect(repairable("go")).resolves.toBe("fixed"); @@ -692,7 +722,6 @@ describe("agent invocation", () => { const steerable = agent({ name: "steerable", - maxTurns: 2, addons: [ async (context, next) => { await next(); @@ -700,7 +729,7 @@ describe("agent invocation", () => { context.nextPrompt = `${context.nextPrompt}\nAdd a short correction because you are running out of turns.`; } }, - repair, + repair({ maxTurns: 2 }), ], }); @@ -726,8 +755,7 @@ describe("agent invocation", () => { const steerable = agent({ name: "steerable", - maxTurns: 2, - addons: [steering(), repair], + addons: [steering(), repair({ maxTurns: 2 })], }); await expect(steerable("go")).resolves.toBe("recovered"); @@ -746,7 +774,6 @@ describe("agent invocation", () => { const snippetGuard = agent({ name: "snippet-guard", - maxTurns: 2, output: s.object({ code: s.string }), addons: [ async (context, next) => { @@ -760,7 +787,7 @@ describe("agent invocation", () => { } } }, - repair, + repair({ maxTurns: 2 }), ], }); @@ -785,12 +812,11 @@ describe("agent invocation", () => { const review = agent({ name: "review", - maxTurns: 2, addons: [ oncePerAgent(async (runtimeAgent, context) => { register(runtimeAgent, context.turn); }), - repair, + repair({ maxTurns: 2 }), ], }); From ed0541c08bdf8223d59e8e351bdbba6d4440cd4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:22:48 +0000 Subject: [PATCH 4/4] Keep repair turn budget on agent Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- README.md | 3 +- .../rig-syntax-copilot-pi-agent-comparison.md | 4 +- skills/rig/SKILL.md | 7 ++- skills/rig/addons.ts | 16 ++---- skills/rig/references/agent-api.md | 2 +- skills/rig/references/composition.md | 14 +++--- skills/rig/rig.ts | 31 +++--------- skills/rig/samples/66-ci-workflow-health.md | 3 +- src/addons.ts | 2 +- src/rig.test.ts | 50 +++++-------------- 10 files changed, 44 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index e89b73f..deb4f2b 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,8 @@ For the common retry flow with last-turn steering or stable default timeouts, op ```ts const review = agent({ - addons: [timeout({ timeout: 30_000 }), steering(), repair({ maxTurns: 3 })], + maxTurns: 3, + addons: [timeout({ timeout: 30_000 }), steering(), repair()], }); ``` diff --git a/docs/rig-syntax-copilot-pi-agent-comparison.md b/docs/rig-syntax-copilot-pi-agent-comparison.md index f2affa3..25139f6 100644 --- a/docs/rig-syntax-copilot-pi-agent-comparison.md +++ b/docs/rig-syntax-copilot-pi-agent-comparison.md @@ -15,7 +15,7 @@ The focus is generation reliability: what an agent can produce quickly with low | `s.object(...)`, `s.enum(...)`, `s.array(...)` | Explicit JSON schema or prompt-constrained JSON validated in app code | Same pattern: schema-constrained JSON validated by the harness/app | | `p.read(...)`, `p.bash(...)` | Tool/context calls orchestrated by the host app before/within turns | Tool/context calls via pi-agent tool integration/orchestration | | `agents: { subagent }` | Multiple sessions/roles coordinated in app orchestration | Multi-agent graph/delegation orchestration | -| Optional `repair({ maxTurns })` addon | Explicit retry + repair loop in app logic | Retry/repair policies in agent workflow/harness | +| `maxTurns`, optional `repair()` addon | Explicit retry + repair loop in app logic | Retry/repair policies in agent workflow/harness | | `permissions` | Host-side policy gates around shell/write operations | Host-side tool permission policies | ## 2) Top 10 scenarios: Copilot SDK APIs (ranked easiest → hardest) @@ -30,7 +30,7 @@ The focus is generation reliability: what an agent can produce quickly with low | 6 | PR triage recommendation | Requires prioritization judgment and policy interpretation. | One/two turns with constrained triage schema and confidence fields. | | 7 | README draft generation | Creative synthesis adds style and completeness ambiguity. | Multi-section structured output with post-parse checks. | | 8 | Release notes generation | Requires grouping/dedup across many commits. | Batched commit input + grouped typed output contract. | -| 9 | Schema-repairing extractor | Needs robust retry when output is invalid or partial. | Optional `repair({ maxTurns })` addon maps to an explicit app-level validation/repair loop. | +| 9 | Schema-repairing extractor | Needs robust retry when output is invalid or partial. | `maxTurns` plus optional `repair()` addon maps to an explicit app-level validation/repair loop. | | 10 | Multi-agent orchestrator | Highest coordination overhead across roles and merges. | `agents` maps to multi-session orchestration and aggregation logic. | ## 3) Top 10 scenarios: pi-agent SDK (ranked easiest → hardest) diff --git a/skills/rig/SKILL.md b/skills/rig/SKILL.md index 9e45458..7728c1d 100644 --- a/skills/rig/SKILL.md +++ b/skills/rig/SKILL.md @@ -47,8 +47,7 @@ export default reviewDiff; | Concern | Location | |---------|----------| -| `name`, `instructions`, `input`, `output`, tools, stable `model`/general turn cap | `agent({ ... })` | -| Parse/schema repair and its turn budget | `repair({ maxTurns })` in `addons` | +| `name`, `instructions`, `input`, `output`, tools, stable `model`/`maxTurns` | `agent({ ... })` | | Per-run `model`, `maxTurns`, `timeout`, `signal` | `myAgent(input, { ... })` | | Stable addons | `addons` in the spec | | Additional addons | `agent.use(addon)` | @@ -97,8 +96,8 @@ Do not replace file intents with `cat` commands or large in-memory strings. `p.w - Define tools with `defineTool(name, { description, parameters: s.object(...), handler })`; schema-based handler arguments are inferred and tools default to `skipPermission: true`. Destructure only handler fields you use. - `agents` is a named object such as `agents: { extractor }`, never an array. Attach every declared subagent to the exported root's graph. - There is no chain or loop primitive; give the coordinator explicit delegation instructions and require one combined output. -- Automatic parse/schema repair requires `repair({ maxTurns: 3 })` from `rig/addons`; the budget includes the initial attempt and retries. -- For a final-turn warning, use `addons: [steering(), repair({ maxTurns: 3 })]`. Custom text uses `steering({ message: "..." })`, not a positional string. Use `oncePerAgent()` for one registration callback per runtime agent. +- Automatic parse/schema repair requires `repair()` from `rig/addons`; put its `maxTurns` budget on the agent spec. +- For a final-turn warning, use `addons: [steering(), repair()]`. Custom text uses `steering({ message: "..." })`, not a positional string. Use `oncePerAgent()` for one registration callback per runtime agent. ## Runnable markdown diff --git a/skills/rig/addons.ts b/skills/rig/addons.ts index 2efc4df..3dc1949 100644 --- a/skills/rig/addons.ts +++ b/skills/rig/addons.ts @@ -8,11 +8,6 @@ export type SteeringOptions = { message?: string; }; -export type RepairOptions = { - /** Maximum total turns, including the initial attempt and all repair retries. */ - maxTurns: number; -}; - export type TimeoutOptions = { timeout: number; }; @@ -26,7 +21,7 @@ export type AgentRegistration = ( * Appends a final-attempt warning to the retry prompt produced by an inner addon. * * Place this before `repair()`, for example - * `addons: [steering({ message: "Return valid JSON now." }), repair({ maxTurns: 3 })]`. + * `addons: [steering({ message: "Return valid JSON now." }), repair()]`. */ export function steering(options: SteeringOptions = {}): AgentAddon { const message = options.message ?? DEFAULT_STEERING_WARNING; @@ -39,12 +34,12 @@ export function steering(options: SteeringOptions = {}): AgentAddon { } /** - * Parses and validates responses, retrying failures within the configured turn budget. + * Parses and validates responses, retrying failures within the agent's turn budget. * - * Agent-spec and call-time `maxTurns` values override this default. + * Configure `maxTurns` on the agent spec. */ -export function repair(options: RepairOptions): AgentAddon { - const addon: AgentAddon = async (context, next) => { +export function repair(): AgentAddon { + return async (context, next) => { await next(); if (context.completed || context.error !== undefined || context.nextPrompt !== undefined) { return; @@ -64,7 +59,6 @@ export function repair(options: RepairOptions): AgentAddon { } context.nextPrompt = defaultRepairPrompt(context.spec, analysis.error); }; - return Object.assign(addon, { maxTurns: options.maxTurns }); } export function timeout(options: TimeoutOptions): AgentAddon { diff --git a/skills/rig/references/agent-api.md b/skills/rig/references/agent-api.md index b79ba8e..665fe65 100644 --- a/skills/rig/references/agent-api.md +++ b/skills/rig/references/agent-api.md @@ -163,6 +163,6 @@ Use only the current API: - `agent({ name, ... })` - `p.*` and ``p`...` `` from `rig` - `s.*` for explicit schemas -- `oncePerAgent`, `repair({ maxTurns })`, `steering`, and `timeout` from `rig/addons` +- `oncePerAgent`, `repair()`, `steering`, and `timeout` from `rig/addons` Do not add deprecated hooks, alternate schema syntaxes, or compatibility bridges. diff --git a/skills/rig/references/composition.md b/skills/rig/references/composition.md index f762596..c560de7 100644 --- a/skills/rig/references/composition.md +++ b/skills/rig/references/composition.md @@ -100,7 +100,7 @@ When a task asks for runnable markdown: ## Repair -Rig starts with no addons. Automatic parse/schema correction requires `repair({ maxTurns })`: +Rig starts with no addons. `maxTurns` is only the total budget; automatic parse/schema correction requires `repair()`: ```ts import { agent } from "rig"; @@ -109,13 +109,14 @@ import { repair } from "rig/addons"; // Agent role: return a valid concise summary. const summarize = agent({ model: "mini", - addons: repair({ maxTurns: 3 }), + maxTurns: 3, + addons: repair(), }); export default summarize; ``` -The budget includes the initial attempt and all retries. An explicit agent-spec or call-time `maxTurns` overrides the addon's default. +The budget includes the initial attempt and all retries. Configure `maxTurns` on the agent spec; a call-time value can override it. ## Final-turn steering @@ -128,13 +129,14 @@ import { repair, steering } from "rig/addons"; // Agent role: return a valid concise summary with final-turn steering. const summarize = agent({ model: "mini", - addons: [steering(), repair({ maxTurns: 3 })], + maxTurns: 3, + addons: [steering(), repair()], }); export default summarize; ``` -Use `repair({ maxTurns })` alone when the validation error is enough guidance. Pass custom warning text in an options object, as in `steering({ message: "Return valid JSON now." })`; a positional string is invalid. Do not use `steering()` without `repair`, because it only augments prompts generated by repair. +Use `repair()` alone when the validation error is enough guidance. Pass custom warning text in an options object, as in `steering({ message: "Return valid JSON now." })`; a positional string is invalid. Do not use `steering()` without `repair()`, because it only augments prompts generated by repair. ## One-time runtime registration @@ -153,7 +155,7 @@ const qa = agent({ oncePerAgent((runtimeAgent) => { initializedAgents.add(runtimeAgent); }), - repair({ maxTurns: 4 }), + repair(), ], }); diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index 9d7cde7..f3f9701 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -553,14 +553,10 @@ export type AgentAddonContext = { * } * }; */ -export type AgentAddon = { - ( - context: AgentAddonContext, - next: () => Promise, - ): void | Promise; - /** Default total turn budget used unless the agent spec or call overrides it. */ - readonly maxTurns?: number; -}; +export type AgentAddon = ( + context: AgentAddonContext, + next: () => Promise, +) => void | Promise; export type ToolHandler = (args: TArgs) => unknown | Promise; export type ToolParameters = Schema | Record; export type Tool = ToolConfig & { name: string }; @@ -597,9 +593,9 @@ export type AgentSpec>; @@ -2043,27 +2039,16 @@ function resolveCallRuntime(spec: NormalizedAgentSpec, options: CallOp systemMessage: unknown; tools: Tool[] | undefined; } { - const addons = normalizeAddons(spec.addons); return { model: options.model ?? spec.model ?? "small", - maxTurns: options.maxTurns ?? spec.maxTurns ?? resolveAddonMaxTurns(addons) ?? 4, + maxTurns: options.maxTurns ?? spec.maxTurns ?? 4, signal: timeoutSignal(options.signal, options.timeout), - addons, + addons: normalizeAddons(spec.addons), systemMessage: spec.systemMessage, tools: spec.tools, }; } -function resolveAddonMaxTurns(addons: AgentAddon[]): number | undefined { - for (let i = addons.length - 1; i >= 0; i -= 1) { - const maxTurns = addons[i]?.maxTurns; - if (maxTurns !== undefined) { - return maxTurns; - } - } - return undefined; -} - function normalizeAddons(addons?: AgentAddon | AgentAddon[]): AgentAddon[] { if (!addons) { return []; diff --git a/skills/rig/samples/66-ci-workflow-health.md b/skills/rig/samples/66-ci-workflow-health.md index d392ebb..84212ec 100644 --- a/skills/rig/samples/66-ci-workflow-health.md +++ b/skills/rig/samples/66-ci-workflow-health.md @@ -20,7 +20,8 @@ const parseYamlSteps = defineTool("parse_yaml_steps", { // Agent role: analyze CI workflow files and return a list of health issues. const ciWorkflowAnalyzer = agent({ model: "mini", - addons: repair({ maxTurns: 3 }), + maxTurns: 3, + addons: repair(), instructions: p`Scan ${p.glob(".github/workflows/*.yml")} and ${p.bashRaw`find .github/workflows -name '*.yaml' 2>/dev/null`} for workflow health issues. Use parse_yaml_steps to inspect individual files.`, output: s.object({ issues: s.array(s.object({ workflow: s.nonEmptyString, severity: s.enum("info", "warning", "error"), message: s.string, fix: s.optional(s.string) })), diff --git a/src/addons.ts b/src/addons.ts index 42d1c3f..faa8a63 100644 --- a/src/addons.ts +++ b/src/addons.ts @@ -1,2 +1,2 @@ export { addons, oncePerAgent, repair, steering, timeout } from "../skills/rig/addons.ts"; -export type { AgentRegistration, RepairOptions, SteeringOptions, TimeoutOptions } from "../skills/rig/addons.ts"; +export type { AgentRegistration, SteeringOptions, TimeoutOptions } from "../skills/rig/addons.ts"; diff --git a/src/rig.test.ts b/src/rig.test.ts index a15cff2..7dc7f8d 100644 --- a/src/rig.test.ts +++ b/src/rig.test.ts @@ -399,7 +399,8 @@ describe("agent invocation", () => { const repairable = agent({ name: "repairable", - addons: repair({ maxTurns: 2 }), + addons: repair(), + maxTurns: 2, }); await expect(repairable("go")).resolves.toBe("repaired"); @@ -408,38 +409,6 @@ describe("agent invocation", () => { expect(prompts[1]).toContain("invalid JSON"); }); - it("uses the repair addon turn budget", async () => { - let calls = 0; - mocks.setSendAndWaitImpl(async () => { - calls += 1; - return "not json"; - }); - - const repairable = agent({ - name: "repairable", - addons: repair({ maxTurns: 2 }), - }); - - await expect(repairable("go")).rejects.toMatchObject({ kind: "parse", turn: 2 }); - expect(calls).toBe(2); - }); - - it("allows call-time maxTurns to override the repair addon default", async () => { - let calls = 0; - mocks.setSendAndWaitImpl(async () => { - calls += 1; - return calls === 1 ? "not json" : JSON.stringify("repaired"); - }); - - const repairable = agent({ - name: "repairable", - addons: repair({ maxTurns: 1 }), - }); - - await expect(repairable("go", { maxTurns: 2 })).resolves.toBe("repaired"); - expect(calls).toBe(2); - }); - it("parses JSON wrapped in a fenced markdown block", async () => { mocks.setSendAndWaitImpl(async () => "```json\n\"hello\"\n```"); @@ -489,8 +458,9 @@ describe("agent invocation", () => { context.nextPrompt = `please fix: ${context.nextPrompt}`; } }, - repair({ maxTurns: 2 }), + repair(), ], + maxTurns: 2, }); await expect(repairable("go")).resolves.toBe("fixed"); @@ -722,6 +692,7 @@ describe("agent invocation", () => { const steerable = agent({ name: "steerable", + maxTurns: 2, addons: [ async (context, next) => { await next(); @@ -729,7 +700,7 @@ describe("agent invocation", () => { context.nextPrompt = `${context.nextPrompt}\nAdd a short correction because you are running out of turns.`; } }, - repair({ maxTurns: 2 }), + repair(), ], }); @@ -755,7 +726,8 @@ describe("agent invocation", () => { const steerable = agent({ name: "steerable", - addons: [steering(), repair({ maxTurns: 2 })], + maxTurns: 2, + addons: [steering(), repair()], }); await expect(steerable("go")).resolves.toBe("recovered"); @@ -774,6 +746,7 @@ describe("agent invocation", () => { const snippetGuard = agent({ name: "snippet-guard", + maxTurns: 2, output: s.object({ code: s.string }), addons: [ async (context, next) => { @@ -787,7 +760,7 @@ describe("agent invocation", () => { } } }, - repair({ maxTurns: 2 }), + repair(), ], }); @@ -812,11 +785,12 @@ describe("agent invocation", () => { const review = agent({ name: "review", + maxTurns: 2, addons: [ oncePerAgent(async (runtimeAgent, context) => { register(runtimeAgent, context.turn); }), - repair({ maxTurns: 2 }), + repair(), ], });