@yolk-sdk/agent 0.1.0-canary.19 → 0.1.0-canary.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +25 -1
  2. package/dist/client/attachments.d.mts +8 -0
  3. package/dist/client/attachments.d.mts.map +1 -0
  4. package/dist/client/attachments.mjs +36 -0
  5. package/dist/client/attachments.mjs.map +1 -0
  6. package/dist/client/index.d.mts +2 -1
  7. package/dist/client/index.mjs +2 -1
  8. package/dist/client/state.d.mts +4 -2
  9. package/dist/client/state.d.mts.map +1 -1
  10. package/dist/client/state.mjs +42 -22
  11. package/dist/client/state.mjs.map +1 -1
  12. package/dist/client/transport.d.mts +1 -1
  13. package/dist/client/transport.mjs +1 -1
  14. package/dist/compaction/transformer.d.mts +1 -1
  15. package/dist/loop/error.d.mts +3 -2
  16. package/dist/loop/error.d.mts.map +1 -1
  17. package/dist/loop/error.mjs +6 -3
  18. package/dist/loop/error.mjs.map +1 -1
  19. package/dist/loop/llm-event.d.mts +1 -1
  20. package/dist/loop/llm-event.mjs +1 -1
  21. package/dist/loop/run.d.mts +1 -1
  22. package/dist/loop/run.d.mts.map +1 -1
  23. package/dist/loop/run.mjs +18 -4
  24. package/dist/loop/run.mjs.map +1 -1
  25. package/dist/loop/services/context-transformer.d.mts +1 -1
  26. package/dist/loop/services/llm-provider.d.mts +1 -1
  27. package/dist/loop/services/loop-config.mjs +1 -1
  28. package/dist/loop/services/loop-config.mjs.map +1 -1
  29. package/dist/loop/services/tool-executor.d.mts +1 -1
  30. package/dist/loop/testing/faux-provider.mjs +1 -1
  31. package/dist/loop/testing/test-tool-executor.mjs +1 -1
  32. package/dist/protocol/content.d.mts +15 -1
  33. package/dist/protocol/content.d.mts.map +1 -1
  34. package/dist/protocol/content.mjs +76 -1
  35. package/dist/protocol/content.mjs.map +1 -1
  36. package/dist/protocol/event.d.mts +17 -5
  37. package/dist/protocol/event.d.mts.map +1 -1
  38. package/dist/protocol/event.mjs +24 -3
  39. package/dist/protocol/event.mjs.map +1 -1
  40. package/dist/protocol/index.d.mts +3 -3
  41. package/dist/protocol/index.d.mts.map +1 -1
  42. package/dist/protocol/index.mjs +3 -3
  43. package/dist/providers/anthropic/claude-provider.d.mts.map +1 -1
  44. package/dist/providers/anthropic/claude-provider.mjs +48 -17
  45. package/dist/providers/anthropic/claude-provider.mjs.map +1 -1
  46. package/dist/providers/openai/codex-provider.d.mts +1 -1
  47. package/dist/providers/openai/codex-provider.d.mts.map +1 -1
  48. package/dist/providers/openai/codex-provider.mjs +60 -18
  49. package/dist/providers/openai/codex-provider.mjs.map +1 -1
  50. package/dist/providers/openai/provider.d.mts.map +1 -1
  51. package/dist/providers/openai/provider.mjs +28 -12
  52. package/dist/providers/openai/provider.mjs.map +1 -1
  53. package/dist/providers/provider-error.d.mts +32 -0
  54. package/dist/providers/provider-error.d.mts.map +1 -0
  55. package/dist/providers/provider-error.mjs +91 -0
  56. package/dist/providers/provider-error.mjs.map +1 -0
  57. package/dist/react/chat-core.d.mts +3 -1
  58. package/dist/react/chat-core.d.mts.map +1 -1
  59. package/dist/react/chat-core.mjs +42 -6
  60. package/dist/react/chat-core.mjs.map +1 -1
  61. package/dist/react/chat-items.d.mts +8 -2
  62. package/dist/react/chat-items.d.mts.map +1 -1
  63. package/dist/react/chat-items.mjs +17 -10
  64. package/dist/react/chat-items.mjs.map +1 -1
  65. package/dist/react/chat-messages.mjs +1 -1
  66. package/dist/react/chat-session-events.d.mts +1 -1
  67. package/dist/react/chat-session-events.mjs +1 -1
  68. package/dist/react/use-agent-chat.mjs +1 -1
  69. package/dist/runtime/error.d.mts +1 -1
  70. package/dist/runtime/error.mjs +1 -1
  71. package/dist/runtime/run-runtime.d.mts +1 -1
  72. package/dist/runtime/session-event-store.d.mts +1 -1
  73. package/dist/runtime/session-event-store.mjs +1 -1
  74. package/dist/tools/question.d.mts +1 -1
  75. package/dist/tools/question.mjs +1 -1
  76. package/dist/tools/registry.d.mts +1 -1
  77. package/dist/tools/registry.mjs +1 -1
  78. package/dist/tools/task.d.mts +1 -1
  79. package/dist/tools/task.mjs +1 -1
  80. package/dist/voice/tool-bridge.mjs +1 -1
  81. package/package.json +1 -1
  82. package/src/client/attachments.ts +45 -0
  83. package/src/client/index.ts +1 -0
  84. package/src/client/state.ts +43 -18
  85. package/src/loop/error.ts +6 -3
  86. package/src/loop/run.ts +27 -4
  87. package/src/loop/services/loop-config.ts +1 -1
  88. package/src/protocol/content.ts +129 -0
  89. package/src/protocol/event.ts +26 -2
  90. package/src/protocol/index.ts +8 -0
  91. package/src/providers/anthropic/claude-provider.ts +74 -23
  92. package/src/providers/openai/codex-provider.ts +81 -19
  93. package/src/providers/openai/provider.ts +45 -19
  94. package/src/providers/provider-error.ts +213 -0
  95. package/src/react/chat-core.ts +46 -5
  96. package/src/react/chat-items.ts +10 -3
@@ -21,6 +21,7 @@ import {
21
21
  type AgentReasoningEffort,
22
22
  type Content,
23
23
  type ContentPart,
24
+ type ProviderFailureKind,
24
25
  type ToolDef
25
26
  } from '@yolk-sdk/agent/protocol'
26
27
  import {
@@ -36,6 +37,12 @@ import {
36
37
  } from '@yolk-sdk/agent/loop'
37
38
  import { openAiCodexAuthorizationHeaders, openAiCodexResponsesUrl } from './codex.ts'
38
39
  import type { OAuthAccessToken } from '@yolk-sdk/agent/oauth'
40
+ import {
41
+ classifyProviderFailure,
42
+ providerErrorInfo,
43
+ providerFailureCause,
44
+ providerFailureRetryable
45
+ } from '../provider-error.ts'
39
46
 
40
47
  export type OpenAiCodexReasoningSummary = 'auto' | 'concise' | 'detailed'
41
48
 
@@ -395,22 +402,50 @@ export const toOpenAiCodexRequestBody = (
395
402
  }
396
403
  })
397
404
 
398
- const responseStatusToCause = (status: number): LLMError['cause'] => {
399
- if (status === 429) {
400
- return 'rate_limit'
405
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
406
+ typeof value === 'object' && value !== null
407
+
408
+ const stringField = (value: unknown, key: string) => {
409
+ if (!isRecord(value)) {
410
+ return undefined
401
411
  }
402
412
 
403
- if (status === 413) {
404
- return 'context_overflow'
413
+ const field = Object.getOwnPropertyDescriptor(value, key)?.value
414
+
415
+ return typeof field === 'string' ? field : undefined
416
+ }
417
+
418
+ const recordField = (value: unknown, key: string) => {
419
+ if (!isRecord(value)) {
420
+ return undefined
405
421
  }
406
422
 
407
- return 'provider_error'
423
+ const field = Object.getOwnPropertyDescriptor(value, key)?.value
424
+
425
+ return isRecord(field) ? field : undefined
408
426
  }
409
427
 
410
- const isRetryableStatus = (status: number) => status === 429 || status >= 500
428
+ const openAiCodexProvider = 'openai_codex'
429
+
430
+ const providerSignalError = (input: {
431
+ readonly message: string
432
+ readonly providerCode?: string
433
+ readonly fallbackKind?: ProviderFailureKind
434
+ }) => {
435
+ const provider = classifyProviderFailure({
436
+ provider: openAiCodexProvider,
437
+ message: input.message,
438
+ ...(input.providerCode === undefined ? {} : { providerCode: input.providerCode }),
439
+ ...(input.fallbackKind === undefined ? {} : { fallbackKind: input.fallbackKind })
440
+ })
411
441
 
412
- const isRecord = (value: unknown): value is Record<string, unknown> =>
413
- typeof value === 'object' && value !== null
442
+ return new LLMError({
443
+ cause: providerFailureCause(provider.kind),
444
+ message: `OpenAI Codex stream error: ${input.message}`,
445
+ retryable: providerFailureRetryable(provider.kind),
446
+ provider
447
+ })
448
+ }
414
449
 
415
450
  const parseToolArguments = (raw: string) =>
416
451
  decodeJsonString(raw, 'Invalid OpenAI Codex tool arguments JSON')
@@ -811,12 +846,26 @@ const processSseData = (
811
846
  }
812
847
  }
813
848
 
849
+ if (parsed.type === 'response.failed') {
850
+ const error = recordField(recordField(parsed, 'response'), 'error')
851
+ const message = stringField(error, 'message') ?? 'OpenAI Codex response failed'
852
+ const providerCode = stringField(error, 'code') ?? stringField(error, 'type')
853
+
854
+ return yield* Effect.fail(
855
+ providerSignalError({
856
+ message,
857
+ ...(providerCode === undefined ? {} : { providerCode })
858
+ })
859
+ )
860
+ }
861
+
814
862
  if (parsed.type === 'error' && typeof parsed.message === 'string') {
863
+ const providerCode = stringField(parsed, 'code') ?? stringField(parsed, 'type')
864
+
815
865
  return yield* Effect.fail(
816
- new LLMError({
817
- cause: 'provider_error',
818
- message: `OpenAI Codex stream error: ${parsed.message}`,
819
- retryable: false
866
+ providerSignalError({
867
+ message: parsed.message,
868
+ ...(providerCode === undefined ? {} : { providerCode })
820
869
  })
821
870
  )
822
871
  }
@@ -915,11 +964,16 @@ const finalizeBodyState = (
915
964
  })
916
965
 
917
966
  const toHttpClientLlmError =
918
- (message: string, retryable: boolean) => (error: HttpClientError.HttpClientError) =>
967
+ (message: string, retryable: boolean, kind: ProviderFailureKind = 'network') =>
968
+ (error: HttpClientError.HttpClientError) =>
919
969
  new LLMError({
920
970
  cause: 'provider_error',
921
971
  message: `${message}: ${error.message}`,
922
- retryable
972
+ retryable,
973
+ provider: providerErrorInfo({
974
+ provider: openAiCodexProvider,
975
+ kind: retryable ? kind : 'unknown'
976
+ })
923
977
  })
924
978
 
925
979
  export const streamOpenAiCodexResponse = (
@@ -929,7 +983,7 @@ export const streamOpenAiCodexResponse = (
929
983
  Ref.make(initialBodyState).pipe(
930
984
  Effect.map(bodyStateRef => {
931
985
  const chunks = response.stream.pipe(
932
- Stream.mapError(toHttpClientLlmError('Could not read OpenAI Codex stream', false)),
986
+ Stream.mapError(toHttpClientLlmError('Could not read OpenAI Codex stream', true, 'stream')),
933
987
  Stream.decodeText,
934
988
  Stream.mapEffect(chunk =>
935
989
  Effect.gen(function* () {
@@ -987,11 +1041,19 @@ const sendOpenAiCodexRequest = (
987
1041
  )
988
1042
  )
989
1043
 
1044
+ const provider = classifyProviderFailure({
1045
+ provider: openAiCodexProvider,
1046
+ status: response.status,
1047
+ headers: response.headers,
1048
+ body: errorText
1049
+ })
1050
+
990
1051
  return yield* Effect.fail(
991
1052
  new LLMError({
992
- cause: responseStatusToCause(response.status),
993
- message: `OpenAI Codex returned ${response.status}: ${errorText}`,
994
- retryable: isRetryableStatus(response.status)
1053
+ cause: providerFailureCause(provider.kind),
1054
+ message: `OpenAI Codex returned ${response.status}`,
1055
+ retryable: providerFailureRetryable(provider.kind),
1056
+ provider
995
1057
  })
996
1058
  )
997
1059
  }
@@ -13,8 +13,10 @@ import {
13
13
  AgentOutputUsage,
14
14
  AgentUsage,
15
15
  attachmentSourceDataUrl,
16
+ attachmentSourceText,
16
17
  assistantContent,
17
18
  assistantHostToolCalls,
19
+ isTextDocumentMimeType,
18
20
  messageContextText,
19
21
  prependMessageContextToContent,
20
22
  type AgentMessage,
@@ -32,6 +34,12 @@ import {
32
34
  type LLMEvent,
33
35
  type LLMRequest
34
36
  } from '@yolk-sdk/agent/loop'
37
+ import {
38
+ classifyProviderFailure,
39
+ providerErrorInfo,
40
+ providerFailureCause,
41
+ providerFailureRetryable
42
+ } from '../provider-error.ts'
35
43
 
36
44
  export type OpenAiProviderConfig = {
37
45
  readonly chatCompletionsUrl?: string
@@ -195,6 +203,24 @@ const unsupportedContentError = (contentType: string) =>
195
203
  retryable: false
196
204
  })
197
205
 
206
+ const textDocumentToOpenAiPart = (part: Extract<ContentPart, { readonly _tag: 'Document' }>) =>
207
+ attachmentSourceText(part.source).pipe(
208
+ Effect.mapError(() => unsupportedContentError('Invalid document text')),
209
+ Effect.flatMap(
210
+ Option.match({
211
+ onNone: () => Effect.fail(unsupportedContentError('Unresolved document source')),
212
+ onSome: text => {
213
+ const block: OpenAiTextContentPart = {
214
+ type: 'text',
215
+ text: `Document: ${part.title ?? part.filename}\n\n${text}`
216
+ }
217
+
218
+ return Effect.succeed(block)
219
+ }
220
+ })
221
+ )
222
+ )
223
+
198
224
  const contentPartToUserPart = (
199
225
  part: ContentPart
200
226
  ): Effect.Effect<OpenAiTextContentPart | OpenAiImageContentPart, LLMError> => {
@@ -207,7 +233,9 @@ const contentPartToUserPart = (
207
233
  onSome: url => Effect.succeed({ type: 'image_url', image_url: { url } })
208
234
  })
209
235
  case 'Document':
210
- return Effect.fail(unsupportedContentError('Document'))
236
+ return isTextDocumentMimeType(part.mimeType)
237
+ ? textDocumentToOpenAiPart(part)
238
+ : Effect.fail(unsupportedContentError('Document'))
211
239
  case 'Audio':
212
240
  return Effect.fail(unsupportedContentError('Audio'))
213
241
  }
@@ -333,20 +361,6 @@ export const toOpenAiRequestBody = (
333
361
  }
334
362
  })
335
363
 
336
- const responseStatusToCause = (status: number): LLMError['cause'] => {
337
- if (status === 429) {
338
- return 'rate_limit'
339
- }
340
-
341
- if (status === 413) {
342
- return 'context_overflow'
343
- }
344
-
345
- return 'provider_error'
346
- }
347
-
348
- const isRetryableStatus = (status: number) => status === 429 || status >= 500
349
-
350
364
  const parseToolArguments = (raw: string) =>
351
365
  decodeJsonString(raw, 'Invalid OpenAI tool arguments JSON')
352
366
 
@@ -396,7 +410,11 @@ const toHttpClientLlmError =
396
410
  new LLMError({
397
411
  cause: 'provider_error',
398
412
  message: `${message}: ${error.message}`,
399
- retryable
413
+ retryable,
414
+ provider: providerErrorInfo({
415
+ provider: 'openai',
416
+ kind: retryable ? 'network' : 'unknown'
417
+ })
400
418
  })
401
419
 
402
420
  const parseOpenAiResponseJson = (
@@ -447,11 +465,19 @@ const sendOpenAiRequest = (
447
465
  )
448
466
  )
449
467
 
468
+ const provider = classifyProviderFailure({
469
+ provider: 'openai',
470
+ status: response.status,
471
+ headers: response.headers,
472
+ body: errorText
473
+ })
474
+
450
475
  return yield* Effect.fail(
451
476
  new LLMError({
452
- cause: responseStatusToCause(response.status),
453
- message: `OpenAI returned ${response.status}: ${errorText}`,
454
- retryable: isRetryableStatus(response.status)
477
+ cause: providerFailureCause(provider.kind),
478
+ message: `OpenAI returned ${response.status}`,
479
+ retryable: providerFailureRetryable(provider.kind),
480
+ provider
455
481
  })
456
482
  )
457
483
  }
@@ -0,0 +1,213 @@
1
+ import {
2
+ ProviderErrorInfo,
3
+ type AgentErrorCode,
4
+ type ProviderFailureKind
5
+ } from '@yolk-sdk/agent/protocol'
6
+
7
+ type HeaderMap = Readonly<Record<string, string>>
8
+
9
+ type ProviderFailureInput = {
10
+ readonly provider: string
11
+ readonly status?: number
12
+ readonly headers?: HeaderMap
13
+ readonly body?: string
14
+ readonly message?: string
15
+ readonly providerCode?: string
16
+ readonly fallbackKind?: ProviderFailureKind
17
+ }
18
+
19
+ type ProviderErrorInfoInput = {
20
+ readonly provider: string
21
+ readonly kind: ProviderFailureKind
22
+ readonly status?: number
23
+ readonly providerCode?: string
24
+ readonly retryAfterMs?: number
25
+ }
26
+
27
+ export type ProviderLlmErrorCause = Extract<
28
+ AgentErrorCode,
29
+ 'provider_error' | 'rate_limit' | 'overloaded' | 'context_overflow' | 'invalid_response'
30
+ >
31
+
32
+ const numericDelayMs = (value: number) =>
33
+ Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined
34
+
35
+ const headerValue = (headers: HeaderMap, name: string) => {
36
+ const lowerName = name.toLowerCase()
37
+
38
+ for (const [key, value] of Object.entries(headers)) {
39
+ if (key.toLowerCase() === lowerName) {
40
+ return value
41
+ }
42
+ }
43
+
44
+ return undefined
45
+ }
46
+
47
+ export const parseRetryAfterMs = (value: string | undefined) => {
48
+ if (value === undefined) {
49
+ return undefined
50
+ }
51
+
52
+ return numericDelayMs(Number(value.trim()))
53
+ }
54
+
55
+ export const parseRetryAfter = (value: string | undefined) => {
56
+ if (value === undefined) {
57
+ return undefined
58
+ }
59
+
60
+ const trimmed = value.trim()
61
+ const seconds = Number(trimmed)
62
+ const secondsDelay = numericDelayMs(seconds * 1000)
63
+
64
+ if (secondsDelay !== undefined) {
65
+ return secondsDelay
66
+ }
67
+
68
+ const timestamp = Date.parse(trimmed)
69
+
70
+ if (Number.isNaN(timestamp)) {
71
+ return undefined
72
+ }
73
+
74
+ return numericDelayMs(timestamp - Date.now())
75
+ }
76
+
77
+ export const retryAfterMsFromHeaders = (headers: HeaderMap | undefined) => {
78
+ if (headers === undefined) {
79
+ return undefined
80
+ }
81
+
82
+ return parseRetryAfterMs(headerValue(headers, 'retry-after-ms')) ??
83
+ parseRetryAfter(headerValue(headers, 'retry-after'))
84
+ }
85
+
86
+ const kindFromStatus = (status: number | undefined): ProviderFailureKind | undefined => {
87
+ if (status === undefined) {
88
+ return undefined
89
+ }
90
+
91
+ if (status === 429) {
92
+ return 'rate_limit'
93
+ }
94
+
95
+ if (status === 529) {
96
+ return 'overloaded'
97
+ }
98
+
99
+ if (status === 413) {
100
+ return 'context_overflow'
101
+ }
102
+
103
+ if (status === 401 || status === 403) {
104
+ return 'auth'
105
+ }
106
+
107
+ if (status >= 500) {
108
+ return 'server_error'
109
+ }
110
+
111
+ return undefined
112
+ }
113
+
114
+ const normalizedSignal = (input: ProviderFailureInput) =>
115
+ [input.providerCode, input.message, input.body]
116
+ .filter(value => value !== undefined)
117
+ .join(' ')
118
+ .toLowerCase()
119
+
120
+ const kindFromSignal = (input: ProviderFailureInput): ProviderFailureKind | undefined => {
121
+ const signal = normalizedSignal(input)
122
+
123
+ if (
124
+ signal.includes('rate_limit') ||
125
+ signal.includes('rate limit') ||
126
+ signal.includes('too_many_requests') ||
127
+ signal.includes('too many request')
128
+ ) {
129
+ return 'rate_limit'
130
+ }
131
+
132
+ if (
133
+ signal.includes('overloaded_error') ||
134
+ signal.includes('overloaded') ||
135
+ signal.includes('overload') ||
136
+ signal.includes('service unavailable')
137
+ ) {
138
+ return 'overloaded'
139
+ }
140
+
141
+ if (
142
+ signal.includes('context_length') ||
143
+ signal.includes('context length') ||
144
+ signal.includes('context_overflow') ||
145
+ signal.includes('context overflow')
146
+ ) {
147
+ return 'context_overflow'
148
+ }
149
+
150
+ return undefined
151
+ }
152
+
153
+ export const providerFailureKind = (input: ProviderFailureInput): ProviderFailureKind =>
154
+ kindFromSignal(input) ??
155
+ kindFromStatus(input.status) ??
156
+ input.fallbackKind ??
157
+ 'unknown'
158
+
159
+ export const providerFailureRetryable = (kind: ProviderFailureKind) => {
160
+ switch (kind) {
161
+ case 'rate_limit':
162
+ case 'overloaded':
163
+ case 'server_error':
164
+ case 'network':
165
+ case 'stream':
166
+ return true
167
+ case 'auth':
168
+ case 'context_overflow':
169
+ case 'invalid_response':
170
+ case 'unknown':
171
+ return false
172
+ }
173
+ }
174
+
175
+ export const providerFailureCause = (kind: ProviderFailureKind): ProviderLlmErrorCause => {
176
+ switch (kind) {
177
+ case 'rate_limit':
178
+ return 'rate_limit'
179
+ case 'overloaded':
180
+ return 'overloaded'
181
+ case 'context_overflow':
182
+ return 'context_overflow'
183
+ case 'invalid_response':
184
+ return 'invalid_response'
185
+ case 'auth':
186
+ case 'network':
187
+ case 'server_error':
188
+ case 'stream':
189
+ case 'unknown':
190
+ return 'provider_error'
191
+ }
192
+ }
193
+
194
+ export const providerErrorInfo = (input: ProviderErrorInfoInput) =>
195
+ ProviderErrorInfo.make({
196
+ provider: input.provider,
197
+ kind: input.kind,
198
+ ...(input.status === undefined ? {} : { status: input.status }),
199
+ ...(input.providerCode === undefined ? {} : { providerCode: input.providerCode }),
200
+ ...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
201
+ })
202
+
203
+ export const classifyProviderFailure = (input: ProviderFailureInput) => {
204
+ const kind = providerFailureKind(input)
205
+
206
+ return providerErrorInfo({
207
+ provider: input.provider,
208
+ kind,
209
+ ...(input.status === undefined ? {} : { status: input.status }),
210
+ ...(input.providerCode === undefined ? {} : { providerCode: input.providerCode }),
211
+ ...(input.headers === undefined ? {} : { retryAfterMs: retryAfterMsFromHeaders(input.headers) })
212
+ })
213
+ }
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  hitlResponseEvent,
3
+ type AgentError,
3
4
  type AgentEvent,
4
5
  type AgentMessage,
6
+ type AgentRetry,
5
7
  type Content,
6
8
  type HitlResponse,
7
9
  type UserMessage
@@ -31,6 +33,8 @@ export type AgentRunStatus = 'idle' | 'running' | 'waiting' | 'done' | 'error' |
31
33
  export type AgentChatState = {
32
34
  readonly status: AgentRunStatus
33
35
  readonly error: string | null
36
+ readonly errorInfo: AgentError | null
37
+ readonly retryInfo: AgentRetry | null
34
38
  readonly chatMessages: ReadonlyArray<AgentChatMessage>
35
39
  readonly sessionEvents: ReadonlyArray<AgentChatSessionEvent>
36
40
  readonly seenEventIds: ReadonlyArray<string>
@@ -39,6 +43,8 @@ export type AgentChatState = {
39
43
  export const initialAgentChatState: AgentChatState = {
40
44
  status: 'idle',
41
45
  error: null,
46
+ errorInfo: null,
47
+ retryInfo: null,
42
48
  chatMessages: [],
43
49
  sessionEvents: [],
44
50
  seenEventIds: []
@@ -52,6 +58,9 @@ const rememberEvent = (state: AgentChatState, event: AgentEvent): AgentChatState
52
58
  ? state
53
59
  : { ...state, seenEventIds: [...state.seenEventIds, event.eventId] }
54
60
 
61
+ const clearRetryInfo = (state: AgentChatState): AgentChatState =>
62
+ state.retryInfo === null ? state : { ...state, retryInfo: null }
63
+
55
64
  export type AgentChatAction =
56
65
  | { readonly _tag: 'HydrateMessage'; readonly message: AgentMessage }
57
66
  | { readonly _tag: 'Submit'; readonly message: UserMessage }
@@ -73,13 +82,17 @@ export const reduceAgentChatState = (
73
82
  return {
74
83
  ...state,
75
84
  chatMessages: appendProtocolMessage(state.chatMessages, action.message),
76
- error: null
85
+ error: null,
86
+ errorInfo: null,
87
+ retryInfo: null
77
88
  }
78
89
  case 'Submit':
79
90
  return {
80
91
  ...state,
81
92
  status: 'running',
82
93
  error: null,
94
+ errorInfo: null,
95
+ retryInfo: null,
83
96
  seenEventIds: [],
84
97
  chatMessages: appendProtocolMessage(state.chatMessages, action.message),
85
98
  sessionEvents: [
@@ -92,6 +105,8 @@ export const reduceAgentChatState = (
92
105
  ...state,
93
106
  chatMessages: appendProtocolMessage(state.chatMessages, action.message),
94
107
  error: null,
108
+ errorInfo: null,
109
+ retryInfo: null,
95
110
  sessionEvents: [
96
111
  ...state.sessionEvents,
97
112
  ProtocolMessageAppended.make({ message: action.message })
@@ -102,6 +117,8 @@ export const reduceAgentChatState = (
102
117
  ...state,
103
118
  status: 'running',
104
119
  error: null,
120
+ errorInfo: null,
121
+ retryInfo: null,
105
122
  seenEventIds: [],
106
123
  chatMessages: applyAgentEventToChatMessages(
107
124
  state.chatMessages,
@@ -118,6 +135,8 @@ export const reduceAgentChatState = (
118
135
  return {
119
136
  ...state,
120
137
  error: null,
138
+ errorInfo: null,
139
+ retryInfo: null,
121
140
  chatMessages: next.messages,
122
141
  sessionEvents: [
123
142
  ...state.sessionEvents,
@@ -139,6 +158,8 @@ export const reduceAgentChatState = (
139
158
  ...state,
140
159
  status: 'running',
141
160
  error: null,
161
+ errorInfo: null,
162
+ retryInfo: null,
142
163
  seenEventIds: [],
143
164
  chatMessages: next.messages,
144
165
  sessionEvents: [
@@ -161,6 +182,8 @@ export const reduceAgentChatState = (
161
182
  ...state,
162
183
  status: 'running',
163
184
  error: null,
185
+ errorInfo: null,
186
+ retryInfo: null,
164
187
  seenEventIds: [],
165
188
  chatMessages: next.messages,
166
189
  sessionEvents: [
@@ -185,6 +208,8 @@ export const reduceAgentChatState = (
185
208
  ...state,
186
209
  status: 'running',
187
210
  error: null,
211
+ errorInfo: null,
212
+ retryInfo: null,
188
213
  chatMessages: applyAgentEventToChatMessages(state.chatMessages, action.event, action)
189
214
  },
190
215
  action.event
@@ -195,6 +220,8 @@ export const reduceAgentChatState = (
195
220
  ...state,
196
221
  status: 'error',
197
222
  error: action.event.message,
223
+ errorInfo: action.event,
224
+ retryInfo: null,
198
225
  chatMessages: applyAgentEventToChatMessages(state.chatMessages, action.event, action)
199
226
  },
200
227
  action.event
@@ -205,6 +232,8 @@ export const reduceAgentChatState = (
205
232
  ...state,
206
233
  status: 'done',
207
234
  error: null,
235
+ errorInfo: null,
236
+ retryInfo: null,
208
237
  chatMessages: applyAgentEventToChatMessages(state.chatMessages, action.event, action)
209
238
  },
210
239
  action.event
@@ -215,13 +244,23 @@ export const reduceAgentChatState = (
215
244
  ...state,
216
245
  status: 'waiting',
217
246
  error: null,
247
+ errorInfo: null,
248
+ retryInfo: null,
218
249
  chatMessages: applyAgentEventToChatMessages(state.chatMessages, action.event, action)
219
250
  },
220
251
  action.event
221
252
  )
222
- case 'AssistantMessage':
223
253
  case 'AgentRetry':
254
+ return rememberEvent(
255
+ {
256
+ ...state,
257
+ retryInfo: action.event,
258
+ chatMessages: applyAgentEventToChatMessages(state.chatMessages, action.event, action)
259
+ },
260
+ action.event
261
+ )
224
262
  case 'CompactionEnd':
263
+ case 'AssistantMessage':
225
264
  case 'CompactionStart':
226
265
  case 'LLMReasoningDelta':
227
266
  case 'LLMStreamEnd':
@@ -246,10 +285,10 @@ export const reduceAgentChatState = (
246
285
  case 'TurnStart':
247
286
  case 'UsageUpdate':
248
287
  return rememberEvent(
249
- {
288
+ clearRetryInfo({
250
289
  ...state,
251
290
  chatMessages: applyAgentEventToChatMessages(state.chatMessages, action.event, action)
252
- },
291
+ }),
253
292
  action.event
254
293
  )
255
294
  }
@@ -259,11 +298,13 @@ export const reduceAgentChatState = (
259
298
  ...state,
260
299
  status: 'error',
261
300
  error: action.message,
301
+ errorInfo: null,
302
+ retryInfo: null,
262
303
  chatMessages: markChatError(state.chatMessages, action.message)
263
304
  }
264
305
  }
265
306
  case 'Abort':
266
- return { ...state, status: 'aborted', error: null }
307
+ return { ...state, status: 'aborted', error: null, errorInfo: null, retryInfo: null }
267
308
  }
268
309
  }
269
310