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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
});
```

Expand Down
4 changes: 2 additions & 2 deletions docs/rig-syntax-copilot-pi-agent-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions skills/rig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 34 additions & 20 deletions skills/rig/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -16,6 +17,12 @@ export type AgentRegistration = (
context: AgentAddonContext,
) => void | Promise<void>;

/**
* 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) => {
Expand All @@ -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) => {
Expand Down
4 changes: 3 additions & 1 deletion skills/rig/references/agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
14 changes: 7 additions & 7 deletions skills/rig/references/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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

Expand All @@ -155,7 +155,7 @@ const qa = agent({
oncePerAgent((runtimeAgent) => {
initializedAgents.add(runtimeAgent);
}),
repair,
repair(),
],
});

Expand Down
2 changes: 1 addition & 1 deletion skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ export type AgentSpec<Input extends Schema = StringSchema, Output extends Schema
model?: string;
/** Maximum number of turns (initial + repair retries). Defaults to `4`. */
maxTurns?: number;
/** Middleware addons that wrap each turn's ask/response cycle, e.g. `repair`, `steering`. */
/** Middleware addons that wrap each turn's ask/response cycle, e.g. `repair()`, `steering()`. */
addons?: AgentAddon | AgentAddon[];
/** Named sub-agents available for delegation from this agent's prompt. */
agents?: Record<string, AgentFn<any, any>>;
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/66-ci-workflow-health.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) })),
Expand All @@ -32,4 +32,3 @@ const ciWorkflowAnalyzer = agent({

export default ciWorkflowAnalyzer;
```

12 changes: 6 additions & 6 deletions src/rig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ describe("agent invocation", () => {

const repairable = agent({
name: "repairable",
addons: repair,
addons: repair(),
maxTurns: 2,
});

Expand Down Expand Up @@ -458,7 +458,7 @@ describe("agent invocation", () => {
context.nextPrompt = `please fix: ${context.nextPrompt}`;
}
},
repair,
repair(),
],
maxTurns: 2,
});
Expand Down Expand Up @@ -700,7 +700,7 @@ describe("agent invocation", () => {
context.nextPrompt = `${context.nextPrompt}\nAdd a short correction because you are running out of turns.`;
}
},
repair,
repair(),
],
});

Expand All @@ -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");
Expand Down Expand Up @@ -760,7 +760,7 @@ describe("agent invocation", () => {
}
}
},
repair,
repair(),
],
});

Expand Down Expand Up @@ -790,7 +790,7 @@ describe("agent invocation", () => {
oncePerAgent(async (runtimeAgent, context) => {
register(runtimeAgent, context.turn);
}),
repair,
repair(),
],
});

Expand Down