mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd/chatd): structured chat error classification and retry hardening (#23275)
> **PR Stack** > 1. #23351 ← `#23282` > 2. #23282 ← `#23275` > 3. **#23275** ← `#23349` *(you are here)* > 4. #23349 ← `main` --- ## Summary Extracts a structured error classification subsystem for agent chat (`chatd`) so that retry and error payloads carry machine-readable metadata — error kind, provider name, HTTP status code, and retryability — instead of raw error strings. This is the **backend half** of the error-handling work. The frontend counterpart is in #23282. ## Changes ### New package: `coderd/chatd/chaterror/` Canonical error classification — extracts error kind, provider, status code, and user-facing message from raw provider errors. One source of truth that drives both retry policy and stream payloads. - **`kind.go`**: Error kind enum (`rate_limit`, `timeout`, `auth`, `config`, `overloaded`, `unknown`). - **`signals.go`**: Signal extraction — parses provider name, HTTP status code, and retryability from error strings and wrapped types. - **`classify.go`**: Classification logic — maps extracted signals to an error kind. - **`message.go`**: User-facing message templates keyed by kind + signals. - **`payload.go`**: Projectors that build `ChatStreamError` and `ChatStreamRetry` payloads from a classified error. ### Modified - **`codersdk/chats.go`**: Added `Kind`, `Provider`, `Retryable`, `StatusCode` fields to `ChatStreamError` and `ChatStreamRetry`. - **`coderd/chatd/chatretry/`**: Thinned to retry-policy only; classification logic moved to `chaterror`. - **`coderd/chatd/chatloop/`**: Added per-attempt first-chunk timeout (60 s) via `guardedStream` wrapper — produces retryable `startup_timeout` errors instead of hanging forever. - **`coderd/chatd/chatd.go`**: Publishes normalized retry/error payloads via `chaterror` projectors.
This commit is contained in:
Generated
+32
-1
@@ -1763,7 +1763,26 @@ export const ChatStatuses: ChatStatus[] = [
|
||||
* ChatStreamError represents an error event in the stream.
|
||||
*/
|
||||
export interface ChatStreamError {
|
||||
/**
|
||||
* Message is the normalized, user-facing error message.
|
||||
*/
|
||||
readonly message: string;
|
||||
/**
|
||||
* Kind classifies the error for consistent client rendering.
|
||||
*/
|
||||
readonly kind?: string;
|
||||
/**
|
||||
* Provider identifies the upstream model provider when known.
|
||||
*/
|
||||
readonly provider?: string;
|
||||
/**
|
||||
* Retryable reports whether the underlying error is transient.
|
||||
*/
|
||||
readonly retryable: boolean;
|
||||
/**
|
||||
* StatusCode is the best-effort upstream HTTP status code.
|
||||
*/
|
||||
readonly status_code?: number;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
@@ -1823,9 +1842,21 @@ export interface ChatStreamRetry {
|
||||
*/
|
||||
readonly delay_ms: number;
|
||||
/**
|
||||
* Error is the error message from the failed attempt.
|
||||
* Error is the normalized error message from the failed attempt.
|
||||
*/
|
||||
readonly error: string;
|
||||
/**
|
||||
* Kind classifies the retry reason for consistent client rendering.
|
||||
*/
|
||||
readonly kind?: string;
|
||||
/**
|
||||
* Provider identifies the upstream model provider when known.
|
||||
*/
|
||||
readonly provider?: string;
|
||||
/**
|
||||
* StatusCode is the best-effort upstream HTTP status code.
|
||||
*/
|
||||
readonly status_code?: number;
|
||||
/**
|
||||
* RetryingAt is the timestamp when the retry will be attempted.
|
||||
*/
|
||||
|
||||
@@ -1570,7 +1570,7 @@ describe("useChatStore", () => {
|
||||
mockSocket.emitData({
|
||||
type: "error",
|
||||
chat_id: chatID,
|
||||
error: { message: "Rate limit exceeded" },
|
||||
error: { message: "Rate limit exceeded", retryable: true },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1629,7 +1629,7 @@ describe("useChatStore", () => {
|
||||
mockSocket.emitData({
|
||||
type: "error",
|
||||
chat_id: chatID,
|
||||
error: { message: " " },
|
||||
error: { message: " ", retryable: false },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1953,7 +1953,7 @@ describe("useChatStore", () => {
|
||||
mockSocket.emitData({
|
||||
type: "error",
|
||||
chat_id: chatID,
|
||||
error: { message: "Rate limit exceeded" },
|
||||
error: { message: "Rate limit exceeded", retryable: true },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2750,7 +2750,7 @@ describe("updateSidebarChat via stream events", () => {
|
||||
mockSocket.emitData({
|
||||
type: "error",
|
||||
chat_id: chatID,
|
||||
error: { message: "something went wrong" },
|
||||
error: { message: "something went wrong", retryable: false },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3026,7 +3026,7 @@ describe("updateSidebarChat via stream events", () => {
|
||||
mockSocket.emitData({
|
||||
type: "error",
|
||||
chat_id: chatID,
|
||||
error: { message: "something broke" },
|
||||
error: { message: "something broke", retryable: false },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user