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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ 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

- 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`
Expand Down
18 changes: 8 additions & 10 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions src/agent/directors/types.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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<string, unknown>;
/** Shape of submit_result's payload, validated with arktype (see subagent/submit-result.ts). */
readonly outputType?: OutputType;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/subagent/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<string> {
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;
Expand Down
29 changes: 10 additions & 19 deletions src/subagent/submit-result.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand All @@ -49,7 +40,7 @@ describe("evaluateSubmitResult", () => {
turnToken: TOKEN,
submittedToken: TOKEN,
result: { verdict: "pass" },
schema,
outputType,
state,
});
expect(second.ok).toBe(true);
Expand All @@ -71,21 +62,21 @@ 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,
});
}
const capped = evaluateSubmitResult({
turnToken: TOKEN,
submittedToken: TOKEN,
result: { x: 1 },
schema,
outputType,
state,
});
expect(capped.ok).toBe(false);
Expand Down
20 changes: 9 additions & 11 deletions src/subagent/submit-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;

export const SUBMIT_RESULT_MAX_CORRECTIONS = 3;

Expand All @@ -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;
}
Expand All @@ -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"),
};
Expand Down
6 changes: 3 additions & 3 deletions src/subagent/task-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions src/subagent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, unknown>;
/** DirectorPackage.reportContract.outputType, when the resolved leaf declares one. */
reportType?: OutputType;
} & SubAgentSandboxDeps;
Loading