feat(service,features): an agent's calls are read with its transcript

Merged
DA
5d ago
+725-140

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 carries sentAt: the instant its words began, which is the session's start plus from_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's startedAt/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, past LINE_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 readAgentTranscript and answers the same TaskTranscript the task's Call tab reads today. Each call comes with id, startedAt, endedAt?, ending?, usage?, model?, by?, and its lines, each with the rowIds of 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/CallEnded per agent without changes. A field on the page would add a voice_sessions query 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 through CODING_AGENT_LINE_COLUMNS (brief, replacing handedPrompt), codingAgentLineOf/codingAgentBriefOf, the summary's brief, 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 completed rather than starting, then failed past STARTING_GRACE with not-started. AgentStanding.blank is 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 blank entry.

3. Starting a blank agent

  • POST /api/plans/{id}/agents takes { idempotencyKey, blank: true }. It must name no prompt, files, from, or note; the wire's startBrief refuses 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 and codingAgentLineOf read brief, with a blank agent reading blank.
  • agent-senders.test.ts and transcript-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's blank key, and the new calls view;
    • the coder instructions' chat-off golden gains a blank entry only.

Notes for review

  • Older desktops: brief is 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:affected here: 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.ts fails in this sandbox on its own; it touches nothing changed here, and CI decides.

Made with Luke


Devin Review