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 scripts/run-sample.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ describe("skill markdown samples", () => {
const runnableCode = withTypecheckModel(code);
expect(code.split("\n").length).toBeLessThanOrEqual(30);
expect(code).toContain("export default");
expect(code).toContain("// Agent role:");
expect(code).toMatch(/\/\/ (?:Agent|Workflow) role:/);
expect(code).not.toContain("console.log");
expect((code.match(/^import .* from "rig";$/gm) ?? [])).toHaveLength(1);
expect(code).not.toMatch(/^await\s+\w+\(/m);
Expand Down
11 changes: 6 additions & 5 deletions skills/rig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ export default reviewDiff;

## Construction rules

1. Import current APIs from `"rig"` once and define agents with `agent({ ... })`.
2. Add a `// Agent role: ...` comment above each agent.
1. Import current APIs from `"rig"` once and define agents with `agent({ ... })` or workflows with `workflow({ ... })`.
2. Add a `// Agent role: ...` comment above each `agent()` and a `// Workflow role: ...` comment above each `workflow()`.
3. Omit `input`/`output` when free-form strings suffice; otherwise use explicit `s.*` schemas.
4. Put known workspace context in ``p`...` `` with `p.read`, `p.bash`, or another intent. Use `input` only for caller-supplied values.
5. Keep outputs strict and small; prefer `s.enum`, `s.literal`, `s.path`, and `s.int` when they express the contract.
6. Add narrow, named subagents only when delegation helps; attach them as `agents: { name }`.
7. Export exactly one root value. Do not invoke it or print its result in generated programs.
7. Export exactly one root value — an `agent` or a `workflow`. Do not invoke it or print its result in generated programs.

Defaults: `name: "agent"`, `model: "small"`, `maxTurns: 4`, string input/output, and no addons.

Expand All @@ -52,6 +52,7 @@ Defaults: `name: "agent"`, `model: "small"`, `maxTurns: 4`, string input/output,
| String-keyed map | `s.record(value)`; keys are always `string` — do not wrap in `s.object`; use `s.record(s.int)` for count maps |
| Numeric schema choice | `s.int` for counts/line numbers; `s.number` for measurements and ratios |
| Optional versus nullable | `s.optional(shape)` for omission; `s.nullable(shape)` for explicit `null` |
| Deterministic TypeScript fan-out | `workflow({ meta, input?, body })` + `export default`; use `call`, `pipeline`, `parallel`, `until` inside `body` |
| Custom model-callable operation | `defineTool(name, { description, parameters, handler })` |
| Structured-output retries | `maxTurns` on the agent plus `addons: [repair()]` |
| Retry with final-turn warning | `addons: [steering(), repair()]` in that order |
Expand All @@ -70,7 +71,7 @@ Prompt intents are declarative instructions, not in-process operations. Prefer f

## Runnable output

For runnable markdown, emit exactly one fenced `rig` block with one default-exported root and no required external input. Never call the root inside the fence.
For runnable markdown, emit exactly one fenced `rig` block with one default-exported root (`agent` or `workflow`) and no required external input. Never call the root inside the fence. Add a `// Agent role: ...` comment above each `agent()` and a `// Workflow role: ...` comment above each `workflow()`.

Before running generated TypeScript:

Expand All @@ -85,7 +86,7 @@ cat program.ts | node skills/rig/rig.ts --typecheck
- Schemas use only current `s.*` helpers and constrain important output.
- Every import, addon, tool, and helper follows the current API.
- Every subagent is named, reachable, and narrowly scoped.
- The program has one default export and no `console.log`.
- The program has one default export (an `agent` or a `workflow`) and no `console.log`.
- Linting and typechecking pass.

## Focused references
Expand Down
35 changes: 35 additions & 0 deletions skills/rig/references/dynamic-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ const results = await runWorkflow(audit, {
`meta` describes the workflow for tools and progress displays. `input` preserves
Rig schema inference in both `body` and `runWorkflow({ args })`.

## Workflow as default export

A `workflow` can be the default export of a rig program. The launcher wraps it
in `runWorkflow` automatically — no manual call is needed:

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

// Agent role: check one file for linting issues.
const lintFile = agent({
name: "lintFile",
model: "nano",
input: s.object({ file: s.path }),
output: s.object({ issues: s.array(s.string) }),
instructions: "Check the file for linting issues.",
});

// Workflow role: lint source files discovered at runtime.
const linter = workflow({
meta: { name: "linter", description: "Lint TypeScript files in parallel", phases: ["Discover", "Lint"] },
body: async ({ call, phase, pipeline }) => {
phase("Discover");
const raw = await call.text("List TypeScript source files to lint, one path per line.");
const files = (raw ?? "").split("\n").map((f) => f.trim()).filter(Boolean);
phase("Lint");
return pipeline(files, (file) => call(lintFile, { file }, { label: file }));
},
});

export default linter;
```

- Omit `input` for a no-input program (inline mode) or add `input: s.object({ ... })` for file-mode programs that read stdin JSON.
- Use `agent()` with `agents:` when an LLM should improvise the coordination order. Use `workflow()` when TypeScript owns the orchestration (fan-out, branching, convergence).

## Context

| Member | Behavior |
Expand Down
30 changes: 28 additions & 2 deletions skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1474,15 +1474,41 @@ function asRootAgent(value: unknown): AgentFn | undefined {
return value as AgentFn;
}

function isWorkflow(value: unknown): value is Workflow<unknown, unknown> {
return (
typeof value === "object" &&
value !== null &&
"meta" in value &&
typeof (value as Record<string, unknown>)["meta"] === "object" &&
"body" in value &&
typeof (value as Record<string, unknown>)["body"] === "function"
);
}

/**
* Normalizes supported launcher root exports to an agent function.
* Strings and prompt builders are wrapped in a default agent.
* Workflow objects are wrapped in a runWorkflow call.
*/
function asRootProgram(value: unknown, name: string): AgentFn | undefined {
const rootAgent = asRootAgent(value);
if (rootAgent) {
return rootAgent;
}
if (isWorkflow(value)) {
const w = value;
const hasInput = "inputSchema" in w;
const inputSchema: Schema = hasInput ? (w.inputSchema as Schema) : defaultStringSchema;
const fn = Object.assign(
async (input: unknown) => runWorkflow(w, hasInput ? { args: input } : {}),
{
inputSchema,
outputSchema: defaultStringSchema as Schema,
agentName: w.meta.name,
},
);
return fn as unknown as AgentFn;
}
if (typeof value === "string" || value instanceof PromptBuilder) {
return agent({ name, instructions: value }) as AgentFn;
}
Expand Down Expand Up @@ -1691,7 +1717,7 @@ async function runRootAgentFromStdin(
const mod = await import(pathToFileURL(resolvedPath).href);
const rootAgent = asRootProgram(mod.default, "launcher-root");
if (!rootAgent) {
throw new Error("Expected program to export a root value (agent, string, or prompt builder) as default export.");
throw new Error("Expected program to export a root value (agent, workflow, string, or prompt builder) as default export.");
}

const result = await rootAgent(coerceStdinInput(rootAgent, prompt));
Expand Down Expand Up @@ -1725,7 +1751,7 @@ async function runProgramCodeFromStdin(
const mod = await import(pathToFileURL(tempProgramPath).href);
const rootAgent = asRootProgram(mod.default, "launcher-inline-root");
if (!rootAgent) {
throw new Error("Expected program to export a root value (agent, string, or prompt builder) as default export.");
throw new Error("Expected program to export a root value (agent, workflow, string, or prompt builder) as default export.");
}
const input = noInputInvocation(rootAgent);
if (input === undefined) {
Expand Down
28 changes: 28 additions & 0 deletions skills/rig/samples/60-workflow-parallel-linter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 60 - Parallel File Linter (Workflow)

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

// Agent role: check one TypeScript file for code quality issues.
const lintFile = agent({
name: "lintFile",
model: "nano",
input: s.object({ file: s.path }),
output: s.object({ issues: s.array(s.string) }),
instructions: "Check the file for linting issues.",
});

// Workflow role: discover and lint TypeScript source files in parallel.
const linter = workflow({
meta: { name: "linter", description: "Lint TypeScript files in parallel", phases: ["Discover", "Lint"] },
body: async ({ call, phase, pipeline }) => {
phase("Discover");
const raw = await call.text("List TypeScript source files to lint, one path per line.");
const files = (raw ?? "").split("\n").map((f) => f.trim()).filter(Boolean);
phase("Lint");
return pipeline(files, (file) => call(lintFile, { file }, { label: file }));
},
});

export default linter;
```
2 changes: 1 addition & 1 deletion src/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ it("requires stdin-mode root agent to be a default export", async () => {

await expect(
runLauncherCli([fixturePath], {}, { stdin, stdout }),
).rejects.toThrow("Expected program to export a root value (agent, string, or prompt builder) as default export.");
).rejects.toThrow("Expected program to export a root value (agent, workflow, string, or prompt builder) as default export.");
});

it("rejects stdin mode when prompt is empty", async () => {
Expand Down