From 86d85b4ac46581c9926cf13ca29cbe4cd4ba4600 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 22:56:13 -0700 Subject: [PATCH 1/2] Mark 0.2.108 in the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f9f9b0..072c6af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ matching `## [X.Y.Z]` section (plus install instructions). Do not maintain parallel copies under `docs/` or `scripts/notes/`. At cut time: rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, then run the release script. -## [Unreleased] +## [0.2.108] - 2026-08-24 ### Agent From 463b05b2ac42cbefbf7b5510f12d70f093f50ca4 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 23:09:29 -0700 Subject: [PATCH 2/2] Validate submit_result with arktype instead of adding ajv The repo already validates with arktype; ajv was added only because arktype cannot consume a JSON Schema document (it exports toJsonSchema but has no fromJsonSchema). Declaring the contract as an arktype type instead removes the need for a second validator, gives compile-time types for free, and produces better correction text for the worker. No director package had declared an outputSchema yet, so no adopted contract changes. --- CHANGELOG.md | 4 ++-- bun.lock | 18 ++++++++---------- package.json | 2 -- src/agent/directors/types.ts | 6 ++++-- src/subagent/run.ts | 2 +- src/subagent/submit-result.test.ts | 29 ++++++++++------------------- src/subagent/submit-result.ts | 20 +++++++++----------- src/subagent/task-tool.ts | 6 +++--- src/subagent/types.ts | 5 +++-- 9 files changed, 40 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 072c6af7..28238555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename ### Agent - Tier 3 leaf workers can now report via `submit_result`, a typed channel alongside - the markdown envelope that validates against a director-declared JSON Schema - and returns a correction (capped at 3 rounds) on an invalid submission. + the markdown envelope that validates against a director-declared shape and + returns a correction (capped at 3 rounds) on an invalid submission. - **`spawn_agent` / `wait_agents` split the fused spawn+wait out of `task()`.** `spawn_agent` starts a worker and returns immediately with `{ agent_id, status: "running" }` — it never awaits the worker's completion. `wait_agents` diff --git a/bun.lock b/bun.lock index 322671bc..dbebe7cc 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,6 @@ "@opentui/core": "0.5.1", "@opentui/keymap": "0.5.1", "@opentui/solid": "0.5.1", - "ajv": "catalog:", "arktype": "catalog:", "highlight.js": "^11.11.1", "solid-js": "1.9.14", @@ -99,7 +98,6 @@ }, "catalog": { "@types/semver": "^7.7.1", - "ajv": "^8.17.1", "arktype": "^2.1.29", "better-auth": "^1.4.18", "drizzle-orm": "^0.45.1", @@ -308,7 +306,7 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -572,7 +570,7 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -816,7 +814,7 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], @@ -824,12 +822,12 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - "glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -844,13 +842,13 @@ "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], diff --git a/package.json b/package.json index 5b5b6377..9bf1bdfb 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ }, "catalog": { "@types/semver": "^7.7.1", - "ajv": "^8.17.1", "arktype": "^2.1.29", "better-auth": "^1.4.18", "drizzle-orm": "^0.45.1", @@ -77,7 +76,6 @@ "@opentui/core": "0.5.1", "@opentui/keymap": "0.5.1", "@opentui/solid": "0.5.1", - "ajv": "catalog:", "arktype": "catalog:", "highlight.js": "^11.11.1", "solid-js": "1.9.14" diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index f237dc05..69e59eff 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -1,6 +1,8 @@ // Closed director package contract for the v1 fleet (CL-5818). // Prompt-first: system prompt is the opinionated core; skills are optional. +import type { OutputType } from "../../subagent/submit-result.js"; + export const DIRECTOR_IDS = [ "skywalker", "build", @@ -68,8 +70,8 @@ export interface NudgePolicy { * Omit entirely to keep a director on the markdown-only path. */ export interface ReportContract { - /** JSON Schema for submit_result's payload, validated with ajv (see subagent/submit-result.ts). */ - readonly outputSchema?: Record; + /** Shape of submit_result's payload, validated with arktype (see subagent/submit-result.ts). */ + readonly outputType?: OutputType; } /** diff --git a/src/subagent/run.ts b/src/subagent/run.ts index 3f11bb9b..8ad655c7 100644 --- a/src/subagent/run.ts +++ b/src/subagent/run.ts @@ -466,7 +466,7 @@ export async function runSubAgent(params: RunSubAgentParams): Promise { turnToken: turnToken!, submittedToken: rawArgs.turn_token, result: rawArgs.result, - ...(params.reportSchema !== undefined ? { schema: params.reportSchema } : {}), + ...(params.reportType !== undefined ? { outputType: params.reportType } : {}), state: submitResultState, }); return outcome.message; diff --git a/src/subagent/submit-result.test.ts b/src/subagent/submit-result.test.ts index a79ad18d..a63e0d17 100644 --- a/src/subagent/submit-result.test.ts +++ b/src/subagent/submit-result.test.ts @@ -1,24 +1,19 @@ import { describe, expect, test } from "bun:test"; +import { type } from "arktype"; + import { createSubmitResultState, evaluateSubmitResult } from "./submit-result.js"; const TOKEN = "turn-abc123"; describe("evaluateSubmitResult", () => { - test("a valid submission against a declared schema succeeds", () => { + test("a valid submission against a declared output type succeeds", () => { const state = createSubmitResultState(); const outcome = evaluateSubmitResult({ turnToken: TOKEN, submittedToken: TOKEN, result: { verdict: "pass", score: 5 }, - schema: { - type: "object", - required: ["verdict", "score"], - properties: { - verdict: { type: "string", enum: ["pass", "fail"] }, - score: { type: "number", minimum: 0, maximum: 10 }, - }, - }, + outputType: type({ verdict: "'pass'|'fail'", score: "0<=number<=10" }), state, }); expect(outcome.ok).toBe(true); @@ -28,17 +23,13 @@ describe("evaluateSubmitResult", () => { test("an invalid submission returns a correction and a resubmit then succeeds", () => { const state = createSubmitResultState(); - const schema = { - type: "object" as const, - required: ["verdict"], - properties: { verdict: { type: "string", enum: ["pass", "fail"] } }, - }; + const outputType = type({ verdict: "'pass'|'fail'" }); const first = evaluateSubmitResult({ turnToken: TOKEN, submittedToken: TOKEN, result: { verdict: "maybe" }, - schema, + outputType, state, }); expect(first.ok).toBe(false); @@ -49,7 +40,7 @@ describe("evaluateSubmitResult", () => { turnToken: TOKEN, submittedToken: TOKEN, result: { verdict: "pass" }, - schema, + outputType, state, }); expect(second.ok).toBe(true); @@ -71,13 +62,13 @@ describe("evaluateSubmitResult", () => { test("correction cap refuses further attempts once reached", () => { const state = createSubmitResultState(); - const schema = { type: "object" as const, required: ["x"] }; + const outputType = type({ x: "number" }); for (let i = 0; i < 3; i++) { evaluateSubmitResult({ turnToken: TOKEN, submittedToken: TOKEN, result: {}, - schema, + outputType, state, }); } @@ -85,7 +76,7 @@ describe("evaluateSubmitResult", () => { turnToken: TOKEN, submittedToken: TOKEN, result: { x: 1 }, - schema, + outputType, state, }); expect(capped.ok).toBe(false); diff --git a/src/subagent/submit-result.ts b/src/subagent/submit-result.ts index a75e11e5..94251d41 100644 --- a/src/subagent/submit-result.ts +++ b/src/subagent/submit-result.ts @@ -4,11 +4,10 @@ * owns the per-turn `SubmitResultState` (one instance per runSubAgent call). */ -import Ajv, { type Schema } from "ajv"; +import { ArkErrors, type Type } from "arktype"; -const ajv = new Ajv({ allErrors: true, strict: false }); - -export type JsonSchema = Schema; +/** A director's declared shape for submit_result's payload. */ +export type OutputType = Type; export const SUBMIT_RESULT_MAX_CORRECTIONS = 3; @@ -27,8 +26,8 @@ export interface SubmitResultInput { submittedToken: unknown; /** The result argument the worker passed. */ result: unknown; - /** Declared output schema, if the director's report contract has one. */ - schema?: JsonSchema; + /** Declared output shape, if the director's report contract has one. */ + outputType?: OutputType; state: SubmitResultState; maxCorrections?: number; } @@ -52,16 +51,15 @@ export function evaluateSubmitResult(input: SubmitResultInput): { message: `Error: submit_result correction cap (${cap}) reached for this turn. No further attempts accepted — finish with the markdown report envelope instead.`, }; } - if (input.schema !== undefined) { - const validate = ajv.compile(input.schema); - const valid = validate(input.result); - if (!valid) { + if (input.outputType !== undefined) { + const checked = input.outputType(input.result); + if (checked instanceof ArkErrors) { input.state.corrections += 1; return { ok: false, message: [ `Invalid submission (${input.state.corrections}/${cap} corrections used):`, - ajv.errorsText(validate.errors, { separator: "\n", dataVar: "result" }), + checked.summary, "Fix and call submit_result again with the same turn_token.", ].join("\n"), }; diff --git a/src/subagent/task-tool.ts b/src/subagent/task-tool.ts index 18e7dd83..fa95ee4c 100644 --- a/src/subagent/task-tool.ts +++ b/src/subagent/task-tool.ts @@ -835,10 +835,10 @@ export function createTaskTool(deps: TaskToolDeps): AgentTool { maxTurns: resolvedMaxTurns, ...(deps.deadlineMs !== undefined ? { deadlineMs: deps.deadlineMs } : {}), // submit_result mount gate (CL-6946): only a resolved Tier 3 leaf - // director gets tier here, and only if it declared an outputSchema. + // director gets tier here, and only if it declared an outputType. ...(resolvedPackage !== undefined ? { tier: resolvedPackage.tier } : {}), - ...(resolvedPackage?.reportContract?.outputSchema !== undefined - ? { reportSchema: resolvedPackage.reportContract.outputSchema } + ...(resolvedPackage?.reportContract?.outputType !== undefined + ? { reportType: resolvedPackage.reportContract.outputType } : {}), }; const result = await run(params); diff --git a/src/subagent/types.ts b/src/subagent/types.ts index 3f54c414..77d4c303 100644 --- a/src/subagent/types.ts +++ b/src/subagent/types.ts @@ -10,6 +10,7 @@ import type { ToolPlugin } from "@intx/tools-posix"; import type { CapabilityFilter, AgentProfile } from "../agent/profiles.js"; import type { ProviderCatalogEntry } from "../config/index.js"; +import type { OutputType } from "./submit-result.js"; import type { Settings } from "../config/settings.js"; import type { ShellTimeoutConfig } from "../plugins/shell-guard-plugin.js"; import type { PermissionGate } from "../permission/gate.js"; @@ -144,6 +145,6 @@ export type RunSubAgentParams = { * gates it, not a new mechanism. */ tier?: SubagentTier; - /** DirectorPackage.reportContract.outputSchema, when the resolved leaf declares one. */ - reportSchema?: Record; + /** DirectorPackage.reportContract.outputType, when the resolved leaf declares one. */ + reportType?: OutputType; } & SubAgentSandboxDeps;