@namzu/sdk 1.3.0 → 2.0.0

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 (171) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/compaction/__tests__/verifier-empty-reply.test.d.ts +12 -0
  3. package/dist/compaction/__tests__/verifier-empty-reply.test.d.ts.map +1 -0
  4. package/dist/compaction/__tests__/verifier-empty-reply.test.js +73 -0
  5. package/dist/compaction/__tests__/verifier-empty-reply.test.js.map +1 -0
  6. package/dist/compaction/verifier.d.ts.map +1 -1
  7. package/dist/compaction/verifier.js +10 -1
  8. package/dist/compaction/verifier.js.map +1 -1
  9. package/dist/config/__tests__/compaction-budget-schema.test.d.ts +18 -0
  10. package/dist/config/__tests__/compaction-budget-schema.test.d.ts.map +1 -0
  11. package/dist/config/__tests__/compaction-budget-schema.test.js +96 -0
  12. package/dist/config/__tests__/compaction-budget-schema.test.js.map +1 -0
  13. package/dist/config/runtime.d.ts.map +1 -1
  14. package/dist/config/runtime.js +20 -11
  15. package/dist/config/runtime.js.map +1 -1
  16. package/dist/manager/run/persistence.d.ts +2 -1
  17. package/dist/manager/run/persistence.d.ts.map +1 -1
  18. package/dist/manager/run/persistence.js +3 -1
  19. package/dist/manager/run/persistence.js.map +1 -1
  20. package/dist/provider/__tests__/errors.test.d.ts +2 -0
  21. package/dist/provider/__tests__/errors.test.d.ts.map +1 -0
  22. package/dist/provider/__tests__/errors.test.js +61 -0
  23. package/dist/provider/__tests__/errors.test.js.map +1 -0
  24. package/dist/provider/errors.d.ts +108 -0
  25. package/dist/provider/errors.d.ts.map +1 -0
  26. package/dist/provider/errors.js +311 -0
  27. package/dist/provider/errors.js.map +1 -0
  28. package/dist/provider/index.d.ts +2 -0
  29. package/dist/provider/index.d.ts.map +1 -1
  30. package/dist/provider/index.js +1 -0
  31. package/dist/provider/index.js.map +1 -1
  32. package/dist/public-runtime.d.ts +11 -11
  33. package/dist/public-runtime.d.ts.map +1 -1
  34. package/dist/public-runtime.js +11 -11
  35. package/dist/public-runtime.js.map +1 -1
  36. package/dist/registry/tool/execute.d.ts.map +1 -1
  37. package/dist/registry/tool/execute.js +45 -12
  38. package/dist/registry/tool/execute.js.map +1 -1
  39. package/dist/registry/tool/execute.test.js +104 -7
  40. package/dist/registry/tool/execute.test.js.map +1 -1
  41. package/dist/registry/toolset/catalog.d.ts.map +1 -1
  42. package/dist/registry/toolset/catalog.js +17 -4
  43. package/dist/registry/toolset/catalog.js.map +1 -1
  44. package/dist/registry/toolset/catalog.test.js +101 -0
  45. package/dist/registry/toolset/catalog.test.js.map +1 -1
  46. package/dist/runtime/query/__tests__/capability-negotiation.test.js +79 -0
  47. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  48. package/dist/runtime/query/__tests__/deferred-tools.test.js +100 -0
  49. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
  50. package/dist/runtime/query/__tests__/long-document-flow.test.js +12 -12
  51. package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -1
  52. package/dist/runtime/query/__tests__/stream-recovery.test.js +55 -2
  53. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -1
  54. package/dist/runtime/query/executor.js +2 -2
  55. package/dist/runtime/query/executor.js.map +1 -1
  56. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  57. package/dist/runtime/query/iteration/index.js +10 -0
  58. package/dist/runtime/query/iteration/index.js.map +1 -1
  59. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.d.ts +32 -0
  60. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.d.ts.map +1 -0
  61. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.js +152 -0
  62. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.js.map +1 -0
  63. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  64. package/dist/runtime/query/iteration/phases/compaction.js +52 -12
  65. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  66. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/stream-turn.js +11 -5
  68. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  69. package/dist/runtime/query/result.d.ts.map +1 -1
  70. package/dist/runtime/query/result.js +11 -1
  71. package/dist/runtime/query/result.js.map +1 -1
  72. package/dist/tools/builtins/__tests__/edit.test.js +216 -10
  73. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  74. package/dist/tools/builtins/__tests__/payload-budget.test.js +30 -16
  75. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
  76. package/dist/tools/builtins/__tests__/write-file.test.js +37 -13
  77. package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -1
  78. package/dist/tools/builtins/atomic-write-file.d.ts +13 -0
  79. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -0
  80. package/dist/tools/builtins/atomic-write-file.js +46 -0
  81. package/dist/tools/builtins/atomic-write-file.js.map +1 -0
  82. package/dist/tools/builtins/bash.js +2 -2
  83. package/dist/tools/builtins/bash.js.map +1 -1
  84. package/dist/tools/builtins/edit.d.ts +2 -5
  85. package/dist/tools/builtins/edit.d.ts.map +1 -1
  86. package/dist/tools/builtins/edit.js +105 -125
  87. package/dist/tools/builtins/edit.js.map +1 -1
  88. package/dist/tools/builtins/file-mutation-lock.d.ts +7 -0
  89. package/dist/tools/builtins/file-mutation-lock.d.ts.map +1 -0
  90. package/dist/tools/builtins/file-mutation-lock.js +25 -0
  91. package/dist/tools/builtins/file-mutation-lock.js.map +1 -0
  92. package/dist/tools/builtins/index.js +2 -2
  93. package/dist/tools/builtins/index.js.map +1 -1
  94. package/dist/tools/builtins/write-file.d.ts +1 -2
  95. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  96. package/dist/tools/builtins/write-file.js +66 -40
  97. package/dist/tools/builtins/write-file.js.map +1 -1
  98. package/dist/tools/coordinator/__tests__/task-list.test.js +14 -0
  99. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  100. package/dist/tools/coordinator/agent.d.ts +6 -8
  101. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  102. package/dist/tools/coordinator/agent.js.map +1 -1
  103. package/dist/tools/coordinator/index.d.ts.map +1 -1
  104. package/dist/tools/coordinator/index.js +11 -32
  105. package/dist/tools/coordinator/index.js.map +1 -1
  106. package/dist/tools/defineTool.d.ts +3 -0
  107. package/dist/tools/defineTool.d.ts.map +1 -1
  108. package/dist/tools/defineTool.js +3 -0
  109. package/dist/tools/defineTool.js.map +1 -1
  110. package/dist/types/provider/chat.d.ts +9 -0
  111. package/dist/types/provider/chat.d.ts.map +1 -1
  112. package/dist/types/provider/error.d.ts +22 -0
  113. package/dist/types/provider/error.d.ts.map +1 -0
  114. package/dist/types/provider/error.js +2 -0
  115. package/dist/types/provider/error.js.map +1 -0
  116. package/dist/types/provider/index.d.ts +1 -0
  117. package/dist/types/provider/index.d.ts.map +1 -1
  118. package/dist/types/run/entity.d.ts +2 -0
  119. package/dist/types/run/entity.d.ts.map +1 -1
  120. package/dist/types/run/events.d.ts +2 -0
  121. package/dist/types/run/events.d.ts.map +1 -1
  122. package/dist/types/run/events.js.map +1 -1
  123. package/dist/types/sandbox/index.d.ts +4 -0
  124. package/dist/types/sandbox/index.d.ts.map +1 -1
  125. package/dist/types/sandbox/index.js.map +1 -1
  126. package/dist/types/tool/index.d.ts +20 -0
  127. package/dist/types/tool/index.d.ts.map +1 -1
  128. package/package.json +2 -1
  129. package/src/compaction/__tests__/verifier-empty-reply.test.ts +96 -0
  130. package/src/compaction/verifier.ts +10 -1
  131. package/src/config/__tests__/compaction-budget-schema.test.ts +114 -0
  132. package/src/config/runtime.ts +20 -11
  133. package/src/manager/run/persistence.ts +3 -1
  134. package/src/provider/__tests__/errors.test.ts +85 -0
  135. package/src/provider/errors.ts +344 -0
  136. package/src/provider/index.ts +15 -0
  137. package/src/public-runtime.ts +53 -10
  138. package/src/registry/tool/execute.test.ts +121 -7
  139. package/src/registry/tool/execute.ts +50 -12
  140. package/src/registry/toolset/catalog.test.ts +118 -0
  141. package/src/registry/toolset/catalog.ts +26 -4
  142. package/src/runtime/query/__tests__/capability-negotiation.test.ts +88 -0
  143. package/src/runtime/query/__tests__/deferred-tools.test.ts +111 -0
  144. package/src/runtime/query/__tests__/long-document-flow.test.ts +12 -12
  145. package/src/runtime/query/__tests__/stream-recovery.test.ts +65 -2
  146. package/src/runtime/query/executor.ts +2 -2
  147. package/src/runtime/query/iteration/index.ts +15 -0
  148. package/src/runtime/query/iteration/phases/compaction-safe-cut.test.ts +186 -0
  149. package/src/runtime/query/iteration/phases/compaction.ts +54 -12
  150. package/src/runtime/query/iteration/stream-turn.ts +11 -6
  151. package/src/runtime/query/result.ts +11 -1
  152. package/src/tools/builtins/__tests__/edit.test.ts +270 -10
  153. package/src/tools/builtins/__tests__/payload-budget.test.ts +40 -20
  154. package/src/tools/builtins/__tests__/write-file.test.ts +52 -13
  155. package/src/tools/builtins/atomic-write-file.ts +58 -0
  156. package/src/tools/builtins/bash.ts +2 -2
  157. package/src/tools/builtins/edit.ts +116 -162
  158. package/src/tools/builtins/file-mutation-lock.ts +27 -0
  159. package/src/tools/builtins/index.ts +2 -2
  160. package/src/tools/builtins/write-file.ts +68 -44
  161. package/src/tools/coordinator/__tests__/task-list.test.ts +16 -0
  162. package/src/tools/coordinator/agent.ts +6 -8
  163. package/src/tools/coordinator/index.ts +11 -34
  164. package/src/tools/defineTool.ts +6 -0
  165. package/src/types/provider/chat.ts +14 -1
  166. package/src/types/provider/error.ts +29 -0
  167. package/src/types/provider/index.ts +5 -0
  168. package/src/types/run/entity.ts +2 -0
  169. package/src/types/run/events.ts +7 -1
  170. package/src/types/sandbox/index.ts +4 -0
  171. package/src/types/tool/index.ts +20 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The EMPTY VERIFIER REPLY.
3
+ *
4
+ * `buildVerifiedSummary` treats only the literal `COMPLETE` as "nothing to add".
5
+ * Anything else is appended under a `## LLM Verification Additions` heading —
6
+ * including the empty string, which is what a truncated turn, a refusal, or an
7
+ * exhausted `llmVerificationMaxTokens` produces. The result is a heading with
8
+ * nothing under it, and because the summary becomes a leading system message it
9
+ * then rides in EVERY subsequent prompt of the run.
10
+ */
11
+
12
+ import { describe, expect, it } from 'vitest'
13
+
14
+ import { CompactionConfigSchema } from '../../config/runtime.js'
15
+ import {
16
+ type Message,
17
+ createAssistantMessage,
18
+ createUserMessage,
19
+ } from '../../types/message/index.js'
20
+ import type { ChatCompletionParams } from '../../types/provider/chat.js'
21
+ import type { LLMProvider } from '../../types/provider/interface.js'
22
+ import type { StreamChunk } from '../../types/provider/stream.js'
23
+ import { WorkingStateManager } from '../manager.js'
24
+ import { buildVerifiedSummary } from '../verifier.js'
25
+
26
+ const ADDITIONS_HEADING = '## LLM Verification Additions'
27
+
28
+ function makeProvider(chunks: StreamChunk[]): LLMProvider & { calls: ChatCompletionParams[] } {
29
+ const calls: ChatCompletionParams[] = []
30
+ return {
31
+ id: 'mock',
32
+ name: 'mock',
33
+ calls,
34
+ chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
35
+ calls.push(params)
36
+ return (async function* () {
37
+ for (const c of chunks) yield c
38
+ })()
39
+ },
40
+ }
41
+ }
42
+
43
+ function makeManager() {
44
+ const config = CompactionConfigSchema.parse({ llmVerification: true })
45
+ const manager = new WorkingStateManager(config)
46
+ manager.setTask('write the quarterly report')
47
+ manager.addDecision('built the report as .docx')
48
+ return { config, manager }
49
+ }
50
+
51
+ const OLDER: Message[] = [
52
+ createUserMessage('write the quarterly report'),
53
+ createAssistantMessage('on it — reading the source numbers first'),
54
+ ]
55
+
56
+ describe('buildVerifiedSummary — empty verifier reply', () => {
57
+ it('does not append an empty "Additions" section when the verifier returns nothing', async () => {
58
+ const { config, manager } = makeManager()
59
+ const provider = makeProvider([])
60
+
61
+ const result = await buildVerifiedSummary(manager, OLDER, provider, config)
62
+
63
+ expect(result).not.toContain(ADDITIONS_HEADING)
64
+ })
65
+
66
+ it('does not append an empty "Additions" section when the turn is truncated by maxTokens', async () => {
67
+ const { config, manager } = makeManager()
68
+ // `length` finish reason with no content — llmVerificationMaxTokens exhausted.
69
+ const provider = makeProvider([{ id: 'c1', delta: { content: '' }, finishReason: 'length' }])
70
+
71
+ const result = await buildVerifiedSummary(manager, OLDER, provider, config)
72
+
73
+ expect(result).not.toContain(ADDITIONS_HEADING)
74
+ })
75
+
76
+ it('does not append an "Additions" section for a whitespace-only reply', async () => {
77
+ const { config, manager } = makeManager()
78
+ const provider = makeProvider([{ id: 'c1', delta: { content: ' \n\n ' } }])
79
+
80
+ const result = await buildVerifiedSummary(manager, OLDER, provider, config)
81
+
82
+ expect(result).not.toContain(ADDITIONS_HEADING)
83
+ })
84
+
85
+ it('still appends real additions', async () => {
86
+ const { config, manager } = makeManager()
87
+ const provider = makeProvider([
88
+ { id: 'c1', delta: { content: '- the user asked for EUR, not USD' } },
89
+ ])
90
+
91
+ const result = await buildVerifiedSummary(manager, OLDER, provider, config)
92
+
93
+ expect(result).toContain(ADDITIONS_HEADING)
94
+ expect(result).toContain('EUR, not USD')
95
+ })
96
+ })
@@ -86,7 +86,16 @@ export async function buildVerifiedSummary(
86
86
 
87
87
  const responseText = response.message.content?.trim() ?? ''
88
88
 
89
- if (responseText === 'COMPLETE') {
89
+ // `COMPLETE` is the verifier saying it found nothing to add. An EMPTY reply
90
+ // says the same thing by accident — a turn truncated at
91
+ // `llmVerificationMaxTokens`, a refusal, or a provider that returned no
92
+ // content at all — and used to fall through to the append below, stamping a
93
+ // bare `## LLM Verification Additions` heading with nothing under it. That
94
+ // empty promise then rides in the compaction summary, and therefore in every
95
+ // subsequent system prompt, for the rest of the run. A heading with no body
96
+ // is not a verification result; treat a silent verifier the same as one that
97
+ // had nothing to say.
98
+ if (responseText === 'COMPLETE' || responseText.length === 0) {
90
99
  return serialized
91
100
  }
92
101
 
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The BUDGET SCHEMA.
3
+ *
4
+ * Every compaction count used to be `z.number().positive()`. Zod's base number
5
+ * check rejects only non-numbers and `NaN`, so `Infinity` and `0.5` both parsed.
6
+ * These are counts of messages, characters, sentences and tokens: a non-integer
7
+ * is meaningless, and `Infinity` does something worse than being meaningless —
8
+ * it turns a bound into a no-op rather than an error. `truncateMessages` compares
9
+ * `charCount > budget`, never true against `Infinity`, so the WHOLE older history
10
+ * was pasted into the verification prompt.
11
+ *
12
+ * They are `.int().positive()` now. `Number.isInteger` is false for `Infinity`
13
+ * and for any fraction, so one validator closes both. These tests are the guard:
14
+ * every count is checked against both illegal shapes, and the documented
15
+ * defaults are pinned so tightening the validator cannot have moved them.
16
+ */
17
+
18
+ import { describe, expect, it } from 'vitest'
19
+
20
+ import { WorkingStateManager } from '../../compaction/manager.js'
21
+ import { buildVerifiedSummary } from '../../compaction/verifier.js'
22
+ import { type Message, createUserMessage } from '../../types/message/index.js'
23
+ import type { ChatCompletionParams } from '../../types/provider/chat.js'
24
+ import type { LLMProvider } from '../../types/provider/interface.js'
25
+ import type { StreamChunk } from '../../types/provider/stream.js'
26
+ import { CompactionConfigSchema, RuntimeConfigSchema } from '../runtime.js'
27
+
28
+ /** Every integer count on CompactionConfigSchema. */
29
+ const COUNT_FIELDS = [
30
+ 'contextWindowTokens',
31
+ 'keepRecentMessages',
32
+ 'maxToolResults',
33
+ 'maxListSize',
34
+ 'llmVerificationMaxTokens',
35
+ 'richStateThreshold',
36
+ 'convoTextBudget',
37
+ 'maxSentencesPerTurn',
38
+ 'maxCharsPerNote',
39
+ 'maxCharsPerRequirement',
40
+ 'maxCharsPerTask',
41
+ ] as const
42
+
43
+ describe('CompactionConfigSchema — count fields', () => {
44
+ for (const field of COUNT_FIELDS) {
45
+ it(`rejects Infinity for ${field}`, () => {
46
+ expect(() => CompactionConfigSchema.parse({ [field]: Number.POSITIVE_INFINITY })).toThrow()
47
+ })
48
+
49
+ it(`rejects a fractional value for ${field}`, () => {
50
+ expect(() => CompactionConfigSchema.parse({ [field]: 0.5 })).toThrow()
51
+ })
52
+ }
53
+
54
+ it('rejects Infinity through the RuntimeConfigSchema envelope', () => {
55
+ expect(() =>
56
+ RuntimeConfigSchema.parse({ compaction: { convoTextBudget: Number.POSITIVE_INFINITY } }),
57
+ ).toThrow()
58
+ })
59
+
60
+ it('still accepts the documented defaults', () => {
61
+ const parsed = CompactionConfigSchema.parse({})
62
+ expect(parsed.convoTextBudget).toBe(12_000)
63
+ expect(parsed.keepRecentMessages).toBe(4)
64
+ })
65
+ })
66
+
67
+ /**
68
+ * Why the schema is the only defence, and what that leaves open.
69
+ *
70
+ * `truncateMessages` bounds the verification prompt with `charCount > budget`,
71
+ * which is never true against `Infinity` — that is the consequence the schema
72
+ * fix above exists to prevent, and it can no longer be reached THROUGH the
73
+ * schema. It is still reachable by a host that hand-builds a `CompactionConfig`
74
+ * object literal, because the inferred type is plain `number`; that is a stated
75
+ * limitation, not a covered case. No silent clamp was added inside
76
+ * `truncateMessages` — a budget that quietly repairs an illegal value hides the
77
+ * caller's bug instead of surfacing it, and the schema is where the boundary
78
+ * belongs.
79
+ *
80
+ * What this test does pin: with a LEGAL budget the prompt really is bounded, so
81
+ * the truncation the schema now guarantees is actually performed.
82
+ */
83
+ describe('truncateMessages bounds the verification prompt at a legal budget', () => {
84
+ it('does not paste the entire older history into the verification prompt', async () => {
85
+ const config = CompactionConfigSchema.parse({
86
+ llmVerification: true,
87
+ convoTextBudget: 12_000,
88
+ })
89
+ const manager = new WorkingStateManager(config)
90
+ manager.setTask('write the quarterly report')
91
+
92
+ const calls: ChatCompletionParams[] = []
93
+ const provider: LLMProvider = {
94
+ id: 'mock',
95
+ name: 'mock',
96
+ chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
97
+ calls.push(params)
98
+ return (async function* () {
99
+ yield { id: 'c1', delta: { content: 'COMPLETE' } } as StreamChunk
100
+ })()
101
+ },
102
+ }
103
+
104
+ // ~200 KB of older history — far past any sane verification budget.
105
+ const older: Message[] = Array.from({ length: 200 }, (_, i) =>
106
+ createUserMessage(`turn ${i} ${'y'.repeat(1000)}`),
107
+ )
108
+
109
+ await buildVerifiedSummary(manager, older, provider, config)
110
+
111
+ const promptChars = (calls[0]?.messages ?? []).reduce((n, m) => n + (m.content?.length ?? 0), 0)
112
+ expect(promptChars).toBeLessThan(100_000)
113
+ })
114
+ })
@@ -26,20 +26,29 @@ export const CompactionConfigSchema = z.object({
26
26
  * `tokenBudget` unlimited (0) set this so compaction fires on
27
27
  * window-pressure instead of being a silent no-op.
28
28
  */
29
- contextWindowTokens: z.number().positive().optional(),
29
+ // Every field below counts something — tokens, messages, characters,
30
+ // sentences. `z.number().positive()` was the wrong validator for all of them:
31
+ // Zod's base number check rejects only non-numbers and NaN, so `Infinity` and
32
+ // `0.5` both passed. `Infinity` is the dangerous one, because it turns a bound
33
+ // into a no-op rather than an error — `convoTextBudget: Infinity` makes the
34
+ // verifier's `charCount > budget` test never true, so `truncateMessages` stops
35
+ // truncating and the WHOLE history is pasted into the verification prompt.
36
+ // A fraction is merely nonsense (`keepRecentMessages: 2.5`). `.int()` rejects
37
+ // both: `Number.isInteger` is false for Infinity and for any fraction.
38
+ contextWindowTokens: z.number().int().positive().optional(),
30
39
  triggerThreshold: z.number().min(0).max(1).default(0.7),
31
40
  resetThreshold: z.number().min(0).max(1).default(0.4),
32
- keepRecentMessages: z.number().positive().default(4),
33
- maxToolResults: z.number().positive().default(30),
34
- maxListSize: z.number().positive().default(25),
41
+ keepRecentMessages: z.number().int().positive().default(4),
42
+ maxToolResults: z.number().int().positive().default(30),
43
+ maxListSize: z.number().int().positive().default(25),
35
44
  llmVerification: z.boolean().default(true),
36
- llmVerificationMaxTokens: z.number().positive().default(2048),
37
- richStateThreshold: z.number().positive().default(15),
38
- convoTextBudget: z.number().positive().default(12_000),
39
- maxSentencesPerTurn: z.number().positive().default(5),
40
- maxCharsPerNote: z.number().positive().default(500),
41
- maxCharsPerRequirement: z.number().positive().default(300),
42
- maxCharsPerTask: z.number().positive().default(400),
45
+ llmVerificationMaxTokens: z.number().int().positive().default(2048),
46
+ richStateThreshold: z.number().int().positive().default(15),
47
+ convoTextBudget: z.number().int().positive().default(12_000),
48
+ maxSentencesPerTurn: z.number().int().positive().default(5),
49
+ maxCharsPerNote: z.number().int().positive().default(500),
50
+ maxCharsPerRequirement: z.number().int().positive().default(300),
51
+ maxCharsPerTask: z.number().int().positive().default(400),
43
52
  })
44
53
 
45
54
  export type CompactionConfig = z.infer<typeof CompactionConfigSchema>
@@ -5,6 +5,7 @@ import { RunDiskStore } from '../../store/run/disk.js'
5
5
  import { type CostInfo, type TokenUsage, accumulateTokenUsage } from '../../types/common/index.js'
6
6
  import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
7
7
  import type { Message } from '../../types/message/index.js'
8
+ import type { ProviderErrorInfo } from '../../types/provider/index.js'
8
9
  import type { CheckpointRunScope, CheckpointStore } from '../../types/run/checkpoint-store.js'
9
10
  import type { EmergencySaveData } from '../../types/run/emergency.js'
10
11
  import type { Run, RunPersistenceConfig, StopReason } from '../../types/run/index.js'
@@ -164,10 +165,11 @@ export class RunPersistence {
164
165
  this.resolveResult()
165
166
  }
166
167
 
167
- markFailed(error: string): void {
168
+ markFailed(error: string, providerError?: ProviderErrorInfo): void {
168
169
  this.run.status = 'failed'
169
170
  this.run.stopReason = 'error'
170
171
  this.run.lastError = error
172
+ if (providerError) this.run.lastProviderError = providerError
171
173
  this.run.endedAt = Date.now()
172
174
  }
173
175
 
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ ProviderRequestError,
5
+ bodySaysContextOverflow,
6
+ classifyProviderHttpStatus,
7
+ isCallerAbortError,
8
+ isProviderRequestError,
9
+ parseRetryAfterMs,
10
+ providerVendorError,
11
+ } from '../errors.js'
12
+
13
+ describe('provider error taxonomy', () => {
14
+ it('classifies only context-specific limit messages as context overflow', () => {
15
+ expect(bodySaysContextOverflow('input token count exceeds the maximum for this model')).toBe(
16
+ true,
17
+ )
18
+ expect(bodySaysContextOverflow('tool count exceeds the maximum allowed')).toBe(false)
19
+ expect(bodySaysContextOverflow('reduce the tool list length')).toBe(false)
20
+ })
21
+
22
+ it('does not treat a generic HTTP 413 as model context overflow', () => {
23
+ expect(classifyProviderHttpStatus(413, 'request body too large at reverse proxy')).toBe(
24
+ 'bad_request',
25
+ )
26
+ expect(classifyProviderHttpStatus(413, 'prompt is too long for requested model')).toBe(
27
+ 'context_overflow',
28
+ )
29
+ })
30
+
31
+ it('parses both Retry-After forms and rejects stale or invalid values', () => {
32
+ expect(parseRetryAfterMs('2', 0)).toBe(2000)
33
+ expect(parseRetryAfterMs('Thu, 01 Jan 1970 00:00:05 GMT', 1000)).toBe(4000)
34
+ expect(parseRetryAfterMs('Thu, 01 Jan 1970 00:00:01 GMT', 1000)).toBeUndefined()
35
+ expect(parseRetryAfterMs('later', 0)).toBeUndefined()
36
+ })
37
+
38
+ it('drops the vendor object and its non-enumerable cause entirely', () => {
39
+ const secret = 'sk-FAKE-DO-NOT-LOG'
40
+ const vendor = new Error(`invalid api key ${secret}`, {
41
+ cause: new Error(`response body ${secret}`),
42
+ })
43
+ Object.assign(vendor, { status: 401 })
44
+
45
+ const classified = providerVendorError({
46
+ providerId: 'test-provider',
47
+ error: vendor,
48
+ })
49
+
50
+ expect(classified).toMatchObject({
51
+ name: 'ProviderRequestError',
52
+ kind: 'auth',
53
+ status: 401,
54
+ providerId: 'test-provider',
55
+ })
56
+ expect(classified.message).not.toContain(secret)
57
+ expect('cause' in classified).toBe(false)
58
+ })
59
+
60
+ it('recognizes a caller abort only when that signal actually aborted', () => {
61
+ const controller = new AbortController()
62
+ const sdkAbort = Object.assign(new Error('request aborted'), {
63
+ name: 'APIUserAbortError',
64
+ })
65
+
66
+ expect(isCallerAbortError(sdkAbort, controller.signal)).toBe(false)
67
+ controller.abort(new Error('user stopped'))
68
+ expect(isCallerAbortError(sdkAbort, controller.signal)).toBe(true)
69
+ expect(isCallerAbortError(controller.signal.reason, controller.signal)).toBe(true)
70
+ })
71
+
72
+ it('requires the complete structural contract when SDK copies differ', () => {
73
+ const classified = new ProviderRequestError({
74
+ kind: 'server',
75
+ providerId: 'test-provider',
76
+ })
77
+ expect(isProviderRequestError(classified)).toBe(true)
78
+
79
+ const impostor = Object.assign(new Error('not classified'), {
80
+ name: 'ProviderRequestError',
81
+ kind: 'anything',
82
+ })
83
+ expect(isProviderRequestError(impostor)).toBe(false)
84
+ })
85
+ })