|
| 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 | +]; |
0 commit comments