From 59d2b354639791637613320599ae447928c07078 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:35:03 +0000 Subject: [PATCH] feat(s): add s.integer schema helper Adds IntegerSchema type and s.integer helper following the same SchemaHelperFactory pattern as s.number. Serializes to {"type":"integer"} in JSON Schema and validates with Number.isInteger(), rejecting floats at the validation layer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- skills/rig/rig.ts | 12 +++++++++--- src/rig.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index e53764e..b1cf7c0 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -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 type TS inference from schema descriptor to runtime type * T:AgentInputValue type input accepting raw values or PromptIntent/PromptBuilder at any nesting level * T:Simplify type flattens intersection types for display @@ -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 @@ -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 = { type: "array"; items: Item; description?: string }; @@ -84,6 +85,7 @@ export type OptionalSchema = Inner & OptionalMark export type Schema = | StringSchema | NumberSchema + | IntegerSchema | BooleanSchema | UnknownSchema | ArraySchema @@ -124,7 +126,7 @@ function isOptionalSchema(schema: Schema): schema is OptionalSchema { return OPTIONAL_SYMBOL in schema; } -function createTypedPrimitiveSchema(type: T["type"]): SchemaHelperFactory { +function createTypedPrimitiveSchema(type: T["type"]): SchemaHelperFactory { const base = markAsSchema({ type } as T); const factory = Object.assign( markAsSchema(((description?: string) => (description === undefined ? base : markAsSchema({ type, description } as T))) as SchemaHelperFactory), @@ -173,6 +175,7 @@ export type InferSchema = T extends OptionalMarker ? InferSchema> | 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[] : @@ -188,6 +191,8 @@ export const s = { string: createTypedPrimitiveSchema("string"), /** Schema for a `number` value. Call as `s.number` or `s.number("description")`. */ number: createTypedPrimitiveSchema("number"), + /** Schema for an integer value. Serializes to `{"type":"integer"}` in JSON Schema. Call as `s.integer` or `s.integer("description")`. */ + integer: createTypedPrimitiveSchema("integer"), /** Schema for a `boolean` value. Call as `s.boolean` or `s.boolean("description")`. */ boolean: createTypedPrimitiveSchema("boolean"), /** Schema for an unconstrained JSON value. Serializes to an empty schema object. */ @@ -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(); diff --git a/src/rig.test.ts b/src/rig.test.ts index 14813ef..4030a8b 100644 --- a/src/rig.test.ts +++ b/src/rig.test.ts @@ -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(() => { @@ -989,6 +989,7 @@ 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({}); }); @@ -996,6 +997,7 @@ describe("toJsonSchema", () => { 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", () => { @@ -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); + }); +});