Conversation
Chat messages gain two optional fields. structured_output_request_id is set on a user message that asked for a structured output, and on a queued message that does; promotion keeps it. structured_output is set on the receipt that closed a request and carries its outcome, with the value passed through as raw JSON, so a succeeded null stays an explicit null. Content is unchanged: internal parts stay hidden and a receipt keeps its text fallback. Malformed or ambiguous metadata leaves the fields unset and never drops the message. Ordinary messages serialize exactly as before. The shared db2sdk conversion parses each message once and fills the fields, so REST, the stream snapshot, message events, history resets, queue updates and send and edit responses stay identical. Generated by make gen: coderd/apidoc/docs.go, coderd/apidoc/swagger.json, docs/reference/api/chats.md, docs/reference/api/schemas.md and site/src/api/typesGenerated.ts. _Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high_ Signed-off-by: Thomas Kosiewski <[email protected]>
A structured output value can be any JSON value, so the generated TypeScript type must be unknown rather than the global Record<string, string> mapping for raw JSON. _Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high_ Signed-off-by: Thomas Kosiewski <[email protected]>
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
|
Remote UAT on
Recording, segment 1 (login, prompt, live reply): 98c7a89d-c-ui-recording.webmRecording, segment 2 (reloaded chat): d8f5cd84-c-ui-recording-seg2.webmGenerated with |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54e77c7ee9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".




Exposes structured output request IDs and results on chat messages. It is the api-projection slice (C1) of the structured-output backend roadmap and is stacked on #29986. Ordinary messages serialize exactly as before.
ChatMessageandChatQueuedMessagegain an optionalstructured_output_request_id. It is set on a user message, or a queued message, whose content holds exactly one valid structured output request part; promotion keeps it because the part moves with the message.ChatMessagegains an optionalstructured_output(ChatStructuredOutput). It is set only on a receipt row: an assistant message visible only to users whose content is a text fallback plus exactly one valid outcome part. The value passes through as raw JSON, so a succeedednullstays an explicitnull.content. Malformed or ambiguous metadata leaves the new fields unset and never drops the message.db2sdk.ChatMessageanddb2sdk.ChatQueuedMessageconversions parse each message once and fill the fields, so REST, the stream snapshot, message events, history resets, queue updates and send and edit responses stay identical.ChatStructuredOutput.valuewas the global raw JSON mappingRecord<string, string>, which is wrong for a value that can be any JSON.scripts/apitypingsrenamesAgentHookRawMessagestoRawMessageFieldsAsUnknownand addsChatStructuredOutput.valueto its field list, so the value is generated asunknown. This resolves the TypeScript finding deferred from the A3a PR.Nothing here makes structured output reachable through the API yet; activation comes later in the stack. No migration, query or frontend component change. Generated by
make gen:coderd/apidoc/docs.go,coderd/apidoc/swagger.json,docs/reference/api/chats.md,docs/reference/api/schemas.mdandsite/src/api/typesGenerated.ts(668 lines). Handwritten: 328 lines, 239 of them tests.The duplicate tool call ID issue described in #29975 is fixed by the next slice in this stack, before the activation slices. Other deferred follow-ups are tracked in #29982.
Validation:
go testand-raceforcoderd/database/db2sdk,codersdkandcoderd/x/chatd, plusgo test ./coderd -run Chat.make gendrift check (no diff) andmake pre-commit, which includeslint/tsover the generated types.make pre-pushhook on push (fullmake test,test-js, site build).chatstatetest, the wholechatdpackage, thecoderdchat message tests includingTestChatMessagesProjectStructuredOutput, thedb2sdkandcodersdktests includingTestChatMessage_StructuredOutput),-race, fuzzing, a strace run with one loopback connection, a generated-files check that reranapidocgen, the docs formatters andapitypings, compared every generated file with the committed one and typechecked the frontend (ChatStructuredOutput.valueisunknown), an independent probe plain and with-race, an end-to-end projection check on a real deployment with a scripted fake provider (raw REST JSON, the live stream and a fresh snapshot agree for a request, succeeded and failed receipts, a queued request ID with itsqueue_update, a queue deletion receipt and a promoted request ID; ordinary messages unchanged; a live model chat matched across REST and the stream), and an ordinary live-model chat smoke with recorded video. All required checks passed in round 1, with no known limitation reproduced. Screenshots and video are in the first comment.📋 Implementation Plan
Coder Agents structured output: backend implementation plan
1. Outcome and scope
Add a per-request
response_formatto the Coder chat API. An agent can use its normal tools, then submit a JSON value. Coder validates that value and publishes a durable, request-correlated success, failure, or cancellation. The later AI SDK adapter reads this result without implementing a finalizer tool or parsing tool names.This plan replaces the previous plan. It targets the inspected checkout at
06d421fbcf2con 2026-09-22, matching the locally availableorigin/mainat inspection. The working tree was clean. Recheck the implementation base before starting; remote freshness was not established by a fetch.Scope: Go server, Go SDK contracts, generated API artifacts, API documentation, and tests. Generated
site/src/api/typesGenerated.tschanges are allowed. No handwritten TypeScript, externalcoder/ai-sdkchanges, UI feature, provider-library upgrade, new endpoint, or new database resource.Guarantee: a successful result contains exactly one server-validated JSON value. This is not a promise of provider-native constrained decoding, factual correctness, or guaranteed model compliance. A model that will not comply produces a bounded failure, never successful plain text.
Initial limitations: inline Draft-07 JSON schemas; no references or custom vocabularies; no partial structured-value streaming; no structured-output contract on plan-mode or specialized child-mode turns. Normal root chats may still use subagents, dynamic tools, MCP tools, and provider tools. The parent's format is not inherited by subagents. Existing authorization and organization scope remain unchanged.
2. Current evidence and corrections
coderd/chat_routes.go:15-98mounts create, messages, edit, queue, interrupt, and stream under/api/v2/chats.codersdk/chats.go:568-683defines create, send, edit, and their response batches.go.mod:111replaces Fantasy withgithub.com/coder/fantasyat9a3598480a71.fantasy.Callhas tools but no output schema;ObjectCallhas a schema but no tools.StreamObject.chatloop/tool_definitions.go:13-47builds a root object fromToolInfo.Parameters, then mutates nested maps throughschema.Normalize.chatloop/chatloop.goexecutes local tools and truncates their ordinary text results;chatprompt.go:765-780stores valid JSON directly, otherwise wraps text.generation.go:562-588loads visible durable history, including compressed rows. Prompt SQL atqueries/chats.sql:496-552drops older compressed input.message_conversion.go:461-475also skips compressed rows.message_conversion.go:307-382writes model-only compaction summaries and copies pending user content into model-only rows.generation.go:1427-1510runs stop hooks before finishing. A hook can keep the turn running.chatstate/transitions.go:602-711replaces edited input with a new row and deletes its suffix and queue.FinishTurn,FinishError, andFinishInterruptionown terminal transitions.waitingorlast_error.db2sdk.go:1740-1755strips internal hook-context parts. Frontend parsers exhaustively switch on public part variants.database/dump.sql:1510-1551advances execution history on any message insert.runner.go:159-173fences active work on history/status, not snapshot changes.stream_loop.go:114-164,226-291gates message deltas on history but stores a snapshot watermark.chatstate/machine.go:254-273reads under a chat-row share lock.chatopenai/responses.goand chatd chain-ID symbols are absent from this checkout.The previous plan also left validator choice,
strict:false, result shape, limits, and delivery boundaries undecided. Those are resolved below.3. Public API contract
3.1 Requests
Add optional
ResponseFormat *ChatResponseFormattoCreateChatRequest,CreateChatMessageRequest, andEditChatMessageRequest.{ "content": [{"type": "text", "text": "Inspect the project and report its test command."}], "response_format": { "type": "json_schema", "json_schema": { "name": "project_report", "schema": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], "additionalProperties": false } } } }nullformat means ordinary text behavior on create/send. Explicit{"type":"text"}is also accepted. Reject an empty format object, unknown type, orjson_schemaaccompanyingtext.json_schemarequiresnameandschema. Name matches^[A-Za-z0-9_-]{1,64}$. Optional description is at most 1024 UTF-8 bytes. Reject unknown fields inside the format envelope, includingstrict. Validation is always enabled; there is no relaxed mode.null. Do not impose an object-only output restriction.nullpreserves the edited message's format; explicittextclears it; explicitjson_schemareplaces it. Read and validate preserved metadata under the same history guard as the edit. The replacement request receives a fresh request ID.codersdk.Response.Validationswith the exactresponse_format...field and HTTP 400. Existing authorization, unavailable-model, conflict, and body-limit statuses keep their meanings.Admission gate: add default-off
chat-structured-outputexperiment (ExperimentChatStructuredOutput) with no development bypass. Gate only acceptance of new formatted create/send/edit requests. When disabled, a syntactically supplied schema request, including an edit preserving a schema, returns HTTP 400 with aresponse_formatvalidation error explaining that the feature is disabled. Explicit text/ordinary requests remain available. Persisted accepted work is still processed when the experiment is disabled. Enable acceptance only after all serving and worker replicas are compatible; drain or cancel accepted work before downgrading. Do not use the experiment to disable recovery or hide persisted results.3.2 Correlation and terminal results
Mint a server-generated UUID once when accepting a structured request. Persist it with the format. Expose optional
structured_output_request_idonChatMessagefor the original input and onChatQueuedMessage. Preserve it through promotion, retries, and compaction copies. Create-chat callers obtain it from the first committed user message via existing message retrieval or streaming; no extra create endpoint is needed.Add optional
ChatMessage.StructuredOutput *ChatStructuredOutput:{ "id": 901, "role": "assistant", "content": [{"type":"text","text":"{\"command\":\"make test\"}"}], "structured_output": { "request_id": "11111111-1111-4111-8111-111111111111", "status": "succeeded", "value": {"command":"make test"} } }The receipt is an ordinary committed message with an additive field. Its assistant role is a transcript convention, not a provider response. It is visible to clients but excluded from provider prompts.
succeededvalue, including explicit JSONnullfailederror: {code, message}not_produced,validation_exhausted,generation_failed,configuration_error.cancellederror: {code, message}interrupted,superseded,queue_deleted. No value.Use
json.RawMessageforvalue, not float64 decoding/re-encoding. Preserve numeric value and exact string data; JSONB may normalize whitespace/key order, so do not promise byte-identical JSON. Do not call the result RFC-canonical JSON.Only committed receipt messages carry
structured_output. Candidate tool arguments, tool acknowledgments,message_partdeltas, and chat status events are not results. Existing message replay/reset/deletion behavior remains authoritative. Clients upsert by message ID and match by request UUID. A successful receipt can arrive while the next queued turn is alreadyrunning.Failure/cancellation receipts have a short text fallback. A success receipt has JSON text as its fallback. Existing clients can display the outcome without recognizing the new field. Whole-chat deletion and retention keep existing resource-deletion semantics; neither is a permanent result archive.
4. Schema validation and resource limits
Use the already-present
github.com/xeipuuv/gojsonschema v1.2.0, promoted from indirect to direct without a version upgrade. PinDraft7and disable dialect auto-detection. Its byte loaders usejson.Number; numeric validation uses rational arithmetic. Do not use the limited Fantasy schema struct as a validator.Keep trusted meta-validation separate from user-schema compilation. Compile only the fixed bundled Draft-07 meta-schema in a fresh trusted loader, validate the parsed user schema as data, then compile it in a different loader with meta-validation already satisfied and a deny-all document loader factory.
SchemaLoader.Validate=truerecursively invokesCompile(NewReferenceLoader(...))and changes loader state, so do not assume a supplied factory controls that nested call. Prove the selected two-stage path offline and with network/filesystem traps before importing it into ingress.The alternative
google/jsonschema-go v0.4.3is already direct and denies remote loads by default, but its current resolver does not fully meta-validate schemas and stores numeric bounds as float64. Avoid adding a second handwritten schema validator to compensate.$schemameans Draft-07. Accept only the canonical Draft-07 identifier when supplied. Reject other dialects rather than silently reinterpret them.$ref,$id, legacyid,$defs,definitions, and custom vocabulary keywords at schema positions. Do not reject an ordinary property or string value merely because it contains those names.anyOf,oneOf,allOf, and fractional numeric bounds. Reject unknown schema keywords. Treat annotations such as title/description/default as annotations; do not apply defaults or coerce values.formatnames use the pinned library's built-in checks. Reject unrecognized names instead of silently skipping them. Document this finite list and thatpatternuses Go regexp syntax. Do not mutate process-global format registries.regexis not in the supported list: the library compiles every instance string, and counted repetitions make that cost not adequately constrained by the selected resource budget.$refrejection.UseNumber; preserveRawMessagefor storage. Apply the same checks to schemas, format strings, model values, and recovered metadata.const/enum/default objects containing$ref,$id, and the trusted meta-schema URI. Those values must neither trigger loading nor shadow the trusted compiler's state.Initial server-side caps, fixed rather than caller-configurable:
allOf/anyOf/oneOfelement, eachnot/if/then/elsesubschema, and each schema-formdependenciesvalue; at most 4 nested branch levelspatternvalues andpatternPropertieskeys)patternPropertiesExploratory measurements against
gojsonschema v1.2.0set the branch, pattern, andpatternPropertieslimits above (cumulative allocation, not peak heap, on the development host). The library recompiles eachpatternPropertiespattern for every key on every validation: 2 small patterns over 4095 keys took 0.26 s and allocated 192 MiB. It keeps every validation error, about 1 KiB each, without fail-fast: 32 failingallOfbranches over 4000 keys produced 128k errors and 131 MiB, and 120 schema-formdependenciesover 4000 keys produced 480k errors and 465 MiB, while 8 branches over 4095 nodes produced 34 MiB in 0.10 s. The schema-position preflight enforces these limits before any library call. The compiler's own resource qualification on the implemented code remains an A1c gate.Enforce limits before expensive operations and on recovered persisted metadata. Use errors for untrusted input, never panics. Assert programmer-owned invariants in tests and return explicit internal errors in production. Add boundary tests, bounded fuzzing, offline loader tests, and worst-case benchmarks. A timeout goroutine around non-cancellable validation is not a resource bound. If qualification shows excessive CPU or memory use at these caps, lower the caps before API activation and update tests/docs together.
5. Durable state and execution design
5.1 Storage and state reconstruction
Use existing JSONB message content, following internal hook-context conventions. Add internal-only request, candidate/control, and outcome metadata shapes. Name the dedicated terminal discriminator
structured-output-outcome; do not reuse it for continuation/control records. Exclude these types from public generated unions, explicitly strip them indb2sdk, and explicitly omit them inchatprompt. The SDK may contain storage structs as it already does for internal parts; mark fields internal for generation. No tables or columns are added. One narrowly scoped execution-history trigger migration is required, as described below.Keep dependencies acyclic:
codersdkowns wire/storage structs and discriminators, with no import of chatd.chatstructuredimportscodersdk, the validator, and Fantasy only. It accepts decoded parts or small evidence records, validates schemas/values, builds a Fantasy definition/runner, and computes pure request state. It does not importchatloop,chatprompt,chatstate,db2sdk, or parent chatd.chatpromptparses database message content and identifies internal-only metadata.db2sdkprojects already-committed receipt metadata; it does not run validation or orchestration.chatloop.ProviderTool, and builds transaction messages. Chatstate uses existing parsing helpers for tool bookkeeping, not parent chatd.Do not create a generic run framework or configurable output-strategy registry.
Every control marker must be visible to the durable-state reader. Attach finish provenance, rejection count evidence, and candidate metadata to existing client-visible assistant/tool rows before their commit. For stop-hook continuation, insert a dedicated
visibility=usercontrol row with short continuation text plus internal invalidation metadata in the same transaction as the model-only hook context. The state reader sees this row even after restart;db2sdkstrips the metadata, and provider prompts omit the row. Treat both control and receipt rows as non-generation rows in all bookkeeping helpers.activeStructuredRequest(history)must:5.2 Finalizer inside the ordinary tool loop
Use reserved local tool
coder_structured_output. It takes exactly{"output": <value>}. It returns a tiny acknowledgment on validation success or bounded tool-error feedback on failure. Its successful value is recorded in internal candidate metadata in the same step commit as its result, outside truncatable tool text.Pass a fresh full
fantasy.FunctionTool.InputSchemathrough the existingchatloop.ProviderTooldefinition plus localRunnerseam. This preserves the wrapper root'sadditionalProperties:falseand the caller's schema without changing all ordinary tool definitions. Do not run the caller schema through mutatingschema.Normalize. Do not add a new Fantasy interface or enable strict tool decoding globally.Qualify configured provider strict-tool settings with actual wire encoders. Disable strictness for this finalizer alone where the pinned provider supports a per-tool override. Otherwise fail the incompatible structured request as
configuration_error; never alter other tools, require optional properties, or rewrite the caller's semantics to satisfy a strict provider. Add a preflight for any incompatible configuration that can be detected locally.ToolChoiceRequiredeverywhere.DynamicToolNames, never dispatched to a workspace, and never accepted through clienttool-resultssubmission.Pre-persistence argument screening: screen the original finalizer argument bytes before converting or committing any assistant row to JSONB, not only when the tool later runs. JSONB otherwise erases duplicate-key evidence and can reject large numbers or Unicode before validation. Enforce the argument byte cap while receiving its deltas. At stream completion, check raw JSON/Unicode/resource limits and finish eligibility before hook admission or step commit. On rejection, persist a small valid placeholder plus server-only rejected-call metadata, then close the tool call with a bounded error result. Never retain the unsafe payload in JSONB or reinterpret the placeholder as valid arguments on replay. The executor still performs schema validation and must check the persisted eligibility marker. Test duplicate keys, excessive exponents, NUL/surrogate escapes, restart after the assistant commit, and interrupted buffered parts.
5.3 Completion, repair, and hooks
A structured request must bypass ordinary text-only completion and unrelated stop-after success. Do not simply add the finalizer to
StopAfterToolsand search for any past successful result.validation_exhaustedif validation failed, otherwisenot_produced. Do not let normal max-step handling report success.5.4 Atomic terminal receipt and cancellation
At successful termination, append the user-visible/model-invisible receipt in the same
ChatMachine.Updatetransaction that invokesFinishTurn, before promotion. For terminal failure, do the equivalent withFinishError. For interruption, include it after partial/cancellation messages but beforeFinishInterruptionpromotes the queue.Use existing
CommitStepwhere legal; extend transition inputs with optional terminal messages only where the current transition cannot append them safely. Reuse message insert/revision publication, not a separate result store. Receipt publication happens only after commit. Failed fence checks produce no receipt. Under the chat lock, an existing receipt for this UUID prevents a duplicate terminal write after ambiguous commit acknowledgment or worker recovery.Receipt rows have
visibility=user, no provider metadata or usage, and normal text plus internal outcome metadata. Explicitly exclude them from completion scans, step counting, unresolved-tool scans, summaries, and provider replay. Visibility filtering alone is not sufficient because generation also reads visible history.Edit ordering is fixed: under the chat lock, collect affected pending UUIDs and existing terminal outcomes first; delete the old suffix and clear the queue; then insert cancellation receipts; then insert the replacement input and its hook suffix. Include every inserted receipt in
EditChatMessageResponse.Messages, in ID order. Do not cancel already-completed requests or delete newly written cancellations in the same edit.Cancellation of a queued request can append a receipt while an unrelated turn is generating or awaiting a tool. Filter receipt/control rows in chatstate pending-tool helpers as well as generation helpers. Regression gate: deleting a queued structured request must neither hide the ordinary turn's pending local/dynamic calls, rerun completed effects, nor make that turn complete.
Execution-history trigger prerequisite: the current message-insert trigger, introduced in migration
000519_chatd_core_state_machine.up.sql, advanceshistory_versionand resetsgeneration_attemptfor every inserted message. A receipt-only insert during unrelated work would therefore invalidate its execution fence. Add a migration replacing the current function to exempt only rows with the dedicated terminal-outcome discriminator,role=assistant, andvisibility=user. Filter transition-table rows before deriving affected chat IDs; a mixed receipt/ordinary-message batch must retain normal fencing. Do not exempt hook continuation or other user-only/control messages.Keep message revision, snapshot version, and stream notifications intact. Receipt-only inserts must not reset execution history, attempts, or retry budgets or restart an unrelated task. Recognize the dedicated server-created terminal row shape, not an arbitrary nested occurrence of its discriminator. Public inputs, dynamic-tool results, and hook payloads cannot manufacture this shape. Receipts are immutable. Keep UPDATE/DELETE triggers unchanged: suffix edits must still invalidate execution. The current dump and later trigger replacements confirm the insert-only seam; use the then-current definitions when implementing the migration.
Independent stream watermark prerequisite:
stream_loop.gocurrently gates fetching and emitting message deltas on execution history, although messagerevisionfollowssnapshot_version. First separate those responsibilities. Fetch on a newer snapshot, query changed messages using the last successfully applied snapshot watermark, and emit message events independently ofhistoryChanged. Preserve existing ID/revision deduplication,after_idreconnect, and tombstone-driven history resets. Keep preview resets and part-sequence clearing tied to execution-history/generation changes, not receipt-only snapshots. Reuse the existing snapshot cursor; no new counter or isolation protocol is needed.ChatMachine.ReadLocktakesFOR SHAREon the chat row within a transaction, excluding theFOR UPDATEtransitions until its message reads finish.applyDBSnapshotalready advancessnapshotVersionafter assembling message events. Preserve those boundaries: a hint or failed read must never advance the cursor. Test initialization, full reload, failed reads, empty deltas, and concurrent commits.streamLoop.partcontinues to filter by execution history, attempt, and sequence, so a receipt does not discard buffered parts.Land stream separation before the trigger migration, and both before any receipt-producing integration. The paired PostgreSQL regression must block an ordinary provider call or dynamic wait, delete a queued structured request, and receive its cancellation through SSE and REST before unblocking. Verify unchanged active task, preview, attempt, and retry accounting; no repeated effect or lost message after resumption; and correct reconnect and edit/reset behavior. Also cover receipt-only and mixed batches, non-exempt controls, malformed discriminator shapes, and rollback atomicity.
cancelled/queue_deletedin the same transaction as deletion. A disconnected client can recover it.cancelled/interrupted; new queued UUID remains separate. No output from partial buffer data.Tx.SetArchivedis disallowed in active states but permitsE1/XE1with queued work. Preserve queued structured requests and UUIDs as suspended, matching ordinary queue semantics; resume only through existing promotion paths. No cancellation on archive.Tx.ClearContextaccepts settledW/E0only, with an empty queue. Existing receipts remain closed; clearing context must not restart their schemas.Tx.ReconcileInvalidStatesynthesizes pending tool cancellations and parks inE0/E1. In the same transaction, close any determinable outstanding active structured request with a failedgeneration_failedreceipt; retain queued requests. Never fabricate success or select an ambiguous request.6. Reviewable delivery sequence
Start with 14 slices across three phases: foundations (A1-A5), durable runtime (B1-B6), and public API/release qualification (C1-C3). Implement one layer at a time on its actual prerequisite. Do not build the whole feature on one branch and split afterward. Each layer includes its own tests, builds independently, leaves ordinary chats usable, and is safe if later layers are delayed.
Size gate for every row: at most 500 handwritten changed lines, including tests and docs; at most 1000 total only when the excess is named generated output. Targets below are budgets, not measured diff sizes. Re-measure against each layer's immediate base. If a layer cannot fit, split at its stated fallback seam before coding further. Never move required tests into a trailing testing PR.
Phase A: internal foundation, HTTP acceptance still absent
coderd/x/chatd/chatstructured/schema.goand tests;go.moddirectness only.chatstructuredtool/value helpers and tests.codersdk/chats.go, variant-tag tests,chatstructured/state.go,chatprompt,db2sdk.stream_loop.goand stream tests.database/dump.sql, PostgreSQL trigger/runner tests.Phase A gate: all new package, variant, prompt, db2sdk, PostgreSQL migration/trigger, runner, and stream tests pass. No unknown public part variant; ordinary chat fixtures unchanged. Stream snapshot isolation and preview preservation are proven before the trigger exception lands. If A1 exceeds budget, split resource scanner/loader qualification from schema compilation, keeping tests with both. A1 uses this split as four layers: A1a raw JSON safety boundary, A1b private schema-position vocabulary screening and sanitization, A1c schema resource limits and regexp screening, and A1d trusted meta-validation, deny-all compilation, output validation, and loader qualification. If A3 exceeds budget, land internal codec/stripping before state reconstruction. Phase A ends at A5; A4 and A5 precede every receipt-producing slice.
Phase B: durable outcomes and runtime, still not publicly accepted
generation.go,message_conversion.go, narrowchatstatetransition inputs if needed.tasks.go,chatstate/transitions.goand state tests.chatd.go,chatstate/transitions.go, stream and state tests.generation.go, stream accumulation/step-commit helpers and tests.generation_preparer.go,generation.go,chatloop, tool-wire tests.generation.go,chatstructured/state.go, hook tests.Phase B gate: internally seeded formatted requests pass real-PostgreSQL lifecycle tests, live stream-isolation tests, and provider wire tests. None of these layers adds an accepted public request option. Earlier candidate/receipt helpers remain dormant unless an internal test explicitly seeds metadata. Ordinary HTTP chats are unchanged. Phase B ends at B6.
If necessary, split B3 into queued deletion and edited-suffix cancellation, B5 into local tool admission and provider-wire compatibility, or B6 into durable repair decisions and stop-hook finalization. These are fallback boundaries, not permission to exceed limits. Every split retains tests for its own invariants and remains unexposed until complete.
Phase C: public API, documentation, and release qualification
codersdk/chats.go,db2sdk,stream_looptests and generated artifacts.codersdk/chats.go,codersdk/deployment.go,exp_chats.go, chatd request options and HTTP tests.EditChatMessageRequest,patchChatMessage, chatd edit plumbing and tests.Critical C2 intermediate behavior: before C3 lands, reject edits of formatted inputs with a documented conflict rather than silently dropping their metadata. Ordinary-message edits keep existing behavior, including cancellation of any structured queued requests they discard. C3 removes that narrow restriction. The public release phase comprises C1-C3; C3 is its final member.
If C2 exceeds its budget, split create activation from send activation. Keep each request type, gate tests, and usage documentation with the endpoint that enforces it. If generation would exceed 1000 total lines, split the affected generator input into separate request/result additions. A regeneration-only follow-up is acceptable only if no intermediate branch has stale required generated files. Do not publish an accepted-but-unenforced request field to shrink a diff.
Delivery mechanics
gh-stackdependency links, each based on its actual prerequisite. No stack or PR is created by this planning task.7. Validation and acceptance
7.1 Automated checks
Each behavior starts with a failing test, then minimal implementation, then a simplification pass with tests still passing. Use existing fixtures in
chatd_test.go,generation_preparer_internal_test.go,message_conversion_test.go,hooks_test.go/stop_test.go,tasks_test.go,chatstate/transitions*_test.go,exp_chats*_test.go, andchatloop/chattestwire helpers. Add focused files rather than expanding giant test functions. Concurrent tests uset.Parallel(), unique IDs, channels or quartz, never timing sleeps.Representative targeted commands after the named packages/tests exist:
Verify new test names actually match; a command reporting no tests is not a gate. Run full touched-package tests after targeted passes. Run lifecycle tests with real PostgreSQL and race detection using repository fixtures, plus
go test -racefor new shared-state logic. Include worker takeover, duplicate completion, rollback, and interrupt-vs-finalize races with deterministic barriers.Provider qualification covers actual pinned request encoders with mock HTTP servers: OpenAI Responses, OpenAI-compatible Chat Completions, and Anthropic with thinking. Prove the schema wrapper and tools remain present, finalizer stays local, malformed/partial output cannot succeed, and transcript replay works. Other transports are not claimed validated without evidence. Provider rejection remains an explicit failure, not text fallback.
Generate the actual affected outputs with
make site/src/api/typesGenerated.tsandmake coderd/apidoc/swagger.json, then runmake genas required by the final diff.make gencovers more than API artifacts and can require Docker/PostgreSQL. Inspect every generated delta. Runmake fmt,make lint,make pre-commit, andmake pre-push; do not bypass hooks or report an unavailable service as a pass. Generated TS still requires the frontend type/lint gates andfrontend-review; no handwritten frontend logic is planned.Follow
write-docsif authoring underdocs/, and run its formatting/lint checks. Add only TODO notes in affectedchatd/ARCHITECTURE.mdsections; the human author writes architecture prose.7.2 Acceptance checklist
nullsucceeds distinctly from absent output. Legacy clients see a usable text fallback without a new content union variant.8. Remote dogfooding and evidence
Use
coder-agents-uatthrough an independent runner on an exact pushed feature SHA. Do not replace remote UAT with a local server in this workspace. No deployment actions are part of the current planning task.Setup and safety gate
./scripts/develop.sh. Use a unique process/resource/port namespace. For a focused binary build, usego build -o <owned-scratch>/coder ./cmd/coder, not fullmake buildduring iteration. On C2 and later, explicitly enablechat-structured-outputonly on the test deployment after verifying its migration and worker versions; also exercise the disabled-admission case. Earlier slices use internal contract fixtures, not a development bypass../scripts/coder-dev.shor an equivalent verified shim with a dedicated config and explicit test URL. HTTP usesUAT_URLand a token minted on that test deployment. Never use shared-deployment tokens or barecoderfor mutations.GO. An empty inherited URL is not proof; verify the hosting deployment from the chat/workspace identity. Repeat the gate after restarts and for later rounds.Hands-on scenarios
Use a small Go/curl harness outside tracked source, not the future TypeScript adapter. Save sanitized requests, committed stream events, REST reads, and validation assertions.
null; validate results independently and match UUIDs.agent-browser. Use snapshots for functionality. Label fake-provider lifecycle tests separately from live-model evidence.Capture PNG screenshots and a playable MP4/WebM covering the steps and final assertions. Logs,
script, or an asciinema cast supplement the evidence but do not replace video. Use a uniquely named agent-browser session; record the real UI or a terminal recording with rendered frames. Attach screenshots for inspection and inspect semantic video frames, not merely file existence or decoder success. If media capture fails, report that gate unverified.Keep evidence out of git, under the run's
.mux-uat/round-N/convention or the current skill's equivalent. Preserve the chat URL, manifest, exact tested SHA, commands, provider modes, raw failure receipts, screenshots, and recording. When attaching evidence to authorized GitHub work, usegh ... --attachwith matching local Markdown paths and verify published links. Do not use browser cookies orgh-image.9. Accepted trade-offs and stopping rules
TurnID. A narrow trigger migration separates terminal receipt publication from execution-history fencing. The default-off admission experiment has no development bypass. Enable it only after all serving/worker replicas and the migration support this contract; disabling acceptance must not stop recovery of already-accepted work. Do not claim a rolling mixed-version or downgrade guarantee before draining accepted work.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:xhigh