@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
@@ -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 { ...state, toolRuns: appendToolInputDelta(state.toolRuns, event.id, event.delta) }
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 retryDelayMs = (baseDelayMs: number, attempt: number) =>
296
- Math.max(0, Math.floor(baseDelayMs * 2 ** Math.max(0, attempt - 1)))
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)),
@@ -14,7 +14,7 @@ export class LoopConfig extends Context.Service<LoopConfig, LoopConfigShape>()(
14
14
  static defaultLayer = this.layer({
15
15
  maxTurns: 500,
16
16
  maxRetries: 2,
17
- retryBaseDelayMs: 1000,
17
+ retryBaseDelayMs: 2000,
18
18
  toolConcurrency: 4
19
19
  })
20
20
  }
@@ -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':
@@ -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', {
@@ -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 === 'application/pdf'
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 responseStatusToCause = (status: number): LLMError['cause'] => {
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) => (error: HttpClientError.HttpClientError) =>
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
- new LLMError({
1139
- cause: 'provider_error',
1182
+ providerSignalError({
1140
1183
  message: stringField(error, 'message') ?? 'Anthropic Claude stream error',
1141
- retryable: false
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', false)),
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: responseStatusToCause(response.status),
1322
- message: `Anthropic Claude returned ${response.status}: ${errorText}`,
1323
- retryable: isRetryableStatus(response.status)
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
  }