Skip to content

Commit 7fb03fd

Browse files
Copilotpelikhan
andauthored
feat: add s.path schema helper, improve addon error messages, update docs
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 552febc commit 7fb03fd

4 files changed

Lines changed: 38 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ s.nonEmptyString // string with minLength: 1
8585
s.nonEmptyString("description")
8686
s.url // string with format: "uri"
8787
s.url("description")
88+
s.path // string with format: "path"; use for file system paths
89+
s.path("description")
8890
s.number
8991
s.integer
9092
s.int // alias for s.integer; prefer for counts, line numbers, integer-only fields

skills/rig/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Declare a structured agent.
9292
| `maxTurns` | Retry budget for invalid JSON or invalid output |
9393
| `addons` | Per-turn addons for steering, validation, and retry customization |
9494
| `agents` | Optional named subagents exposed to the harness |
95+
| `tools` | Tool definitions for function-calling (registered via `defineTool`) |
9596

9697
Use `agent({ name, ... })` as the only agent declaration form. `name` is optional; when omitted rig normalizes it to `"agent"`.
9798

@@ -140,6 +141,8 @@ s.nonEmptyString // string with minLength: 1
140141
s.nonEmptyString("description")
141142
s.url // string with format: "uri"
142143
s.url("description")
144+
s.path // string with format: "path"; use for file system paths
145+
s.path("description")
143146
s.number
144147
s.integer
145148
s.int // alias for s.integer
@@ -179,6 +182,7 @@ s.nullable(s.string) // string | null
179182
s.literal("done") // exactly "done"
180183
s.nonEmptyString // non-empty string required
181184
s.url // valid URL string
185+
s.path // file system path; prefer over s.string for file/directory inputs
182186
s.int // integer number (no floats); prefer over s.number for counts, line numbers, etc.
183187
s.nonEmptyArray(s.string) // string[] with at least one element
184188
s.nonEmptyObject(s.string) // Record<string, string> with at least one key

skills/rig/rig.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* T:LauncherIo type {stdin,stdout,stderr} override for launcher subprocess
3030
* T:JsonSchemaObject type {[key:string]:unknown} plain JSON Schema object
3131
* s.string/number/integer/boolean/null SchemaHelperFactory primitives; call as value or fn(desc)
32-
* s.int alias for s.integer; s.nonEmptyString string with minLength:1; s.url string with format:"uri"
32+
* s.int alias for s.integer; s.nonEmptyString string with minLength:1; s.url string with format:"uri"; s.path string with format:"path"
3333
* s.array(items,desc?) ArraySchema; use for homogeneous lists, e.g. s.array(s.string)
3434
* s.nonEmptyArray(items,desc?) ArraySchema with minItems:1; validates array has at least one element
3535
* s.object(props,desc?) ObjectSchema; s.optional(inner) marks field optional; s.nullable(inner) accepts inner|null; use for fixed-key shapes
@@ -225,6 +225,8 @@ export const s = {
225225
nonEmptyString: createConstrainedStringSchema({ minLength: 1 }),
226226
/** Schema for a URL string (format: "uri"). Call as `s.url` or `s.url("description")`. */
227227
url: createConstrainedStringSchema({ format: "uri" }),
228+
/** Schema for a file system path string (format: "path"). Use instead of `s.string` when the value is a file or directory path; improves readability and hints to the runtime about path-based context resolution. Call as `s.path` or `s.path("description")`. */
229+
path: createConstrainedStringSchema({ format: "path" }),
228230
/** Schema for a `number` value. Call as `s.number` or `s.number("description")`. */
229231
number: createTypedPrimitiveSchema<NumberSchema>("number"),
230232
/** Schema for an integer value. Serializes to `{"type":"integer"}` in JSON Schema. Call as `s.integer` or `s.integer("description")`. */
@@ -2052,9 +2054,11 @@ function normalizeAddons(addons?: AgentAddon | AgentAddon[]): AgentAddon[] {
20522054
return [];
20532055
}
20542056
const items = Array.isArray(addons) ? [...addons] : [addons];
2055-
for (const addon of items) {
2057+
for (let i = 0; i < items.length; i++) {
2058+
const addon = items[i];
20562059
if (typeof addon !== "function") {
2057-
throw new Error("Agent addon entries must be functions.");
2060+
const got = addon === null ? "null" : typeof addon;
2061+
throw new Error(`Agent addon entries must be functions (entry at index ${i} is ${got}).`);
20582062
}
20592063
}
20602064
return items;

src/rig.test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ describe("agent invocation", () => {
356356
});
357357

358358
expect(() => greet.use([null as unknown as any] as any)).toThrow(
359-
"Agent addon entries must be functions.",
359+
"Agent addon entries must be functions (entry at index 0 is null).",
360360
);
361361
});
362362

@@ -771,7 +771,7 @@ describe("agent invocation", () => {
771771
mocks.setSendAndWaitImpl(async () => JSON.stringify("ok"));
772772
const guarded = agent({ name: "guarded", addons: [null as unknown as any] as any });
773773
await expect(guarded("go")).rejects.toThrow(
774-
"Agent addon entries must be functions.",
774+
"Agent addon entries must be functions (entry at index 0 is null).",
775775
);
776776
});
777777

@@ -1166,6 +1166,8 @@ describe("toJsonSchema", () => {
11661166
expect(toJsonSchema(s.nonEmptyString("A non-empty value"))).toEqual({ type: "string", minLength: 1, description: "A non-empty value" });
11671167
expect(toJsonSchema(s.url)).toEqual({ type: "string", format: "uri" });
11681168
expect(toJsonSchema(s.url("A URL"))).toEqual({ type: "string", format: "uri", description: "A URL" });
1169+
expect(toJsonSchema(s.path)).toEqual({ type: "string", format: "path" });
1170+
expect(toJsonSchema(s.path("source path"))).toEqual({ type: "string", format: "path", description: "source path" });
11691171
});
11701172

11711173
it("includes description when present", () => {
@@ -1365,6 +1367,27 @@ describe("s.url", () => {
13651367
});
13661368
});
13671369

1370+
describe("s.path", () => {
1371+
it("serializes to {type:'string', format:'path'}", () => {
1372+
expect(toJsonSchema(s.path)).toEqual({ type: "string", format: "path" });
1373+
expect(toJsonSchema(s.path("source file"))).toEqual({ type: "string", format: "path", description: "source file" });
1374+
});
1375+
1376+
it("accepts any string value", () => {
1377+
const result = analyzeResponse(JSON.stringify("src/index.ts"), s.path, "test", 1);
1378+
expect(result.ok).toBe(true);
1379+
});
1380+
1381+
it("is usable as an object field", () => {
1382+
const schema = s.object({ filePath: s.path });
1383+
expect(toJsonSchema(schema)).toEqual({
1384+
type: "object",
1385+
properties: { filePath: { type: "string", format: "path" } },
1386+
required: ["filePath"],
1387+
});
1388+
});
1389+
});
1390+
13681391
describe("s.null", () => {
13691392
it("serializes to {type:'null'}", () => {
13701393
expect(toJsonSchema(s.null)).toEqual({ type: "null" });

0 commit comments

Comments
 (0)