import type { EffortLevel } from "./effort.ts"; export type HarnessEventKind = | "session.start" | "session.end" | "prompt.submit" | "tool.before" | "tool.after" | "tool.failure" | "shell.before" | "shell.after" | "mcp.before" | "mcp.after" | "read.before" | "edit.after" | "subagent.start" | "subagent.stop" | "stop" | "compact.before" | "response.after" | "thought.after"; export const HARNESS_EVENT_KINDS: readonly HarnessEventKind[] = [ "session.start", "session.end", "prompt.submit", "tool.before", "tool.after", "tool.failure", "shell.before", "shell.after", "mcp.before", "mcp.after", "read.before", "edit.after", "subagent.start", "subagent.stop", "stop", "compact.before", "response.after", "thought.after", ]; export type HarnessUsage = { inputTokens?: number; outputTokens?: number; cachedInputTokens?: number; }; export type HarnessEvent = { provider: string; event: HarnessEventKind; sessionKey: string; projectDir: string; /** * The host's own report of the actual working directory for this event — the worktree root after * the agent enters one, or the directory after a `cd` ([/decisions/ad-114.md](/decisions/ad-114.md)). * Absent when the host does not report a per-event working directory. Distinct from `projectDir`, * which anchors state/config and deliberately does not move into a worktree. */ cwd?: string; // hazard: the `spawn*` pair describes the child of a spawn; the unprefixed fields describe the // running agent. Conflating them clobbers sticky parent state. model?: string; spawnModel?: string; effort?: EffortLevel; text?: string; toolName?: string; toolInput?: Record; command?: string; filePath?: string; subagentType?: string; spawnSubagentType?: string; /** What the host calls a spawn — its name, which the spawning agent chooses. Never a type to match against. */ spawnAgentLabel?: string; status?: "completed" | "aborted" | "error"; loopCount?: number; /** Provider's own permission posture, when it exposes one. Absent means unknown, not permissive. */ permissionMode?: string; contextUsagePercent?: number; transcriptPath?: string; usage?: HarnessUsage; /** * What the tool returned, when the host delivers it. * * why: measured across 69,034 real records before this existed. Every host that delivers it uses its own field * name and its own type — three names and two types across the two adapters — so the translation belongs to * each adapter and this is the one shape core reads ([/decisions/ad-077.md](/decisions/ad-077.md)). * * invariant: absent is absent. Two of the after-events measured carry nothing on 21,167 records, so a reader * that assumed presence would be blind on the majority of one host's traffic. */ toolOutput?: string; /** * The new text an Edit/Write-shaped tool call is about to introduce, when this project has confirmed the * shape: full file content for a whole-file write, or the replacement fragment for a targeted edit. Absent * for every other tool_name — the signal a downstream check uses to abstain rather than guess a field name * (same per-adapter-translation pattern as `toolOutput` above, [/decisions/ad-077.md](/decisions/ad-077.md)). */ proposedContent?: string; /** Targeted-edit shape only: the text being replaced, needed to locate the edit's real position in the on-disk file. */ proposedOldContent?: string; /** Adapter-only escape hatch — core must not read this. */ raw: Record; };