diff --git a/README.md b/README.md index 7c01566..deb4f2b 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,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()], }); ``` diff --git a/docs/rig-syntax-copilot-pi-agent-comparison.md b/docs/rig-syntax-copilot-pi-agent-comparison.md index 5df8c5e..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 | -| `maxTurns`, optional `rig/addons` repair 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. | `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. | `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 78e3482..7728c1d 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 `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 76e6bf7..3dc1949 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,26 +33,33 @@ export function steering(options: SteeringOptions = {}): AgentAddon { }; } -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); -}; +/** + * Parses and validates responses, retrying failures within the agent's turn budget. + * + * Configure `maxTurns` on the agent spec. + */ +export function repair(): AgentAddon { + return 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 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 9bb1e66..665fe65 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: @@ -161,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()`, `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 7b45a8f..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. `maxTurns` is only the total budget; automatic parse/schema correction requires `repair`: +Rig starts with no addons. `maxTurns` is only the total budget; automatic parse/schema correction requires `repair()`: ```ts import { agent } from "rig"; @@ -110,17 +110,17 @@ import { repair } from "rig/addons"; const summarize = agent({ model: "mini", maxTurns: 3, - addons: repair, + addons: repair(), }); export default summarize; ``` -The budget includes the initial attempt and all retries. +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 -`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 @@ -155,7 +155,7 @@ const qa = agent({ oncePerAgent((runtimeAgent) => { initializedAgents.add(runtimeAgent); }), - repair, + repair(), ], }); diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index 2db72e2..f3f9701 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -595,7 +595,7 @@ export type AgentSpec>; diff --git a/skills/rig/samples/66-ci-workflow-health.md b/skills/rig/samples/66-ci-workflow-health.md index 0a7a402..84212ec 100644 --- a/skills/rig/samples/66-ci-workflow-health.md +++ b/skills/rig/samples/66-ci-workflow-health.md @@ -21,7 +21,7 @@ const parseYamlSteps = defineTool("parse_yaml_steps", { const ciWorkflowAnalyzer = agent({ model: "mini", maxTurns: 3, - addons: repair, + 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) })), @@ -32,4 +32,3 @@ const ciWorkflowAnalyzer = agent({ export default ciWorkflowAnalyzer; ``` - diff --git a/src/rig.test.ts b/src/rig.test.ts index c3fd09e..7dc7f8d 100644 --- a/src/rig.test.ts +++ b/src/rig.test.ts @@ -399,7 +399,7 @@ describe("agent invocation", () => { const repairable = agent({ name: "repairable", - addons: repair, + addons: repair(), maxTurns: 2, }); @@ -458,7 +458,7 @@ describe("agent invocation", () => { context.nextPrompt = `please fix: ${context.nextPrompt}`; } }, - repair, + repair(), ], maxTurns: 2, }); @@ -700,7 +700,7 @@ describe("agent invocation", () => { context.nextPrompt = `${context.nextPrompt}\nAdd a short correction because you are running out of turns.`; } }, - repair, + repair(), ], }); @@ -727,7 +727,7 @@ describe("agent invocation", () => { const steerable = agent({ name: "steerable", maxTurns: 2, - addons: [steering(), repair], + addons: [steering(), repair()], }); await expect(steerable("go")).resolves.toBe("recovered"); @@ -760,7 +760,7 @@ describe("agent invocation", () => { } } }, - repair, + repair(), ], }); @@ -790,7 +790,7 @@ describe("agent invocation", () => { oncePerAgent(async (runtimeAgent, context) => { register(runtimeAgent, context.turn); }), - repair, + repair(), ], });