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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ gh skills clone githubnext/rig
```ts
import {
agent,
addons,
configureAgent,
defineTool,
oncePerAgent,
p,
repair,
s,
steering,
timeout,
} from "rig";
import { addons, oncePerAgent, repair, steering, timeout } from "rig/addons";
```

- `agent(spec)` creates a typed agent function.
Expand All @@ -36,7 +40,7 @@ import { addons, oncePerAgent, repair, steering, timeout } from "rig/addons";
- `defineTool(name, config)` creates an SDK-neutral tool definition and accepts rig `s.*` schemas for `parameters`.
- `addons` accepts express-like `(context, next)` turn addons for steering, inline validation, and runtime agent access.
- `rig` starts with no default addons.
- `rig/addons` provides optional addon helpers: `oncePerAgent`, `repair`, `steering`, `timeout`, and `addons.{oncePerAgent,repair,steering,timeout}`.
- `rig` exports addon helpers: `oncePerAgent`, `repair`, `steering`, `timeout`, and `addons.{oncePerAgent,repair,steering,timeout}`.
- `p\`...\`` returns a prompt builder and renders intent values when coerced to string; prefer `${p.read(...)}` / `${p.bash(...)}` when the context source is already known.

## Embedding in markdown
Expand Down
6 changes: 3 additions & 3 deletions skills/rig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default reviewDiff;

## Construction rules

1. Use one `import { ... } from "rig"` statement and `agent({ ... })`; add a `// Agent role: ...` comment above every agent.
1. Use one `import { ... } from "rig"` statement (including addons such as `repair`/`steering`) and `agent({ ... })`; add a `// Agent role: ...` comment above every agent.
2. Set examples to `model: "large"`, `"mini"`, or `"nano"`.
3. Omit `input` and `output` when the default free-form `s.string` schemas are enough; otherwise use explicit `s.*` schemas.
4. Put known workspace context directly in `p\`...\`` with `${p.read(...)}` or `${p.bash(...)}`. Add `input` only for values supplied by the caller.
Expand Down Expand Up @@ -109,7 +109,7 @@ For `p.readOptional` fallbacks, pass a value the model can parse in context (for
- Define tools with `defineTool(name, { description, parameters: s.object(...), handler })`; schema-based handler arguments are inferred and tools default to `skipPermission: true`. `s.unknown` is valid in tool parameters when the tool needs to compare or echo arbitrary JSON-like values, for example `parameters: s.object({ currentValue: s.unknown, recommendedValue: s.unknown })`. Handlers can be sync or async and return a string or any JSON-serializable value; return plain JS values (do not `JSON.stringify`) because Rig serializes non-string results automatically. Async handlers may import Node built-ins with `await import("node:child_process")`. Destructure only handler fields you use. Use `defineTool` for external operations and deterministic transforms that support reasoning — not to replace the core classification or judgment step with in-process TypeScript logic.
- `agents` must be a named object — `agents: { extractor }` — never an array (`agents: [extractor]` is a type error). 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`; `repair()` takes no arguments. Put retries on the agent spec: `maxTurns: 3, addons: repair()`. Do not pass retries to `repair()` (`addons: repair({ maxTurns: 3 })` is invalid).
- Automatic parse/schema repair uses `repair()` from `rig`; `repair()` takes no arguments. Put retries on the agent spec: `maxTurns: 3, addons: repair()`. Do not pass retries to `repair()` (`addons: repair({ maxTurns: 3 })` is invalid).
- `addons` accepts a single addon or an array. Use `addons: repair()` for one addon, and `addons: [steering(), repair()]` when combining. **`steering()` must come before `repair()` in the array** — write `[steering(), repair()]`, not `[repair(), steering()]`. `steering()` (default warning) or `steering({ message: "..." })` (custom text) should run before `repair()` to append a last-chance instruction on the final retry. `oncePerAgent(callback)` invokes the callback once per runtime agent instance — its internal `WeakSet` already deduplicates, so no external tracking is needed.

## Runnable markdown
Expand All @@ -134,7 +134,7 @@ Run inline input or a program file with `node skills/rig/rig.ts`; add `--server`

- Known context uses `p.*`; true runtime data uses `input`.
- Important outputs are explicitly typed and constrained.
- Every helper and import uses the current `rig` or `rig/addons` API.
- Every helper and import uses the current `rig` API.
- Generated TypeScript passes `node skills/rig/eslint/lint.js <program.ts>` and typechecking.
- Every subagent is named, reachable, and narrowly scoped.
- Snippets have one default export and no `console.log`.
Expand Down
104 changes: 2 additions & 102 deletions skills/rig/addons.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,2 @@
import { analyzeResponse, defaultRepairPrompt } from "./rig.ts";
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;
};

export type TimeoutOptions = {
timeout: number;
};

export type AgentRegistration = (
agent: Agent,
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) => {
await next();
if (context.nextPrompt && context.turn + 1 === context.maxTurns) {
context.nextPrompt = `${context.nextPrompt}\n${message}`;
}
};
}

/**
* 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) => {
context.signal = timeoutSignal(context.signal, options.timeout);
await next();
};
}

export function oncePerAgent(register: AgentRegistration): AgentAddon {
const seen = new WeakSet<Agent>();
return async (context, next) => {
if (!seen.has(context.agent)) {
await register(context.agent, context);
seen.add(context.agent);
}
await next();
};
}

function timeoutSignal(parent?: AbortSignal, timeoutMs?: number): AbortSignal | undefined {
if (!timeoutMs) {
return parent;
}
const controller = new AbortController();
const onAbort = () => controller.abort(parent?.reason);
parent?.addEventListener("abort", onAbort, { once: true });
const timer = setTimeout(
() => controller.abort(new Error(`Timed out after ${timeoutMs}ms`)),
timeoutMs,
);
controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
return controller.signal;
}

export const addons = {
oncePerAgent,
timeout,
repair,
steering,
};
export { addons, oncePerAgent, repair, steering, timeout } from "./rig.ts";
export type { AgentRegistration, SteeringOptions, TimeoutOptions } from "./rig.ts";
2 changes: 1 addition & 1 deletion skills/rig/references/agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,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`

Do not add deprecated hooks, alternate schema syntaxes, or compatibility bridges.
9 changes: 3 additions & 6 deletions skills/rig/references/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ When a task asks for runnable markdown:
Rig starts with no addons. `maxTurns` is only the total budget; automatic parse/schema correction requires `repair()`:

```ts
import { agent } from "rig";
import { repair } from "rig/addons";
import { agent, repair } from "rig";

// Agent role: return a valid concise summary.
const summarize = agent({
Expand Down Expand Up @@ -170,8 +169,7 @@ addons: 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";
import { repair, steering } from "rig/addons";
import { agent, repair, steering } from "rig";

// Agent role: return a valid concise summary with final-turn steering.
const summarize = agent({
Expand All @@ -198,8 +196,7 @@ Use `repair()` alone when the validation error is enough guidance. Pass custom w
`oncePerAgent(register)` invokes its callback exactly once per runtime agent instance — not once per turn and not once per retry. The callback receives `(agent: Agent, context: AgentAddonContext)`. Use it for one-time initialization such as registering a tool adapter or constructing a client:

```ts
import { agent, s } from "rig";
import { oncePerAgent, repair } from "rig/addons";
import { agent, oncePerAgent, repair, s } from "rig";

// Agent role: answer after one-time runtime initialization.
const qa = agent({
Expand Down
70 changes: 70 additions & 0 deletions skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,16 @@ export type AgentAddon = (
context: AgentAddonContext,
next: () => Promise<void>,
) => void | Promise<void>;
export type SteeringOptions = {
message?: string;
};
export type TimeoutOptions = {
timeout: number;
};
export type AgentRegistration = (
agent: Agent,
context: AgentAddonContext,
) => void | Promise<void>;
export type ToolHandler<TArgs = unknown> = (args: TArgs) => unknown | Promise<unknown>;
export type ToolParameters = Schema | Record<string, unknown>;
export type Tool<TArgs = unknown> = ToolConfig<TArgs> & { name: string };
Expand Down Expand Up @@ -1256,6 +1266,66 @@ export class AgentError extends Error {
}
}

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 function steering(options: SteeringOptions = {}): AgentAddon {
const message = options.message ?? DEFAULT_STEERING_WARNING;
return async (context, next) => {
await next();
if (context.nextPrompt && context.turn + 1 === context.maxTurns) {
context.nextPrompt = `${context.nextPrompt}\n${message}`;
}
};
}

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) => {
context.signal = timeoutSignal(context.signal, options.timeout);
await next();
};
}

export function oncePerAgent(register: AgentRegistration): AgentAddon {
const seen = new WeakSet<Agent>();
return async (context, next) => {
if (!seen.has(context.agent)) {
await register(context.agent, context);
seen.add(context.agent);
}
await next();
};
}

export const addons = {
oncePerAgent,
timeout,
repair,
steering,
};

let currentAgentFactory: AgentFactory = defaultAgentFactory();

/**
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/100-workspace-config-drift-2.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 100 - Workspace Config Drift 2

```rig
import { agent, p, s, defineTool } from "rig";
import { repair } from "rig/addons";
import { agent, p, s, defineTool, repair } from "rig";

const parseJson = defineTool("parseJson", {
description: "Parse a JSON string and return parsed object or error",
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/101-commit-format-suggester-2.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 101 - Commit Format Suggester 2

```rig
import { agent, p, s } from "rig";
import { repair, steering } from "rig/addons";
import { agent, p, s, repair, steering } from "rig";

// Agent role: suggest conventional commit format for recent git commit messages.
const commitFormatSuggester = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/103-hotspot-file-analyzer-2.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 103 - Hotspot File Analyzer 2

```rig
import { agent, p, s } from "rig";
import { steering } from "rig/addons";
import { agent, p, s, steering } from "rig";

// Agent role: identify hot-spot files by measuring commit churn and contributor spread.
const hotspotFileAnalyzer = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/105-import-cycle-detector-2.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 105 - Import Cycle Detector 2

```rig
import { agent, p, s } from "rig";
import { repair } from "rig/addons";
import { agent, p, s, repair } from "rig";

// Agent role: detect circular import cycles in TypeScript source files.
const importCycleDetector = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/107-git-rename-tracker.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 107 - Git Rename Tracker

```rig
import { agent, p, s } from "rig";
import { repair } from "rig/addons";
import { agent, p, s, repair } from "rig";

// Agent role: track file renames in git history and produce a structured rename log.
const gitRenameTracker = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/110-workspace-config-drift-3.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 110 - Workspace Config Drift 3

```rig
import { agent, defineTool, p, s } from "rig";
import { repair } from "rig/addons";
import { agent, defineTool, p, s, repair } from "rig";

// Agent role: detect drift in workspace config files against a baseline.
const workspaceConfigDrift = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/111-conventional-commit-suggester.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 111 - Conventional Commit Suggester

```rig
import { agent, p, s } from "rig";
import { repair, steering } from "rig/addons";
import { agent, p, s, repair, steering } from "rig";

// Agent role: suggest conventional-format rewrites for recent git commit messages.
const conventionalCommitSuggester = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/113-git-hotspot-analyzer.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 113 - Git Hotspot Analyzer

```rig
import { agent, p, s } from "rig";
import { steering } from "rig/addons";
import { agent, p, s, steering } from "rig";

// Agent role: identify hot-spot files by git churn and top contributors.
const gitHotspotAnalyzer = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/115-import-cycle-detector.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 115 - Import Cycle Detector

```rig
import { agent, p, s } from "rig";
import { repair } from "rig/addons";
import { agent, p, s, repair } from "rig";

// Agent role: detect circular import cycles in the TypeScript project.
const importCycleDetector = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/118-pr-review-checklist.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 118 - Pr Review Checklist

```rig
import { agent, p, s } from "rig";
import { repair } from "rig/addons";
import { agent, p, s, repair } from "rig";

// Agent role: generate a PR review checklist from the recent git diff.
const prReviewChecklist = agent({
Expand Down
3 changes: 1 addition & 2 deletions skills/rig/samples/120-git-hotspot-v2.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# 120 - Git Hotspot V2

```rig
import { agent, p, s, defineTool } from "rig";
import { repair } from "rig/addons";
import { agent, p, s, defineTool, repair } from "rig";

const getFileCommitCount = defineTool("getFileCommitCount", {
description: "Count how many commits touched a specific file",
Expand Down
Loading