@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
package/src/client/state.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AgentError,
|
|
2
3
|
type AgentEvent,
|
|
3
4
|
type AgentMessage,
|
|
5
|
+
type AgentRetry,
|
|
4
6
|
type HitlRequest,
|
|
5
7
|
type QuestionRequest,
|
|
6
8
|
type QuestionResponse,
|
|
@@ -63,6 +65,8 @@ export type AgentClientState = {
|
|
|
63
65
|
readonly reasoning: string
|
|
64
66
|
readonly toolRuns: ReadonlyArray<AgentToolRun>
|
|
65
67
|
readonly error: string | null
|
|
68
|
+
readonly errorInfo: AgentError | null
|
|
69
|
+
readonly retryInfo: AgentRetry | null
|
|
66
70
|
readonly seenEventIds: ReadonlyArray<string>
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -80,9 +84,14 @@ export const initialAgentClientState: AgentClientState = {
|
|
|
80
84
|
reasoning: '',
|
|
81
85
|
toolRuns: [],
|
|
82
86
|
error: null,
|
|
87
|
+
errorInfo: null,
|
|
88
|
+
retryInfo: null,
|
|
83
89
|
seenEventIds: []
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
const clearRetryInfo = (state: AgentClientState): AgentClientState =>
|
|
93
|
+
state.retryInfo === null ? state : { ...state, retryInfo: null }
|
|
94
|
+
|
|
86
95
|
const hasSeenEvent = (state: AgentClientState, event: AgentEvent) =>
|
|
87
96
|
event.eventId !== undefined && state.seenEventIds.includes(event.eventId)
|
|
88
97
|
|
|
@@ -260,16 +269,18 @@ const applyAgentEventUnchecked = (
|
|
|
260
269
|
reasoning: '',
|
|
261
270
|
liveMessages: [],
|
|
262
271
|
toolRuns: completedToolRuns(state.toolRuns),
|
|
263
|
-
error: null
|
|
272
|
+
error: null,
|
|
273
|
+
errorInfo: null,
|
|
274
|
+
retryInfo: null
|
|
264
275
|
}
|
|
265
276
|
case 'AgentError':
|
|
266
|
-
return markAgentError(state, event.message)
|
|
277
|
+
return markAgentError(state, event.message, event)
|
|
267
278
|
case 'LLMTextDelta':
|
|
268
|
-
return { ...state, text: `${state.text}${event.text}` }
|
|
279
|
+
return clearRetryInfo({ ...state, text: `${state.text}${event.text}` })
|
|
269
280
|
case 'LLMReasoningDelta':
|
|
270
|
-
return { ...state, reasoning: `${state.reasoning}${event.text}` }
|
|
281
|
+
return clearRetryInfo({ ...state, reasoning: `${state.reasoning}${event.text}` })
|
|
271
282
|
case 'ToolInputStart':
|
|
272
|
-
return {
|
|
283
|
+
return clearRetryInfo({
|
|
273
284
|
...state,
|
|
274
285
|
toolRuns: replaceToolRun(state.toolRuns, {
|
|
275
286
|
_tag: 'InputStreaming',
|
|
@@ -277,14 +288,17 @@ const applyAgentEventUnchecked = (
|
|
|
277
288
|
name: event.name,
|
|
278
289
|
input: ''
|
|
279
290
|
})
|
|
280
|
-
}
|
|
291
|
+
})
|
|
281
292
|
case 'ToolInputDelta':
|
|
282
|
-
return {
|
|
293
|
+
return clearRetryInfo({
|
|
294
|
+
...state,
|
|
295
|
+
toolRuns: appendToolInputDelta(state.toolRuns, event.id, event.delta)
|
|
296
|
+
})
|
|
283
297
|
case 'ToolInputEnd':
|
|
284
|
-
return {
|
|
298
|
+
return clearRetryInfo({
|
|
285
299
|
...state,
|
|
286
300
|
toolRuns: replaceToolRun(state.toolRuns, { _tag: 'InputReady', call: event.call })
|
|
287
|
-
}
|
|
301
|
+
})
|
|
288
302
|
case 'ToolApprovalRequested':
|
|
289
303
|
return {
|
|
290
304
|
...state,
|
|
@@ -370,21 +384,21 @@ const applyAgentEventUnchecked = (
|
|
|
370
384
|
})
|
|
371
385
|
}
|
|
372
386
|
case 'ProviderToolResult':
|
|
373
|
-
return {
|
|
387
|
+
return clearRetryInfo({
|
|
374
388
|
...state,
|
|
375
389
|
toolRuns: replaceToolRun(state.toolRuns, {
|
|
376
390
|
_tag: 'ProviderCompleted',
|
|
377
391
|
call: event.call,
|
|
378
392
|
result: event.result
|
|
379
393
|
})
|
|
380
|
-
}
|
|
394
|
+
})
|
|
381
395
|
case 'AssistantMessage':
|
|
382
|
-
return {
|
|
396
|
+
return clearRetryInfo({
|
|
383
397
|
...state,
|
|
384
398
|
liveMessages: [...state.liveMessages, event.message],
|
|
385
399
|
text: '',
|
|
386
400
|
reasoning: ''
|
|
387
|
-
}
|
|
401
|
+
})
|
|
388
402
|
case 'AgentEnd':
|
|
389
403
|
return {
|
|
390
404
|
...state,
|
|
@@ -393,7 +407,8 @@ const applyAgentEventUnchecked = (
|
|
|
393
407
|
liveMessages: [],
|
|
394
408
|
text: '',
|
|
395
409
|
reasoning: '',
|
|
396
|
-
toolRuns: completedToolRuns(state.toolRuns)
|
|
410
|
+
toolRuns: completedToolRuns(state.toolRuns),
|
|
411
|
+
retryInfo: null
|
|
397
412
|
}
|
|
398
413
|
case 'AgentAwaitingInput':
|
|
399
414
|
return {
|
|
@@ -403,9 +418,12 @@ const applyAgentEventUnchecked = (
|
|
|
403
418
|
liveMessages: [],
|
|
404
419
|
text: '',
|
|
405
420
|
reasoning: '',
|
|
406
|
-
error: null
|
|
421
|
+
error: null,
|
|
422
|
+
errorInfo: null,
|
|
423
|
+
retryInfo: null
|
|
407
424
|
}
|
|
408
425
|
case 'AgentRetry':
|
|
426
|
+
return { ...state, retryInfo: event }
|
|
409
427
|
case 'CompactionEnd':
|
|
410
428
|
case 'CompactionStart':
|
|
411
429
|
case 'LLMStreamEnd':
|
|
@@ -431,24 +449,31 @@ export const submitAgentUserMessage = (
|
|
|
431
449
|
reasoning: '',
|
|
432
450
|
toolRuns: completedToolRuns(state.toolRuns),
|
|
433
451
|
error: null,
|
|
452
|
+
errorInfo: null,
|
|
453
|
+
retryInfo: null,
|
|
434
454
|
seenEventIds: []
|
|
435
455
|
})
|
|
436
456
|
|
|
437
457
|
export const markAgentError = (
|
|
438
458
|
state: AgentClientState,
|
|
439
|
-
message = 'Agent request failed'
|
|
459
|
+
message = 'Agent request failed',
|
|
460
|
+
errorInfo: AgentError | null = null
|
|
440
461
|
): AgentClientState => ({
|
|
441
462
|
...state,
|
|
442
463
|
status: 'error',
|
|
443
464
|
toolRuns: completedToolRuns(state.toolRuns),
|
|
444
|
-
error: message
|
|
465
|
+
error: message,
|
|
466
|
+
errorInfo,
|
|
467
|
+
retryInfo: null
|
|
445
468
|
})
|
|
446
469
|
|
|
447
470
|
export const markAgentAborted = (state: AgentClientState): AgentClientState => ({
|
|
448
471
|
...state,
|
|
449
472
|
status: 'aborted',
|
|
450
473
|
toolRuns: completedToolRuns(state.toolRuns),
|
|
451
|
-
error: null
|
|
474
|
+
error: null,
|
|
475
|
+
errorInfo: null,
|
|
476
|
+
retryInfo: null
|
|
452
477
|
})
|
|
453
478
|
|
|
454
479
|
export const reduceAgentEvents = (
|
package/src/loop/error.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import * as Schema from 'effect/Schema'
|
|
2
|
-
import { AgentError, type AgentErrorCode } from '@yolk-sdk/agent/protocol'
|
|
2
|
+
import { AgentError, ProviderErrorInfo, type AgentErrorCode } from '@yolk-sdk/agent/protocol'
|
|
3
3
|
|
|
4
4
|
export class LLMError extends Schema.TaggedErrorClass<LLMError>()('LLMError', {
|
|
5
5
|
cause: Schema.Literals([
|
|
6
6
|
'validation_error',
|
|
7
7
|
'provider_error',
|
|
8
8
|
'rate_limit',
|
|
9
|
+
'overloaded',
|
|
9
10
|
'context_overflow',
|
|
10
11
|
'invalid_response'
|
|
11
12
|
]),
|
|
12
13
|
message: Schema.String,
|
|
13
|
-
retryable: Schema.Boolean
|
|
14
|
+
retryable: Schema.Boolean,
|
|
15
|
+
provider: Schema.optional(ProviderErrorInfo)
|
|
14
16
|
}) {}
|
|
15
17
|
|
|
16
18
|
export class FauxExhaustedError extends Schema.TaggedErrorClass<FauxExhaustedError>()(
|
|
@@ -75,7 +77,8 @@ export const agentLoopErrorToAgentError = (error: AgentLoopError): AgentError =>
|
|
|
75
77
|
return AgentError.make({
|
|
76
78
|
code: error.cause,
|
|
77
79
|
message: error.message,
|
|
78
|
-
retryable: error.retryable
|
|
80
|
+
retryable: error.retryable,
|
|
81
|
+
...(error.provider === undefined ? {} : { provider: error.provider })
|
|
79
82
|
})
|
|
80
83
|
case 'ToolError':
|
|
81
84
|
return AgentError.make({
|
package/src/loop/run.ts
CHANGED
|
@@ -292,8 +292,30 @@ type TurnStreamInput = {
|
|
|
292
292
|
readonly turn: number
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const
|
|
296
|
-
|
|
295
|
+
const maxUnhintedRetryDelayMs = 30_000
|
|
296
|
+
const maxHintedRetryDelayMs = 2_147_483_647
|
|
297
|
+
|
|
298
|
+
const validDelayMs = (delayMs: number) =>
|
|
299
|
+
Number.isFinite(delayMs) && delayMs >= 0 ? Math.floor(delayMs) : undefined
|
|
300
|
+
|
|
301
|
+
const hintedRetryDelayMs = (error: LLMError) => {
|
|
302
|
+
const delayMs = error.provider?.retryAfterMs
|
|
303
|
+
const validDelay = delayMs === undefined ? undefined : validDelayMs(delayMs)
|
|
304
|
+
|
|
305
|
+
return validDelay === undefined ? undefined : Math.min(validDelay, maxHintedRetryDelayMs)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const retryDelayMs = (baseDelayMs: number, attempt: number, error: LLMError) => {
|
|
309
|
+
const hintedDelay = hintedRetryDelayMs(error)
|
|
310
|
+
|
|
311
|
+
if (hintedDelay !== undefined) {
|
|
312
|
+
return hintedDelay
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const delayMs = validDelayMs(baseDelayMs * 2 ** Math.max(0, attempt - 1)) ?? 0
|
|
316
|
+
|
|
317
|
+
return Math.min(delayMs, maxUnhintedRetryDelayMs)
|
|
318
|
+
}
|
|
297
319
|
|
|
298
320
|
const retryReason = (error: LLMError): AgentErrorCode => error.cause
|
|
299
321
|
|
|
@@ -332,13 +354,14 @@ const withProviderRetries = (
|
|
|
332
354
|
return failAgentLoopError(error)
|
|
333
355
|
}
|
|
334
356
|
|
|
335
|
-
const delayMs = retryDelayMs(loopConfig.retryBaseDelayMs, attempt)
|
|
357
|
+
const delayMs = retryDelayMs(loopConfig.retryBaseDelayMs, attempt, error)
|
|
336
358
|
return Stream.make(
|
|
337
359
|
AgentRetry.make({
|
|
338
360
|
attempt,
|
|
339
361
|
reason: retryReason(error),
|
|
340
362
|
delayMs,
|
|
341
|
-
message: error.message
|
|
363
|
+
message: error.message,
|
|
364
|
+
...(error.provider === undefined ? {} : { provider: error.provider })
|
|
342
365
|
})
|
|
343
366
|
).pipe(
|
|
344
367
|
Stream.concat(sleepStream(delayMs)),
|
package/src/protocol/content.ts
CHANGED
|
@@ -201,6 +201,135 @@ export const attachmentSourceDataUrl = (source: AttachmentSource, mimeType: stri
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
const normalizeMimeType = (mimeType: string) => mimeType.split(';', 1)[0]?.trim().toLowerCase() ?? ''
|
|
205
|
+
|
|
206
|
+
const textDocumentMimeTypeByExtension: Readonly<Record<string, string>> = {
|
|
207
|
+
'.csv': 'text/csv',
|
|
208
|
+
'.css': 'text/css',
|
|
209
|
+
'.gql': 'application/graphql',
|
|
210
|
+
'.graphql': 'application/graphql',
|
|
211
|
+
'.html': 'text/html',
|
|
212
|
+
'.js': 'application/javascript',
|
|
213
|
+
'.jsx': 'application/javascript',
|
|
214
|
+
'.json': 'application/json',
|
|
215
|
+
'.jsonl': 'application/x-ndjson',
|
|
216
|
+
'.md': 'text/markdown',
|
|
217
|
+
'.markdown': 'text/markdown',
|
|
218
|
+
'.sql': 'application/sql',
|
|
219
|
+
'.toml': 'application/toml',
|
|
220
|
+
'.ts': 'application/typescript',
|
|
221
|
+
'.tsx': 'application/typescript',
|
|
222
|
+
'.txt': 'text/plain',
|
|
223
|
+
'.xml': 'application/xml',
|
|
224
|
+
'.yaml': 'application/yaml',
|
|
225
|
+
'.yml': 'application/yaml'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const isUnknownDocumentMimeType = (mimeType: string) =>
|
|
229
|
+
mimeType.length === 0 || mimeType === 'application/octet-stream' || mimeType === 'binary/octet-stream'
|
|
230
|
+
|
|
231
|
+
export const isTextDocumentMimeType = (mimeType: string) => {
|
|
232
|
+
const normalized = normalizeMimeType(mimeType)
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
normalized.startsWith('text/') ||
|
|
236
|
+
normalized === 'application/json' ||
|
|
237
|
+
normalized === 'application/ld+json' ||
|
|
238
|
+
normalized === 'application/jsonl' ||
|
|
239
|
+
normalized === 'application/x-ndjson' ||
|
|
240
|
+
normalized === 'application/javascript' ||
|
|
241
|
+
normalized === 'application/x-javascript' ||
|
|
242
|
+
normalized === 'application/typescript' ||
|
|
243
|
+
normalized === 'application/x-typescript' ||
|
|
244
|
+
normalized === 'application/xml' ||
|
|
245
|
+
normalized === 'application/yaml' ||
|
|
246
|
+
normalized === 'application/x-yaml' ||
|
|
247
|
+
normalized === 'application/toml' ||
|
|
248
|
+
normalized === 'application/markdown' ||
|
|
249
|
+
normalized === 'application/sql' ||
|
|
250
|
+
normalized === 'application/graphql' ||
|
|
251
|
+
normalized.endsWith('+json') ||
|
|
252
|
+
normalized.endsWith('+xml')
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const textDocumentMimeTypeFromFilename = (filename: string) => {
|
|
257
|
+
const normalized = filename.trim().toLowerCase()
|
|
258
|
+
const entry = Object.entries(textDocumentMimeTypeByExtension).find(([extension]) =>
|
|
259
|
+
normalized.endsWith(extension)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return entry?.[1]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const inferTextDocumentMimeType = (input: {
|
|
266
|
+
readonly filename: string
|
|
267
|
+
readonly mimeType: string
|
|
268
|
+
}) => {
|
|
269
|
+
const normalized = normalizeMimeType(input.mimeType)
|
|
270
|
+
|
|
271
|
+
if (isTextDocumentMimeType(normalized)) return normalized
|
|
272
|
+
if (!isUnknownDocumentMimeType(normalized)) return undefined
|
|
273
|
+
|
|
274
|
+
return textDocumentMimeTypeFromFilename(input.filename)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const textToBase64Utf8 = (text: string) => {
|
|
278
|
+
const bytes = new TextEncoder().encode(text)
|
|
279
|
+
let binary = ''
|
|
280
|
+
|
|
281
|
+
for (const byte of bytes) {
|
|
282
|
+
binary += String.fromCharCode(byte)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return globalThis.btoa(binary)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const documentPartFromText = (input: {
|
|
289
|
+
readonly text: string
|
|
290
|
+
readonly filename: string
|
|
291
|
+
readonly mimeType: string
|
|
292
|
+
readonly title?: string
|
|
293
|
+
}) => {
|
|
294
|
+
const normalized = normalizeMimeType(input.mimeType)
|
|
295
|
+
const mimeType = inferTextDocumentMimeType({
|
|
296
|
+
filename: input.filename,
|
|
297
|
+
mimeType: input.mimeType
|
|
298
|
+
}) ?? (isUnknownDocumentMimeType(normalized) ? 'text/plain' : undefined)
|
|
299
|
+
|
|
300
|
+
if (mimeType === undefined) return undefined
|
|
301
|
+
|
|
302
|
+
const base = {
|
|
303
|
+
source: inlineBase64AttachmentSource(textToBase64Utf8(input.text)),
|
|
304
|
+
mimeType,
|
|
305
|
+
filename: input.filename
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return input.title === undefined
|
|
309
|
+
? DocumentPart.make(base)
|
|
310
|
+
: DocumentPart.make({ ...base, title: input.title })
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const decodeBase64Utf8 = (data: string) => {
|
|
314
|
+
const binary = globalThis.atob(data)
|
|
315
|
+
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0))
|
|
316
|
+
|
|
317
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const attachmentSourceText = (source: AttachmentSource) => {
|
|
321
|
+
switch (source._tag) {
|
|
322
|
+
case 'InlineBase64':
|
|
323
|
+
return Effect.try({
|
|
324
|
+
try: () => Option.some(decodeBase64Utf8(source.data)),
|
|
325
|
+
catch: error => error
|
|
326
|
+
})
|
|
327
|
+
case 'Url':
|
|
328
|
+
case 'Ref':
|
|
329
|
+
return Effect.succeed(Option.none<string>())
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
204
333
|
export const attachmentSourceBase64 = (source: AttachmentSource) => {
|
|
205
334
|
switch (source._tag) {
|
|
206
335
|
case 'InlineBase64':
|
package/src/protocol/event.ts
CHANGED
|
@@ -27,6 +27,7 @@ export const AgentErrorCode = Schema.Literals([
|
|
|
27
27
|
'validation_error',
|
|
28
28
|
'provider_error',
|
|
29
29
|
'rate_limit',
|
|
30
|
+
'overloaded',
|
|
30
31
|
'context_overflow',
|
|
31
32
|
'invalid_response',
|
|
32
33
|
'tool_error',
|
|
@@ -40,6 +41,27 @@ export const AgentErrorCode = Schema.Literals([
|
|
|
40
41
|
])
|
|
41
42
|
export type AgentErrorCode = typeof AgentErrorCode.Type
|
|
42
43
|
|
|
44
|
+
export const ProviderFailureKind = Schema.Literals([
|
|
45
|
+
'rate_limit',
|
|
46
|
+
'overloaded',
|
|
47
|
+
'server_error',
|
|
48
|
+
'network',
|
|
49
|
+
'stream',
|
|
50
|
+
'auth',
|
|
51
|
+
'context_overflow',
|
|
52
|
+
'invalid_response',
|
|
53
|
+
'unknown'
|
|
54
|
+
])
|
|
55
|
+
export type ProviderFailureKind = typeof ProviderFailureKind.Type
|
|
56
|
+
|
|
57
|
+
export class ProviderErrorInfo extends Schema.Class<ProviderErrorInfo>('ProviderErrorInfo')({
|
|
58
|
+
provider: NonEmptyTrimmedString,
|
|
59
|
+
kind: ProviderFailureKind,
|
|
60
|
+
status: Schema.optional(Schema.Number),
|
|
61
|
+
providerCode: Schema.optional(Schema.String),
|
|
62
|
+
retryAfterMs: Schema.optional(Schema.Number)
|
|
63
|
+
}) {}
|
|
64
|
+
|
|
43
65
|
export class AgentStart extends Schema.TaggedClass<AgentStart>()('AgentStart', {
|
|
44
66
|
...EventIdentity
|
|
45
67
|
}) {}
|
|
@@ -48,7 +70,8 @@ export class AgentError extends Schema.TaggedClass<AgentError>()('AgentError', {
|
|
|
48
70
|
...EventIdentity,
|
|
49
71
|
code: AgentErrorCode,
|
|
50
72
|
message: Schema.String,
|
|
51
|
-
retryable: Schema.Boolean
|
|
73
|
+
retryable: Schema.Boolean,
|
|
74
|
+
provider: Schema.optional(ProviderErrorInfo)
|
|
52
75
|
}) {}
|
|
53
76
|
|
|
54
77
|
export class AgentEnd extends Schema.TaggedClass<AgentEnd>()('AgentEnd', {
|
|
@@ -79,7 +102,8 @@ export class AgentRetry extends Schema.TaggedClass<AgentRetry>()('AgentRetry', {
|
|
|
79
102
|
attempt: Schema.Number,
|
|
80
103
|
reason: AgentErrorCode,
|
|
81
104
|
delayMs: Schema.Number,
|
|
82
|
-
message: Schema.String
|
|
105
|
+
message: Schema.String,
|
|
106
|
+
provider: Schema.optional(ProviderErrorInfo)
|
|
83
107
|
}) {}
|
|
84
108
|
|
|
85
109
|
export class CompactionStart extends Schema.TaggedClass<CompactionStart>()('CompactionStart', {
|
package/src/protocol/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
attachmentSourceBase64,
|
|
11
11
|
attachmentSourceDataUrl,
|
|
12
12
|
AttachmentSource,
|
|
13
|
+
attachmentSourceText,
|
|
13
14
|
type AttachmentContentPart,
|
|
14
15
|
type AttachmentSourceResolver,
|
|
15
16
|
attachmentSourcePreview,
|
|
@@ -22,14 +23,19 @@ export {
|
|
|
22
23
|
contentPreview,
|
|
23
24
|
contentText,
|
|
24
25
|
DocumentPart,
|
|
26
|
+
documentPartFromText,
|
|
25
27
|
ImagePart,
|
|
28
|
+
inferTextDocumentMimeType,
|
|
26
29
|
InlineBase64AttachmentSource,
|
|
27
30
|
inlineBase64AttachmentSource,
|
|
28
31
|
inlineBase64Source,
|
|
29
32
|
isContentEmpty,
|
|
33
|
+
isTextDocumentMimeType,
|
|
30
34
|
refAttachmentSource,
|
|
31
35
|
RefAttachmentSource,
|
|
32
36
|
resolveContentAttachmentSources,
|
|
37
|
+
textDocumentMimeTypeFromFilename,
|
|
38
|
+
textToBase64Utf8,
|
|
33
39
|
urlAttachmentSource,
|
|
34
40
|
UrlAttachmentSource,
|
|
35
41
|
TextPart
|
|
@@ -49,6 +55,8 @@ export {
|
|
|
49
55
|
LLMStreamEnd,
|
|
50
56
|
LLMStreamStart,
|
|
51
57
|
LLMTextDelta,
|
|
58
|
+
ProviderErrorInfo,
|
|
59
|
+
ProviderFailureKind,
|
|
52
60
|
ProviderToolResult,
|
|
53
61
|
QuestionAnswered,
|
|
54
62
|
QuestionCancelled,
|
|
@@ -12,13 +12,16 @@ import {
|
|
|
12
12
|
AgentUsage,
|
|
13
13
|
ToolCall,
|
|
14
14
|
attachmentSourceBase64,
|
|
15
|
+
attachmentSourceText,
|
|
15
16
|
assistantContent,
|
|
16
17
|
assistantHostToolCalls,
|
|
18
|
+
isTextDocumentMimeType,
|
|
17
19
|
messageContextText,
|
|
18
20
|
prependMessageContextToContent,
|
|
19
21
|
type AgentMessage,
|
|
20
22
|
type Content,
|
|
21
23
|
type ContentPart,
|
|
24
|
+
type ProviderFailureKind,
|
|
22
25
|
type ToolDef
|
|
23
26
|
} from '@yolk-sdk/agent/protocol'
|
|
24
27
|
import {
|
|
@@ -39,6 +42,12 @@ import {
|
|
|
39
42
|
anthropicClaudeOAuthUserAgent
|
|
40
43
|
} from './claude.ts'
|
|
41
44
|
import type { OAuthAccessToken } from '@yolk-sdk/agent/oauth'
|
|
45
|
+
import {
|
|
46
|
+
classifyProviderFailure,
|
|
47
|
+
providerErrorInfo,
|
|
48
|
+
providerFailureCause,
|
|
49
|
+
providerFailureRetryable
|
|
50
|
+
} from '../provider-error.ts'
|
|
42
51
|
|
|
43
52
|
export type AnthropicClaudeProviderConfig = {
|
|
44
53
|
readonly token: OAuthAccessToken
|
|
@@ -433,6 +442,24 @@ const unsupportedContentError = (contentType: string) =>
|
|
|
433
442
|
retryable: false
|
|
434
443
|
})
|
|
435
444
|
|
|
445
|
+
const textDocumentToAnthropicBlock = (part: Extract<ContentPart, { readonly _tag: 'Document' }>) =>
|
|
446
|
+
attachmentSourceText(part.source).pipe(
|
|
447
|
+
Effect.mapError(() => unsupportedContentError('Invalid document text')),
|
|
448
|
+
Effect.flatMap(
|
|
449
|
+
Option.match({
|
|
450
|
+
onNone: () => Effect.fail(unsupportedContentError('Unresolved document source')),
|
|
451
|
+
onSome: text => {
|
|
452
|
+
const block: AnthropicTextBlock = {
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: `Document: ${part.title ?? part.filename}\n\n${text}`
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return Effect.succeed(block)
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
|
|
436
463
|
const contentPartToUserBlock = (part: ContentPart): Effect.Effect<AnthropicUserBlock, LLMError> => {
|
|
437
464
|
switch (part._tag) {
|
|
438
465
|
case 'Text':
|
|
@@ -450,7 +477,9 @@ const contentPartToUserBlock = (part: ContentPart): Effect.Effect<AnthropicUserB
|
|
|
450
477
|
})
|
|
451
478
|
})
|
|
452
479
|
case 'Document':
|
|
453
|
-
return part.mimeType
|
|
480
|
+
return isTextDocumentMimeType(part.mimeType)
|
|
481
|
+
? textDocumentToAnthropicBlock(part)
|
|
482
|
+
: part.mimeType === 'application/pdf'
|
|
454
483
|
? Option.match(attachmentSourceBase64(part.source), {
|
|
455
484
|
onNone: () => Effect.fail(unsupportedContentError('Unresolved document source')),
|
|
456
485
|
onSome: data => Effect.succeed({
|
|
@@ -795,26 +824,19 @@ export const toAnthropicClaudeRequestBody = (
|
|
|
795
824
|
}
|
|
796
825
|
})
|
|
797
826
|
|
|
798
|
-
const
|
|
799
|
-
if (status === 429) {
|
|
800
|
-
return 'rate_limit'
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (status === 413 || status === 400) {
|
|
804
|
-
return 'context_overflow'
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
return 'provider_error'
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
const isRetryableStatus = (status: number) => status === 429 || status >= 500
|
|
827
|
+
const anthropicClaudeProvider = 'anthropic_claude'
|
|
811
828
|
|
|
812
829
|
const toHttpClientLlmError =
|
|
813
|
-
(message: string, retryable: boolean
|
|
830
|
+
(message: string, retryable: boolean, kind: ProviderFailureKind = 'network') =>
|
|
831
|
+
(error: HttpClientError.HttpClientError) =>
|
|
814
832
|
new LLMError({
|
|
815
833
|
cause: 'provider_error',
|
|
816
834
|
message: `${message}: ${error.message}`,
|
|
817
|
-
retryable
|
|
835
|
+
retryable,
|
|
836
|
+
provider: providerErrorInfo({
|
|
837
|
+
provider: anthropicClaudeProvider,
|
|
838
|
+
kind: retryable ? kind : 'unknown'
|
|
839
|
+
})
|
|
818
840
|
})
|
|
819
841
|
|
|
820
842
|
const decodeJsonString = (raw: string, message: string) =>
|
|
@@ -840,6 +862,26 @@ const stringField = (value: unknown, key: string) => {
|
|
|
840
862
|
return typeof raw === 'string' ? raw : undefined
|
|
841
863
|
}
|
|
842
864
|
|
|
865
|
+
const providerSignalError = (input: {
|
|
866
|
+
readonly message: string
|
|
867
|
+
readonly providerCode?: string
|
|
868
|
+
readonly fallbackKind?: ProviderFailureKind
|
|
869
|
+
}) => {
|
|
870
|
+
const provider = classifyProviderFailure({
|
|
871
|
+
provider: anthropicClaudeProvider,
|
|
872
|
+
message: input.message,
|
|
873
|
+
...(input.providerCode === undefined ? {} : { providerCode: input.providerCode }),
|
|
874
|
+
...(input.fallbackKind === undefined ? {} : { fallbackKind: input.fallbackKind })
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
return new LLMError({
|
|
878
|
+
cause: providerFailureCause(provider.kind),
|
|
879
|
+
message: input.message,
|
|
880
|
+
retryable: providerFailureRetryable(provider.kind),
|
|
881
|
+
provider
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
|
|
843
885
|
const numberField = (value: unknown, key: string) => {
|
|
844
886
|
const raw = field(value, key)
|
|
845
887
|
return typeof raw === 'number' ? raw : undefined
|
|
@@ -1134,11 +1176,12 @@ const makeAnthropicStreamEmitter = () => {
|
|
|
1134
1176
|
|
|
1135
1177
|
if (type === 'error') {
|
|
1136
1178
|
const error = field(data, 'error')
|
|
1179
|
+
const providerCode = stringField(error, 'type') ?? stringField(error, 'code')
|
|
1180
|
+
|
|
1137
1181
|
return Effect.fail(
|
|
1138
|
-
|
|
1139
|
-
cause: 'provider_error',
|
|
1182
|
+
providerSignalError({
|
|
1140
1183
|
message: stringField(error, 'message') ?? 'Anthropic Claude stream error',
|
|
1141
|
-
|
|
1184
|
+
...(providerCode === undefined ? {} : { providerCode })
|
|
1142
1185
|
})
|
|
1143
1186
|
)
|
|
1144
1187
|
}
|
|
@@ -1258,7 +1301,7 @@ export const streamAnthropicClaudeResponse = (
|
|
|
1258
1301
|
Effect.map(bodyStateRef => {
|
|
1259
1302
|
const emitData = makeAnthropicStreamEmitter()
|
|
1260
1303
|
const chunks = response.stream.pipe(
|
|
1261
|
-
Stream.mapError(toHttpClientLlmError('Could not read Anthropic Claude stream',
|
|
1304
|
+
Stream.mapError(toHttpClientLlmError('Could not read Anthropic Claude stream', true, 'stream')),
|
|
1262
1305
|
Stream.decodeText,
|
|
1263
1306
|
Stream.mapEffect(chunk =>
|
|
1264
1307
|
Effect.gen(function* () {
|
|
@@ -1316,11 +1359,19 @@ const sendAnthropicClaudeRequest = (
|
|
|
1316
1359
|
)
|
|
1317
1360
|
)
|
|
1318
1361
|
|
|
1362
|
+
const provider = classifyProviderFailure({
|
|
1363
|
+
provider: anthropicClaudeProvider,
|
|
1364
|
+
status: response.status,
|
|
1365
|
+
headers: response.headers,
|
|
1366
|
+
body: errorText
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1319
1369
|
return yield* Effect.fail(
|
|
1320
1370
|
new LLMError({
|
|
1321
|
-
cause:
|
|
1322
|
-
message: `Anthropic Claude returned ${response.status}
|
|
1323
|
-
retryable:
|
|
1371
|
+
cause: providerFailureCause(provider.kind),
|
|
1372
|
+
message: `Anthropic Claude returned ${response.status}`,
|
|
1373
|
+
retryable: providerFailureRetryable(provider.kind),
|
|
1374
|
+
provider
|
|
1324
1375
|
})
|
|
1325
1376
|
)
|
|
1326
1377
|
}
|