diff --git a/desktop/electron/resources/agent_families.generated.json b/desktop/electron/resources/agent_families.generated.json index 907b5229..9fd8b758 100644 --- a/desktop/electron/resources/agent_families.generated.json +++ b/desktop/electron/resources/agent_families.generated.json @@ -89,9 +89,11 @@ "payload": { "description": "$.description", "last_tool_name": "$.last_tool_name", + "subagent_type": "$.subagent_type", "subtype": "$.subtype", "task_id": "$.task_id", - "text": "$.description" + "text": "$.description", + "tool_use_id": "$.tool_use_id" } } }, @@ -140,6 +142,9 @@ "producer": "agent", "payload": { "message_id": "$$.message.id", + "parent_tool_use_id": "$$.parent_tool_use_id", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type", "text": "$.text" } } @@ -154,7 +159,10 @@ "payload": { "id": "$.id", "input": "$.input", - "name": "$.name" + "name": "$.name", + "parent_tool_use_id": "$$.parent_tool_use_id", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type" } } }, @@ -168,7 +176,10 @@ "payload": { "marker_only": "absent($.thinking)", "message_id": "$$.message.id", + "parent_tool_use_id": "$$.parent_tool_use_id", "signature_present": "present($.signature)", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type", "text": "nonempty($.thinking) || \"Thinking…\"" } } @@ -190,7 +201,10 @@ "message_id": "$.message.id", "model": "$.message.model", "output_tokens": "$.message.usage.output_tokens", - "service_tier": "$.message.usage.service_tier" + "parent_tool_use_id": "$.parent_tool_use_id", + "service_tier": "$.message.usage.service_tier", + "subagent": "present($.parent_tool_use_id)", + "subagent_type": "$.subagent_type" } } }, @@ -213,6 +227,9 @@ "payload": { "content": "$.content", "is_error": "$.is_error", + "parent_tool_use_id": "$$.parent_tool_use_id", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type", "tool_use_id": "$.tool_use_id" } } diff --git a/desktop/src/i18n/index.ts b/desktop/src/i18n/index.ts index a203064d..1f7be35c 100644 --- a/desktop/src/i18n/index.ts +++ b/desktop/src/i18n/index.ts @@ -451,6 +451,10 @@ const en: Dict = { 'tx.dock.todos': 'Todos ({done}/{total})', 'tx.dock.close': 'Close panel', 'tx.dock.noTodos': 'No todos in the latest plan.', + // R5 — one sub-agent's own activity. + 'tx.dock.back': 'Back to the list', + 'tx.dock.subQuiet': 'This sub-agent has not done anything yet.', + 'tx.dock.subUnreported': 'This engine does not report which events are a sub-agent’s, so its work cannot be separated from the main agent’s here. The request above is what it was asked to do.', 'tx.now': 'now', 'composer.attach': 'Attach file or image', 'composer.slashMenu': 'Slash commands', @@ -2894,6 +2898,10 @@ const zh: Dict = { 'tx.dock.todos': '待办 ({done}/{total})', 'tx.dock.close': '关闭面板', 'tx.dock.noTodos': '最新的计划中没有待办。', + // R5 — 单个子代理的活动。 + 'tx.dock.back': '返回列表', + 'tx.dock.subQuiet': '该子代理尚未有任何动作。', + 'tx.dock.subUnreported': '该引擎不会标明哪些事件来自子代理,因此这里无法把它的工作与主代理分开。上方显示的是它被要求完成的任务。', 'tx.now': '刚刚', 'composer.attach': '附加文件或图片', 'composer.slashMenu': '斜杠命令', diff --git a/desktop/src/styles/partials/05-transcript-boards.css b/desktop/src/styles/partials/05-transcript-boards.css index da5530e7..61648972 100644 --- a/desktop/src/styles/partials/05-transcript-boards.css +++ b/desktop/src/styles/partials/05-transcript-boards.css @@ -703,6 +703,79 @@ padding: var(--spacing-s4) 0; font-size: var(--font-size-body-small); } + +/* R5 — a sub-agent row opens that agent's own activity. The button carries + the row's layout so the drill-down affordance doesn't reflow the list; only + the chevron is new. Shell rows keep the plain
  • and are unaffected. */ +.sd-row-open { + display: flex; + align-items: center; + gap: var(--spacing-s6); + flex: 1; + min-width: 0; + padding: 0; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sd-row-open:hover { + background: var(--surface-hover); +} +.sd-row-chev { + margin-left: auto; + opacity: 0.5; +} +.sd-sub { + display: flex; + flex-direction: column; + gap: var(--spacing-s4); + min-width: 0; +} +.sd-sub-type { + display: inline-flex; + align-items: center; + gap: var(--spacing-s4); + color: var(--text-muted); + font-size: var(--font-size-caption); +} +/* The prompt is the one thing every engine can show, so it gets room to be + read — wrapped and selectable, not clamped to a row. */ +.sd-sub-prompt { + max-height: 7em; + overflow-y: auto; + padding: var(--spacing-s4) var(--spacing-s8); + border-left: 2px solid var(--border); + color: var(--text); + font-size: var(--font-size-body-small); + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.sd-sub-now { + font-size: var(--font-size-caption); + font-style: italic; +} +.sd-sub-line { + gap: var(--spacing-s4); +} +.sd-sub-detail { + min-width: 0; + overflow: hidden; + color: var(--text-muted); + text-overflow: ellipsis; + white-space: nowrap; +} +/* Prose the sub-agent produced reads as prose, not as a tool row. */ +.sd-sub-line.k-text .sd-sub-detail { + color: var(--text); +} +.sd-sub-line.k-thought .sd-sub-detail { + font-style: italic; + opacity: 0.8; +} .sd-row-name { flex: 0 0 auto; font-weight: 600; diff --git a/desktop/src/surfaces/AgentTranscript.tsx b/desktop/src/surfaces/AgentTranscript.tsx index e713177a..e3a05cf9 100644 --- a/desktop/src/surfaces/AgentTranscript.tsx +++ b/desktop/src/surfaces/AgentTranscript.tsx @@ -21,7 +21,7 @@ import { agentIsBusy, errorLabel, eventIsError, FEED_LENSES, isHiddenInFeed, mat import { collapseStreamingPartials } from '../ui/streamingPartials'; import { groupToolCalls, toolCallUpdateParentId, type FeedRow } from '../ui/toolGroups'; import { ToolGroupCard } from '../ui/ToolGroupCard'; -import { deriveStateDock } from '../ui/stateDock'; +import { deriveStateDock, subagentDetail, type DockCall } from '../ui/stateDock'; // NB explicit .tsx: the pure module `stateDock.ts` differs from this file // name only in casing, so on macOS's case-insensitive filesystem a bare // '../ui/StateDock' import resolves to stateDock.ts (TS1149). The extension @@ -396,6 +396,13 @@ export function AgentTranscript({ agentId, sessionId }: { agentId: string; sessi () => deriveStateDock(feed, { nameById, resultById, updateById }), [feed, nameById, resultById, updateById], ); + // R5 — one sub-agent's own activity, resolved on demand. A callback rather + // than a precomputed map because only the row the director opened needs the + // scan, and the dock re-derives on every event. + const subagentDetailFor = useCallback( + (call: DockCall) => subagentDetail(feed, call, { nameById, resultById, updateById }), + [feed, nameById, resultById, updateById], + ); // P1 tool-group collapse state (kimi-web parity — plan §7 decision 3): // groups are EXPANDED by default and never auto-collapse; the header click // is the only toggle, opt-in per group instance. Keyed by the group's @@ -1314,7 +1321,7 @@ export function AgentTranscript({ agentId, sessionId }: { agentId: string; sessi )} {/* P2 state dock — live mode only, outside the virtual list, directly above the composer. */} - + {/* R2 — the ring sits in the composer's own row, not the header strip, because its question ("is there room for this prompt?") is asked while typing. The strip keeps the same numbers as text diff --git a/desktop/src/ui/StateDock.tsx b/desktop/src/ui/StateDock.tsx index 679aafe8..9bd83080 100644 --- a/desktop/src/ui/StateDock.tsx +++ b/desktop/src/ui/StateDock.tsx @@ -2,7 +2,14 @@ import { useState } from 'react'; import { Icon, type IconName } from './Icon'; import { useT, type TLookup } from '../i18n'; import { statusGlyph } from './ToolGroupCard'; -import { visibleDockChips, type DockCall, type DockKind, type DockTodo, type StateDockModel } from './stateDock'; +import { + visibleDockChips, + type DockCall, + type DockKind, + type DockTodo, + type StateDockModel, + type SubagentDetail, +} from './stateDock'; /// P2 state dock (agent-transcript-redesign §6 P2, kimi-web ChatDock.vue /// parity): ambient session-state chips above the composer — `Tasks (n)` · @@ -49,19 +56,46 @@ function todoGlyph(status: string): { icon: IconName; cls: string } { /// Task/sub-agent rows: kind icon + tool name + key argument + status glyph /// (the ToolGroupCard row idiom, minus the lazy detail). -function CallRows({ kind, calls }: { kind: DockKind; calls: DockCall[] }): JSX.Element { +/// +/// A sub-agent row is a button (R5): it opens that agent's own activity. Shell +/// rows stay plain — a bash call's output is its tool_result, already a card in +/// the transcript, so there is nothing a drill-down would add. +function CallRows({ + kind, + calls, + onOpen, +}: { + kind: DockKind; + calls: DockCall[]; + onOpen?: (call: DockCall) => void; +}): JSX.Element { return (
      {calls.map((c) => { const glyph = statusGlyph(c.status); - return ( -
    • + const body = ( + <> {c.name} {c.arg !== undefined && {truncate(c.arg)}} + + ); + if (onOpen === undefined) { + return ( +
    • + {body} +
    • + ); + } + return ( +
    • +
    • ); })} @@ -69,6 +103,52 @@ function CallRows({ kind, calls }: { kind: DockKind; calls: DockCall[] }): JSX.E ); } +/// One sub-agent's own activity (R5). The header is what the parent asked for +/// — available for every engine, since it comes off the spawning call — and +/// below it, the agent's own events when the engine says which are its. +function SubagentDetailView({ detail }: { detail: SubagentDetail }): JSX.Element { + const t = useT(); + return ( +
      + {detail.subagentType !== undefined && ( +
      + + {detail.subagentType} +
      + )} + {detail.prompt !== undefined &&
      {detail.prompt}
      } + {detail.lastActivity !== undefined && ( +
      {truncate(detail.lastActivity)}
      + )} + {detail.lines.length > 0 ? ( +
        + {detail.lines.map((l) => { + const glyph = l.status !== undefined ? statusGlyph(l.status) : undefined; + return ( +
      • + {l.label !== '' && {l.label}} + {l.detail !== undefined && {truncate(l.detail, 140)}} + {glyph !== undefined && ( + + )} +
      • + ); + })} +
      + ) : ( + // Two different facts, two different sentences: the engine never says + // which events are a sub-agent's, versus it does and this one has not + // acted yet. +
      + {detail.reported ? t('tx.dock.subQuiet') : t('tx.dock.subUnreported')} +
      + )} +
      + ); +} + /// Todo rows (kimi-web TodoCard): shared glyph + content; completed reads /// strikethrough + faint, in-progress medium weight. function TodoRows({ items }: { items: DockTodo[] }): JSX.Element { @@ -95,11 +175,23 @@ function TodoRows({ items }: { items: DockTodo[] }): JSX.Element { ); } -export function StateDock({ model }: { model: StateDockModel }): JSX.Element | null { +export function StateDock({ + model, + subagentDetailFor, +}: { + model: StateDockModel; + /// R5 — resolve one sub-agent's activity on demand. Omitted (the assistant + /// dock, which has no session feed to scan) keeps the flat list. + subagentDetailFor?: (call: DockCall) => SubagentDetail; +}): JSX.Element | null { const t = useT(); // One panel at a time; re-tapping the open chip (or the close control) // dismisses it. const [open, setOpen] = useState(null); + // R5 drill-down: the sub-agent row being inspected, by TOOL id. Keyed by id + // rather than holding the row object so a re-derived model (every new event + // rebuilds it) keeps the panel open on the same agent instead of resetting. + const [openSub, setOpenSub] = useState(null); const kinds = visibleDockChips(model); // No visible chips → no dock at all. A stale `open` is harmless: if the // chip reappears, its panel simply reopens (the chip reads as a toggle). @@ -107,32 +199,71 @@ export function StateDock({ model }: { model: StateDockModel }): JSX.Element | n // A panel whose chip lost visibility (e.g. the last running task finished) // closes with its chip. const openKind = open !== null && kinds.includes(open) ? open : null; + // The drilled-into row, re-resolved from the CURRENT model each render so a + // running sub-agent's status stays live while its panel is open. A row that + // ages out of the capped list closes the drill-down rather than freezing a + // stale copy on screen. + const subCall = + openKind === 'subagents' && openSub !== null + ? model.subagentCalls.find((c) => c.toolId === openSub) + : undefined; + const subDetail = subCall !== undefined && subagentDetailFor !== undefined ? subagentDetailFor(subCall) : undefined; return (
      {openKind !== null && (
      - - {chipLabel(openKind, model, t)} + {subCall !== undefined ? ( + <> + + {subCall.arg ?? subCall.name} + + ) : ( + <> + + {chipLabel(openKind, model, t)} + + )}
      - {openKind === 'todos' ? ( + {subDetail !== undefined ? ( + + ) : openKind === 'todos' ? ( model.todos === undefined || model.todos.items.length === 0 ? (
      {t('tx.dock.noTodos')}
      ) : ( ) ) : ( - + setOpenSub(c.toolId ?? null) + : undefined + } + /> )}
      )} diff --git a/desktop/src/ui/stateDock.test.ts b/desktop/src/ui/stateDock.test.ts index 0ddd99ab..872100b7 100644 --- a/desktop/src/ui/stateDock.test.ts +++ b/desktop/src/ui/stateDock.test.ts @@ -14,6 +14,7 @@ import assert from 'node:assert/strict'; import { deriveStateDock, isShellTaskName, + subagentDetail, isSubagentName, MAX_DOCK_CALLS, toolCallKeyArg, @@ -267,3 +268,112 @@ test('toolCallKeyArg: command/prompt-class keys win; first string value is the f assert.equal(toolCallKeyArg(undefined), undefined); assert.equal(toolCallKeyArg('nope'), undefined); }); + +// --- R5: one sub-agent's own activity ----------------------------------------- + +/// Frames shaped like the real thing. The field names come from a MEASURED +/// claude-code 2.1.220 run under `--print --output-format stream-json` (a real +/// Agent call), translated by the frame profile: a sub-agent's own events carry +/// `parent_tool_use_id` + `subagent: true`, and the task_* system frames carry +/// the same id as `tool_use_id`. +function subEvent(kind: string, parentId: string, payload: Record): FeedEvent { + return ev(kind, { ...payload, parent_tool_use_id: parentId, subagent: true, subagent_type: 'general-purpose' }); +} + +function detailFor(events: FeedEvent[], toolId: string) { + const model = derive(events); + const row = model.subagentCalls.find((c) => c.toolId === toolId); + assert.ok(row !== undefined, `no dock row for ${toolId}`); + return subagentDetail(events, row, mapsFor(events)); +} + +test('a dock row carries the TOOL id, not just its event id', () => { + // The join key. `id` is the event row (`c-t1` here); `toolId` is what a + // sub-agent's events name as their parent. Asserting only on `id` would pass + // with the two confused, and the panel would then be empty for every agent. + const events = [call('t1', 'Agent', { input: { description: 'go look' } })]; + const row = derive(events).subagentCalls[0]; + assert.equal(row.toolId, 't1'); + assert.notEqual(row.id, row.toolId); +}); + +test('a sub-agent detail collects only ITS events, not a sibling’s', () => { + // The separating input: two sub-agents running at once. A filter that merely + // asked "is this event a subagent's?" (the `subagent:true` boolean alone) + // would put both agents' work under each row and read as one agent doing + // twice the work. + const events = [ + call('t1', 'Agent', { input: { prompt: 'first job', subagent_type: 'general-purpose' } }), + call('t2', 'Agent', { input: { prompt: 'second job' } }), + subEvent('tool_call', 't1', { id: 'inner-1', name: 'Bash', input: { command: 'ls /one' } }), + subEvent('tool_call', 't2', { id: 'inner-2', name: 'Bash', input: { command: 'ls /two' } }), + subEvent('text', 't1', { text: 'first is done' }), + ]; + const d1 = detailFor(events, 't1'); + assert.equal(d1.prompt, 'first job'); + assert.equal(d1.subagentType, 'general-purpose'); + assert.deepEqual(d1.lines.map((l) => l.detail), ['ls /one', 'first is done']); + + const d2 = detailFor(events, 't2'); + assert.deepEqual(d2.lines.map((l) => l.detail), ['ls /two']); + assert.equal(d2.prompt, 'second job'); +}); + +test('task_progress joins on tool_use_id and the newest one is what it is doing now', () => { + const events = [ + call('t1', 'Agent', { input: { description: 'dig' } }), + ev('system', { subtype: 'task_progress', tool_use_id: 't1', description: 'Running Read' }), + ev('system', { subtype: 'task_progress', tool_use_id: 't1', description: 'Running Grep' }), + ]; + const d = detailFor(events, 't1'); + assert.equal(d.lastActivity, 'Running Grep', 'forward scan — newest wins'); + assert.equal(d.lines.length, 2, 'both progress lines are the activity stream'); +}); + +test('the spawning call is the header, never a line in its own stream', () => { + const events = [call('t1', 'Agent', { input: { prompt: 'p' } }), subEvent('text', 't1', { text: 'x' })]; + const d = detailFor(events, 't1'); + assert.equal(d.lines.length, 1); + assert.equal(d.lines[0].detail, 'x'); +}); + +test('an engine that reports no provenance says so; a quiet sub-agent does not', () => { + // Two empty lists, two different sentences. codex/gemini publish no + // per-sub-agent correlation at all, so the panel must not imply the agent + // has simply been idle. + const silent = [call('t1', 'Agent', { input: { prompt: 'ask codex' } })]; + const d = detailFor(silent, 't1'); + assert.equal(d.lines.length, 0); + assert.equal(d.reported, false, 'no event in the session carries provenance'); + assert.equal(d.prompt, 'ask codex', 'the request is still worth showing'); + + // Same shape, but another sub-agent in the session HAS reported — so the + // engine does publish provenance and this one is merely quiet. + const quiet = [ + call('t1', 'Agent', { input: { prompt: 'not started' } }), + call('t2', 'Agent', { input: { prompt: 'busy' } }), + subEvent('tool_call', 't2', { id: 'i', name: 'Bash', input: { command: 'ls' } }), + ]; + assert.equal(detailFor(quiet, 't1').reported, true); +}); + +test('a tool_result does not double the row its call already shows', () => { + const events = [ + call('t1', 'Agent', { input: { prompt: 'p' } }), + subEvent('tool_call', 't1', { id: 'inner', name: 'Bash', input: { command: 'ls' } }), + ev('tool_result', { tool_use_id: 'inner', parent_tool_use_id: 't1', subagent: true, content: 'ok' }), + ]; + const d = detailFor(events, 't1'); + assert.deepEqual(d.lines.map((l) => l.kind), ['tool_call']); + assert.equal(d.lines[0].status, 'done', 'the result resolves the call’s status instead'); +}); + +test('kimi’s agent-id provenance is accepted as the same join', () => { + const events = [ + call('agent-9', 'task', { input: { prompt: 'kimi job' } }), + ev('text', { text: 'sub report', subagent: true, kimi_agent_id: 'agent-9' }), + ]; + const d = detailFor(events, 'agent-9'); + assert.deepEqual(d.lines.map((l) => l.detail), ['sub report']); + assert.equal(d.reported, true); +}); diff --git a/desktop/src/ui/stateDock.ts b/desktop/src/ui/stateDock.ts index c0ab85e6..19384db0 100644 --- a/desktop/src/ui/stateDock.ts +++ b/desktop/src/ui/stateDock.ts @@ -121,6 +121,11 @@ export interface DockCall { name: string; arg?: string; status: ToolStatus; + /// The TOOL id (`tool_use_id`/`id`), distinct from `id` (the event row). + /// R5's drill-down joins on this: a sub-agent's own events carry it as + /// `parent_tool_use_id`, and claude's `task_*` system frames as + /// `tool_use_id`. Absent when the call's payload carried no tool id. + toolId?: string; } /// One plan entry, normalized: content + the raw status string (ACP: @@ -226,6 +231,7 @@ export function deriveStateDock( name, arg: toolCallKeyArg(p['input']), status: toolStatusOf(ev, maps.resultById, maps.updateById), + toolId: id, }; (shellHit ? shell : subs).push(call); } @@ -238,6 +244,161 @@ export function deriveStateDock( }; } +// --------------------------------------------------------------------------- +// R5 — one sub-agent's own activity. +// +// A delegated agent's work used to land in the transcript looking exactly like +// the main agent's: same rows, same order, no marking. The producer half of R5 +// fixed that at the source — claude's M2 wire carries `parent_tool_use_id` on +// every frame (null for the main agent, the spawning Agent/Task call's id for a +// sub-agent's), MEASURED against claude-code 2.1.220, and the frame profile now +// stamps it plus a cross-engine `subagent` boolean. This is the consumer: +// given the dock row's tool id, everything that agent did. +// +// Correlation is claude's today. kimi's M4 tap marks its sub-agent events with +// `kimi_agent_id` instead, which is a different join (agent id, not spawning +// call) with no dock row to hang it on — supported here as a second accepted +// key so a kimi row lights up the moment one is available to test against, +// never guessed at. codex and gemini report nothing of the kind: for them the +// header below is the whole honest answer, and `reported` says so. +// --------------------------------------------------------------------------- + +/// One line of a sub-agent's activity — compact by design: the dock is an +/// ambient mirror, so a delegated tool call reads as one row here and stays a +/// full card in the transcript. +export interface SubagentLine { + id: string; + kind: string; + /// Tool name, or the system frame's description; '' for prose. + label: string; + /// Key argument / text / result summary. + detail?: string; + status?: ToolStatus; +} + +export interface SubagentDetail { + /// The prompt the parent handed the sub-agent (claude's Agent call carries + /// it as `input.prompt`; `description` is the short form the row shows). + prompt?: string; + /// e.g. claude's `general-purpose`. + subagentType?: string; + /// The newest `task_progress` line — what it is doing right now. + lastActivity?: string; + /// The sub-agent's own events, in feed order. + lines: SubagentLine[]; + /// False when this engine publishes no per-sub-agent correlation at all. + /// Distinct from `lines: []`, which means it reported one and the agent has + /// not done anything yet — "nothing to show" and "cannot be shown" are + /// different sentences and the panel prints different ones. + reported: boolean; +} + +/// Every event whose provenance points at [toolId]. Accepts claude's +/// `parent_tool_use_id` and kimi's `kimi_agent_id`; the `task_*` system frames +/// join on `tool_use_id`, which is the same id under a third name — claude's +/// own wire spells it differently depending on whether the frame is ABOUT the +/// sub-agent (task lifecycle) or FROM it (its own work). +function belongsToSubagent(p: Entity, toolId: string): boolean { + return p['parent_tool_use_id'] === toolId || p['kimi_agent_id'] === toolId || p['tool_use_id'] === toolId; +} + +/// True when any event in the session carries per-sub-agent provenance at all. +/// Engine-agnostic: it asks the DATA, not the engine name, so an engine that +/// starts reporting tomorrow needs no change here. +function anyProvenance(events: FeedEvent[]): boolean { + for (const ev of events) { + const p = ev.payload; + if (typeof p['parent_tool_use_id'] === 'string' && p['parent_tool_use_id'] !== '') return true; + if (typeof p['kimi_agent_id'] === 'string' && p['kimi_agent_id'] !== '') return true; + } + return false; +} + +/// Compact one-line summary of a sub-agent event. +function lineOf(ev: FeedEvent, maps: DockMaps): SubagentLine | undefined { + const p = ev.payload; + switch (ev.kind) { + case 'tool_call': { + const id = callToolId(p); + const name = (typeof p['name'] === 'string' ? p['name'] : undefined) ?? (id !== undefined ? maps.nameById.get(id) : undefined) ?? ''; + return { id: ev.id, kind: ev.kind, label: name, detail: toolCallKeyArg(p['input']), status: toolStatusOf(ev, maps.resultById, maps.updateById) }; + } + case 'text': { + const text = typeof p['text'] === 'string' ? p['text'] : ''; + if (text === '') return undefined; + return { id: ev.id, kind: ev.kind, label: '', detail: text }; + } + case 'thought': + return { id: ev.id, kind: ev.kind, label: '', detail: typeof p['text'] === 'string' ? p['text'] : undefined }; + case 'system': { + // task_progress lines are the sub-agent's live narration; the other + // task_* frames are lifecycle the header already summarizes. + const d = p['description']; + if (p['subtype'] !== 'task_progress' || typeof d !== 'string') return undefined; + return { id: ev.id, kind: ev.kind, label: '', detail: d }; + } + // tool_result is deliberately absent: it folds into its call's status, + // exactly as the transcript's own tool grouping does. Showing both would + // double every row. + default: + return undefined; + } +} + +interface DockMaps { + nameById: Map; + resultById: Map; + updateById: Map; +} + +/// Derive one sub-agent's detail from the FULL session event list. [call] is +/// the dock row that was clicked — its own payload supplies the header, so the +/// panel is useful even for an engine that reports no inner events at all. +export function subagentDetail(events: FeedEvent[], call: DockCall, maps: DockMaps): SubagentDetail { + const lines: SubagentLine[] = []; + let prompt: string | undefined; + let subagentType: string | undefined; + let lastActivity: string | undefined; + + // The spawning call itself carries the prompt + description. + for (const ev of events) { + if (ev.id !== call.id) continue; + const input = ev.payload['input']; + if (input !== null && typeof input === 'object') { + const inp = input as Entity; + if (typeof inp['prompt'] === 'string') prompt = inp['prompt']; + if (typeof inp['subagent_type'] === 'string') subagentType = inp['subagent_type']; + } + break; + } + + if (call.toolId !== undefined && call.toolId !== '') { + for (const ev of events) { + if (ev.id === call.id) continue; // the spawning call is the header, not a line + if (!belongsToSubagent(ev.payload, call.toolId)) continue; + const st = ev.payload['subagent_type']; + if (subagentType === undefined && typeof st === 'string') subagentType = st; + if (ev.kind === 'system' && ev.payload['subtype'] === 'task_progress') { + const d = ev.payload['description']; + if (typeof d === 'string') lastActivity = d; // forward scan — newest wins + } + const line = lineOf(ev, maps); + if (line !== undefined) lines.push(line); + } + } + + return { + prompt, + subagentType, + lastActivity, + lines, + // Reported when THIS sub-agent produced correlated events, or when the + // session shows the engine does publish provenance (so an idle sub-agent + // reads as "nothing yet" rather than "unsupported"). + reported: lines.length > 0 || anyProvenance(events), + }; +} + /// The chips to render, in dock order (plan §6 P2 visibility rules): Tasks /// iff a shell task is RUNNING; Sub-agents iff a sub-agent call is RUNNING; /// Todos iff any plan event exists. diff --git a/docs/changelog-desktop.md b/docs/changelog-desktop.md index 5b71f76c..13369497 100644 --- a/docs/changelog-desktop.md +++ b/docs/changelog-desktop.md @@ -42,6 +42,16 @@ This complements: ## Unreleased ### Added +- **You can now see what a sub-agent actually did.** The Sub-agents chip above + the composer lists the delegated agents; clicking one opens what it was asked + to do and everything it has done — its tool calls, its notes, and what it is + running right now. (vision-parity R5) + + For an engine that does not say which events belong to a sub-agent, the panel + still shows the request and says plainly that the rest cannot be separated — + which is a different sentence from "this sub-agent has not done anything + yet", and the panel picks the right one. + - **The transcript now shows which model is about to answer — and lets you change it where that is actually possible.** Two pills sit beside the context ring, in the composer's own row, because these are questions asked @@ -60,6 +70,15 @@ This complements: and "unknown" are different facts and should not look the same. ### Fixed +- **A sub-agent's work used to be reported as the main agent's.** claude's + stream carries a `parent_tool_use_id` on every frame — null for the main + agent, the delegating call's id for a sub-agent's — and the translator threw + it away. So a delegated tool call appeared in the transcript as if the agent + you were talking to had run it, and the guard that keeps a sub-agent's token + usage out of the session's turn counts (added for kimi) never fired for + claude, quietly inflating them. Both translators now stamp it, along with the + sub-agent's type, and a real recording of a delegated run is pinned in the + parity corpus so the two implementations cannot drift apart on it. - **Three of the four runtime switches could never have worked, and nothing said so.** The registry recorded *how* a mode/model switch travels but not *whether* a given field can travel at all, and a switch that rewrites a @@ -72,6 +91,7 @@ This complements: cannot exist. Families now declare `runtime_switch_fields` per field, the hub checks it before routing, and the UI hides a control it knows would be refused. Only claude's model switch was ever real; it still is. + - **The Companion can now run a *codex* session on this machine too.** Pick `codex` in the local picker and the dock drives it through the vendor's own `app-server` — a JSON-RPC thread that streams as it writes, folds tool calls diff --git a/docs/plans/desktop-companion-vision-parity.md b/docs/plans/desktop-companion-vision-parity.md index 848ef700..4cd03798 100644 --- a/docs/plans/desktop-companion-vision-parity.md +++ b/docs/plans/desktop-companion-vision-parity.md @@ -3,8 +3,10 @@ > **Type:** plan > **Status:** In flight (2026-08-05) — **W1 + W2 + W3 complete** > (F1 F2 L1 E1 R1 · L2 E2 R2 R3 F3 · L3a L3b E3 E4 R4; L3c deferrable). -> **W4 in flight (2026-08-16)**: F4, L4a–L4c and R6 shipped — codex is -> drivable locally and the composer carries model/mode pills. Left: R5. +> **W4 complete (2026-08-17)**: F4, lane L4 (L4a–L4c), R5 and R6 all +> shipped — codex is drivable locally, a sub-agent's work is attributed +> to it rather than to the main agent, and the composer carries +> model/mode pills. Remaining in the plan: L3c only (deferrable). > **Audience:** principal · contributors · maintainers > **Last verified vs code:** 2026.730.1231-alpha (`cea267fa`) — every > anchor below re-verified against that tip by the authoring audit @@ -1015,10 +1017,51 @@ Audit ground truth: claude M2 = `driver_stdio.go`, codex M2 = Neither client had ever resolved an externalized payload leaf; mobile's blob code is all artifact viewers, a different case. - **R5 — subagent panel.** The dock's flat name-match rows - (`stateDock.ts`) gain a detail panel: click a subagent chip → side - panel with that subagent's filtered event stream (events already - carry `subagent` marking — the digest fold skips `subagent:true` - usage). Phase bars are kimi-only data — out (D-5). + (`ui/stateDock.ts` — the pure half; the plan said `state/`) gain a + detail panel: click a subagent row → that subagent's filtered event + stream. Phase bars are kimi-only data — out (D-5). + + ★★ *"Events already carry `subagent` marking" was true of exactly one + engine, and not the one the Companion drives (measured 2026-08-16).* + The only producer of `subagent: true` was the **kimi M4 wire-tail** + (`kimi_code/mapper.go:235`). Built as written, the panel would have + been permanently empty for claude and codex. + + The probe that settled it: a real `claude --print --output-format + stream-json` run making one Agent call (no session log on the dev box + had a sidechain in it, so there was nothing to read). claude-code + 2.1.220 turns out to carry **more** than the plan assumed: + + - **`parent_tool_use_id` on every assistant/user frame** — `null` for + the main agent, the spawning Agent call's `tool_use_id` for a + sub-agent's work. This is the correlation the panel needs. + - `subagent_type` + `task_description` alongside it. + - four `system` subtypes: `task_started` (carries the full prompt), + `task_progress` (`last_tool_name` + running usage), `task_updated`, + `task_notification` (final summary + output file). + - the tool is named **`Agent`**, not `Task` — the dock's existing + name set already matches it. + + **Our translator dropped all of it**, so a sub-agent's tool calls and + prose landed in the transcript *indistinguishable from the main + agent's own work* — and #374's `subagent:true` guard, which exists to + keep a sub-agent's usage out of the session's turn counts, skipped + nothing on claude. R5 therefore ships the producer half first: the + frame profile stamps `parent_tool_use_id` + a derived `subagent` + boolean (the same one kimi already sets, so one vocabulary answers for + both engines and the two existing consumers start working with no + change), the legacy translator mirrors it field-for-field, and the + 15-frame recording is now in the parity corpus — its first sidechain + coverage. The desktop's TS interpreter reads the same regenerated + fixture, so the local Companion gets it too. + + *Honesty note.* codex and gemini publish no per-sub-agent correlation + at all. The panel still opens for them: the header (what the + sub-agent was asked to do) comes off the spawning call, which every + engine has. Below it, "this engine does not report which events are a + sub-agent's" — a different sentence from "this sub-agent has not done + anything yet", which is what an empty stream means when the engine + *does* report. Two facts, two sentences (D-4). - **R6 — composer pills.** Model pill and permission-mode pill wired to the **existing but unused** input kinds `set_model` / `set_mode` (`handlers_agent_input.go:415-503`), sourced from `session.init` diff --git a/hub/internal/agentfamilies/agent_families.yaml b/hub/internal/agentfamilies/agent_families.yaml index 1e21f932..7b606396 100644 --- a/hub/internal/agentfamilies/agent_families.yaml +++ b/hub/internal/agentfamilies/agent_families.yaml @@ -176,6 +176,12 @@ families: # mirrors description so collapsed previews + copy read the # human line. Two-key match beats the generic system rule # below (most-specific wins). + # R5 adds tool_use_id + subagent_type: #374 dropped the frame's + # uuid/session_id/usage NOISE, but took the correlation with it, + # so a progress line could not be tied to the Agent call that + # produced it. tool_use_id is the same id `parent_tool_use_id` + # carries on the sub-agent's own events — it is what joins the + # dock row, the progress line and the stream into one sub-agent. - match: { type: system, subtype: task_progress } emit: kind: system @@ -183,6 +189,8 @@ families: payload: subtype: "$.subtype" task_id: "$.task_id" + tool_use_id: "$.tool_use_id" + subagent_type: "$.subagent_type" description: "$.description" last_tool_name: "$.last_tool_name" text: "$.description" @@ -222,6 +230,33 @@ families: # for_each walks the content blocks; sub_rules dispatch on # the inner block type. Outer scope ($$) gives sub-rules # access to message.id for transcript pairing. + # + # SUB-AGENT PROVENANCE (vision-parity R5). Every assistant and + # user frame carries `parent_tool_use_id`: null for the main + # agent, the spawning Agent/Task call's `tool_use_id` for work + # a sub-agent did. MEASURED against claude-code 2.1.220 under + # `--print --output-format stream-json` (a real Agent call + # reading a file), because no session log on the dev box had a + # sidechain in it and the field appears in no vendor doc we + # hold. + # + # Until R5 the translator dropped it, so a sub-agent's tool + # calls and prose landed in the transcript INDISTINGUISHABLE + # from the main agent's own work — read as the agent doing it + # itself, and counted as such by #374's turn/usage guard, which + # skips `subagent:true` and therefore skipped nothing here. + # + # `subagent` is derived, not passed through: it is the same + # boolean the kimi M4 wire-tail already stamps + # (kimi_code/mapper.go), so both engines answer one vocabulary + # and the two existing consumers (digest_fold.go, + # transcriptStats.ts) start working for claude with no change. + # Deriving it HERE rather than in each consumer is deliberate — + # a predicate re-derived per consumer is the duplication this + # repo has been bitten by. It costs a `subagent: false` on + # main-agent events; `present()` reads the VALUE, and claude + # sends an explicit `null`, so a main frame is false and not + # (invertedly) true. - match: { type: assistant } for_each: $.message.content sub_rules: @@ -230,16 +265,22 @@ families: kind: text producer: agent payload: - text: "$.text" - message_id: "$$.message.id" + text: "$.text" + message_id: "$$.message.id" + parent_tool_use_id: "$$.parent_tool_use_id" + subagent: "present($$.parent_tool_use_id)" + subagent_type: "$$.subagent_type" - match: { type: tool_use } emit: kind: tool_call producer: agent payload: - id: "$.id" - name: "$.name" - input: "$.input" + id: "$.id" + name: "$.name" + input: "$.input" + parent_tool_use_id: "$$.parent_tool_use_id" + subagent: "present($$.parent_tool_use_id)" + subagent_type: "$$.subagent_type" # Extended reasoning. claude-code 2.1.x signs the block and # ships `thinking` empty, so the typed event is usually a # marker; when a build DOES populate it we forward the text @@ -252,28 +293,40 @@ families: kind: thought producer: agent payload: - text: 'nonempty($.thinking) || "Thinking…"' - marker_only: "absent($.thinking)" - signature_present: "present($.signature)" - message_id: "$$.message.id" + text: 'nonempty($.thinking) || "Thinking…"' + marker_only: "absent($.thinking)" + signature_present: "present($.signature)" + message_id: "$$.message.id" + parent_tool_use_id: "$$.parent_tool_use_id" + subagent: "present($$.parent_tool_use_id)" + subagent_type: "$$.subagent_type" # ── assistant.message.usage → usage (when present) ───── # Telemetry strip in mobile; gated by when_present so it # doesn't fire empty rows for assistant frames that omit # the usage block (post-result deltas, for example). + # R5 provenance here is not decoration: usage is the ONE kind + # #374's guard actually reads (digest_fold's token fold and + # transcriptStats' turn accounting both skip subagent:true + # USAGE frames), and a sub-agent's assistant frames carry + # message.usage too (corpus frame 25). No for_each, so the + # frame is the inner scope — `$.`, not the sub_rules' `$$.`. - match: { type: assistant } when_present: $.message.usage emit: kind: usage producer: agent payload: - input_tokens: "$.message.usage.input_tokens" - output_tokens: "$.message.usage.output_tokens" - cache_read: "$.message.usage.cache_read_input_tokens || $.message.usage.cache_read" - cache_create: "$.message.usage.cache_creation_input_tokens || $.message.usage.cache_create" - service_tier: "$.message.usage.service_tier" - message_id: "$.message.id" - model: "$.message.model" + input_tokens: "$.message.usage.input_tokens" + output_tokens: "$.message.usage.output_tokens" + cache_read: "$.message.usage.cache_read_input_tokens || $.message.usage.cache_read" + cache_create: "$.message.usage.cache_creation_input_tokens || $.message.usage.cache_create" + service_tier: "$.message.usage.service_tier" + message_id: "$.message.id" + model: "$.message.model" + parent_tool_use_id: "$.parent_tool_use_id" + subagent: "present($.parent_tool_use_id)" + subagent_type: "$.subagent_type" # ── user.message.content[].type=tool_result → tool_result ─ # In stream-json, `user` frames carry the agent's own view @@ -288,9 +341,12 @@ families: kind: tool_result producer: agent payload: - tool_use_id: "$.tool_use_id" - content: "$.content" - is_error: "$.is_error" + tool_use_id: "$.tool_use_id" + content: "$.content" + is_error: "$.is_error" + parent_tool_use_id: "$$.parent_tool_use_id" + subagent: "present($$.parent_tool_use_id)" + subagent_type: "$$.subagent_type" # ── result → turn.result + completion ────────────────── # turn.result is the canonical typed kind; completion is a diff --git a/hub/internal/hostrunner/driver_stdio.go b/hub/internal/hostrunner/driver_stdio.go index bcefafe3..68586334 100644 --- a/hub/internal/hostrunner/driver_stdio.go +++ b/hub/internal/hostrunner/driver_stdio.go @@ -335,9 +335,15 @@ func (d *StdioDriver) legacyTranslate(ctx context.Context, frame map[string]any) // human line. The frame-profile rule in agent_families.yaml // mirrors this shape — keep them in lockstep (the parity // test diffs both paths over the corpus). + // R5 adds tool_use_id + subagent_type: #374 meant to drop the + // frame's uuid/session_id/usage noise, but took the + // correlation with it, so nothing could tie a progress line + // to the Agent call that produced it. payload := map[string]any{ "subtype": frame["subtype"], "task_id": frame["task_id"], + "tool_use_id": frame["tool_use_id"], + "subagent_type": frame["subagent_type"], "description": frame["description"], "last_tool_name": frame["last_tool_name"], "text": frame["description"], @@ -355,6 +361,26 @@ func (d *StdioDriver) legacyTranslate(ctx context.Context, frame map[string]any) msg, _ := frame["message"].(map[string]any) messageID, _ := msg["id"].(string) blocks, _ := msg["content"].([]any) + // Sub-agent provenance (vision-parity R5). `parent_tool_use_id` is + // null on the main agent's frames and carries the spawning + // Agent/Task call's tool_use_id on a sub-agent's — the only thing + // on the M2 wire that says which agent did the work. Mirrors the + // YAML rule field-for-field; the parity test compares KEY SETS, so + // these are set unconditionally (nil when absent) exactly as + // `payload[k] = Eval(expr)` does on the profile path. + // + // `subagent` is derived here rather than read off the frame so the + // boolean the kimi M4 tap already stamps means one thing across + // engines. profile_eval.IsPresent for the same reason the thinking + // block uses it below: claude sends an explicit `null`, and a + // second local definition of "present" would be a divergence the + // parity test cannot see. + subProv := func(out map[string]any) map[string]any { + out["parent_tool_use_id"] = frame["parent_tool_use_id"] + out["subagent"] = profile_eval.IsPresent(frame["parent_tool_use_id"]) + out["subagent_type"] = frame["subagent_type"] + return out + } for _, b := range blocks { block, _ := b.(map[string]any) bt, _ := block["type"].(string) @@ -368,7 +394,7 @@ func (d *StdioDriver) legacyTranslate(ctx context.Context, frame map[string]any) if messageID != "" { out["message_id"] = messageID } - _ = d.emit(ctx, "text", "agent", out) + _ = d.emit(ctx, "text", "agent", subProv(out)) case "thinking": // Extended reasoning. claude-code 2.1.x signs the block // for API-side verification and ships `thinking` empty, @@ -400,14 +426,14 @@ func (d *StdioDriver) legacyTranslate(ctx context.Context, frame map[string]any) if messageID != "" { out["message_id"] = messageID } - _ = d.emit(ctx, "thought", "agent", out) + _ = d.emit(ctx, "thought", "agent", subProv(out)) case "tool_use": _ = d.emit(ctx, "tool_call", "agent", - map[string]any{ + subProv(map[string]any{ "id": block["id"], "name": block["name"], "input": block["input"], - }) + })) default: _ = d.emit(ctx, "raw", "agent", block) } @@ -426,7 +452,14 @@ func (d *StdioDriver) legacyTranslate(ctx context.Context, frame map[string]any) if model, ok := msg["model"].(string); ok && model != "" { out["model"] = model } - _ = d.emit(ctx, "usage", "agent", out) + // Usage is the event #374's guard actually reads: digest_fold's + // InTokens/OutTokens fold and transcriptStats' turn accounting + // both skip `subagent:true` USAGE frames. A sub-agent's + // assistant frames carry `message.usage` too (corpus frame 25), + // so leaving the stamp off this one emit keeps the delegated + // agent's tokens inside the main turn — the exact inflation R5 + // set out to remove. + _ = d.emit(ctx, "usage", "agent", subProv(out)) } case "user": @@ -441,11 +474,18 @@ func (d *StdioDriver) legacyTranslate(ctx context.Context, frame map[string]any) if bt != "tool_result" { continue } + // Same sub-agent provenance as the assistant branch: a + // sub-agent's tool RESULTS carry parent_tool_use_id too, and + // without it the tool half of a delegated call reads as the + // main agent's. Mirrors the YAML sub_rule. _ = d.emit(ctx, "tool_result", "agent", map[string]any{ - "tool_use_id": block["tool_use_id"], - "content": block["content"], - "is_error": block["is_error"], + "tool_use_id": block["tool_use_id"], + "content": block["content"], + "is_error": block["is_error"], + "parent_tool_use_id": frame["parent_tool_use_id"], + "subagent": profile_eval.IsPresent(frame["parent_tool_use_id"]), + "subagent_type": frame["subagent_type"], }) } diff --git a/hub/internal/hostrunner/profile_subagent_usage_test.go b/hub/internal/hostrunner/profile_subagent_usage_test.go new file mode 100644 index 00000000..502ba10a --- /dev/null +++ b/hub/internal/hostrunner/profile_subagent_usage_test.go @@ -0,0 +1,90 @@ +package hostrunner + +import ( + "testing" + + "github.com/termipod/hub/internal/agentfamilies" +) + +// TestProfile_ClaudeCode_SubagentUsageIsFlagged pins the R5 stamp on the ONE +// kind #374's guard actually reads. digest_fold's token fold and +// transcriptStats' turn accounting skip `subagent:true` USAGE frames — not +// text, not tool_call — and a sub-agent's assistant frames carry +// `message.usage` like every other assistant frame (corpus frame 25 is a real +// recorded one). Stamping the four content kinds but not usage fixes the +// transcript's attribution while leaving the delegated agent's tokens counted +// inside the main turn — the exact inflation R5 set out to remove, surviving +// its own fix. +// +// A hand-built frame rather than the corpus, and semantic assertions rather +// than the pinned fixture, on purpose: the fixture is REGENERATED on drift +// (`-update-frame-fixture`), so a future edit that drops the stamp would +// simply re-pin the bug. This test states the requirement independently, for +// both translators. +func TestProfile_ClaudeCode_SubagentUsageIsFlagged(t *testing.T) { + f, ok := agentfamilies.ByName("claude-code") + if !ok || f.FrameProfile == nil { + t.Fatal("claude-code frame_profile not embedded") + } + + frameWith := func(parent any) map[string]any { + return map[string]any{ + "type": "assistant", + "parent_tool_use_id": parent, + "subagent_type": map[bool]any{true: "general-purpose", false: nil}[parent != nil], + "message": map[string]any{ + "id": "msg_1", + "model": "claude-opus-4-7", + "content": []any{ + map[string]any{"type": "text", "text": "done"}, + }, + "usage": map[string]any{ + "input_tokens": float64(7), + "output_tokens": float64(3), + }, + }, + } + } + + usageOf := func(events []EmittedEvent) map[string]any { + t.Helper() + for _, e := range events { + if e.Kind == "usage" { + return e.Payload + } + } + t.Fatal("no usage event emitted") + return nil + } + + for _, tc := range []struct { + name string + parent any + want bool + }{ + // claude sends an explicit null on main-agent frames, not an absent + // key — the derivation must read the VALUE (IsPresent), or every + // main-agent usage row flips to subagent:true and the guard starves + // the real turn counts instead. + {"sidechain usage is the sub-agent's", "toolu_parent_1", true}, + {"main-agent usage stays the turn's", nil, false}, + } { + t.Run(tc.name, func(t *testing.T) { + frame := frameWith(tc.parent) + for path, events := range map[string][]EmittedEvent{ + "profile": ApplyProfile(frame, f.FrameProfile), + "legacy": runLegacyTranslate(t, frame), + } { + p := usageOf(events) + if got := p["subagent"]; got != tc.want { + t.Errorf("%s: usage subagent = %v; want %v", path, got, tc.want) + } + if tc.want { + if got := p["parent_tool_use_id"]; got != tc.parent { + t.Errorf("%s: usage parent_tool_use_id = %v; want %v", path, got, tc.parent) + } + } + } + }) + } +} diff --git a/hub/internal/hostrunner/testdata/profiles/claude-code/corpus.jsonl b/hub/internal/hostrunner/testdata/profiles/claude-code/corpus.jsonl index eeae82f7..98cd7c5d 100644 --- a/hub/internal/hostrunner/testdata/profiles/claude-code/corpus.jsonl +++ b/hub/internal/hostrunner/testdata/profiles/claude-code/corpus.jsonl @@ -1,17 +1,32 @@ -{"type":"system","subtype":"init","session_id":"sess-abc","model":"claude-opus-4-7","cwd":"/tmp/wt","permissionMode":"acceptEdits","tools":["Read","Write","Bash"],"mcpServers":[{"name":"git"}],"slashCommands":["/help","/compact"],"agents":["@code","@docs"],"skills":[],"plugins":[],"claude_code_version":"1.5.0","outputStyle":"default"} -{"type":"system","subtype":"rate_limit_event","rateLimitType":"5h","status":"allowed","resetsAt":"2026-04-29T08:00:00Z"} -{"type":"system","subtype":"task_started","task_id":"brgb0gz57","agent":"researcher","description":"investigate stale sessions"} -{"type":"system","subtype":"task_updated","task_id":"brgb0gz57","patch":{"is_backgrounded":true}} -{"type":"system","subtype":"task_notification","task_id":"brgb0gz57","message":"agent paused for input"} -{"type":"system","subtype":"task_progress","task_id":"brgb0gz57","tool_use_id":"toolu_9","description":"investigate stale sessions","last_tool_name":"Bash","usage":{"total_tokens":1234,"tool_uses":3,"duration_ms":9000},"uuid":"u-9","session_id":"sess-abc"} -{"type":"rate_limit_event","rateLimitType":"5h","status":"warn","resetsAt":"2026-04-29T13:00:00Z"} -{"type":"rate_limit_event","rate_limit_info":{"rateLimitType":"five_hour","status":"allowed","resetsAt":1777443000,"overageDisabledReason":"org_level_disabled_until","isUsingOverage":false}} -{"type":"assistant","message":{"id":"msg_text_only","model":"claude-opus-4-7","content":[{"type":"text","text":"Reading the file."}]}} -{"type":"assistant","message":{"id":"msg_with_tool","model":"claude-opus-4-7","content":[{"type":"text","text":"I'll read it."},{"type":"tool_use","id":"toolu_1","name":"Read","input":{"file_path":"/etc/hosts"}}],"usage":{"input_tokens":120,"output_tokens":40,"cache_read_input_tokens":9100,"cache_creation_input_tokens":80,"service_tier":"standard"}}} -{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"127.0.0.1 localhost","is_error":false}]}} -{"type":"result","subtype":"success","duration_ms":4500,"num_turns":3,"total_cost_usd":0.0123,"permission_denials":[]} -{"type":"error","error":"connection refused","code":"ECONNREFUSED"} -{"type":"telemetry_v2","data":"hypothetical future shape","session_id":"sess-x"} -{"type":"assistant","message":{"id":"msg_thinking_signed","model":"claude-opus-4-7","content":[{"type":"thinking","thinking":"","signature":"EqQBCkYIBRgCKkB9"},{"type":"text","text":"Done."}]}} -{"type":"assistant","message":{"id":"msg_thinking_text","model":"claude-opus-4-7","content":[{"type":"thinking","thinking":"Let me check the file first."}]}} -{"type":"result","subtype":"success","duration_ms":8100,"num_turns":5,"total_cost_usd":0.42,"permission_denials":[],"modelUsage":{"claude-opus-4-7":{"inputTokens":1200,"outputTokens":340,"cacheReadInputTokens":9000,"cacheCreationInputTokens":120,"costUSD":0.42,"contextWindow":1000000,"maxOutputTokens":64000}}} +{"agents":["@code","@docs"],"claude_code_version":"1.5.0","cwd":"/tmp/wt","mcpServers":[{"name":"git"}],"model":"claude-opus-4-7","outputStyle":"default","permissionMode":"acceptEdits","plugins":[],"session_id":"sess-abc","skills":[],"slashCommands":["/help","/compact"],"subtype":"init","tools":["Read","Write","Bash"],"type":"system"} +{"rateLimitType":"5h","resetsAt":"2026-04-29T08:00:00Z","status":"allowed","subtype":"rate_limit_event","type":"system"} +{"agent":"researcher","description":"investigate stale sessions","subtype":"task_started","task_id":"brgb0gz57","type":"system"} +{"patch":{"is_backgrounded":true},"subtype":"task_updated","task_id":"brgb0gz57","type":"system"} +{"message":"agent paused for input","subtype":"task_notification","task_id":"brgb0gz57","type":"system"} +{"description":"investigate stale sessions","last_tool_name":"Bash","session_id":"sess-abc","subtype":"task_progress","task_id":"brgb0gz57","tool_use_id":"toolu_9","type":"system","usage":{"duration_ms":9000,"tool_uses":3,"total_tokens":1234},"uuid":"u-9"} +{"rateLimitType":"5h","resetsAt":"2026-04-29T13:00:00Z","status":"warn","type":"rate_limit_event"} +{"rate_limit_info":{"isUsingOverage":false,"overageDisabledReason":"org_level_disabled_until","rateLimitType":"five_hour","resetsAt":1777443000,"status":"allowed"},"type":"rate_limit_event"} +{"message":{"content":[{"text":"Reading the file.","type":"text"}],"id":"msg_text_only","model":"claude-opus-4-7"},"type":"assistant"} +{"message":{"content":[{"text":"I'll read it.","type":"text"},{"id":"toolu_1","input":{"file_path":"/etc/hosts"},"name":"Read","type":"tool_use"}],"id":"msg_with_tool","model":"claude-opus-4-7","usage":{"cache_creation_input_tokens":80,"cache_read_input_tokens":9100,"input_tokens":120,"output_tokens":40,"service_tier":"standard"}},"type":"assistant"} +{"message":{"content":[{"content":"127.0.0.1 localhost","is_error":false,"tool_use_id":"toolu_1","type":"tool_result"}],"role":"user"},"type":"user"} +{"duration_ms":4500,"num_turns":3,"permission_denials":[],"subtype":"success","total_cost_usd":0.0123,"type":"result"} +{"code":"ECONNREFUSED","error":"connection refused","type":"error"} +{"data":"hypothetical future shape","session_id":"sess-x","type":"telemetry_v2"} +{"message":{"content":[{"signature":"EqQBCkYIBRgCKkB9","thinking":"","type":"thinking"},{"text":"Done.","type":"text"}],"id":"msg_thinking_signed","model":"claude-opus-4-7"},"type":"assistant"} +{"message":{"content":[{"thinking":"Let me check the file first.","type":"thinking"}],"id":"msg_thinking_text","model":"claude-opus-4-7"},"type":"assistant"} +{"duration_ms":8100,"modelUsage":{"claude-opus-4-7":{"cacheCreationInputTokens":120,"cacheReadInputTokens":9000,"contextWindow":1000000,"costUSD":0.42,"inputTokens":1200,"maxOutputTokens":64000,"outputTokens":340}},"num_turns":5,"permission_denials":[],"subtype":"success","total_cost_usd":0.42,"type":"result"} +{"rate_limit_info":{"isUsingOverage":false,"overageDisabledReason":"out_of_credits","overageStatus":"rejected","rateLimitType":"five_hour","resetsAt":1786904400,"status":"allowed"},"session_id":"sess-subagent-probe","type":"rate_limit_event","uuid":"47648e08-94a5-427d-9f13-a8a044a43582"} +{"estimated_tokens":50,"estimated_tokens_delta":50,"session_id":"sess-subagent-probe","subtype":"thinking_tokens","type":"system","uuid":"d317903f-9e0b-4759-bb4e-8de4159902fd"} +{"estimated_tokens":97,"estimated_tokens_delta":47,"session_id":"sess-subagent-probe","subtype":"thinking_tokens","type":"system","uuid":"2784b051-657d-4071-9924-09657be1f645"} +{"message":{"content":[{"signature":"Ev4CCokBCBAYAipATV6ediQCbUn3d5iXugGjEiNZxd+Hqd5mhYL5LloK6els8XCqPKa+69mj+ON2ZAKIammvpRDDQvhYgKDiFFyyUzIPY2xhdWRlLXNvbm5ldC01OABCCHRoaW5raW5nWiQwZDJlOGMzMy1hZDQyLTRkZWMtYWM3ZS0xZjkxODc0Mzk3MDMSDPtIaQH+yl8xcKbwkBoM0PrgC4XP+7niwUeZIjBoNHuKO8KCCVn5/K1fYYLKf7WDQsoIuVh4XDL6YOdcNGAfZ6p8FvWHd/Sgj+dKsNIqoQHHu1vYCRuwE3MMEQQxQx6sRHNZm3MAIKLYuYohm+9vFV+5C5ykOrytbshG3lRAivic7+DNJ6hmMfFwp1oUet8JCMIff8C6FpSKCCESY3nMfnAv29rz7nuYaLsc4XZw0GLAFScOnmCqeq+ctPSAXmdOaOC7focw3sdiFPzlmu8+/73lytQrr+PkOe3ux7qPnsQiPM0HbdHKDDfCPMztyOfnoRgB","thinking":"","type":"thinking"}],"context_management":null,"diagnostics":null,"id":"msg_011Ce6mKzDwb4V8cyEtwQrnj","model":"claude-sonnet-5","role":"assistant","stop_details":null,"stop_reason":null,"stop_sequence":null,"type":"message","usage":{"cache_creation":{"ephemeral_1h_input_tokens":8695,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":8695,"cache_read_input_tokens":23976,"inference_geo":"not_available","input_tokens":2,"output_tokens":2,"service_tier":"standard"}},"parent_tool_use_id":null,"request_id":"req_011Ce6mKyPLiHcAPALX6tqBT","session_id":"sess-subagent-probe","timestamp":"2026-08-16T16:45:15.438Z","type":"assistant","uuid":"989718f4-2801-4e00-9926-98f67bd9f2ec"} +{"message":{"content":[{"caller":{"type":"direct"},"id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","input":{"description":"Read note.txt contents","prompt":"Read note.txt in the current directory and reply with its contents, nothing else.","run_in_background":false,"subagent_type":"general-purpose"},"name":"Agent","type":"tool_use"}],"context_management":null,"diagnostics":null,"id":"msg_011Ce6mKzDwb4V8cyEtwQrnj","model":"claude-sonnet-5","role":"assistant","stop_details":null,"stop_reason":null,"stop_sequence":null,"type":"message","usage":{"cache_creation":{"ephemeral_1h_input_tokens":8695,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":8695,"cache_read_input_tokens":23976,"inference_geo":"not_available","input_tokens":2,"output_tokens":2,"service_tier":"standard"}},"parent_tool_use_id":null,"request_id":"req_011Ce6mKyPLiHcAPALX6tqBT","session_id":"sess-subagent-probe","timestamp":"2026-08-16T16:45:16.201Z","type":"assistant","uuid":"51f78fd5-6be0-4a7b-9f61-b15f56d200cc"} +{"description":"Read note.txt contents","prompt":"Read note.txt in the current directory and reply with its contents, nothing else.","session_id":"sess-subagent-probe","subagent_type":"general-purpose","subtype":"task_started","task_id":"ad21f6bf1e8cd9e23","task_type":"local_agent","tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","type":"system","uuid":"03a9bf2d-0548-4ad3-a08f-05ec0795c8ea"} +{"message":{"content":[{"text":"Read note.txt in the current directory and reply with its contents, nothing else.","type":"text"}],"role":"user"},"parent_tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","session_id":"sess-subagent-probe","subagent_type":"general-purpose","task_description":"Read note.txt contents","timestamp":"2026-08-16T16:45:16.208Z","type":"user","uuid":"bf2fe48e-3931-4b97-b18f-4dbfa7ba6e11"} +{"description":"Running Read note.txt in current directory","last_tool_name":"Bash","session_id":"sess-subagent-probe","subagent_type":"general-purpose","subtype":"task_progress","task_id":"ad21f6bf1e8cd9e23","tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","type":"system","usage":{"duration_ms":1887,"tool_uses":1,"total_tokens":14370},"uuid":"b92161ea-1144-4172-87bb-8b14e8c3e09f"} +{"message":{"content":[{"caller":{"type":"direct"},"id":"toolu_01CZN44DqkfWBSdbzPBDZvAj","input":{"command":"cat \"note.txt\"","description":"Read note.txt in current directory"},"name":"Bash","type":"tool_use"}],"context_management":null,"diagnostics":null,"id":"msg_011Ce6mLCFFFKr6tCGj9xDig","model":"claude-sonnet-5","role":"assistant","stop_details":null,"stop_reason":null,"stop_sequence":null,"type":"message","usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":14351},"cache_creation_input_tokens":14351,"cache_read_input_tokens":0,"inference_geo":"not_available","input_tokens":2,"output_tokens":17,"service_tier":"standard"}},"parent_tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","request_id":"req_011Ce6mLBNuYbL7Wf12tb8tu","session_id":"sess-subagent-probe","subagent_type":"general-purpose","task_description":"Read note.txt contents","timestamp":"2026-08-16T16:45:18.092Z","type":"assistant","uuid":"cea43d4d-fc8b-4ee6-ac44-5787571ef5b2"} +{"message":{"content":[{"content":"hello from the probe file","is_error":false,"tool_use_id":"toolu_01CZN44DqkfWBSdbzPBDZvAj","type":"tool_result"}],"role":"user"},"parent_tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","session_id":"sess-subagent-probe","subagent_type":"general-purpose","task_description":"Read note.txt contents","timestamp":"2026-08-16T16:45:18.202Z","type":"user","uuid":"de041662-bf5f-4f85-9b1d-28cc7e9f769b"} +{"patch":{"end_time":1786898719260,"status":"completed"},"session_id":"sess-subagent-probe","subtype":"task_updated","task_id":"ad21f6bf1e8cd9e23","type":"system","uuid":"54bb259f-9ee0-404d-b4f1-4755aae9afcc"} +{"output_file":"/tmp/tasks/ad21f6bf1e8cd9e23.output","session_id":"sess-subagent-probe","status":"completed","subtype":"task_notification","summary":"hello from the probe file","task_id":"ad21f6bf1e8cd9e23","tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","type":"system","usage":{"duration_ms":3053,"tool_uses":1,"total_tokens":15274},"uuid":"d1decfe7-a84f-47e4-9efa-1ffe146b077b"} +{"message":{"content":[{"content":[{"text":"hello from the probe file","type":"text"},{"text":"agentId: ad21f6bf1e8cd9e23 (use SendMessage with to: 'ad21f6bf1e8cd9e23', summary: '<5-10 word recap>' to continue this agent)\nsubagent_tokens: 15265\ntool_uses: 1\nduration_ms: 3054","type":"text"}],"tool_use_id":"toolu_016w6UKd3wKShW1Z3BUZ9UtB","type":"tool_result"}],"role":"user"},"parent_tool_use_id":null,"session_id":"sess-subagent-probe","timestamp":"2026-08-16T16:45:19.263Z","tool_use_result":{"agentId":"ad21f6bf1e8cd9e23","agentType":"general-purpose","content":[{"text":"hello from the probe file","type":"text"}],"prompt":"Read note.txt in the current directory and reply with its contents, nothing else.","resolvedModel":"claude-sonnet-5","status":"completed","toolStats":{"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0,"readCount":0,"searchCount":0},"totalDurationMs":3054,"totalTokens":15265,"totalToolUseCount":1,"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":903},"cache_creation_input_tokens":903,"cache_read_input_tokens":14351,"inference_geo":"not_available","input_tokens":2,"iterations":[{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":903},"cache_creation_input_tokens":903,"cache_read_input_tokens":14351,"input_tokens":2,"output_tokens":9,"type":"message"}],"output_tokens":9,"server_tool_use":{"web_fetch_requests":0,"web_search_requests":0},"service_tier":"standard","speed":"standard"}},"type":"user","uuid":"669df765-c189-4d18-9448-38ee4c1a3ffc"} +{"message":{"content":[{"text":"The subagent reported the contents of note.txt as: **\"hello from the probe file\"**","type":"text"}],"context_management":null,"diagnostics":null,"id":"msg_011Ce6mLRtnEZR9eJj6avHfV","model":"claude-sonnet-5","role":"assistant","stop_details":null,"stop_reason":null,"stop_sequence":null,"type":"message","usage":{"cache_creation":{"ephemeral_1h_input_tokens":322,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":322,"cache_read_input_tokens":32671,"inference_geo":"not_available","input_tokens":2,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"request_id":"req_011Ce6mLQNkSX472BzveDbAA","session_id":"sess-subagent-probe","timestamp":"2026-08-16T16:45:20.335Z","type":"assistant","uuid":"fc4abe2d-d9d4-4cd6-8c4c-8c9a89012bf3"} +{"api_error_status":null,"duration_api_ms":7747,"duration_ms":7100,"fast_mode_disabled_reason":"sdk_opt_in_required","fast_mode_state":"off","is_error":false,"modelUsage":{"claude-haiku-4-5-20251001":{"cacheCreationInputTokens":0,"cacheReadInputTokens":0,"canonicalModel":"claude-haiku-4-5","contextWindow":200000,"costUSD":0.000652,"inputTokens":567,"maxOutputTokens":32000,"outputTokens":17,"provider":"firstParty","webSearchRequests":0},"claude-sonnet-5":{"cacheCreationInputTokens":24271,"cacheReadInputTokens":70998,"canonicalModel":"claude-sonnet-5","contextWindow":1000000,"costUSD":0.1374879,"inputTokens":8,"maxOutputTokens":64000,"outputTokens":324,"provider":"firstParty","webSearchRequests":0}},"num_turns":2,"permission_denials":[],"result":"The subagent reported the contents of note.txt as: **\"hello from the probe file\"**","session_id":"sess-subagent-probe","stop_reason":"end_turn","subtype":"success","terminal_reason":"completed","time_to_request_ms":135,"total_cost_usd":0.13813989999999998,"ttft_ms":2176,"ttft_stream_ms":1057,"type":"result","usage":{"cache_creation":{"ephemeral_1h_input_tokens":9017,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":9017,"cache_read_input_tokens":56647,"inference_geo":"not_available","input_tokens":4,"iterations":[{"cache_creation":{"ephemeral_1h_input_tokens":322,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":322,"cache_read_input_tokens":32671,"input_tokens":2,"output_tokens":29,"type":"message"}],"output_tokens":231,"server_tool_use":{"web_fetch_requests":0,"web_search_requests":0},"service_tier":"standard","speed":"standard"},"uuid":"ce9ce2e5-ab21-4c34-a1b4-9042a18455f0"} diff --git a/hub/internal/hostrunner/testdata/profiles/claude-code/parity.json b/hub/internal/hostrunner/testdata/profiles/claude-code/parity.json index 0395e97f..c1404bb6 100644 --- a/hub/internal/hostrunner/testdata/profiles/claude-code/parity.json +++ b/hub/internal/hostrunner/testdata/profiles/claude-code/parity.json @@ -59,9 +59,11 @@ "payload": { "description": "$.description", "last_tool_name": "$.last_tool_name", + "subagent_type": "$.subagent_type", "subtype": "$.subtype", "task_id": "$.task_id", - "text": "$.description" + "text": "$.description", + "tool_use_id": "$.tool_use_id" } } }, @@ -110,6 +112,9 @@ "producer": "agent", "payload": { "message_id": "$$.message.id", + "parent_tool_use_id": "$$.parent_tool_use_id", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type", "text": "$.text" } } @@ -124,7 +129,10 @@ "payload": { "id": "$.id", "input": "$.input", - "name": "$.name" + "name": "$.name", + "parent_tool_use_id": "$$.parent_tool_use_id", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type" } } }, @@ -138,7 +146,10 @@ "payload": { "marker_only": "absent($.thinking)", "message_id": "$$.message.id", + "parent_tool_use_id": "$$.parent_tool_use_id", "signature_present": "present($.signature)", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type", "text": "nonempty($.thinking) || \"Thinking…\"" } } @@ -160,7 +171,10 @@ "message_id": "$.message.id", "model": "$.message.model", "output_tokens": "$.message.usage.output_tokens", - "service_tier": "$.message.usage.service_tier" + "parent_tool_use_id": "$.parent_tool_use_id", + "service_tier": "$.message.usage.service_tier", + "subagent": "present($.parent_tool_use_id)", + "subagent_type": "$.subagent_type" } } }, @@ -183,6 +197,9 @@ "payload": { "content": "$.content", "is_error": "$.is_error", + "parent_tool_use_id": "$$.parent_tool_use_id", + "subagent": "present($$.parent_tool_use_id)", + "subagent_type": "$$.subagent_type", "tool_use_id": "$.tool_use_id" } } @@ -338,9 +355,11 @@ "payload": { "description": "investigate stale sessions", "last_tool_name": "Bash", + "subagent_type": null, "subtype": "task_progress", "task_id": "brgb0gz57", - "text": "investigate stale sessions" + "text": "investigate stale sessions", + "tool_use_id": "toolu_9" } } ], @@ -378,6 +397,9 @@ "producer": "agent", "payload": { "message_id": "msg_text_only", + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null, "text": "Reading the file." } } @@ -388,6 +410,9 @@ "producer": "agent", "payload": { "message_id": "msg_with_tool", + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null, "text": "I'll read it." } }, @@ -399,7 +424,10 @@ "input": { "file_path": "/etc/hosts" }, - "name": "Read" + "name": "Read", + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null } }, { @@ -412,7 +440,10 @@ "message_id": "msg_with_tool", "model": "claude-opus-4-7", "output_tokens": 40, - "service_tier": "standard" + "parent_tool_use_id": null, + "service_tier": "standard", + "subagent": false, + "subagent_type": null } } ], @@ -423,6 +454,9 @@ "payload": { "content": "127.0.0.1 localhost", "is_error": false, + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null, "tool_use_id": "toolu_1" } } @@ -482,7 +516,10 @@ "payload": { "marker_only": true, "message_id": "msg_thinking_signed", + "parent_tool_use_id": null, "signature_present": true, + "subagent": false, + "subagent_type": null, "text": "Thinking…" } }, @@ -491,6 +528,9 @@ "producer": "agent", "payload": { "message_id": "msg_thinking_signed", + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null, "text": "Done." } } @@ -502,7 +542,10 @@ "payload": { "marker_only": false, "message_id": "msg_thinking_text", + "parent_tool_use_id": null, "signature_present": false, + "subagent": false, + "subagent_type": null, "text": "Let me check the file first." } } @@ -554,6 +597,401 @@ "type": "result" } } + ], + [ + { + "kind": "rate_limit", + "producer": "agent", + "payload": { + "is_using_overage": false, + "overage_status": "rejected", + "reason": "out_of_credits", + "resets_at": 1786904400, + "status": "allowed", + "window": "five_hour" + } + } + ], + [ + { + "kind": "system", + "producer": "agent", + "payload": { + "estimated_tokens": 50, + "estimated_tokens_delta": 50, + "session_id": "sess-subagent-probe", + "subtype": "thinking_tokens", + "type": "system", + "uuid": "d317903f-9e0b-4759-bb4e-8de4159902fd" + } + } + ], + [ + { + "kind": "system", + "producer": "agent", + "payload": { + "estimated_tokens": 97, + "estimated_tokens_delta": 47, + "session_id": "sess-subagent-probe", + "subtype": "thinking_tokens", + "type": "system", + "uuid": "2784b051-657d-4071-9924-09657be1f645" + } + } + ], + [ + { + "kind": "thought", + "producer": "agent", + "payload": { + "marker_only": true, + "message_id": "msg_011Ce6mKzDwb4V8cyEtwQrnj", + "parent_tool_use_id": null, + "signature_present": true, + "subagent": false, + "subagent_type": null, + "text": "Thinking…" + } + }, + { + "kind": "usage", + "producer": "agent", + "payload": { + "cache_create": 8695, + "cache_read": 23976, + "input_tokens": 2, + "message_id": "msg_011Ce6mKzDwb4V8cyEtwQrnj", + "model": "claude-sonnet-5", + "output_tokens": 2, + "parent_tool_use_id": null, + "service_tier": "standard", + "subagent": false, + "subagent_type": null + } + } + ], + [ + { + "kind": "tool_call", + "producer": "agent", + "payload": { + "id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB", + "input": { + "description": "Read note.txt contents", + "prompt": "Read note.txt in the current directory and reply with its contents, nothing else.", + "run_in_background": false, + "subagent_type": "general-purpose" + }, + "name": "Agent", + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null + } + }, + { + "kind": "usage", + "producer": "agent", + "payload": { + "cache_create": 8695, + "cache_read": 23976, + "input_tokens": 2, + "message_id": "msg_011Ce6mKzDwb4V8cyEtwQrnj", + "model": "claude-sonnet-5", + "output_tokens": 2, + "parent_tool_use_id": null, + "service_tier": "standard", + "subagent": false, + "subagent_type": null + } + } + ], + [ + { + "kind": "system", + "producer": "agent", + "payload": { + "description": "Read note.txt contents", + "prompt": "Read note.txt in the current directory and reply with its contents, nothing else.", + "session_id": "sess-subagent-probe", + "subagent_type": "general-purpose", + "subtype": "task_started", + "task_id": "ad21f6bf1e8cd9e23", + "task_type": "local_agent", + "tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB", + "type": "system", + "uuid": "03a9bf2d-0548-4ad3-a08f-05ec0795c8ea" + } + } + ], + [], + [ + { + "kind": "system", + "producer": "agent", + "payload": { + "description": "Running Read note.txt in current directory", + "last_tool_name": "Bash", + "subagent_type": "general-purpose", + "subtype": "task_progress", + "task_id": "ad21f6bf1e8cd9e23", + "text": "Running Read note.txt in current directory", + "tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB" + } + } + ], + [ + { + "kind": "tool_call", + "producer": "agent", + "payload": { + "id": "toolu_01CZN44DqkfWBSdbzPBDZvAj", + "input": { + "command": "cat \"note.txt\"", + "description": "Read note.txt in current directory" + }, + "name": "Bash", + "parent_tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB", + "subagent": true, + "subagent_type": "general-purpose" + } + }, + { + "kind": "usage", + "producer": "agent", + "payload": { + "cache_create": 14351, + "cache_read": 0, + "input_tokens": 2, + "message_id": "msg_011Ce6mLCFFFKr6tCGj9xDig", + "model": "claude-sonnet-5", + "output_tokens": 17, + "parent_tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB", + "service_tier": "standard", + "subagent": true, + "subagent_type": "general-purpose" + } + } + ], + [ + { + "kind": "tool_result", + "producer": "agent", + "payload": { + "content": "hello from the probe file", + "is_error": false, + "parent_tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB", + "subagent": true, + "subagent_type": "general-purpose", + "tool_use_id": "toolu_01CZN44DqkfWBSdbzPBDZvAj" + } + } + ], + [ + { + "kind": "system", + "producer": "agent", + "payload": { + "patch": { + "end_time": 1786898719260, + "status": "completed" + }, + "session_id": "sess-subagent-probe", + "subtype": "task_updated", + "task_id": "ad21f6bf1e8cd9e23", + "type": "system", + "uuid": "54bb259f-9ee0-404d-b4f1-4755aae9afcc" + } + } + ], + [ + { + "kind": "system", + "producer": "agent", + "payload": { + "output_file": "/tmp/tasks/ad21f6bf1e8cd9e23.output", + "session_id": "sess-subagent-probe", + "status": "completed", + "subtype": "task_notification", + "summary": "hello from the probe file", + "task_id": "ad21f6bf1e8cd9e23", + "tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB", + "type": "system", + "usage": { + "duration_ms": 3053, + "tool_uses": 1, + "total_tokens": 15274 + }, + "uuid": "d1decfe7-a84f-47e4-9efa-1ffe146b077b" + } + } + ], + [ + { + "kind": "tool_result", + "producer": "agent", + "payload": { + "content": [ + { + "text": "hello from the probe file", + "type": "text" + }, + { + "text": "agentId: ad21f6bf1e8cd9e23 (use SendMessage with to: 'ad21f6bf1e8cd9e23', summary: '\u003c5-10 word recap\u003e' to continue this agent)\n\u003cusage\u003esubagent_tokens: 15265\ntool_uses: 1\nduration_ms: 3054\u003c/usage\u003e", + "type": "text" + } + ], + "is_error": null, + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null, + "tool_use_id": "toolu_016w6UKd3wKShW1Z3BUZ9UtB" + } + } + ], + [ + { + "kind": "text", + "producer": "agent", + "payload": { + "message_id": "msg_011Ce6mLRtnEZR9eJj6avHfV", + "parent_tool_use_id": null, + "subagent": false, + "subagent_type": null, + "text": "The subagent reported the contents of note.txt as: **\"hello from the probe file\"**" + } + }, + { + "kind": "usage", + "producer": "agent", + "payload": { + "cache_create": 322, + "cache_read": 32671, + "input_tokens": 2, + "message_id": "msg_011Ce6mLRtnEZR9eJj6avHfV", + "model": "claude-sonnet-5", + "output_tokens": 1, + "parent_tool_use_id": null, + "service_tier": "standard", + "subagent": false, + "subagent_type": null + } + } + ], + [ + { + "kind": "turn.result", + "producer": "agent", + "payload": { + "by_model": { + "claude-haiku-4-5-20251001": { + "cache_create": 0, + "cache_read": 0, + "context_window": 200000, + "cost_usd": 0.000652, + "input": 567, + "max_output_tokens": 32000, + "output": 17 + }, + "claude-sonnet-5": { + "cache_create": 24271, + "cache_read": 70998, + "context_window": 1000000, + "cost_usd": 0.1374879, + "input": 8, + "max_output_tokens": 64000, + "output": 324 + } + }, + "cost_usd": 0.13813989999999998, + "duration_ms": 7100, + "fast_mode_state": "off", + "num_turns": 2, + "permission_denials": [], + "terminal_reason": "completed" + } + }, + { + "kind": "completion", + "producer": "agent", + "payload": { + "api_error_status": null, + "duration_api_ms": 7747, + "duration_ms": 7100, + "fast_mode_disabled_reason": "sdk_opt_in_required", + "fast_mode_state": "off", + "is_error": false, + "modelUsage": { + "claude-haiku-4-5-20251001": { + "cacheCreationInputTokens": 0, + "cacheReadInputTokens": 0, + "canonicalModel": "claude-haiku-4-5", + "contextWindow": 200000, + "costUSD": 0.000652, + "inputTokens": 567, + "maxOutputTokens": 32000, + "outputTokens": 17, + "provider": "firstParty", + "webSearchRequests": 0 + }, + "claude-sonnet-5": { + "cacheCreationInputTokens": 24271, + "cacheReadInputTokens": 70998, + "canonicalModel": "claude-sonnet-5", + "contextWindow": 1000000, + "costUSD": 0.1374879, + "inputTokens": 8, + "maxOutputTokens": 64000, + "outputTokens": 324, + "provider": "firstParty", + "webSearchRequests": 0 + } + }, + "num_turns": 2, + "permission_denials": [], + "result": "The subagent reported the contents of note.txt as: **\"hello from the probe file\"**", + "session_id": "sess-subagent-probe", + "stop_reason": "end_turn", + "subtype": "success", + "terminal_reason": "completed", + "time_to_request_ms": 135, + "total_cost_usd": 0.13813989999999998, + "ttft_ms": 2176, + "ttft_stream_ms": 1057, + "type": "result", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 9017, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 9017, + "cache_read_input_tokens": 56647, + "inference_geo": "not_available", + "input_tokens": 4, + "iterations": [ + { + "cache_creation": { + "ephemeral_1h_input_tokens": 322, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 322, + "cache_read_input_tokens": 32671, + "input_tokens": 2, + "output_tokens": 29, + "type": "message" + } + ], + "output_tokens": 231, + "server_tool_use": { + "web_fetch_requests": 0, + "web_search_requests": 0 + }, + "service_tier": "standard", + "speed": "standard" + }, + "uuid": "ce9ce2e5-ab21-4c34-a1b4-9042a18455f0" + } + } ] ] }