Skip to content

Commit 463b05b

Browse files
committed
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.
1 parent 86d85b4 commit 463b05b

9 files changed

Lines changed: 40 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1616
### Agent
1717

1818
- Tier 3 leaf workers can now report via `submit_result`, a typed channel alongside
19-
the markdown envelope that validates against a director-declared JSON Schema
20-
and returns a correction (capped at 3 rounds) on an invalid submission.
19+
the markdown envelope that validates against a director-declared shape and
20+
returns a correction (capped at 3 rounds) on an invalid submission.
2121
- **`spawn_agent` / `wait_agents` split the fused spawn+wait out of `task()`.**
2222
`spawn_agent` starts a worker and returns immediately with `{ agent_id,
2323
status: "running" }` — it never awaits the worker's completion. `wait_agents`

bun.lock

Lines changed: 8 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
},
5555
"catalog": {
5656
"@types/semver": "^7.7.1",
57-
"ajv": "^8.17.1",
5857
"arktype": "^2.1.29",
5958
"better-auth": "^1.4.18",
6059
"drizzle-orm": "^0.45.1",
@@ -77,7 +76,6 @@
7776
"@opentui/core": "0.5.1",
7877
"@opentui/keymap": "0.5.1",
7978
"@opentui/solid": "0.5.1",
80-
"ajv": "catalog:",
8179
"arktype": "catalog:",
8280
"highlight.js": "^11.11.1",
8381
"solid-js": "1.9.14"

src/agent/directors/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Closed director package contract for the v1 fleet (CL-5818).
22
// Prompt-first: system prompt is the opinionated core; skills are optional.
33

4+
import type { OutputType } from "../../subagent/submit-result.js";
5+
46
export const DIRECTOR_IDS = [
57
"skywalker",
68
"build",
@@ -68,8 +70,8 @@ export interface NudgePolicy {
6870
* Omit entirely to keep a director on the markdown-only path.
6971
*/
7072
export interface ReportContract {
71-
/** JSON Schema for submit_result's payload, validated with ajv (see subagent/submit-result.ts). */
72-
readonly outputSchema?: Record<string, unknown>;
73+
/** Shape of submit_result's payload, validated with arktype (see subagent/submit-result.ts). */
74+
readonly outputType?: OutputType;
7375
}
7476

7577
/**

src/subagent/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<string> {
466466
turnToken: turnToken!,
467467
submittedToken: rawArgs.turn_token,
468468
result: rawArgs.result,
469-
...(params.reportSchema !== undefined ? { schema: params.reportSchema } : {}),
469+
...(params.reportType !== undefined ? { outputType: params.reportType } : {}),
470470
state: submitResultState,
471471
});
472472
return outcome.message;

src/subagent/submit-result.test.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
import { describe, expect, test } from "bun:test";
22

3+
import { type } from "arktype";
4+
35
import { createSubmitResultState, evaluateSubmitResult } from "./submit-result.js";
46

57
const TOKEN = "turn-abc123";
68

79
describe("evaluateSubmitResult", () => {
8-
test("a valid submission against a declared schema succeeds", () => {
10+
test("a valid submission against a declared output type succeeds", () => {
911
const state = createSubmitResultState();
1012
const outcome = evaluateSubmitResult({
1113
turnToken: TOKEN,
1214
submittedToken: TOKEN,
1315
result: { verdict: "pass", score: 5 },
14-
schema: {
15-
type: "object",
16-
required: ["verdict", "score"],
17-
properties: {
18-
verdict: { type: "string", enum: ["pass", "fail"] },
19-
score: { type: "number", minimum: 0, maximum: 10 },
20-
},
21-
},
16+
outputType: type({ verdict: "'pass'|'fail'", score: "0<=number<=10" }),
2217
state,
2318
});
2419
expect(outcome.ok).toBe(true);
@@ -28,17 +23,13 @@ describe("evaluateSubmitResult", () => {
2823

2924
test("an invalid submission returns a correction and a resubmit then succeeds", () => {
3025
const state = createSubmitResultState();
31-
const schema = {
32-
type: "object" as const,
33-
required: ["verdict"],
34-
properties: { verdict: { type: "string", enum: ["pass", "fail"] } },
35-
};
26+
const outputType = type({ verdict: "'pass'|'fail'" });
3627

3728
const first = evaluateSubmitResult({
3829
turnToken: TOKEN,
3930
submittedToken: TOKEN,
4031
result: { verdict: "maybe" },
41-
schema,
32+
outputType,
4233
state,
4334
});
4435
expect(first.ok).toBe(false);
@@ -49,7 +40,7 @@ describe("evaluateSubmitResult", () => {
4940
turnToken: TOKEN,
5041
submittedToken: TOKEN,
5142
result: { verdict: "pass" },
52-
schema,
43+
outputType,
5344
state,
5445
});
5546
expect(second.ok).toBe(true);
@@ -71,21 +62,21 @@ describe("evaluateSubmitResult", () => {
7162

7263
test("correction cap refuses further attempts once reached", () => {
7364
const state = createSubmitResultState();
74-
const schema = { type: "object" as const, required: ["x"] };
65+
const outputType = type({ x: "number" });
7566
for (let i = 0; i < 3; i++) {
7667
evaluateSubmitResult({
7768
turnToken: TOKEN,
7869
submittedToken: TOKEN,
7970
result: {},
80-
schema,
71+
outputType,
8172
state,
8273
});
8374
}
8475
const capped = evaluateSubmitResult({
8576
turnToken: TOKEN,
8677
submittedToken: TOKEN,
8778
result: { x: 1 },
88-
schema,
79+
outputType,
8980
state,
9081
});
9182
expect(capped.ok).toBe(false);

src/subagent/submit-result.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
* owns the per-turn `SubmitResultState` (one instance per runSubAgent call).
55
*/
66

7-
import Ajv, { type Schema } from "ajv";
7+
import { ArkErrors, type Type } from "arktype";
88

9-
const ajv = new Ajv({ allErrors: true, strict: false });
10-
11-
export type JsonSchema = Schema;
9+
/** A director's declared shape for submit_result's payload. */
10+
export type OutputType = Type<unknown>;
1211

1312
export const SUBMIT_RESULT_MAX_CORRECTIONS = 3;
1413

@@ -27,8 +26,8 @@ export interface SubmitResultInput {
2726
submittedToken: unknown;
2827
/** The result argument the worker passed. */
2928
result: unknown;
30-
/** Declared output schema, if the director's report contract has one. */
31-
schema?: JsonSchema;
29+
/** Declared output shape, if the director's report contract has one. */
30+
outputType?: OutputType;
3231
state: SubmitResultState;
3332
maxCorrections?: number;
3433
}
@@ -52,16 +51,15 @@ export function evaluateSubmitResult(input: SubmitResultInput): {
5251
message: `Error: submit_result correction cap (${cap}) reached for this turn. No further attempts accepted — finish with the markdown report envelope instead.`,
5352
};
5453
}
55-
if (input.schema !== undefined) {
56-
const validate = ajv.compile(input.schema);
57-
const valid = validate(input.result);
58-
if (!valid) {
54+
if (input.outputType !== undefined) {
55+
const checked = input.outputType(input.result);
56+
if (checked instanceof ArkErrors) {
5957
input.state.corrections += 1;
6058
return {
6159
ok: false,
6260
message: [
6361
`Invalid submission (${input.state.corrections}/${cap} corrections used):`,
64-
ajv.errorsText(validate.errors, { separator: "\n", dataVar: "result" }),
62+
checked.summary,
6563
"Fix and call submit_result again with the same turn_token.",
6664
].join("\n"),
6765
};

src/subagent/task-tool.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -835,10 +835,10 @@ export function createTaskTool(deps: TaskToolDeps): AgentTool {
835835
maxTurns: resolvedMaxTurns,
836836
...(deps.deadlineMs !== undefined ? { deadlineMs: deps.deadlineMs } : {}),
837837
// submit_result mount gate (CL-6946): only a resolved Tier 3 leaf
838-
// director gets tier here, and only if it declared an outputSchema.
838+
// director gets tier here, and only if it declared an outputType.
839839
...(resolvedPackage !== undefined ? { tier: resolvedPackage.tier } : {}),
840-
...(resolvedPackage?.reportContract?.outputSchema !== undefined
841-
? { reportSchema: resolvedPackage.reportContract.outputSchema }
840+
...(resolvedPackage?.reportContract?.outputType !== undefined
841+
? { reportType: resolvedPackage.reportContract.outputType }
842842
: {}),
843843
};
844844
const result = await run(params);

src/subagent/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ToolPlugin } from "@intx/tools-posix";
1010

1111
import type { CapabilityFilter, AgentProfile } from "../agent/profiles.js";
1212
import type { ProviderCatalogEntry } from "../config/index.js";
13+
import type { OutputType } from "./submit-result.js";
1314
import type { Settings } from "../config/settings.js";
1415
import type { ShellTimeoutConfig } from "../plugins/shell-guard-plugin.js";
1516
import type { PermissionGate } from "../permission/gate.js";
@@ -144,6 +145,6 @@ export type RunSubAgentParams = {
144145
* gates it, not a new mechanism.
145146
*/
146147
tier?: SubagentTier;
147-
/** DirectorPackage.reportContract.outputSchema, when the resolved leaf declares one. */
148-
reportSchema?: Record<string, unknown>;
148+
/** DirectorPackage.reportContract.outputType, when the resolved leaf declares one. */
149+
reportType?: OutputType;
149150
} & SubAgentSandboxDeps;

0 commit comments

Comments
 (0)