Skip to content

Commit 588a362

Browse files
committed
feat: define agent adapter contract
1 parent cfae401 commit 588a362

3 files changed

Lines changed: 145 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ The `graph-execution` preset treats `TaskGraph` as the concrete execute-phase ru
152152

153153
Phase presets serialize as a versioned `schemaVersion: 1` catalog. Legacy arrays migrate explicitly; unsupported versions, malformed catalogs, and gate evaluation with missing top-level required evidence refs or artifact ids fail closed.
154154

155+
Agent execution stays adapter-neutral. The `AgentAdapterContract` describes required launch/send/capture/health/readiness/report/substrate behavior for Pi, Codex, and Claude Code compatible workers without coupling the domain layer to any one CLI. Readiness requires a nonce-equivalent proof, worker reports require `taskId`, `status`, `evidenceRefs`, and `summary`, and health can be supported or best-effort but not absent.
156+
155157
The domain boundary is explicit: `Decomposer` creates the concrete graph, `Scheduler` computes ready tasks, `WorkerRuntime` dispatches and heartbeats work, `Verifier` validates evidence refs, and `GateEngine` decides transitions.
156158

157159
## Workflow Map

src/domain/agent-adapter.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
export type AgentCapability = "interactive-shell" | "workspace" | "process" | "heartbeat" | "structured-report";
2+
export type AdapterOperationSupport = "supported" | "best-effort" | "unsupported";
3+
export type ExecutionSubstrateKind = "tmux" | "worktree" | "process";
4+
export type WorkerReportFormat = "json" | "markdown" | "text";
5+
6+
export interface AgentAdapterContract {
7+
id: string;
8+
displayName: string;
9+
capabilities: AgentCapability[];
10+
launch: AdapterOperationSupport;
11+
send: AdapterOperationSupport;
12+
capture: AdapterOperationSupport;
13+
interrupt: AdapterOperationSupport;
14+
health: AdapterOperationSupport;
15+
readiness: {
16+
requiresNonce: boolean;
17+
timeoutSeconds: number;
18+
};
19+
report: {
20+
format: WorkerReportFormat;
21+
requiredFields: string[];
22+
};
23+
substrates: ExecutionSubstrateKind[];
24+
compatibilityNotes: string[];
25+
}
26+
27+
export interface AgentAdapterValidationResult {
28+
ok: boolean;
29+
issues: string[];
30+
}
31+
32+
export function validateAgentAdapterContract(contract: AgentAdapterContract): AgentAdapterValidationResult {
33+
const issues: string[] = [];
34+
if (!contract.id.trim()) issues.push("adapter id is required");
35+
if (!contract.displayName.trim()) issues.push("adapter displayName is required");
36+
if (contract.capabilities.length === 0) issues.push("adapter requires at least one capability");
37+
if (contract.substrates.length === 0) issues.push("adapter requires at least one execution substrate");
38+
if (contract.launch === "unsupported" && contract.send === "unsupported") issues.push("adapter must support launch or send");
39+
if (contract.capture === "unsupported" && contract.report.format !== "json") issues.push("adapter without capture must provide JSON reports");
40+
if (contract.health === "unsupported") issues.push("adapter health behavior must be supported or best-effort");
41+
if (contract.readiness.timeoutSeconds <= 0 || !Number.isFinite(contract.readiness.timeoutSeconds)) issues.push("readiness timeout must be positive");
42+
if (!contract.readiness.requiresNonce) issues.push("readiness must require a nonce or equivalent proof");
43+
for (const field of ["taskId", "status", "evidenceRefs", "summary"]) {
44+
if (!contract.report.requiredFields.includes(field)) issues.push(`worker report requires ${field}`);
45+
}
46+
const compatibilityText = [contract.id, contract.displayName, ...contract.compatibilityNotes].join("\n");
47+
if (!/codex/i.test(compatibilityText)) issues.push("Codex compatibility note is required");
48+
if (!/\bpi\b/i.test(compatibilityText)) issues.push("Pi compatibility note is required");
49+
if (!/claude/i.test(compatibilityText)) issues.push("Claude Code compatibility note is required");
50+
return { ok: issues.length === 0, issues };
51+
}
52+
53+
export const DEFAULT_AGENT_ADAPTER_MATRIX: readonly AgentAdapterContract[] = [
54+
{
55+
id: "codex",
56+
displayName: "Codex CLI",
57+
capabilities: ["interactive-shell", "workspace", "process", "heartbeat", "structured-report"],
58+
launch: "supported",
59+
send: "supported",
60+
capture: "supported",
61+
interrupt: "best-effort",
62+
health: "best-effort",
63+
readiness: { requiresNonce: true, timeoutSeconds: 60 },
64+
report: { format: "json", requiredFields: ["taskId", "status", "evidenceRefs", "summary"] },
65+
substrates: ["tmux", "worktree", "process"],
66+
compatibilityNotes: ["Codex can run through tmux/process substrate with workspace isolation.", "Pi compatibility uses the same structured report fields.", "Claude Code compatibility uses the same readiness nonce contract."],
67+
},
68+
{
69+
id: "pi",
70+
displayName: "Pi",
71+
capabilities: ["interactive-shell", "workspace", "heartbeat", "structured-report"],
72+
launch: "supported",
73+
send: "supported",
74+
capture: "supported",
75+
interrupt: "best-effort",
76+
health: "supported",
77+
readiness: { requiresNonce: true, timeoutSeconds: 60 },
78+
report: { format: "json", requiredFields: ["taskId", "status", "evidenceRefs", "summary"] },
79+
substrates: ["tmux", "worktree", "process"],
80+
compatibilityNotes: ["Pi is the native interactive shell substrate.", "Codex-compatible runs use the same launch/send/capture model.", "Claude Code can share the process/worktree substrate."],
81+
},
82+
{
83+
id: "claude-code",
84+
displayName: "Claude Code",
85+
capabilities: ["interactive-shell", "workspace", "process", "heartbeat", "structured-report"],
86+
launch: "supported",
87+
send: "supported",
88+
capture: "supported",
89+
interrupt: "best-effort",
90+
health: "best-effort",
91+
readiness: { requiresNonce: true, timeoutSeconds: 60 },
92+
report: { format: "json", requiredFields: ["taskId", "status", "evidenceRefs", "summary"] },
93+
substrates: ["tmux", "worktree", "process"],
94+
compatibilityNotes: ["Claude Code can run as a process-backed worker.", "Pi compatibility uses the shared substrate contract.", "Codex compatibility uses the same structured report fields."],
95+
},
96+
];

test/agent-adapter.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
import { DEFAULT_AGENT_ADAPTER_MATRIX, validateAgentAdapterContract } from "../src/domain/agent-adapter.js";
4+
5+
test("default agent adapter matrix captures Codex, Pi, and Claude Code substrate contracts", () => {
6+
assert.deepEqual(
7+
DEFAULT_AGENT_ADAPTER_MATRIX.map((adapter) => adapter.id),
8+
["codex", "pi", "claude-code"],
9+
);
10+
11+
for (const adapter of DEFAULT_AGENT_ADAPTER_MATRIX) {
12+
assert.deepEqual(validateAgentAdapterContract(adapter), { ok: true, issues: [] });
13+
assert.equal(adapter.readiness.requiresNonce, true);
14+
assert.ok(adapter.report.requiredFields.includes("taskId"));
15+
assert.ok(adapter.report.requiredFields.includes("status"));
16+
assert.ok(adapter.report.requiredFields.includes("evidenceRefs"));
17+
assert.ok(adapter.report.requiredFields.includes("summary"));
18+
}
19+
});
20+
21+
test("agent adapter validation fails closed for missing readiness, health, report, and compatibility guarantees", () => {
22+
const result = validateAgentAdapterContract({
23+
id: "bad",
24+
displayName: "Bad",
25+
capabilities: [],
26+
launch: "unsupported",
27+
send: "unsupported",
28+
capture: "unsupported",
29+
interrupt: "unsupported",
30+
health: "unsupported",
31+
readiness: { requiresNonce: false, timeoutSeconds: 0 },
32+
report: { format: "text", requiredFields: [] },
33+
substrates: [],
34+
compatibilityNotes: [],
35+
});
36+
37+
assert.equal(result.ok, false);
38+
assert.ok(result.issues.includes("adapter requires at least one capability"));
39+
assert.ok(result.issues.includes("adapter requires at least one execution substrate"));
40+
assert.ok(result.issues.includes("adapter must support launch or send"));
41+
assert.ok(result.issues.includes("adapter health behavior must be supported or best-effort"));
42+
assert.ok(result.issues.includes("readiness must require a nonce or equivalent proof"));
43+
assert.ok(result.issues.includes("worker report requires evidenceRefs"));
44+
assert.ok(result.issues.includes("Codex compatibility note is required"));
45+
assert.ok(result.issues.includes("Pi compatibility note is required"));
46+
assert.ok(result.issues.includes("Claude Code compatibility note is required"));
47+
});

0 commit comments

Comments
 (0)