Skip to content

Commit 08403d6

Browse files
Copilotpelikhan
andauthored
feat: support workflow as default export, update SKILL.md and docs
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 2c52c4f commit 08403d6

6 files changed

Lines changed: 99 additions & 10 deletions

File tree

scripts/run-sample.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ describe("skill markdown samples", () => {
257257
const runnableCode = withTypecheckModel(code);
258258
expect(code.split("\n").length).toBeLessThanOrEqual(30);
259259
expect(code).toContain("export default");
260-
expect(code).toContain("// Agent role:");
260+
expect(code).toMatch(/\/\/ (?:Agent|Workflow) role:/);
261261
expect(code).not.toContain("console.log");
262262
expect((code.match(/^import .* from "rig";$/gm) ?? [])).toHaveLength(1);
263263
expect(code).not.toMatch(/^await\s+\w+\(/m);

skills/rig/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ export default reviewDiff;
2929

3030
## Construction rules
3131

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

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

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

@@ -68,7 +68,7 @@ Prompt intents are declarative instructions, not in-process operations. Prefer f
6868

6969
## Runnable output
7070

71-
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.
71+
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()`.
7272

7373
Before running generated TypeScript:
7474

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

8989
## Focused references

skills/rig/references/dynamic-workflows.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,41 @@ const results = await runWorkflow(audit, {
4040
`meta` describes the workflow for tools and progress displays. `input` preserves
4141
Rig schema inference in both `body` and `runWorkflow({ args })`.
4242

43+
## Workflow as default export
44+
45+
A `workflow` can be the default export of a rig program. The launcher wraps it
46+
in `runWorkflow` automatically — no manual call is needed:
47+
48+
```ts
49+
import { agent, s, workflow } from "rig";
50+
51+
// Agent role: check one file for linting issues.
52+
const lintFile = agent({
53+
name: "lintFile",
54+
model: "nano",
55+
input: s.object({ file: s.path }),
56+
output: s.object({ issues: s.array(s.string) }),
57+
instructions: "Check the file for linting issues.",
58+
});
59+
60+
// Workflow role: lint source files discovered at runtime.
61+
const linter = workflow({
62+
meta: { name: "linter", description: "Lint TypeScript files in parallel", phases: ["Discover", "Lint"] },
63+
body: async ({ call, phase, pipeline }) => {
64+
phase("Discover");
65+
const raw = await call.text("List TypeScript source files to lint, one path per line.");
66+
const files = (raw ?? "").split("\n").map((f) => f.trim()).filter(Boolean);
67+
phase("Lint");
68+
return pipeline(files, (file) => call(lintFile, { file }, { label: file }));
69+
},
70+
});
71+
72+
export default linter;
73+
```
74+
75+
- Omit `input` for a no-input program (inline mode) or add `input: s.object({ ... })` for file-mode programs that read stdin JSON.
76+
- Use `agent()` with `agents:` when an LLM should improvise the coordination order. Use `workflow()` when TypeScript owns the orchestration (fan-out, branching, convergence).
77+
4378
## Context
4479

4580
| Member | Behavior |

skills/rig/rig.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,15 +1474,41 @@ function asRootAgent(value: unknown): AgentFn | undefined {
14741474
return value as AgentFn;
14751475
}
14761476

1477+
function isWorkflow(value: unknown): value is Workflow<unknown, unknown> {
1478+
return (
1479+
typeof value === "object" &&
1480+
value !== null &&
1481+
"meta" in value &&
1482+
typeof (value as Record<string, unknown>)["meta"] === "object" &&
1483+
"body" in value &&
1484+
typeof (value as Record<string, unknown>)["body"] === "function"
1485+
);
1486+
}
1487+
14771488
/**
14781489
* Normalizes supported launcher root exports to an agent function.
14791490
* Strings and prompt builders are wrapped in a default agent.
1491+
* Workflow objects are wrapped in a runWorkflow call.
14801492
*/
14811493
function asRootProgram(value: unknown, name: string): AgentFn | undefined {
14821494
const rootAgent = asRootAgent(value);
14831495
if (rootAgent) {
14841496
return rootAgent;
14851497
}
1498+
if (isWorkflow(value)) {
1499+
const w = value;
1500+
const hasInput = "inputSchema" in w;
1501+
const inputSchema: Schema = hasInput ? (w.inputSchema as Schema) : defaultStringSchema;
1502+
const fn = Object.assign(
1503+
async (input: unknown) => runWorkflow(w, hasInput ? { args: input } : {}),
1504+
{
1505+
inputSchema,
1506+
outputSchema: defaultStringSchema as Schema,
1507+
agentName: w.meta.name,
1508+
},
1509+
);
1510+
return fn as unknown as AgentFn;
1511+
}
14861512
if (typeof value === "string" || value instanceof PromptBuilder) {
14871513
return agent({ name, instructions: value }) as AgentFn;
14881514
}
@@ -1691,7 +1717,7 @@ async function runRootAgentFromStdin(
16911717
const mod = await import(pathToFileURL(resolvedPath).href);
16921718
const rootAgent = asRootProgram(mod.default, "launcher-root");
16931719
if (!rootAgent) {
1694-
throw new Error("Expected program to export a root value (agent, string, or prompt builder) as default export.");
1720+
throw new Error("Expected program to export a root value (agent, workflow, string, or prompt builder) as default export.");
16951721
}
16961722

16971723
const result = await rootAgent(coerceStdinInput(rootAgent, prompt));
@@ -1725,7 +1751,7 @@ async function runProgramCodeFromStdin(
17251751
const mod = await import(pathToFileURL(tempProgramPath).href);
17261752
const rootAgent = asRootProgram(mod.default, "launcher-inline-root");
17271753
if (!rootAgent) {
1728-
throw new Error("Expected program to export a root value (agent, string, or prompt builder) as default export.");
1754+
throw new Error("Expected program to export a root value (agent, workflow, string, or prompt builder) as default export.");
17291755
}
17301756
const input = noInputInvocation(rootAgent);
17311757
if (input === undefined) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# 60 - Parallel File Linter (Workflow)
2+
3+
```rig
4+
import { agent, s, workflow } from "rig";
5+
6+
// Agent role: check one TypeScript file for code quality issues.
7+
const lintFile = agent({
8+
name: "lintFile",
9+
model: "nano",
10+
input: s.object({ file: s.path }),
11+
output: s.object({ issues: s.array(s.string) }),
12+
instructions: "Check the file for linting issues.",
13+
});
14+
15+
// Workflow role: discover and lint TypeScript source files in parallel.
16+
const linter = workflow({
17+
meta: { name: "linter", description: "Lint TypeScript files in parallel", phases: ["Discover", "Lint"] },
18+
body: async ({ call, phase, pipeline }) => {
19+
phase("Discover");
20+
const raw = await call.text("List TypeScript source files to lint, one path per line.");
21+
const files = (raw ?? "").split("\n").map((f) => f.trim()).filter(Boolean);
22+
phase("Lint");
23+
return pipeline(files, (file) => call(lintFile, { file }, { label: file }));
24+
},
25+
});
26+
27+
export default linter;
28+
```

src/launcher.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ it("requires stdin-mode root agent to be a default export", async () => {
190190

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

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

0 commit comments

Comments
 (0)