feat(service,host,features): start a task and its first coding agent in one request

Merged
DA
13d ago
+1368-105

PR 6 of the New task, prompt-first redesign. tasks.start (POST /api/plans) now takes an optional agent, and the service makes the task and starts a coding agent on it in the same request. This covers the wire, the host and the service. There is no desktop change here; PR 7 switches the desktop over.

What changes

Wire (task-wire.ts)

  • taskCreateRequestSchema.agent is the new taskAgentStartSchema. It holds the fields of an agent's Start (idempotencyKey, model, effort, contextWindow, prompt, attachments, disabledServers) and reuses codingAgentStartRequestFields and the startBrief words-or-files check. It leaves out from and note, because a new task has no agent to take over from.
  • The prompt travels as prompt, the same key the act and gateway params already use. The task key on an agent Start's body exists only for released desktops, and none of them sends this field.
  • The schema refuses a request that names both agent and planning: true. With agent, the task starts with planning: false.
  • New taskStartAnswerSchema: { plan, agent?: CodingAgentSummary, agentRefusal?: { error } }.

Service

  • startRequestedAgent in coder-host/start.ts is now the one shared path for a Start: it pairs the choice, maps a catalog refusal to invalid, calls startCodingAgent and builds the summary. Both POST /api/plans/{id}/agents (now a thin caller) and POST /api/plans use it.
  • When POST /api/plans names an agent:
    • Without a repository, it is refused 409 no-repository, the same refusal an agent Start gives for a task with no repository. Nothing is written.
    • It needs the developer's bearer, which the agent's session is opened over. A cookie-only start is refused unauthenticated before anything is written.
    • If an agent already stands under that idempotencyKey on a task in the named workspace, it answers 200 with that task and the agent, so a retry starts neither a second task nor a second agent. A key already used anywhere else is refused invalid-request.
    • If the Start fails after its agent was made (eve took the session, then a store step failed), it answers that agent rather than a refusal.
    • Otherwise it creates the task, then starts the agent. If the agent start is refused, it answers 201 with the task and agentRefusal. The task is not rolled back.
  • The body bound for a start now has room for a Start's prompt and files. The Start body bounds moved into start.ts.

Host and gateway

  • HostedTaskClient.create answers a TaskStarted: { task, agent?: CodingAgentAgentAnswer }. A refusal is read the way an agent Start reads it (startRefusedAs). A start that names an agent waits up to the Start's 120s bound.
  • The tasks.start answer gains agentStart?: CodingAgentAgentAnswer. host-operator forwards agent, and the TASKS_START act opens no call when agent is set.
  • Older desktops, which send no agent, get the same behaviour and answers as before.

Tests

  • coding-agents-app.test.ts, on a real database with a fake eve:
    • a start with an agent makes an empty-plan task and opens the agent's session with the prompt;
    • a retry under the same key answers the same task and agent, with one session opened and one task listed;
    • eve not taking the agent answers the task with agentRefusal: unavailable, and the task still stands;
    • no repository or a null repository is refused no-repository, planning: true with an agent is refused invalid-request, and in every case nothing is made.
  • task-client.test.ts: the agent and its refusal as the client reads them; planning: true with an agent is never sent.
  • compose-tasks.test.ts: agentStart is carried to the window. tasks-acts.test.ts: an agent start opens no call. host-operator.test.ts: agent is forwarded.
  • JSON-schema goldens re-recorded with LUKE_UPDATE_FIXTURES=1: taskCreateRequestSchema gains agent, and three new schema files are added for the new exports.

pnpm check:affected passed typecheck, lint, knip and every test except two failures caused by the sandbox: discarded-effect ran out of memory at the default heap (it passes with --max-old-space-size=6144), and app-menu.test.ts found no Electron binary in the sandbox (it passes when run alone). The web tests were also run on their own and pass.

Note

A retry after a lost answer is idempotent only when the agent was started. If the first request's agent was refused, a retry makes a second task. The desktop does not hit this, because on a refusal it opens the task it was answered and goes on from there.

Made with Luke


Devin Review