@namzu/sdk 5.0.0 → 5.2.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 (181) hide show
  1. package/CHANGELOG.md +463 -0
  2. package/README.md +2 -2
  3. package/dist/agents/__tests__/run-agent-forwarding.test.d.ts +2 -0
  4. package/dist/agents/__tests__/run-agent-forwarding.test.d.ts.map +1 -0
  5. package/dist/agents/__tests__/run-agent-forwarding.test.js +76 -0
  6. package/dist/agents/__tests__/run-agent-forwarding.test.js.map +1 -0
  7. package/dist/agents/__tests__/run-completed-stop-reason.test.d.ts +2 -0
  8. package/dist/agents/__tests__/run-completed-stop-reason.test.d.ts.map +1 -0
  9. package/dist/agents/__tests__/run-completed-stop-reason.test.js +50 -0
  10. package/dist/agents/__tests__/run-completed-stop-reason.test.js.map +1 -0
  11. package/dist/agents/index.d.ts +1 -0
  12. package/dist/agents/index.d.ts.map +1 -1
  13. package/dist/agents/index.js +4 -0
  14. package/dist/agents/index.js.map +1 -1
  15. package/dist/agents/runAgent.d.ts +34 -0
  16. package/dist/agents/runAgent.d.ts.map +1 -1
  17. package/dist/agents/runAgent.js +5 -3
  18. package/dist/agents/runAgent.js.map +1 -1
  19. package/dist/config/runtime.d.ts +30 -30
  20. package/dist/constants/tools/__tests__/dangerous-patterns.test.d.ts +2 -0
  21. package/dist/constants/tools/__tests__/dangerous-patterns.test.d.ts.map +1 -0
  22. package/dist/constants/tools/__tests__/dangerous-patterns.test.js +54 -0
  23. package/dist/constants/tools/__tests__/dangerous-patterns.test.js.map +1 -0
  24. package/dist/constants/tools/index.d.ts.map +1 -1
  25. package/dist/constants/tools/index.js +28 -1
  26. package/dist/constants/tools/index.js.map +1 -1
  27. package/dist/contracts/a2a.d.ts +44 -44
  28. package/dist/directory/__tests__/derive.test.d.ts +2 -0
  29. package/dist/directory/__tests__/derive.test.d.ts.map +1 -0
  30. package/dist/directory/__tests__/derive.test.js +111 -0
  31. package/dist/directory/__tests__/derive.test.js.map +1 -0
  32. package/dist/directory/__tests__/load-diagnostics.test.d.ts +2 -0
  33. package/dist/directory/__tests__/load-diagnostics.test.d.ts.map +1 -0
  34. package/dist/directory/__tests__/load-diagnostics.test.js +106 -0
  35. package/dist/directory/__tests__/load-diagnostics.test.js.map +1 -0
  36. package/dist/directory/__tests__/load-directory.test.d.ts +2 -0
  37. package/dist/directory/__tests__/load-directory.test.d.ts.map +1 -0
  38. package/dist/directory/__tests__/load-directory.test.js +196 -0
  39. package/dist/directory/__tests__/load-directory.test.js.map +1 -0
  40. package/dist/directory/__tests__/subagents.test.d.ts +2 -0
  41. package/dist/directory/__tests__/subagents.test.d.ts.map +1 -0
  42. package/dist/directory/__tests__/subagents.test.js +117 -0
  43. package/dist/directory/__tests__/subagents.test.js.map +1 -0
  44. package/dist/directory/__tests__/supervisor-budget.test.d.ts +2 -0
  45. package/dist/directory/__tests__/supervisor-budget.test.d.ts.map +1 -0
  46. package/dist/directory/__tests__/supervisor-budget.test.js +97 -0
  47. package/dist/directory/__tests__/supervisor-budget.test.js.map +1 -0
  48. package/dist/directory/derive-supervisor.d.ts +58 -0
  49. package/dist/directory/derive-supervisor.d.ts.map +1 -0
  50. package/dist/directory/derive-supervisor.js +88 -0
  51. package/dist/directory/derive-supervisor.js.map +1 -0
  52. package/dist/directory/derive.d.ts +12 -0
  53. package/dist/directory/derive.d.ts.map +1 -0
  54. package/dist/directory/derive.js +58 -0
  55. package/dist/directory/derive.js.map +1 -0
  56. package/dist/directory/index.d.ts +7 -0
  57. package/dist/directory/index.d.ts.map +1 -0
  58. package/dist/directory/index.js +5 -0
  59. package/dist/directory/index.js.map +1 -0
  60. package/dist/directory/load.d.ts +14 -0
  61. package/dist/directory/load.d.ts.map +1 -0
  62. package/dist/directory/load.js +488 -0
  63. package/dist/directory/load.js.map +1 -0
  64. package/dist/directory/scan.d.ts +36 -0
  65. package/dist/directory/scan.d.ts.map +1 -0
  66. package/dist/directory/scan.js +136 -0
  67. package/dist/directory/scan.js.map +1 -0
  68. package/dist/directory/types.d.ts +215 -0
  69. package/dist/directory/types.d.ts.map +1 -0
  70. package/dist/directory/types.js +8 -0
  71. package/dist/directory/types.js.map +1 -0
  72. package/dist/plugin/__tests__/path-containment.test.d.ts +2 -0
  73. package/dist/plugin/__tests__/path-containment.test.d.ts.map +1 -0
  74. package/dist/plugin/__tests__/path-containment.test.js +91 -0
  75. package/dist/plugin/__tests__/path-containment.test.js.map +1 -0
  76. package/dist/plugin/lifecycle.d.ts.map +1 -1
  77. package/dist/plugin/lifecycle.js +3 -3
  78. package/dist/plugin/lifecycle.js.map +1 -1
  79. package/dist/plugin/loader.d.ts.map +1 -1
  80. package/dist/plugin/loader.js +12 -2
  81. package/dist/plugin/loader.js.map +1 -1
  82. package/dist/provider/__tests__/model-version.test.d.ts +2 -0
  83. package/dist/provider/__tests__/model-version.test.d.ts.map +1 -0
  84. package/dist/provider/__tests__/model-version.test.js +80 -0
  85. package/dist/provider/__tests__/model-version.test.js.map +1 -0
  86. package/dist/provider/__tests__/strict-schema.test.d.ts +2 -0
  87. package/dist/provider/__tests__/strict-schema.test.d.ts.map +1 -0
  88. package/dist/provider/__tests__/strict-schema.test.js +109 -0
  89. package/dist/provider/__tests__/strict-schema.test.js.map +1 -0
  90. package/dist/provider/__tests__/typed-error-classification.test.d.ts +2 -0
  91. package/dist/provider/__tests__/typed-error-classification.test.d.ts.map +1 -0
  92. package/dist/provider/__tests__/typed-error-classification.test.js +123 -0
  93. package/dist/provider/__tests__/typed-error-classification.test.js.map +1 -0
  94. package/dist/provider/model-version.d.ts +60 -0
  95. package/dist/provider/model-version.d.ts.map +1 -0
  96. package/dist/provider/model-version.js +64 -0
  97. package/dist/provider/model-version.js.map +1 -0
  98. package/dist/provider/retry.d.ts.map +1 -1
  99. package/dist/provider/retry.js +23 -7
  100. package/dist/provider/retry.js.map +1 -1
  101. package/dist/provider/strict-schema.d.ts +49 -0
  102. package/dist/provider/strict-schema.d.ts.map +1 -0
  103. package/dist/provider/strict-schema.js +112 -0
  104. package/dist/provider/strict-schema.js.map +1 -0
  105. package/dist/public-runtime.d.ts +6 -1
  106. package/dist/public-runtime.d.ts.map +1 -1
  107. package/dist/public-runtime.js +21 -1
  108. package/dist/public-runtime.js.map +1 -1
  109. package/dist/public-tools.d.ts +1 -0
  110. package/dist/public-tools.d.ts.map +1 -1
  111. package/dist/public-tools.js +4 -0
  112. package/dist/public-tools.js.map +1 -1
  113. package/dist/public-types.d.ts +2 -0
  114. package/dist/public-types.d.ts.map +1 -1
  115. package/dist/registry/tool/__tests__/strict-registration.test.d.ts +2 -0
  116. package/dist/registry/tool/__tests__/strict-registration.test.d.ts.map +1 -0
  117. package/dist/registry/tool/__tests__/strict-registration.test.js +82 -0
  118. package/dist/registry/tool/__tests__/strict-registration.test.js.map +1 -0
  119. package/dist/registry/tool/execute.d.ts.map +1 -1
  120. package/dist/registry/tool/execute.js +16 -0
  121. package/dist/registry/tool/execute.js.map +1 -1
  122. package/dist/runtime/query/__tests__/stream-recovery.test.js +5 -0
  123. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -1
  124. package/dist/runtime/query/result.d.ts.map +1 -1
  125. package/dist/runtime/query/result.js +5 -0
  126. package/dist/runtime/query/result.js.map +1 -1
  127. package/dist/tools/builtins/__tests__/edit-insert-reachable.test.js +7 -1
  128. package/dist/tools/builtins/__tests__/edit-insert-reachable.test.js.map +1 -1
  129. package/dist/tools/builtins/__tests__/edit.test.js +1 -1
  130. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  131. package/dist/tools/builtins/edit.d.ts.map +1 -1
  132. package/dist/tools/builtins/edit.js +20 -2
  133. package/dist/tools/builtins/edit.js.map +1 -1
  134. package/dist/types/plugin/index.d.ts +4 -4
  135. package/dist/types/provider/errors.d.ts +0 -17
  136. package/dist/types/provider/errors.d.ts.map +1 -1
  137. package/dist/types/provider/errors.js +60 -0
  138. package/dist/types/provider/errors.js.map +1 -1
  139. package/dist/types/run/events.d.ts +17 -2
  140. package/dist/types/run/events.d.ts.map +1 -1
  141. package/dist/types/run/events.js.map +1 -1
  142. package/dist/types/verification/index.d.ts +6 -6
  143. package/package.json +1 -1
  144. package/src/agents/__tests__/run-agent-forwarding.test.ts +85 -0
  145. package/src/agents/__tests__/run-completed-stop-reason.test.ts +59 -0
  146. package/src/agents/index.ts +8 -0
  147. package/src/agents/runAgent.ts +30 -4
  148. package/src/constants/tools/__tests__/dangerous-patterns.test.ts +62 -0
  149. package/src/constants/tools/index.ts +28 -1
  150. package/src/directory/__tests__/derive.test.ts +135 -0
  151. package/src/directory/__tests__/load-diagnostics.test.ts +126 -0
  152. package/src/directory/__tests__/load-directory.test.ts +243 -0
  153. package/src/directory/__tests__/subagents.test.ts +155 -0
  154. package/src/directory/__tests__/supervisor-budget.test.ts +117 -0
  155. package/src/directory/derive-supervisor.ts +149 -0
  156. package/src/directory/derive.ts +70 -0
  157. package/src/directory/index.ts +27 -0
  158. package/src/directory/load.ts +556 -0
  159. package/src/directory/scan.ts +163 -0
  160. package/src/directory/types.ts +254 -0
  161. package/src/plugin/__tests__/path-containment.test.ts +107 -0
  162. package/src/plugin/lifecycle.ts +3 -3
  163. package/src/plugin/loader.ts +12 -2
  164. package/src/provider/__tests__/model-version.test.ts +93 -0
  165. package/src/provider/__tests__/strict-schema.test.ts +133 -0
  166. package/src/provider/__tests__/typed-error-classification.test.ts +143 -0
  167. package/src/provider/model-version.ts +98 -0
  168. package/src/provider/retry.ts +23 -7
  169. package/src/provider/strict-schema.ts +125 -0
  170. package/src/public-runtime.ts +31 -0
  171. package/src/public-tools.ts +4 -0
  172. package/src/public-types.ts +8 -0
  173. package/src/registry/tool/__tests__/strict-registration.test.ts +115 -0
  174. package/src/registry/tool/execute.ts +16 -0
  175. package/src/runtime/query/__tests__/stream-recovery.test.ts +5 -0
  176. package/src/runtime/query/result.ts +5 -0
  177. package/src/tools/builtins/__tests__/edit-insert-reachable.test.ts +8 -2
  178. package/src/tools/builtins/__tests__/edit.test.ts +1 -1
  179. package/src/tools/builtins/edit.ts +20 -2
  180. package/src/types/provider/errors.ts +71 -0
  181. package/src/types/run/events.ts +15 -2
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ import type { ToolDefinition } from '../../../types/tool/index.js'
5
+ import { ToolRegistry } from '../execute.js'
6
+
7
+ /**
8
+ * A tool that asks for constrained generation and hands over a schema the
9
+ * constrained dialect cannot express is wrong at the moment it is DECLARED,
10
+ * whichever model it later meets.
11
+ *
12
+ * The first attempt at this check lived in a provider driver. That caught the
13
+ * bug — but per request, in one of the two drivers that mark tools strict, and
14
+ * only once something actually ran. The registry already refused
15
+ * `enforceModelInput` without a `modelInputSchema`, with a comment stating the
16
+ * principle exactly: "Refusing at registration puts the error where the author
17
+ * can fix it rather than at the first request." The rule was written down; the
18
+ * check was in the wrong place.
19
+ *
20
+ * So the pair is here now. One asks whether a model schema EXISTS, the other
21
+ * whether it can carry the guarantee the tool just requested.
22
+ */
23
+
24
+ function tool(overrides: Partial<ToolDefinition>): ToolDefinition {
25
+ return {
26
+ name: 'sample',
27
+ description: 'a tool',
28
+ inputSchema: z.object({}),
29
+ execute: async () => ({ success: true, output: 'ok' }),
30
+ ...overrides,
31
+ } as ToolDefinition
32
+ }
33
+
34
+ describe('a tool cannot register a schema its own guarantee cannot carry', () => {
35
+ it('refuses a keyword outside the strict subset, naming the path', () => {
36
+ const registry = new ToolRegistry()
37
+
38
+ expect(() =>
39
+ registry.register(
40
+ tool({
41
+ name: 'edit',
42
+ enforceModelInput: true,
43
+ modelInputSchema: {
44
+ type: 'object',
45
+ properties: { insertLine: { oneOf: [{ type: 'integer' }, { const: 'end' }] } },
46
+ },
47
+ }),
48
+ ),
49
+ ).toThrow(/edit\.properties\.insertLine\.oneOf/)
50
+ })
51
+
52
+ it('names the remedy, not just the offence', () => {
53
+ const registry = new ToolRegistry()
54
+
55
+ expect(() =>
56
+ registry.register(
57
+ tool({ enforceModelInput: true, modelInputSchema: { properties: { n: { minimum: 0 } } } }),
58
+ ),
59
+ ).toThrow(/enforce at execution/)
60
+ })
61
+
62
+ it('admits the same union spelled the way the subset accepts', () => {
63
+ const registry = new ToolRegistry()
64
+
65
+ expect(() =>
66
+ registry.register(
67
+ tool({
68
+ enforceModelInput: true,
69
+ modelInputSchema: {
70
+ type: 'object',
71
+ properties: { insertLine: { anyOf: [{ type: 'integer' }, { const: 'end' }] } },
72
+ additionalProperties: false,
73
+ },
74
+ }),
75
+ ),
76
+ ).not.toThrow()
77
+ })
78
+
79
+ it('leaves a tool that never asked for the guarantee alone', () => {
80
+ // Without `enforceModelInput` nothing is marked strict, so the schema is
81
+ // sent as ordinary JSON Schema and `oneOf` is perfectly legal there.
82
+ // Refusing it would break working setups for no reason.
83
+ const registry = new ToolRegistry()
84
+
85
+ expect(() =>
86
+ registry.register(
87
+ tool({ modelInputSchema: { properties: { a: { oneOf: [{ type: 'string' }] } } } }),
88
+ ),
89
+ ).not.toThrow()
90
+ })
91
+
92
+ it('still refuses enforcement with no model schema at all', () => {
93
+ // The check this one was added beside. Kept in the same file so a
94
+ // future edit sees both halves of the pair together.
95
+ const registry = new ToolRegistry()
96
+
97
+ expect(() => registry.register(tool({ enforceModelInput: true }))).toThrow(
98
+ /does not define modelInputSchema/,
99
+ )
100
+ })
101
+
102
+ it('refuses through every registration shape', () => {
103
+ // `register` has three overloads and only one of them was exercised
104
+ // above; a check on the wrong one would look like coverage.
105
+ const bad = tool({
106
+ name: 'bad',
107
+ enforceModelInput: true,
108
+ modelInputSchema: { properties: { a: { oneOf: [] } } },
109
+ })
110
+
111
+ expect(() => new ToolRegistry().register(bad)).toThrow(/oneOf/)
112
+ expect(() => new ToolRegistry().register('bad', bad)).toThrow(/oneOf/)
113
+ expect(() => new ToolRegistry().register([bad])).toThrow(/oneOf/)
114
+ })
115
+ })
@@ -1,4 +1,5 @@
1
1
  import { SpanStatusCode, context as otelContext, trace } from '@opentelemetry/api'
2
+ import { assertStrictSchema } from '../../provider/strict-schema.js'
2
3
  import { GENAI, NAMZU, toolSpanName } from '../../telemetry/attributes.js'
3
4
  import { recordToolCall } from '../../telemetry/metrics.js'
4
5
  import { getTracer } from '../../telemetry/runtime-accessors.js'
@@ -177,6 +178,21 @@ export class ToolRegistry extends ManagedRegistry<ToolDefinition> {
177
178
  `Tool "${id}" enables enforceModelInput but does not define modelInputSchema. Constrained input generation requires an explicit provider-safe model schema.`,
178
179
  )
179
180
  }
181
+ // …and the schema has to be one a constrained decoder can actually be
182
+ // given. The check above asks whether a model schema EXISTS; this asks
183
+ // whether it can carry the guarantee the tool just requested.
184
+ //
185
+ // Both belong here for the reason the comment above already states.
186
+ // Strict validation runs against a SUBSET of JSON Schema, and a keyword
187
+ // outside it is not degraded — the request is rejected whole, so one
188
+ // unexpressible field takes down every other tool in the call. The
189
+ // first version of this check lived in a provider driver, which meant
190
+ // it fired per request, in one driver, long after the author had moved
191
+ // on. A tool that asks for a guarantee its own schema cannot carry is
192
+ // wrong at the moment it is declared, whichever model it later meets.
193
+ if (tool.enforceModelInput) {
194
+ assertStrictSchema(id, tool.modelInputSchema)
195
+ }
180
196
  if (tool.tier && this.tierConfig) {
181
197
  const validIds = this.tierConfig.tiers.map((t) => t.id)
182
198
  if (!validIds.includes(tool.tier)) {
@@ -182,6 +182,11 @@ describe('query stream recovery', () => {
182
182
  const run = await drainQuery(
183
183
  {
184
184
  provider: new ClassifiedFailureProvider(),
185
+ // Retry off: this pins METADATA at the boundary, and a throttle
186
+ // is genuinely retryable — leaving retry on would spend the run's
187
+ // whole timeout backing off and settle it as a timeout instead,
188
+ // testing the retry policy rather than the thing named here.
189
+ retry: { maxRetries: 0 },
185
190
  tools: new ToolRegistry(),
186
191
  runConfig: {
187
192
  model: 'mock-model',
@@ -47,6 +47,11 @@ export class ResultAssembler {
47
47
  type: 'run_completed',
48
48
  runId: runMgr.id,
49
49
  result: runMgr.getRun().result ?? '',
50
+ // Read AFTER `markCompleted`, which is where a run that was stopped
51
+ // mid-flight has its reason settled. Carried on the event so a
52
+ // consumer can tell "answered" from "ran out of budget" without
53
+ // holding the `Run`.
54
+ ...(runMgr.getRun().stopReason ? { stopReason: runMgr.getRun().stopReason } : {}),
50
55
  })
51
56
  yield* drainPending()
52
57
 
@@ -70,12 +70,18 @@ describe('the model can emit the idiom the description recommends', () => {
70
70
  })
71
71
 
72
72
  it('admits only "end" as a string, so a synonym cannot be generated', () => {
73
- const insert = (schema().properties as Record<string, { oneOf?: unknown[] }>).insertLine
73
+ const insert = (schema().properties as Record<string, { anyOf?: unknown[] }>).insertLine
74
74
 
75
75
  // The schema is where the synonym problem is solved for a provider that
76
76
  // constrains: `"EOF"` is not emittable because `"end"` is the only
77
77
  // string the union admits.
78
- expect(insert?.oneOf).toEqual([{ type: 'integer', minimum: 0 }, { const: 'end' }])
78
+ //
79
+ // `anyOf`, not `oneOf`: strict tool use validates against a subset of
80
+ // JSON Schema that excludes `oneOf`, and the vendor rejects the whole
81
+ // request rather than one field — so the spelling here is load-bearing,
82
+ // not stylistic. The two are equivalent for disjoint branches.
83
+ // `minimum` is gone for the same reason; the execution schema keeps it.
84
+ expect(insert?.anyOf).toEqual([{ type: 'integer' }, { const: 'end' }])
79
85
  })
80
86
  })
81
87
 
@@ -39,7 +39,7 @@ describe('EditTool', () => {
39
39
  'Exact replacement text. May be empty to delete old_string. Keep under 12000 characters.',
40
40
  },
41
41
  insertLine: {
42
- oneOf: [{ type: 'integer', minimum: 0 }, { const: 'end' }],
42
+ anyOf: [{ type: 'integer' }, { const: 'end' }],
43
43
  description:
44
44
  'Insert instead of replacing. The new_string goes after this 1-indexed line; 0 inserts before the first line; "end" appends. Omit for a find-and-replace.',
45
45
  },
@@ -102,12 +102,30 @@ const modelInputSchema: Record<string, unknown> = {
102
102
  },
103
103
  insertLine: {
104
104
  // The union the execution schema already accepts, stated so a
105
- // constrained decoder can emit it. Declaring it as `oneOf` of an
105
+ // constrained decoder can emit it. Stating it as a union of an
106
106
  // integer and the literal `"end"` also makes the synonym problem
107
107
  // structurally impossible: `"EOF"`, `"append"` and `"last"` are
108
108
  // not emittable, because `"end"` is the only string the schema
109
109
  // admits.
110
- oneOf: [{ type: 'integer', minimum: 0 }, { const: 'end' }],
110
+ //
111
+ // `anyOf`, NOT `oneOf`. Strict tool use validates against a SUBSET
112
+ // of JSON Schema, and `oneOf` is not in it — the vendor rejects the
113
+ // whole request with `tools.N.custom: Schema type 'oneOf' is not
114
+ // supported`, so the tool never mounts and the turn dies before a
115
+ // single token. Measured against the live API: strict + `oneOf` is
116
+ // a 400, strict + `anyOf` is accepted, and non-strict + `oneOf` is
117
+ // accepted — which is why nothing caught it. Both halves were
118
+ // individually fine; only their combination fails, and strict is on
119
+ // for every model at or above the gate.
120
+ //
121
+ // The two branches are disjoint, so `anyOf` and `oneOf` mean the
122
+ // same thing here — nothing is loosened.
123
+ //
124
+ // `minimum` is gone for the same reason: numeric constraints are
125
+ // outside the strict subset too. The bound is not lost — the
126
+ // execution schema still enforces it, which is where a value that
127
+ // crosses a boundary should be checked anyway.
128
+ anyOf: [{ type: 'integer' }, { const: 'end' }],
111
129
  description:
112
130
  'Insert instead of replacing. The new_string goes after this 1-indexed line; 0 inserts before the first line; "end" appends. Omit for a find-and-replace.',
113
131
  },
@@ -362,6 +362,62 @@ function codeFromStructure(err: unknown): ProviderErrorCode | undefined {
362
362
  *
363
363
  * Aborts are passed through untouched — see {@link isAbortError}.
364
364
  */
365
+ /**
366
+ * Every `ProviderErrorKind`, mapped to what the runtime acts on.
367
+ *
368
+ * Read structurally rather than with `instanceof`: a driver in one package
369
+ * throws this and the runtime in another reads it, and two copies of the SDK
370
+ * in one process make `instanceof` unreliable — the same reason
371
+ * `isProviderRequestError` exists.
372
+ *
373
+ * `retryable` here is about whether resending the SAME request could succeed.
374
+ * `context_overflow` is correctly false — an identical prompt overflows
375
+ * identically — and it maps to `context_length_exceeded` so the loop can reach
376
+ * for compaction, which is a different remedy than a retry.
377
+ */
378
+ const KIND_TO_CODE: Readonly<Record<string, { code: ProviderErrorCode; retryable: boolean }>> = {
379
+ throttle: { code: 'rate_limit', retryable: true },
380
+ network: { code: 'network', retryable: true },
381
+ server: { code: 'server_error', retryable: true },
382
+ auth: { code: 'auth', retryable: false },
383
+ context_overflow: { code: 'context_length_exceeded', retryable: false },
384
+ bad_request: { code: 'invalid_request', retryable: false },
385
+ }
386
+
387
+ function classifyFromProviderRequestError(
388
+ err: unknown,
389
+ providerId: string | undefined,
390
+ now: number,
391
+ ): ProviderError | undefined {
392
+ if (!(err instanceof Error) || err.name !== 'ProviderRequestError') return undefined
393
+ const candidate = err as Error & {
394
+ kind?: unknown
395
+ providerId?: unknown
396
+ status?: unknown
397
+ retryAfterMs?: unknown
398
+ }
399
+ if (typeof candidate.kind !== 'string') return undefined
400
+ const mapped = KIND_TO_CODE[candidate.kind]
401
+ if (!mapped) return undefined
402
+
403
+ const retryAfterMs =
404
+ typeof candidate.retryAfterMs === 'number' ? candidate.retryAfterMs : readRetryAfterMs(err, now)
405
+
406
+ return new ProviderError({
407
+ code: mapped.code,
408
+ message: err.message,
409
+ retryable: mapped.retryable,
410
+ cause: err,
411
+ ...(typeof candidate.providerId === 'string'
412
+ ? { providerId: candidate.providerId }
413
+ : providerId !== undefined
414
+ ? { providerId }
415
+ : {}),
416
+ ...(typeof candidate.status === 'number' ? { status: candidate.status } : {}),
417
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
418
+ })
419
+ }
420
+
365
421
  export function classifyProviderError(
366
422
  err: unknown,
367
423
  providerId?: string,
@@ -369,6 +425,21 @@ export function classifyProviderError(
369
425
  ): ProviderError {
370
426
  if (isProviderError(err)) return err
371
427
 
428
+ // A driver that already classified its own failure is read FIRST, and by
429
+ // its `kind` — the field it set on purpose.
430
+ //
431
+ // Without this the classifier fell through to the status heuristics, where
432
+ // a `ProviderRequestError` carrying `kind: 'context_overflow'` and a 400
433
+ // became `invalid_request`, non-retryable. Three of the six kinds landed
434
+ // wrong that way, and the consequences were not cosmetic: the loop's
435
+ // overflow branch tests for `context_length_exceeded`, so compaction relief
436
+ // — the one provider failure this kernel can actually do something about —
437
+ // was unreachable for exactly the drivers that had diagnosed it correctly.
438
+ // A driver that classified its own error came out worse than one that did
439
+ // not, which is the opposite of the incentive the type was created for.
440
+ const fromKind = classifyFromProviderRequestError(err, providerId, now)
441
+ if (fromKind) return fromKind
442
+
372
443
  const message = err instanceof Error ? err.message : String(err)
373
444
  const status = readStatus(err)
374
445
  const errno = readErrnoCode(err)
@@ -16,7 +16,7 @@ import type { PlanStep } from '../plan/index.js'
16
16
  import type { PluginHookEvent, PluginHookResult } from '../plugin/index.js'
17
17
  import type { TaskStatus } from '../task/index.js'
18
18
  import type { Lineage } from './lineage.js'
19
- import type { MessageStopReason } from './stop-reason.js'
19
+ import type { MessageStopReason, StopReason } from './stop-reason.js'
20
20
  import type {
21
21
  SubsessionIdledEvent,
22
22
  SubsessionMessagedEvent,
@@ -252,7 +252,20 @@ type CoreRunEvent =
252
252
  guardrail?: string
253
253
  reason?: string
254
254
  }
255
- | { type: 'run_completed'; runId: RunId; result: string }
255
+ /**
256
+ * The run reached its end without throwing.
257
+ *
258
+ * `completed` is not `succeeded`. A run stopped by its token budget, its
259
+ * timeout, its iteration cap, a cancellation or a blocking output guardrail
260
+ * all arrive here — `run_failed` is emitted only from the throw path — so a
261
+ * consumer that treated this event as success reported one for a run whose
262
+ * answer was refused. Measured: `max_iterations` produces
263
+ * `status: 'completed'` with `result` holding whatever partial text existed.
264
+ *
265
+ * `stopReason` is what separates them, and it is on the event because the
266
+ * alternative is asking every consumer to hold the `Run` as well.
267
+ */
268
+ | { type: 'run_completed'; runId: RunId; result: string; stopReason?: StopReason }
256
269
  /**
257
270
  * The run failed.
258
271
  *