@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.
- package/README.md +25 -1
- package/dist/client/attachments.d.mts +8 -0
- package/dist/client/attachments.d.mts.map +1 -0
- package/dist/client/attachments.mjs +36 -0
- package/dist/client/attachments.mjs.map +1 -0
- package/dist/client/index.d.mts +2 -1
- package/dist/client/index.mjs +2 -1
- package/dist/client/state.d.mts +4 -2
- package/dist/client/state.d.mts.map +1 -1
- package/dist/client/state.mjs +42 -22
- package/dist/client/state.mjs.map +1 -1
- package/dist/client/transport.d.mts +1 -1
- package/dist/client/transport.mjs +1 -1
- package/dist/compaction/transformer.d.mts +1 -1
- package/dist/loop/error.d.mts +3 -2
- package/dist/loop/error.d.mts.map +1 -1
- package/dist/loop/error.mjs +6 -3
- package/dist/loop/error.mjs.map +1 -1
- package/dist/loop/llm-event.d.mts +1 -1
- package/dist/loop/llm-event.mjs +1 -1
- package/dist/loop/run.d.mts +1 -1
- package/dist/loop/run.d.mts.map +1 -1
- package/dist/loop/run.mjs +18 -4
- package/dist/loop/run.mjs.map +1 -1
- package/dist/loop/services/context-transformer.d.mts +1 -1
- package/dist/loop/services/llm-provider.d.mts +1 -1
- package/dist/loop/services/loop-config.mjs +1 -1
- package/dist/loop/services/loop-config.mjs.map +1 -1
- package/dist/loop/services/tool-executor.d.mts +1 -1
- package/dist/loop/testing/faux-provider.mjs +1 -1
- package/dist/loop/testing/test-tool-executor.mjs +1 -1
- package/dist/protocol/content.d.mts +15 -1
- package/dist/protocol/content.d.mts.map +1 -1
- package/dist/protocol/content.mjs +76 -1
- package/dist/protocol/content.mjs.map +1 -1
- package/dist/protocol/event.d.mts +17 -5
- package/dist/protocol/event.d.mts.map +1 -1
- package/dist/protocol/event.mjs +24 -3
- package/dist/protocol/event.mjs.map +1 -1
- package/dist/protocol/index.d.mts +3 -3
- package/dist/protocol/index.d.mts.map +1 -1
- package/dist/protocol/index.mjs +3 -3
- package/dist/providers/anthropic/claude-provider.d.mts.map +1 -1
- package/dist/providers/anthropic/claude-provider.mjs +48 -17
- package/dist/providers/anthropic/claude-provider.mjs.map +1 -1
- package/dist/providers/openai/codex-provider.d.mts +1 -1
- package/dist/providers/openai/codex-provider.d.mts.map +1 -1
- package/dist/providers/openai/codex-provider.mjs +60 -18
- package/dist/providers/openai/codex-provider.mjs.map +1 -1
- package/dist/providers/openai/provider.d.mts.map +1 -1
- package/dist/providers/openai/provider.mjs +28 -12
- package/dist/providers/openai/provider.mjs.map +1 -1
- package/dist/providers/provider-error.d.mts +32 -0
- package/dist/providers/provider-error.d.mts.map +1 -0
- package/dist/providers/provider-error.mjs +91 -0
- package/dist/providers/provider-error.mjs.map +1 -0
- package/dist/react/chat-core.d.mts +3 -1
- package/dist/react/chat-core.d.mts.map +1 -1
- package/dist/react/chat-core.mjs +42 -6
- package/dist/react/chat-core.mjs.map +1 -1
- package/dist/react/chat-items.d.mts +8 -2
- package/dist/react/chat-items.d.mts.map +1 -1
- package/dist/react/chat-items.mjs +17 -10
- package/dist/react/chat-items.mjs.map +1 -1
- package/dist/react/chat-messages.mjs +1 -1
- package/dist/react/chat-session-events.d.mts +1 -1
- package/dist/react/chat-session-events.mjs +1 -1
- package/dist/react/use-agent-chat.mjs +1 -1
- package/dist/runtime/error.d.mts +1 -1
- package/dist/runtime/error.mjs +1 -1
- package/dist/runtime/run-runtime.d.mts +1 -1
- package/dist/runtime/session-event-store.d.mts +1 -1
- package/dist/runtime/session-event-store.mjs +1 -1
- package/dist/tools/question.d.mts +1 -1
- package/dist/tools/question.mjs +1 -1
- package/dist/tools/registry.d.mts +1 -1
- package/dist/tools/registry.mjs +1 -1
- package/dist/tools/task.d.mts +1 -1
- package/dist/tools/task.mjs +1 -1
- package/dist/voice/tool-bridge.mjs +1 -1
- package/package.json +1 -1
- package/src/client/attachments.ts +45 -0
- package/src/client/index.ts +1 -0
- package/src/client/state.ts +43 -18
- package/src/loop/error.ts +6 -3
- package/src/loop/run.ts +27 -4
- package/src/loop/services/loop-config.ts +1 -1
- package/src/protocol/content.ts +129 -0
- package/src/protocol/event.ts +26 -2
- package/src/protocol/index.ts +8 -0
- package/src/providers/anthropic/claude-provider.ts +74 -23
- package/src/providers/openai/codex-provider.ts +81 -19
- package/src/providers/openai/provider.ts +45 -19
- package/src/providers/provider-error.ts +213 -0
- package/src/react/chat-core.ts +46 -5
- 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
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
423
|
+
const field = Object.getOwnPropertyDescriptor(value, key)?.value
|
|
424
|
+
|
|
425
|
+
return isRecord(field) ? field : undefined
|
|
408
426
|
}
|
|
409
427
|
|
|
410
|
-
const
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
|
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',
|
|
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:
|
|
993
|
-
message: `OpenAI Codex returned ${response.status}
|
|
994
|
-
retryable:
|
|
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
|
|
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:
|
|
453
|
-
message: `OpenAI returned ${response.status}
|
|
454
|
-
retryable:
|
|
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
|
+
}
|
package/src/react/chat-core.ts
CHANGED
|
@@ -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
|
|