mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: integrate agent context snapshots into chats (#26389)
Makes the chat context foundation from #26385 live. That PR added the storage columns, writer queries, and a dormant `agentapi.ContextDirtyMarker` trigger with no production callers; this PR wires them together end to end. When a workspace agent pushes a context snapshot, bound chats now hydrate to that snapshot's hash, and a later push with a different hash flips already-pinned chats to dirty (emitting a `context_dirty` watch event after the transaction commits). Chat creation pins the agent's latest snapshot when one already exists. The experimental chat API exposes this as `Chat.Context` (`*ChatContext` with `dirty`, `dirty_since`, `error`), and a new `PUT /api/experimental/chats/{chat}/context` endpoint re-pins the agent's latest snapshot and clears the dirty marker. `context_dirty_resources` stays NULL (the resource-level diff is deferred to the UI phase) and the live per-turn context pull is unchanged. The end-to-end test provisions a workspace agent via the echo provisioner, connects it over the Agent API v2.10, and exercises the full path: an initial push hydrates a bound chat (clean), a second push with a different hash marks it dirty, the API reports the dirty state, and the refresh endpoint clears it. <details> <summary>Decision log</summary> - **API shape — sub-struct.** Dirty state is surfaced as `codersdk.Chat.Context *ChatContext { Dirty bool; DirtySince *time.Time; Error string }` rather than flat fields, matching the RFC's named `ChatContext` type and leaving room for future fields (resource diff, sources). `db2sdk.Chat` populates it when the chat is context-tracked (`len(ContextAggregateHash) > 0`), dirty, or carries a snapshot error, and leaves it nil (`omitempty`) otherwise. `Dirty` mirrors `context_dirty_since` being set. - **Marker wiring.** The chat daemon is injected directly as the `agentapi.ContextDirtyMarker`. It is unconditionally constructed (only its background worker is gated), so the marker is always non-nil and the wiring matches every other `api.chatDaemon` call site. `agentapi` still treats a nil marker as "chatd absent", so `PushContextState` stays a pure write path for any future caller that does not wire chatd in. - **Refresh is atomic.** `RefreshChatContext` reads the agent's latest snapshot and re-pins the chat in one repeatable-read transaction, so a concurrent push cannot land between the read and the write and leave the chat pinned to a stale hash with the dirty marker cleared. - **Hydrate + dirty run inside the push transaction.** The fan-out shares the push's transaction so a concurrent refresh cannot interleave with the version gate; `context_dirty` watch events publish only after commit. The pinned hash on dirtied chats is intentionally left unchanged — the refresh endpoint re-pins it. - **Dirtied chats keep their pinned hash.** Drift is advisory: a dirty chat stays usable, and refreshing is the only path that advances the pinned hash. - **Test binds `chats.agent_id` directly.** In production the binding is set lazily during a chat turn (`chatd.persistBuildAgentBinding`); the test sets it via `dbgen` so it exercises the context flow rather than turn resolution. Plan: `coderd/x/chatd` context integration + E2E (sub-struct API, create-time + push-time hydration, refresh endpoint; `context_dirty_resources` and the per-turn pull untouched). </details> 🤖 Generated by Coder Agents on behalf of @kylecarbs
This commit is contained in:
Generated
+66
-2
@@ -549,6 +549,42 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/chats/{chat}/context": {
|
||||
"put": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Chats"
|
||||
],
|
||||
"summary": "Refresh chat context",
|
||||
"operationId": "refresh-chat-context",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Chat ID",
|
||||
"name": "chat",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Chat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/experimental/chats/{chat}/diff": {
|
||||
"get": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
@@ -16450,6 +16486,14 @@ const docTemplate = `{
|
||||
"client_type": {
|
||||
"$ref": "#/definitions/codersdk.ChatClientType"
|
||||
},
|
||||
"context": {
|
||||
"description": "Context reports the chat's pinned workspace-context state and\nwhether it has drifted from the agent's latest pushed snapshot.\nNil when the chat has no pinned context yet.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ChatContext"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
@@ -16605,6 +16649,24 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatContext": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dirty": {
|
||||
"description": "Dirty is true when the agent's latest snapshot hash differs from the\nchat's pinned hash.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"dirty_since": {
|
||||
"description": "DirtySince is when drift was first detected; nil when not dirty.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"error": {
|
||||
"description": "Error is the snapshot-level error copied from the pinned snapshot\n(empty when healthy).",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatDiffContents": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17544,7 +17606,8 @@ const docTemplate = `{
|
||||
"created",
|
||||
"deleted",
|
||||
"diff_status_change",
|
||||
"action_required"
|
||||
"action_required",
|
||||
"context_dirty"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ChatWatchEventKindStatusChange",
|
||||
@@ -17553,7 +17616,8 @@ const docTemplate = `{
|
||||
"ChatWatchEventKindCreated",
|
||||
"ChatWatchEventKindDeleted",
|
||||
"ChatWatchEventKindDiffStatusChange",
|
||||
"ChatWatchEventKindActionRequired"
|
||||
"ChatWatchEventKindActionRequired",
|
||||
"ChatWatchEventKindContextDirty"
|
||||
]
|
||||
},
|
||||
"codersdk.ConnectionLatency": {
|
||||
|
||||
Reference in New Issue
Block a user