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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,14 @@ p.read("README.md")
p.readOptional("Dockerfile") // returns "" if file is absent
p.readOptional(".eslintrc.json", "{}") // returns "{}" if file is absent
p.readAll(["src/index.ts", "src/utils.ts"]) // reads all files and concatenates their contents
p.readInput("path") // reads the file at the single path held in input.path
p.write("README.md", "# Updated\n") // write-file instruction; does NOT return the path
p.writeOutput("report", "todo-report.md") // after generation, write output field "report" to file
p.glob("src/**/*.ts")
p.env("GITHUB_TOKEN") // returns "" if variable is not set
p.env("GITHUB_TOKEN", "unset") // returns "unset" if variable is not set
p.json({ repo: "rig", stars: 42 })
p.inputField("files") // returns "input.files" for use in prompt prose

const reviewWorkspace = agent({
instructions: p`Review ${p.read("README.md")} against ${p.bash("git status --short")}.`,
Expand All @@ -146,6 +148,8 @@ const reviewWorkspace = agent({

`p.readAll(paths)` reads all files in the array and concatenates their contents into a single block. Use it instead of repeated `p.read(...)` calls or `p.bash("cat ...")` when you need the full contents of a known set of files as one context block.

`p.readInput(field)` reads the file at the **single** path held in the named input field. Use `p.inputField(field)` when the input field is not a file path but you still need to reference its value (array, string, etc.) in the prompt prose. `p.inputField("files")` returns `"input.files"` for use in template expressions — an explicit, documented alternative to the opaque `${"input.files"}` literal.

```ts
const b = p();
const repo = b.var("repo", "rig");
Expand Down
5 changes: 3 additions & 2 deletions skills/rig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ Descriptions go first for scalar helpers, second after the shape for containers,
| Shell command with literal backslashes | ``p.bashRaw`command` `` |
| Known required/optional file | `p.read(path)` / `p.readOptional(path, fallback?)` |
| Several known files | `p.readAll(paths)` |
| Path supplied in an input field | `p.readInput(field)` |
| Single path supplied in an input field | `p.readInput(field)` |
| Workspace discovery | `p.glob(pattern)` |
| Environment or structured inline data | `p.env(name, fallback?)` / `p.json(value)` |
| Reference an input field value in prose | `p.inputField(field)` |
| Write content known while building the prompt | `p.write(path, content)` |
| Write an LLM-generated output field | `p.writeOutput(field, path)` |

Expand All @@ -89,7 +90,7 @@ Prefer:
instructions: p`Review ${p.read("README.md")} against ${p.bash("git status --short")}.`
```

Do not replace file intents with `cat` commands or large in-memory strings. `p.readOptional(path, fallback?)` injects the fallback text directly into prompt context when the file is absent. `p.write` does not return a path; `p.writeOutput` requires a matching output-schema field. `p.readAll(paths)` accepts a known path list, not a glob pattern. `p.bash` and `p.bashRaw` accept only static strings; to run a command that depends on a caller-supplied value, describe it in the instructions prose and reference `input.<field>` by name.
Do not replace file intents with `cat` commands or large in-memory strings. `p.readOptional(path, fallback?)` injects the fallback text directly into prompt context when the file is absent. `p.write` does not return a path; `p.writeOutput` requires a matching output-schema field. `p.readAll(paths)` accepts a known path list, not a glob pattern. `p.readInput(field)` reads the file at a **single** path held in the named input field; for an array of paths use a coordinator + subagent pattern (see [Prompt intents](references/prompt-intents.md)). `p.bash` and `p.bashRaw` accept only static strings; to run a command that depends on a caller-supplied value, describe it in the instructions prose and reference `input.<field>` by name — use `p.inputField(field)` to reference a non-path input value explicitly in prose instead of the opaque `${"input.field"}` literal.

## Tools, composition, and reliability

Expand Down
11 changes: 11 additions & 0 deletions skills/rig/references/agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ Strict TypeScript compilation reports unused handler bindings. Destructure only

Use `defineTool` for I/O operations and external calls the LLM should be able to invoke — fetching a URL, running a query, reading a dynamic path. Do not use it to replace LLM inference: if the handler implements the classification or reasoning logic entirely in TypeScript, the model is never exercised for that step. Express classification rules as prompt instructions instead, and use a tool only when a discrete external operation is needed to support the model's reasoning.

### `defineTool` vs `p.bash` decision table

| Situation | Use |
|-----------|-----|
| Static shell command known at definition time | `p.bash(command)` |
| Command contains literal backslashes or regex | ``p.bashRaw`command` `` |
| External I/O the model should invoke conditionally (fetch URL, read dynamic path, query DB) | `defineTool` |
| Deterministic transform that gives the model intermediate data to reason about | `defineTool` |
| Full classification or judgment logic implemented in TypeScript | Neither — use prompt instructions |
| Command depends on a caller-supplied value | Prose description in instructions; reference field with `p.inputField(field)` |

## Call-time options

Use call-time options only for per-run changes:
Expand Down
2 changes: 1 addition & 1 deletion skills/rig/references/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const summarize = agent({
export default summarize;
```

`repair()` takes no options. The budget includes the initial attempt and all retries, so configure `maxTurns` on the agent spec; a call-time value can override it.
`repair()` takes no options. The budget includes the initial attempt and all retries — for example, `maxTurns: 3` means one initial attempt plus two repair retries. Configure `maxTurns` on the agent spec; a call-time value can override it.

## Final-turn steering

Expand Down
26 changes: 25 additions & 1 deletion skills/rig/references/prompt-intents.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ Intent values are also accepted in agent inputs, but prefer template expressions
| `p.read(path)` | Required file at a literal path |
| `p.readOptional(path, fallback?)` | Literal path that may be absent; default fallback is `""` and is injected as prompt text |
| `p.readAll(paths)` | Concatenated contents of several known files (path list only; no glob overload) |
| `p.readInput(field)` | File path taken from `input.<field>` at runtime |
| `p.readInput(field)` | File contents at the **single** path held in `input.<field>` at runtime |
| `p.write(path, content)` | Prompt instruction to write content already known |
| `p.writeOutput(field, path)` | Post-generation write of an output field |
| `p.glob(pattern)` | Runtime workspace path discovery |
| `p.env(name, fallback?)` | Environment variable; default fallback is `""` |
| `p.json(value)` | Immediate pretty-printed JSON for inline structured context |
| `p.inputField(field)` | Returns `"input.<field>"` for explicit reference to a non-path input value in prose |

Examples:

Expand All @@ -59,6 +60,7 @@ p.glob("src/**/*.ts")
p.env("GITHUB_TOKEN")
p.env("GITHUB_TOKEN", "unset")
p.json({ repo: "rig", stars: 42 })
p.inputField("files")
```

Use ``p.bashRaw`...` `` when regex or shell syntax contains sequences such as `\.`, `\|`, or other backslashes. A normal `p.bash("...")` argument follows TypeScript string escaping rules.
Expand Down Expand Up @@ -127,8 +129,30 @@ const fileAnalyzer = agent({
export default fileAnalyzer;
```

`p.readInput(field)` reads the file at the **single** path held in the named input field. It cannot accept an array field. For an array of caller-supplied paths, use the coordinator + subagent pattern described in "Runtime lists of file paths" below.

The argument is the input field name, not the path itself. Passing full contents remains reasonable for small payloads; pass a path for larger files to reduce prompt size.

## Referencing non-path input values in prose

When you need to refer to a caller-supplied input value that is **not** a file path — such as an array of paths, a name, or a list — use `p.inputField(field)` to produce an explicit `"input.<field>"` reference in the prompt prose. This replaces the opaque `${"input.fieldName"}` string literal:

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

// Agent role: merge a caller-supplied list of JSON config files.
const configMerger = agent({
model: "mini",
input: s.object({ files: s.array(s.path) }),
instructions: p`You are given a list of JSON config file paths: ${p.inputField("files")}\nRead and merge them into one combined config.`,
output: s.object({ merged: s.unknown }),
});

export default configMerger;
```

`p.inputField("files")` returns the string `"input.files"`, which the model reads as a reference to the caller-supplied `files` field. Use `p.readInput(field)` when the field holds a **single** file path whose contents the model should read; use `p.inputField(field)` when the field value itself (array, string, number, etc.) should appear in the prompt text.

## Runtime lists of file paths

When input contains an array of paths, there is no `p.readInputAll(...)` helper. Use a coordinator + subagent pattern: let the coordinator iterate and delegate one file at a time to a subagent that uses `p.readInput("path")`. Likewise, there is no `p.readAll(globPattern)` helper: use `p.glob(...)` for discovery, then delegate per path.
Expand Down
21 changes: 20 additions & 1 deletion skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@
* p.writeOutput(field,path,opts?) PromptIntent post-generation write declaration; writes output field value to path
* p.glob(pattern,opts?) PromptIntent glob file-list declaration (not run in-process)
* p.readAll(paths,opts?) PromptIntent multi-file read declaration; reads all listed paths and concatenates their contents
* p.readInput(field,opts?) PromptIntent file read declaration using a runtime input field as the path; reads the file at the path given by input.<field> [NEW]
* p.readInput(field,opts?) PromptIntent file read declaration using a runtime input field as the path; reads the file at the single path given by input.<field> [NEW]
* p.env(name,fallback?,opts?) PromptIntent env var read declaration; returns fallback (default "") if not set
* p.json(value) string JSON.stringify helper for inlining structured values in prompt templates
* p.inputField(field) string returns "input.<field>" for explicit, documented reference to a caller-supplied input field in prompt prose [NEW]
* p.var(name,value) PromptVariable<T> named variable binding for prompt templates [NEW]
* p.region(language,body) string wraps body in a fenced code block for the given language [NEW]
* F:agent(spec) AgentFn<I,O>; spec={name,description,input,output,prompt,addons,maxTurns}
Expand Down Expand Up @@ -814,6 +815,21 @@ type PromptHelpers = {
* const prompt = p`Context: ${p.json({ repo: "rig", stars: 42 })}`;
*/
json(value: unknown): string;
/**
* Returns the string `"input.<field>"` for explicit, documented reference to
* a caller-supplied input field in prompt prose. Use this instead of the
* opaque `${"input.fieldName"}` literal when you need to tell the model to
* use a non-path input value — for example, to reference an array of file
* paths or a string value in the instructions.
*
* For reading the **file contents** of a path held in an input field, use
* `p.readInput(field)` instead.
*
* @example
* // Reference a caller-supplied array of config files in the prompt:
* instructions: p`Merge the JSON config files listed in ${p.inputField("files")}.`
*/
inputField(field: string): string;
var<T>(name: string, value: T): PromptVariable<T>;
region(language: string, body: unknown): string;
};
Expand Down Expand Up @@ -957,6 +973,9 @@ export const p: PromptHelpers = Object.assign(
json(value: unknown): string {
return json(value);
},
inputField(field: string): string {
return `input.${field}`;
},
var<T>(name: string, value: T): PromptVariable<T> {
return createPromptVariable(name, value);
},
Expand Down
11 changes: 11 additions & 0 deletions src/rig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,17 @@ describe("prompt intents", () => {
expect(p.json("hello")).toBe('"hello"');
});

it("p.inputField returns input.<field> string for use in prompt prose", () => {
expect(p.inputField("files")).toBe("input.files");
expect(p.inputField("path")).toBe("input.path");
expect(p.inputField("repoName")).toBe("input.repoName");
});

it("p.inputField can be used inline in a prompt template", () => {
const builder = p`Merge the config files listed in ${p.inputField("files")}.`;
expect(String(builder)).toContain("input.files");
});

it("strips AbortSignal from intent options", () => {
const controller = new AbortController();
const intent = p.bash("echo hi", { cwd: "/tmp", signal: controller.signal });
Expand Down