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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,45 @@ Use these samples to quickly gauge how well `rig` supports increasingly agentic

Per call, you can override `model`, `timeout`, `maxTurns`, and `signal`.

## Dynamic workflows

Use `workflow()` when ordinary TypeScript should own multi-agent fan-out, branching,
and convergence instead of asking one model to improvise the orchestration:

```ts
import { agent, s } from "rig";
import { runWorkflow, workflow } from "rig";

const inspect = agent({
input: s.string,
output: s.object({ finding: s.string }),
instructions: "Inspect the requested file.",
});

const audit = workflow({
meta: { name: "audit", description: "Inspect files concurrently", phases: ["Audit"] },
input: s.object({ files: s.array(s.string) }),
body: async ({ input, call, phase, pipeline }) => {
phase("Audit");
return pipeline(input.files, (file) => call(inspect, file, { label: file }));
},
});

const findings = await runWorkflow(audit, {
args: { files: ["src/a.ts", "src/b.ts"] },
limits: { concurrency: 8, maxAgents: 100 },
});
```

`call` returns `null` when an agent fails. `pipeline` and `parallel` preserve input
order and use the run's shared agent concurrency limit. `until` provides a bounded
loop with optional repeated-progress detection. `phase`, `log`, and `onEvent`
expose structured progress without coupling workflows to a UI. Runs default to at
most 1,000 agent calls and warn after 25.

See [Dynamic workflows](skills/rig/references/dynamic-workflows.md) for limits,
events, failure behavior, and the complete API.

## Debug logging

Set `RIG_DEBUG` to emit lazy JSONL diagnostics to stderr. A category enables itself and its children, `*` enables everything, and a `-` prefix excludes a category:
Expand Down
1 change: 1 addition & 0 deletions skills/rig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,6 @@ Read only when the task needs the listed detail:
- [Agent API and schemas](references/agent-api.md) — spec fields, schema overloads, tools, and invocation options.
- [Prompt intents](references/prompt-intents.md) — complete helper semantics, dynamic inputs, writes, and failure behavior.
- [Composition and addons](references/composition.md) — delegation patterns, dynamic sets, repair, steering, and addon lifecycle.
- [Dynamic workflows](references/dynamic-workflows.md) — bounded fan-out, failure semantics, limits, events, and convergence loops.
- [Running and engines](references/runtime.md) — markdown/file launch modes, typechecking, Agentic Workflows, and SDK adapters.
- [Linting](references/linting.md) — linter usage, autofixes, rules, and rule development.
113 changes: 113 additions & 0 deletions skills/rig/references/dynamic-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Dynamic workflows

Read this reference when deterministic TypeScript should coordinate multiple
typed agents.

## Define and run

Import workflow APIs from `rig`:

```ts
import { agent, s } from "rig";
import { runWorkflow, workflow } from "rig";

const review = agent({
input: s.object({ file: s.string }),
output: s.object({ findings: s.array(s.string) }),
instructions: "Review the requested file.",
});

const audit = workflow({
meta: {
name: "audit",
description: "Review files in parallel",
phases: ["Review"],
},
input: s.object({ files: s.array(s.string) }),
body: async ({ input, call, phase, pipeline }) => {
phase("Review");
return pipeline(input.files, (file) =>
call(review, { file }, { label: file }));
},
});

const results = await runWorkflow(audit, {
args: { files: ["src/a.ts", "src/b.ts"] },
limits: { concurrency: 8 },
});
```

`meta` describes the workflow for tools and progress displays. `input` preserves
Rig schema inference in both `body` and `runWorkflow({ args })`.

## Context

| Member | Behavior |
| --- | --- |
| `input` | Typed workflow arguments |
| `call(worker, input, options?)` | Runs a typed agent; returns its output or `null` on agent failure |
| `call.text(prompt, options?)` | Runs a one-off string-output agent |
| `pipeline(items, fn)` | Starts every item immediately; agent calls flow through the shared limiter |
| `parallel(thunks)` | Runs all thunks as a barrier and preserves their order |
| `until(options, step)` | Runs a bounded convergence loop |
| `phase(name)` | Sets the phase attached to subsequent events |
| `log(message)` | Emits a structured log event |
| `signal` | Run cancellation signal for non-agent work |

Call options support `label` plus the normal per-call `model`, `timeout`,
`maxTurns`, and `signal` overrides.

`parallel` turns rejected thunks into `null` holes. Agent failures passed through
`pipeline` are already `null` because `call` handles them; other pipeline callback
errors fail the run rather than hiding programming bugs. `WorkflowLimitError` is
never converted to `null`: exceeding `maxAgents` fails the whole run so runaway
scheduling cannot be hidden as an ordinary worker failure. Exceptions thrown
elsewhere in `body` also fail the run.

## Limits

```ts
await runWorkflow(job, {
args,
limits: {
concurrency: 8,
maxAgents: 500,
maxWallMs: 45 * 60_000,
warnAgents: 25,
},
signal,
onEvent: (event) => process.stderr.write(`${JSON.stringify(event)}\n`),
});
```

- `concurrency` defaults to the machine parallelism clamped between 2 and 16.
- `maxAgents` defaults to 1,000.
- `warnAgents` defaults to 25 and emits one advisory warning when exceeded.
- `maxWallMs` is optional. When reached, it aborts in-flight calls and fails the run.
- The limiter is shared by all nested primitives and is acquired only by agent
calls, so nested pipelines do not multiply concurrency or deadlock.

`onEvent` receives `run_start`, `phase_start`, `agent_start`, `agent_done`,
`agent_failed`, `log`, `warning`, `run_done`, and `run_failed`. Event observer
errors never affect the run.

## Convergence

Use `until` instead of an open-ended loop:

```ts
const final = await until(
{ max: 8, noProgressRounds: 2 },
async (previous, round) => {
const state = await checkAndFix(previous, round);
return {
state,
done: state.passed,
progressKey: state.failureSummary,
};
},
);
```

The loop stops when `done` is true, after `max` rounds, or after
`noProgressRounds` consecutive equal defined progress keys.
Loading