Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1700,6 +1700,8 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
LastModelConfigID: c.LastModelConfigID,
Title: c.Title,
Status: codersdk.ChatStatus(c.Status),
HistoryVersion: c.HistoryVersion,
GenerationAttempt: c.GenerationAttempt,
Archived: c.Archived,
Shared: len(c.UserACL) > 0 || len(c.GroupACL) > 0,
PinOrder: c.PinOrder,
Expand Down
2 changes: 2 additions & 0 deletions coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,8 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
LastReasoningEffort: database.NullChatReasoningEffort{ChatReasoningEffort: database.ChatReasoningEffortHigh, Valid: true},
Title: "all-fields-test",
Status: database.ChatStatusRunning,
HistoryVersion: 7,
GenerationAttempt: 2,
ClientType: database.ChatClientTypeUi,
LastError: pqtype.NullRawMessage{RawMessage: lastErrorRaw, Valid: true},
LastTurnSummary: sql.NullString{String: "turn completed", Valid: true},
Expand Down
11 changes: 7 additions & 4 deletions coderd/x/chatd/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -869,17 +869,20 @@ The stream loop powers the `GET /api/experimental/chats/{chat}/stream` endpoint.

The following chat stream events, delivered to the client over WebSocket, are supported:

- `message_part`: a streaming message part emitted by the chat worker.
- compared to the current implementation, these should additionally include the `history_version` and `generation_attempt` fields, so a client knows which episode a message part comes from
- `message_part`: a streaming message part emitted by the chat worker. Its payload includes `history_version`, `generation_attempt`, and `seq`, which identify the part's episode and position.
- `message`: a committed chat message present in the database.
- `status`: the chat's status.
- `status`: the chat's status plus the current `history_version` and `generation_attempt`. Clients can use this tuple as a lower bound when rejecting stale parts.
- `error`: the chat's persisted error payload.
- `queue_update`: the full current queued-message list.
- `action_required`: a dynamic tool call was issued by the chat worker, the client must execute it and submit the result.
- `retry`: emitted when the chat worker is waiting before retrying a failed generation attempt.
- `preview_reset`: a reset of the stream's preview state (message parts), emitted when the worker's LLM call fails mid-way for whatever reason.
- `history_reset`: a reset of the stream's history state (committed messages), emitted when the message history is edited and some messages are removed from the history.

Database-derived events from one `Sync` are emitted in the application order described below, including `message` before `queue_update` before `status`. Relayed `message_part` events are multiplexed independently, so there is no total ordering between parts and database-derived events.

The single-chat REST response exposes the same `history_version` and `generation_attempt` tuple as status events. A pure status transition advances `snapshot_version` but keeps this tuple stable, so it must not fence out parts from the active episode. A history change advances `history_version` and resets `generation_attempt` to `0`; the next generation starts at the same history version with a higher attempt.

## Endpoint lifecycle

When a client connects, the endpoint:
Expand Down Expand Up @@ -1079,7 +1082,7 @@ Status sync happens inside `Sync`.
Flow:

1. Compare database status to local status.
2. If they differ, emit `status`.
2. If they differ, emit `status` with `db.status`, `db.history_version`, and `db.generation_attempt`.

### Error synchronization

Expand Down
6 changes: 5 additions & 1 deletion coderd/x/chatd/stream_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ func (l *streamLoop) applyDBSnapshot(snapshot streamDBSnapshot) []codersdk.ChatS
events = append(events, codersdk.ChatStreamEvent{
Type: codersdk.ChatStreamEventTypeStatus,
ChatID: l.chatID,
Status: &codersdk.ChatStreamStatus{Status: codersdk.ChatStatus(chat.Status)},
Status: &codersdk.ChatStreamStatus{
Status: codersdk.ChatStatus(chat.Status),
HistoryVersion: chat.HistoryVersion,
GenerationAttempt: chat.GenerationAttempt,
},
})
}

Expand Down
93 changes: 93 additions & 0 deletions coderd/x/chatd/stream_loop_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ func TestStreamLoopQueueStatusRetryErrorActionRequiredAndPreviewReset(t *testing
codersdk.ChatStreamEventTypeRetry,
codersdk.ChatStreamEventTypePreviewReset,
)
require.Equal(t, int64(2), events[1].Status.HistoryVersion)
require.Equal(t, int64(2), events[1].Status.GenerationAttempt)
require.Equal(t, chatError.Message, events[2].Error.Message)
require.Equal(t, retry.Attempt, events[3].Retry.Attempt)

Expand All @@ -231,9 +233,100 @@ func TestStreamLoopQueueStatusRetryErrorActionRequiredAndPreviewReset(t *testing
codersdk.ChatStreamEventTypeActionRequired,
codersdk.ChatStreamEventTypePreviewReset,
)
require.Equal(t, int64(1), actionEvents[0].Status.HistoryVersion)
require.Zero(t, actionEvents[0].Status.GenerationAttempt)
require.Equal(t, "call-1", actionEvents[1].ActionRequired.ToolCalls[0].ToolCallID)
}

func TestStreamLoopStatusCarriesPartConsistentEpisode(t *testing.T) {
t.Parallel()

chatID := uuid.New()
loop := newStreamLoop(database.Chat{ID: chatID}, nil, slogtest.Make(t, nil), 0)
loop.state.snapshotVersion = 10
loop.state.historyVersion = 7
loop.state.generationAttempt = 1
loop.state.status = database.ChatStatusRunning

// A pure status transition must keep the active part episode even though
// snapshot_version advances.
events := loop.applyDBSnapshot(streamDBSnapshot{chat: database.Chat{
ID: chatID,
Status: database.ChatStatusInterrupting,
SnapshotVersion: 11,
HistoryVersion: 7,
GenerationAttempt: 1,
}})
requireEventTypes(t, events, codersdk.ChatStreamEventTypeStatus)
require.Equal(t, int64(7), events[0].Status.HistoryVersion)
require.Equal(t, int64(1), events[0].Status.GenerationAttempt)
_, accepted, err := loop.part(StreamPart{
HistoryVersion: 7,
GenerationAttempt: 1,
Seq: 1,
Part: codersdk.ChatMessageText("draining"),
})
require.NoError(t, err)
require.True(t, accepted)

// History changes reset the attempt; retries advance it within that history.
events = loop.applyDBSnapshot(streamDBSnapshot{chat: database.Chat{
ID: chatID,
Status: database.ChatStatusRunning,
SnapshotVersion: 12,
HistoryVersion: 12,
GenerationAttempt: 0,
}})
requireEventTypes(t, events,
codersdk.ChatStreamEventTypeStatus,
codersdk.ChatStreamEventTypePreviewReset,
)
require.Equal(t, int64(12), events[0].Status.HistoryVersion)
require.Zero(t, events[0].Status.GenerationAttempt)

events = loop.applyDBSnapshot(streamDBSnapshot{chat: database.Chat{
ID: chatID,
Status: database.ChatStatusRunning,
SnapshotVersion: 13,
HistoryVersion: 12,
GenerationAttempt: 1,
}})
requireEventTypes(t, events, codersdk.ChatStreamEventTypePreviewReset)
_, accepted, err = loop.part(StreamPart{
HistoryVersion: 12,
GenerationAttempt: 1,
Seq: 1,
Part: codersdk.ChatMessageText("new turn"),
})
require.NoError(t, err)
require.True(t, accepted)

events = loop.applyDBSnapshot(streamDBSnapshot{chat: database.Chat{
ID: chatID,
Status: database.ChatStatusRunning,
SnapshotVersion: 14,
HistoryVersion: 12,
GenerationAttempt: 2,
}})
requireEventTypes(t, events, codersdk.ChatStreamEventTypePreviewReset)
_, accepted, err = loop.part(StreamPart{
HistoryVersion: 12,
GenerationAttempt: 1,
Seq: 2,
Part: codersdk.ChatMessageText("stale retry"),
})
require.NoError(t, err)
require.False(t, accepted)
_, accepted, err = loop.part(StreamPart{
HistoryVersion: 12,
GenerationAttempt: 2,
Seq: 1,
Part: codersdk.ChatMessageText("retry"),
})
require.NoError(t, err)
require.True(t, accepted)
}

func TestStreamLoopActionRequiredFromHistory(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 5 additions & 1 deletion codersdk/chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ type Chat struct {
LastReasoningEffort *string `json:"last_reasoning_effort,omitempty"`
Title string `json:"title"`
Status ChatStatus `json:"status"`
HistoryVersion int64 `json:"history_version,omitempty"`
GenerationAttempt int64 `json:"generation_attempt,omitempty"`
PlanMode ChatPlanMode `json:"plan_mode,omitempty"`
LastError *ChatError `json:"last_error,omitempty"`
LastTurnSummary *string `json:"last_turn_summary"`
Expand Down Expand Up @@ -1649,7 +1651,9 @@ type ChatStreamMessagePart struct {

// ChatStreamStatus represents an updated chat status.
type ChatStreamStatus struct {
Status ChatStatus `json:"status"`
Status ChatStatus `json:"status"`
HistoryVersion int64 `json:"history_version,omitempty"`
GenerationAttempt int64 `json:"generation_attempt,omitempty"`
}

// ChatErrorKind classifies chat errors for consistent client rendering.
Expand Down
Loading
Loading