diff --git a/pi-subagents/package-lock.json b/pi-subagents/package-lock.json index 495524a..1a6e383 100644 --- a/pi-subagents/package-lock.json +++ b/pi-subagents/package-lock.json @@ -9,13 +9,13 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@earendil-works/pi-schema": "1.5.0", "yaml": "2.9.1" }, "devDependencies": { "@earendil-works/pi-ai": "1.3.0", "@earendil-works/pi-coding-agent": "1.3.0", "@types/node": "26.6.5", - "typebox": "1.3.37", "typescript": "7.0.2" }, "engines": { @@ -24,8 +24,7 @@ "peerDependencies": { "@earendil-works/pi-ai": "^1.0.0", "@earendil-works/pi-coding-agent": "^1.0.0", - "@earendil-works/pi-tui": "^1.0.0", - "typebox": "~1.3.27" + "@earendil-works/pi-tui": "^1.0.0" }, "peerDependenciesMeta": { "@earendil-works/pi-ai": { @@ -36,9 +35,6 @@ }, "@earendil-works/pi-tui": { "optional": true - }, - "typebox": { - "optional": true } } }, @@ -702,6 +698,15 @@ "node": ">=22.19.0" } }, + "node_modules/@earendil-works/pi-schema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-schema/-/pi-schema-1.5.0.tgz", + "integrity": "sha512-RW0MbT2tR65OY6xMsgB/yVBb7m/RTNwA+9hurSN1vVXfHZRVmVnXbruBPrx/PzbrQnW5yLVlMRWxErGDQtF1Hg==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/@earendil-works/pi-telemetry": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-1.3.0.tgz", @@ -1923,13 +1928,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/typebox": { - "version": "1.3.37", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.37.tgz", - "integrity": "sha512-k+Qogwk7lztlItGvFf5v4GZPPStFAFehAyOKYeuqIHzi3dfTFqPOTx38JG09hmpcK6TQgctrTD06U/0CTTpM1g==", - "dev": true, - "license": "MIT" - }, "node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", diff --git a/pi-subagents/package.json b/pi-subagents/package.json index ebe8f19..2fb6437 100644 --- a/pi-subagents/package.json +++ b/pi-subagents/package.json @@ -53,8 +53,7 @@ "peerDependencies": { "@earendil-works/pi-ai": "^1.0.0", "@earendil-works/pi-coding-agent": "^1.0.0", - "@earendil-works/pi-tui": "^1.0.0", - "typebox": "~1.3.27" + "@earendil-works/pi-tui": "^1.0.0" }, "peerDependenciesMeta": { "@earendil-works/pi-ai": { @@ -65,12 +64,10 @@ }, "@earendil-works/pi-tui": { "optional": true - }, - "typebox": { - "optional": true } }, "dependencies": { + "@earendil-works/pi-schema": "1.5.0", "yaml": "2.9.1" }, "overrides": { @@ -80,7 +77,6 @@ "@earendil-works/pi-ai": "1.3.0", "@earendil-works/pi-coding-agent": "1.3.0", "@types/node": "26.6.5", - "typescript": "7.0.2", - "typebox": "1.3.37" + "typescript": "7.0.2" } } diff --git a/pi-subagents/src/diff.ts b/pi-subagents/src/diff.ts index bdef18d..9f80c72 100644 --- a/pi-subagents/src/diff.ts +++ b/pi-subagents/src/diff.ts @@ -1,13 +1,14 @@ import path from "node:path"; import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; -import { Type } from "typebox"; +import { bool, obj, opt, str, type Static } from "@earendil-works/pi-schema"; import { git } from "./workspace.ts"; const MAX_CHARS = 24000; -const Params = Type.Object({ - path: Type.Optional(Type.String({ minLength: 1, maxLength: 2048 })), - stat: Type.Optional(Type.Boolean()), -}, { additionalProperties: false }); +const Params = obj({ + path: opt(str({ minLength: 1, maxLength: 2048 })), + stat: opt(bool()), +}); +type DiffArguments = Static; export function diffPath(value?: string): string[] { if (value === undefined) return []; @@ -32,7 +33,7 @@ export function diffTool(root: string, head?: string): ToolDefinition; const BOUNDARY = `You are a sub-agent, not the agent. Perform only your task. You cannot delegate or resume other agents. Use contact_agent for missing decisions; do not improvise around missing required safety policy or tools. Your tools run with the host's OS permissions, not in a sandbox. Stay in your assigned workspace and scope. @@ -43,7 +44,7 @@ export function contactAgentTool(context: RunContext): ToolDefinition(values: T): Schema { + return { type: "string", enum: [...values] }; +} + +const ThinkingSchema = stringEnum(THINKING_LEVELS); +const TaskSchema = obj({ + agent: str({ minLength: 1, maxLength: 64 }), + task: str({ minLength: 1, maxLength: 32768 }), + cwd: opt(str({ minLength: 1, maxLength: 4096 })), + model: opt(str({ minLength: 3, maxLength: 512 })), + thinking: opt(ThinkingSchema), +}); +/** Object-root tool input with eight closed per-action branches; no cross-action field mixes. */ +export const Parameters = { + ...union([ + obj({ action: lit("run"), tasks: arr(TaskSchema, { minItems: 1, maxItems: 4 }) }), + obj({ action: lit("list"), cwd: opt(str({ minLength: 1, maxLength: 4096, description: "Profile discovery directory for list" })) }), + obj({ action: lit("status"), id: opt(str({ minLength: 1, maxLength: 64 })) }), + obj({ action: lit("stop"), id: str({ minLength: 1, maxLength: 64 }) }), + obj({ action: lit("resume"), id: str({ minLength: 1, maxLength: 64 }) }), + obj({ action: lit("steer"), id: str({ minLength: 1, maxLength: 64 }), message: str({ minLength: 1, maxLength: 8192 }) }), + obj({ action: lit("followUp"), id: str({ minLength: 1, maxLength: 64 }), message: str({ minLength: 1, maxLength: 8192 }) }), + obj({ action: lit("reply"), id: str({ minLength: 1, maxLength: 64 }), requestId: str({ minLength: 1, maxLength: 64 }), message: str({ minLength: 1, maxLength: 8192 }) }), + ]), + type: "object", +}; export type Request = | { action: "run"; tasks: Task[] } @@ -163,46 +171,45 @@ export function resultPreview(result: unknown): string { return serialize({ ...summary, truncated: true, message: "Fields omitted. Full result is in details/structuredContent; for runs, use status by exact ID and read metadata before acting." }); } -const closed = { additionalProperties: false }; -const ModeSchema = Type.String({ enum: ["inspect", "edit"] }); -const StateSchema = Type.String({ enum: ["starting", "running", "waiting_for_agent", "cancelling", "completed", "failed", "cancelled", "cleanup_unknown"] }); -const ResumeSchema = Type.Object({ - eligible: Type.Boolean(), attempts: Type.Number(), maxAttempts: Type.Number(), - kind: Type.Optional(Type.String({ enum: ["transient_provider", "provider", "cancelled", "unknown"] })), -}, closed); -const RunSchema = Type.Object({ - id: Type.String(), state: StateSchema, - agent: Type.Optional(Type.String()), taskPreview: Type.Optional(Type.String()), - model: Type.Optional(Type.String()), thinking: Type.Optional(ThinkingSchema), - currentTool: Type.Optional(Type.String()), - contextUsage: Type.Optional(Type.Object({ - tokens: Type.Union([Type.Number(), Type.Null()]), - contextWindow: Type.Union([Type.Number(), Type.Null()]), - percent: Type.Union([Type.Number(), Type.Null()]), - }, closed)), - question: Type.Optional(Type.Object({ id: Type.String(), message: Type.String() }, closed)), - reportPath: Type.Optional(Type.String()), metadataPath: Type.Optional(Type.String()), - error: Type.Optional(Type.String()), notificationError: Type.Optional(Type.String()), - resume: Type.Optional(ResumeSchema), - truncated: Type.Optional(Type.Literal(true)), -}, closed); +const ModeSchema = stringEnum(["inspect", "edit"]); +const StateSchema = stringEnum(["starting", "running", "waiting_for_agent", "cancelling", "completed", "failed", "cancelled", "cleanup_unknown"]); +const ResumeSchema = obj({ + eligible: bool(), attempts: num(), maxAttempts: num(), + kind: opt(stringEnum(["transient_provider", "provider", "cancelled", "unknown"])), +}); +const RunSchema = obj({ + id: str(), state: StateSchema, + agent: opt(str()), taskPreview: opt(str()), + model: opt(str()), thinking: opt(ThinkingSchema), + currentTool: opt(str()), + contextUsage: opt(obj({ + tokens: union([num(), nul()]), + contextWindow: union([num(), nul()]), + percent: union([num(), nul()]), + })), + question: opt(obj({ id: str(), message: str() })), + reportPath: opt(str()), metadataPath: opt(str()), + error: opt(str()), notificationError: opt(str()), + resume: opt(ResumeSchema), + truncated: opt(lit(true)), +}); /** The same compact contract reaches the model and native/codemode callers. */ -export const OutputSchema = Type.Union([ - Type.Object({ - runs: Type.Array(Type.Union([ - Type.Object({ id: Type.String(), cwd: Type.String(), workspace: Type.String(), model: Type.String(), thinking: ThinkingSchema }, closed), - Type.Object({ id: Type.String(), agent: Type.String(), state: StateSchema, taskPreview: Type.String(), - resume: Type.Optional(Type.Object({ eligible: Type.Literal(true), attempts: Type.Number(), maxAttempts: Type.Number() }, closed)), - }, closed), +export const OutputSchema = union([ + obj({ + runs: arr(union([ + obj({ id: str(), cwd: str(), workspace: str(), model: str(), thinking: ThinkingSchema }), + obj({ id: str(), agent: str(), state: StateSchema, taskPreview: str(), + resume: opt(obj({ eligible: lit(true), attempts: num(), maxAttempts: num() })), + }), ])), - blocked: Type.Optional(Type.Union([Type.Literal(true), Type.Object({ id: Type.String(), metadataPath: Type.String() }, closed)])), - }, closed), - Type.Array(Type.Object({ - name: Type.String(), description: Type.String(), mode: ModeSchema, - model: Type.Optional(Type.String()), thinking: Type.Optional(Type.String()), - }, closed)), + blocked: opt(union([lit(true), obj({ id: str(), metadataPath: str() })])), + }), + arr(obj({ + name: str(), description: str(), mode: ModeSchema, + model: opt(str()), thinking: opt(str()), + })), RunSchema, - Type.Object({ ok: Type.Literal(true) }, closed), + obj({ ok: lit(true) }), ]); export const DESCRIPTION = `Delegate only when requested by the operator or applicable instructions. diff --git a/pi-subagents/test/contract.test.ts b/pi-subagents/test/contract.test.ts index 117eb7d..c8a3c13 100644 --- a/pi-subagents/test/contract.test.ts +++ b/pi-subagents/test/contract.test.ts @@ -2,7 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; -import { Check } from "typebox/value"; +import { compile } from "@earendil-works/pi-schema"; import { loadProfiles, parseProfile } from "../src/agents.ts"; import { DEFAULT_CONFIG, parseConfig } from "../src/config.ts"; import { parseCommand, resolveRun } from "../src/inspect.ts"; @@ -12,6 +12,8 @@ import type { RunRecord } from "../src/types.ts"; import { formatElapsed, nextLive, plain, rows } from "../src/ui.ts"; import { temp } from "./helpers.ts"; +const checkOutput = compile(OutputSchema); + test("requests reject invalid action/field combinations", () => { assert.deepEqual(parseRequest({ action: "run", tasks: [{ agent: "worker", task: "Implement" }] }), { action: "run", tasks: [{ agent: "worker", task: "Implement" }] }); assert.deepEqual(parseRequest({ action: "run", tasks: [{ agent: "worker", task: "Implement", thinking: "low" }] }), { action: "run", tasks: [{ agent: "worker", task: "Implement", thinking: "low" }] }); @@ -42,11 +44,11 @@ test("exact-ID status presents cached context and current tool inside the closed const view = presentRun(base); assert.equal(view.currentTool, "bash"); assert.deepEqual(view.contextUsage, { tokens: 10, contextWindow: 100, percent: 10 }); - assert(Check(OutputSchema, view)); + assert(checkOutput.check(view)); const unknown = presentRun({ ...base, currentTool: undefined, contextUsage: { tokens: null, percent: null, contextWindow: 64000 } }); assert(!("currentTool" in unknown)); assert.deepEqual(unknown.contextUsage, { tokens: null, percent: null, contextWindow: 64000 }, "unknown stays unknown, never zero"); - assert(Check(OutputSchema, unknown)); + assert(checkOutput.check(unknown)); }); test("config is bounded and rejects unknown or unsafe limits", () => { diff --git a/pi-subagents/test/extension-native.test.ts b/pi-subagents/test/extension-native.test.ts index 8ca4a9f..71e9422 100644 --- a/pi-subagents/test/extension-native.test.ts +++ b/pi-subagents/test/extension-native.test.ts @@ -4,12 +4,14 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai"; import { ModelRegistry, ModelRuntime, SessionManager, type ExtensionAPI, type ExtensionToolContext, type ToolDefinition } from "@earendil-works/pi-coding-agent"; -import { Check } from "typebox/value"; +import { compile } from "@earendil-works/pi-schema"; import register from "../index.ts"; import { OutputSchema } from "../src/tool.ts"; import type { RunRecord } from "../src/types.ts"; import { temp, until } from "./helpers.ts"; +const checkOutput = compile(OutputSchema); + test("public tool: native launch/question/reply/completion stay compact while exact details retain evidence", async t => { const root = await temp(t), agentDir = path.join(root, "agent"), cwd = path.join(root, "repo"); await mkdir(agentDir); await mkdir(cwd); @@ -63,14 +65,14 @@ test("public tool: native launch/question/reply/completion stay compact while ex } as unknown as ExtensionToolContext; const execute = (input: unknown) => tool.execute("public-call", input, undefined, undefined, ctx); const launch = await execute({ action: "run", tasks: [{ agent: "reviewer", task: "PRIVATE_TASK_BRIEF" }] }); - assert(Check(OutputSchema, launch.structuredContent)); + assert(checkOutput.check(launch.structuredContent)); const { runs: [receipt] } = launch.structuredContent as { runs: { id: string; cwd: string; workspace: string; model: string; thinking: string }[] }; assert.deepEqual(Object.keys(receipt!).sort(), ["cwd", "id", "model", "thinking", "workspace"]); assert.equal(receipt!.model, "fixture/test"); assert.equal(receipt!.thinking, "off"); await until(() => notices.some(message => message.startsWith("Sub-agent question"))); const waiting = await execute({ action: "status", id: receipt!.id }); - assert(Check(OutputSchema, waiting.structuredContent)); + assert(checkOutput.check(waiting.structuredContent)); assert(snapshots.some(snapshot => snapshot.sessionId === ctx.sessionManager.getSessionId() && snapshot.tasks.some(task => task.id === receipt!.id && task.state === "waiting"))); const countBeforeRequest = snapshots.length; bus.get("pi:background-tasks:request")?.({ version: 1, sessionId: "foreign" }); @@ -92,7 +94,7 @@ test("public tool: native launch/question/reply/completion stay compact while ex assert(!JSON.stringify(final.structuredContent).includes("PRIVATE_TASK_BRIEF")); const finished = notices.find(message => message.startsWith("Sub-agent finished"))!; const payload = JSON.parse(finished.slice(finished.indexOf("{"))); - assert(Check(OutputSchema, payload)); + assert(checkOutput.check(payload)); assert.deepEqual(Object.keys(payload).sort(), ["agent", "id", "reportPath", "state", "taskPreview"]); assert.equal((payload as { agent: string }).agent, "reviewer"); assert.equal(calls, 2); @@ -156,12 +158,12 @@ test("public tool: a transient native failure resumes through the tool on the sa assert.equal(failedRecord.state, "failed"); assert.equal(failedRecord.resume?.eligible, true); assert.equal(failedRecord.resume?.kind, "transient_provider"); - assert(Check(OutputSchema, failed.structuredContent)); + assert(checkOutput.check(failed.structuredContent)); const failedView = failed.structuredContent as { resume: { eligible: boolean; attempts: number; maxAttempts: number; kind: string } }; assert.deepEqual(failedView.resume, { eligible: true, attempts: 0, maxAttempts: 3, kind: "transient_provider" }); await assert.rejects(execute({ action: "resume", id: "foreign-run" }), /Unknown run ID/); const resumed = await execute({ action: "resume", id: receipt!.id }); - assert(Check(OutputSchema, resumed.structuredContent)); + assert(checkOutput.check(resumed.structuredContent)); await until(() => notices.some(message => message.includes('"state":"completed"'))); const final = await execute({ action: "status", id: receipt!.id }); const record = final.details as RunRecord; diff --git a/pi-subagents/test/extension.test.ts b/pi-subagents/test/extension.test.ts index cc3a572..6d89bc9 100644 --- a/pi-subagents/test/extension.test.ts +++ b/pi-subagents/test/extension.test.ts @@ -4,11 +4,13 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { SessionManager, type ExtensionAPI, type ExtensionCommandContext, type ExtensionToolContext, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import register from "../index.ts"; -import { Check } from "typebox/value"; +import { compile } from "@earendil-works/pi-schema"; import { DESCRIPTION, OutputSchema, Parameters, presentFinished, presentLaunch, presentNotice, presentRun, presentSummary, resultPreview } from "../src/tool.ts"; import type { RunRecord } from "../src/types.ts"; import { temp } from "./helpers.ts"; +const checkOutput = compile(OutputSchema); + test("extension registers one tool/command; rejects unknown/headless/untrusted/foreign actions; unloads cleanly", async t => { const root = await temp(t); const before = process.env.PI_CODING_AGENT_DIR; @@ -46,7 +48,7 @@ test("extension registers one tool/command; rejects unknown/headless/untrusted/f const result = await run({ action: "list" }); assert.deepEqual((result.details as { name: string }[]).map(profile => profile.name), ["reviewer", "worker"]); assert.deepEqual(result.structuredContent, result.details); - assert(Check(OutputSchema, result.structuredContent)); + assert(checkOutput.check(result.structuredContent)); const target = path.join(root, "target"); await mkdir(path.join(target, ".pi", "agents"), { recursive: true }); await writeFile(path.join(target, ".pi", "agents", "reviewer.md"), "---\nname: reviewer\ndescription: Target-specific role\nmode: edit\n---\nTarget role\n"); @@ -74,7 +76,7 @@ test("extension registers one tool/command; rejects unknown/headless/untrusted/f register(api); // New extension instance, not restored execution authority. const reloaded = await tools[1]!.execute("test-reload", { action: "status" }, undefined, undefined, ctx); assert.deepEqual(reloaded.structuredContent, { runs: [], blocked: cause }); - assert(Check(OutputSchema, reloaded.structuredContent)); + assert(checkOutput.check(reloaded.structuredContent)); await assert.rejects(tools[1]!.execute("test-block", { action: "run", tasks: [{ agent: "worker", task: "Must not start" }] }, undefined, undefined, ctx), /old-run\/run.json/); await hooks.get("session_shutdown")!(); } finally { @@ -102,11 +104,11 @@ test("action views are allowlisted and match the closed output schema", () => { resume: { eligible: true, attempts: 0, maxAttempts: 3 } }); assert.deepEqual(presentSummary({ ...failed, resume: { ...failed.resume, eligible: false } }), { id: "id", agent: "worker", state: "failed", taskPreview: "Add feature A" }); for (const result of [receipt, presentRun(enriched), finished, { runs: [presentSummary(record)] }, { runs: [presentSummary(failed)] }, { ok: true }]) { - assert(Check(OutputSchema, result)); + assert(checkOutput.check(result)); assert(!resultPreview(result).includes("futureInternalField")); assert(resultPreview(result).length < 300); } - assert(!Check(OutputSchema, { ...finished, owner: "p" })); + assert(!checkOutput.check({ ...finished, owner: "p" })); assert(DESCRIPTION.length < 1200); assert(JSON.stringify(OutputSchema).length < 3000); }); diff --git a/pi-subagents/test/package.test.ts b/pi-subagents/test/package.test.ts index b2c4a57..f6e0de6 100644 --- a/pi-subagents/test/package.test.ts +++ b/pi-subagents/test/package.test.ts @@ -23,18 +23,21 @@ test("packed package is public, contains no retired runtime, and loads with host assert.equal(manifest.private, undefined); assert.equal(manifest.name, "@gagansd/pi-subagents"); assert.deepEqual(Object.keys(manifest.exports), ["."]); - assert.deepEqual(Object.keys(manifest.dependencies), ["yaml"]); + assert.deepEqual(Object.keys(manifest.dependencies).sort(), ["@earendil-works/pi-schema", "yaml"]); assert.equal(manifest.bin, undefined); for (const peer of ["pi-ai", "pi-coding-agent", "pi-tui"]) { assert.equal(manifest.peerDependencies[`@earendil-works/${peer}`], "^1.0.0", "Pi minor/patch upgrades must remain installable"); assert.deepEqual(manifest.peerDependenciesMeta[`@earendil-works/${peer}`], { optional: true }); } + assert.equal(manifest.peerDependencies["@earendil-works/pi-schema"], undefined, "pi-schema ships as a runtime dependency"); + assert.deepEqual(manifest.peerDependenciesMeta["@earendil-works/pi-schema"], undefined); const lock = JSON.parse(await readFile(path.join(repo, "package-lock.json"), "utf8")); assert.deepEqual(lock.packages[""].peerDependencies, manifest.peerDependencies); // Local dependencies are supplied explicitly, just as required for a local Pi // package. No SDK is placed beside it: Pi must resolve host peers itself. - await mkdir(path.join(packaged, "node_modules")); + await mkdir(path.join(packaged, "node_modules", "@earendil-works"), { recursive: true }); await symlink(path.join(repo, "node_modules", "yaml"), path.join(packaged, "node_modules", "yaml")); + await symlink(path.join(repo, "node_modules", "@earendil-works", "pi-schema"), path.join(packaged, "node_modules", "@earendil-works", "pi-schema")); const agentDir = path.join(root, "agent"), cwd = path.join(root, "workspace"); await mkdir(agentDir); await mkdir(cwd); const loader = new DefaultResourceLoader({ diff --git a/pi-subagents/test/schema.test.ts b/pi-subagents/test/schema.test.ts index a4cfe70..6f5e828 100644 --- a/pi-subagents/test/schema.test.ts +++ b/pi-subagents/test/schema.test.ts @@ -1,11 +1,14 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { Check } from "typebox/value"; +import { compile } from "@earendil-works/pi-schema"; import { normalizeContext, type Model } from "@earendil-works/pi-ai"; import { stream } from "@earendil-works/pi-ai/api/openai-completions"; import { DESCRIPTION, OutputSchema, Parameters, parseRequest } from "../src/tool.ts"; import { THINKING_LEVELS } from "../src/types.ts"; +const checkParameters = compile(Parameters); +const checkOutput = compile(OutputSchema); + const task = { agent: "worker", task: "Implement the approved change" }; const valid = [ { action: "run", tasks: [task] }, @@ -66,27 +69,27 @@ test("input parameters retain an object root and eight closed action branches; o assert(schema.anyOf.every((branch: { type: string; additionalProperties: boolean }) => branch.type === "object" && branch.additionalProperties === false)); assert.equal(schema.anyOf[0].properties.tasks.items.additionalProperties, false); assert.equal(JSON.parse(JSON.stringify(OutputSchema)).type, undefined); - assert(Check(OutputSchema, []), "profile-list output must still allow arrays"); - assert(Check(OutputSchema, { id: "r", state: "waiting_for_agent" })); + assert(checkOutput.check([]), "profile-list output must still allow arrays"); + assert(checkOutput.check({ id: "r", state: "waiting_for_agent" })); }); test("schema and runtime guards agree on all actions, optional fields, bounds and closed field combinations", () => { for (const input of valid) { - assert(Check(Parameters, input), JSON.stringify(input)); + assert(checkParameters.check(input), JSON.stringify(input)); assert.deepEqual(parseRequest(input), input); } const boundary = { action: "run", tasks: [{ agent: "a".repeat(64), task: "x".repeat(32768), cwd: "x".repeat(4096), model: "p/" + "x".repeat(510) }] }; - assert(Check(Parameters, boundary)); + assert(checkParameters.check(boundary)); assert.deepEqual(parseRequest(boundary), boundary); for (const input of invalid) { - assert(!Check(Parameters, input), "schema accepted invalid input"); + assert(!checkParameters.check(input), "schema accepted invalid input"); assert.throws(() => parseRequest(input)); } // Existing runtime text/model policy is deliberately stricter than JSON string lengths. for (const field of ["agent", "task", "cwd", "model"]) { for (const value of [" ", "a\0b"]) { const input = { action: "run", tasks: [{ ...task, [field]: value }] }; - assert(Check(Parameters, input)); + assert(checkParameters.check(input)); assert.throws(() => parseRequest(input)); } }