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
12 changes: 9 additions & 3 deletions skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @purpose Minimal TypeScript multi-agent harness: typed input/output schemas, prompt intents, sub-agent delegation, Copilot SDK runtime
* @deps @github/copilot-sdk (CopilotClient,RuntimeConnection,approveAll); node:path,url,fs/promises,child_process,util
* T:Json type null|bool|num|str|Json[]|{[k]:Json}
* T:Schema type StringSchema|NumberSchema|BooleanSchema|UnknownSchema|ArraySchema|ObjectSchema|RecordSchema|EnumSchema|OptionalSchema
* T:Schema type StringSchema|NumberSchema|IntegerSchema|BooleanSchema|UnknownSchema|ArraySchema|ObjectSchema|RecordSchema|EnumSchema|OptionalSchema
* T:InferSchema<T> type TS inference from schema descriptor to runtime type
* T:AgentInputValue<T> type input accepting raw values or PromptIntent/PromptBuilder at any nesting level
* T:Simplify<T> type flattens intersection types for display
Expand All @@ -26,7 +26,7 @@
* T:LaunchOptions type options for launchRigProgram (server,token,headers,cwd,args)
* T:LauncherIo type {stdin,stdout,stderr} override for launcher subprocess
* T:JsonSchemaObject type {[key:string]:unknown} plain JSON Schema object
* s.string/number/boolean SchemaHelperFactory primitives; call as value or fn(desc)
* s.string/number/integer/boolean SchemaHelperFactory primitives; call as value or fn(desc)
* s.array(items,desc?) ArraySchema
* s.object(props,desc?) ObjectSchema; s.optional(inner) marks field optional
* s.record(valSchema,desc?) RecordSchema keyed by string
Expand Down Expand Up @@ -66,6 +66,7 @@ export type ValidationResult = { ok: true } | { ok: false; error: string };

export type StringSchema = { type: "string"; description?: string };
export type NumberSchema = { type: "number"; description?: string };
export type IntegerSchema = { type: "integer"; description?: string };
export type BooleanSchema = { type: "boolean"; description?: string };
export type UnknownSchema = { description?: string };
export type ArraySchema<Item extends Schema = Schema> = { type: "array"; items: Item; description?: string };
Expand All @@ -84,6 +85,7 @@ export type OptionalSchema<Inner extends Schema = Schema> = Inner & OptionalMark
export type Schema =
| StringSchema
| NumberSchema
| IntegerSchema
| BooleanSchema
| UnknownSchema
| ArraySchema<any>
Expand Down Expand Up @@ -124,7 +126,7 @@ function isOptionalSchema(schema: Schema): schema is OptionalSchema<Schema> {
return OPTIONAL_SYMBOL in schema;
}

function createTypedPrimitiveSchema<T extends StringSchema | NumberSchema | BooleanSchema>(type: T["type"]): SchemaHelperFactory<T> {
function createTypedPrimitiveSchema<T extends StringSchema | NumberSchema | IntegerSchema | BooleanSchema>(type: T["type"]): SchemaHelperFactory<T> {
const base = markAsSchema({ type } as T);
const factory = Object.assign(
markAsSchema(((description?: string) => (description === undefined ? base : markAsSchema({ type, description } as T))) as SchemaHelperFactory<T>),
Expand Down Expand Up @@ -173,6 +175,7 @@ export type InferSchema<T> =
T extends OptionalMarker ? InferSchema<UnwrapOptional<T>> | undefined :
T extends { type: "string" } ? string :
T extends { type: "number" } ? number :
T extends { type: "integer" } ? number :
T extends { type: "boolean" } ? boolean :
T extends { enum: infer Values extends readonly unknown[] } ? Values[number] :
T extends { type: "array"; items: infer Item } ? InferSchema<Item>[] :
Expand All @@ -188,6 +191,8 @@ export const s = {
string: createTypedPrimitiveSchema<StringSchema>("string"),
/** Schema for a `number` value. Call as `s.number` or `s.number("description")`. */
number: createTypedPrimitiveSchema<NumberSchema>("number"),
/** Schema for an integer value. Serializes to `{"type":"integer"}` in JSON Schema. Call as `s.integer` or `s.integer("description")`. */
integer: createTypedPrimitiveSchema<IntegerSchema>("integer"),
/** Schema for a `boolean` value. Call as `s.boolean` or `s.boolean("description")`. */
boolean: createTypedPrimitiveSchema<BooleanSchema>("boolean"),
/** Schema for an unconstrained JSON value. Serializes to an empty schema object. */
Expand Down Expand Up @@ -1385,6 +1390,7 @@ function validateSchema(value: unknown, schema: Schema, path: string, optional:
if ("type" in schema) {
if (schema.type === "string") return typeof value === "string" ? ok() : bad(path, "string", value);
if (schema.type === "number") return typeof value === "number" ? ok() : bad(path, "number", value);
if (schema.type === "integer") return (typeof value === "number" && Number.isInteger(value)) ? ok() : bad(path, "integer", value);
if (schema.type === "boolean") return typeof value === "boolean" ? ok() : bad(path, "boolean", value);
}
return ok();
Expand Down
25 changes: 24 additions & 1 deletion src/rig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ vi.mock("@github/copilot-sdk", () => ({
RuntimeConnection: { forUri: mocks.forUri, forStdio: mocks.forStdio },
}));

import { AgentError, PromptBuilder, agent, configureAgent, copilotEngine, defineTool, p, s, toJsonSchema } from "rig";
import { AgentError, PromptBuilder, agent, analyzeResponse, configureAgent, copilotEngine, defineTool, p, s, toJsonSchema } from "rig";
import { oncePerAgent, repair, steering, timeout } from "rig/addons";

beforeEach(() => {
Expand Down Expand Up @@ -989,13 +989,15 @@ describe("toJsonSchema", () => {
it("converts primitive schemas", () => {
expect(toJsonSchema(s.string)).toEqual({ type: "string" });
expect(toJsonSchema(s.number)).toEqual({ type: "number" });
expect(toJsonSchema(s.integer)).toEqual({ type: "integer" });
expect(toJsonSchema(s.boolean)).toEqual({ type: "boolean" });
expect(toJsonSchema(s.unknown)).toEqual({});
});

it("includes description when present", () => {
expect(toJsonSchema(s.string("A text value"))).toEqual({ type: "string", description: "A text value" });
expect(toJsonSchema(s.number("A numeric value"))).toEqual({ type: "number", description: "A numeric value" });
expect(toJsonSchema(s.integer("A count"))).toEqual({ type: "integer", description: "A count" });
});

it("converts enum schemas", () => {
Expand Down Expand Up @@ -1063,3 +1065,24 @@ describe("toJsonSchema", () => {
expect(s.toJsonSchema(s.string)).toEqual({ type: "string" });
});
});

describe("s.integer validation", () => {
it("accepts whole numbers", () => {
const intAgent = agent({ output: s.integer });
expect(intAgent.outputSchema).toEqual(s.integer);
expect(toJsonSchema(s.integer)).toEqual({ type: "integer" });
});

it("rejects floats in validation", () => {
const result = analyzeResponse(JSON.stringify(1.5), s.integer, "test", 1);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toContain("expected integer");
}
});

it("accepts integers in validation", () => {
const result = analyzeResponse(JSON.stringify(3), s.integer, "test", 1);
expect(result.ok).toBe(true);
});
});
Loading