feat(service,features): an agent's calls are read with its transcript
Merged
DA
5d ago+725+725+012345678901234567890123456789-140-140-012345678901234567890123456789
PR 5a of the Unified agents rollout (STAG-322). It gives the agent tab everything it needs to draw calls inside an agent's own transcript, and makes a blank agent read as blank, so 5b can be client-only.
What changed
1. Spoken lines in the agent's transcript read (GET /api/agents/{id}/messages, coder-host/transcript.ts)
- Spoken lines, the developer's and the agent's, come through in seq order like every other row. Each one keeps its voice-channel metadata (
channel: "voice",voice_session_id) and now carriessentAt: the instant its words began, which is the session's start plusfrom_ms. - An agent's spoken line no longer picks up the record of the turn it was read aloud from. The writer links that turn (
readAloudFrom), and before this change the read stamped the spoken line with that turn'sstartedAt/finishedAt/model/usage, so it would have drawn as a second turn. - A spoken line no longer counts as "a line awaiting its turn" (
newestAwaitingLines). Before, a line the voice answered by itself left the agent reading as running and then, pastLINE_GRACE, as failed. A spoken ask opens its turn through the asks, and that turn row is what reads the agent as running.
Call boundaries: a separate read, GET /api/agents/{id}/calls (agents.calls; client calls(agentId) → CodingAgentCallsAnswerView)
- It is served over the existing
readAgentTranscriptand answers the sameTaskTranscriptthe task's Call tab reads today. Each call comes withid,startedAt,endedAt?,ending?,usage?,model?,by?, and its lines, each with therowIdsof its rows in the agent's conversation. - Why a separate read and not a field on the messages page: it is the smaller change. It reuses a store read and a wire shape that already exist and are tested, and 5b can feed
transcript-model.ts/CallCheckpoint/CallEndedper agent without changes. A field on the page would add avoice_sessionsquery to every look inside the held read, plus a new wire shape. Call boundaries only move when a call starts or ends, which the desktop already knows. - Members of the agent's workspace can read it; anyone else gets not found (tested).
2. A blank agent reads as one
- The new
CODING_AGENT_BRIEF.BLANK = "blank"is carried throughCODING_AGENT_LINE_COLUMNS(brief, replacinghandedPrompt),codingAgentLineOf/codingAgentBriefOf, the summary'sbrief, and the wire. - On the panel, the first transcript row is read as a plain line: no Plan card and no task sender (
agent-tab.tsx). The copied transcript has no brief either (transcript-copy.ts). - With no turn yet, a blank agent reads
completedrather than starting, then failed pastSTARTING_GRACEwithnot-started.AgentStanding.blankis threaded into the reads that judge it: summary, transcript, lists, Stop/archive, the task archive guard, factory, and tools. - Instructions: a blank agent gets its own brief words ("do what the developer asks of you in this conversation") and is told to read the plan before editing it. The plan and prompt instructions are byte-identical; the chat-off golden only gains a
blankentry.
3. Starting a blank agent
POST /api/plans/{id}/agentstakes{ idempotencyKey, blank: true }. It must name no prompt, files,from, or note; the wire'sstartBriefrefuses one that does.- It works on a task with or without a repository. It runs on the account's default (or a named choice), like any Start.
- It hands the agent nothing and opens no eve session. The first steer or spoken ask opens one, through 2b's path in
steer.ts. - Any other Start on a task with no repository is still refused
no-repository.
Tests
Each test was run against the code without its change and fails there.
coding-agents-app.test.ts, "a blank agent and the calls into an agent":- a blank Start on a task with no repository and on one with a repository; it reads blank and completed past the grace; a blank Start with a prompt is refused, and a plain Start on a task with no repository is still refused.
- spoken lines through the messages read: order, channel, call,
sentAt, no turn record, and no running status. - the calls read for the starter and a teammate, not found for an outsider and for an id that is no agent.
coding-agent-store.test.ts(database): list lines andcodingAgentLineOfreadbrief, with a blank agent readingblank.agent-senders.test.tsandtranscript-copy.test.ts(panel): a blank agent's first row is a line, not a Plan card or a task.- Goldens re-recorded with
LUKE_UPDATE_FIXTURES=1:- the wire JSON schemas gain
brief: "blank", the Start'sblankkey, and the new calls view; - the coder instructions' chat-off golden gains a
blankentry only.
- the wire JSON schemas gain
Notes for review
- Older desktops:
briefis a closed literal set on the wire, so a desktop built before this PR refuses a summary that says"blank". Only 5b's client creates blank agents, so it matters once a teammate on a newer build starts one on a task that someone on an older build has open. Under the rollout's no-backwards-compatibility decision I didn't work around it. pnpm check:affectedhere: lint, knip, and discarded-effect passed. Typecheck ran out of memory in this sandbox, so I typechecked the touched packages one by one (features, database, service, panel, web) and all passed.agent-runtime/prepare-script.test.tsfails in this sandbox on its own; it touches nothing changed here, and CI decides.
Made with Luke