@namzu/sdk 0.4.4 → 0.5.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 (180) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/dist/advisory/executor.d.ts.map +1 -1
  3. package/dist/advisory/executor.js +3 -2
  4. package/dist/advisory/executor.js.map +1 -1
  5. package/dist/advisory/executor.test.js +36 -14
  6. package/dist/advisory/executor.test.js.map +1 -1
  7. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  8. package/dist/agents/ReactiveAgent.js +1 -0
  9. package/dist/agents/ReactiveAgent.js.map +1 -1
  10. package/dist/agents/RouterAgent.d.ts.map +1 -1
  11. package/dist/agents/RouterAgent.js +3 -2
  12. package/dist/agents/RouterAgent.js.map +1 -1
  13. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  14. package/dist/agents/SupervisorAgent.js +2 -0
  15. package/dist/agents/SupervisorAgent.js.map +1 -1
  16. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  17. package/dist/bridge/a2a/mapper.js +23 -9
  18. package/dist/bridge/a2a/mapper.js.map +1 -1
  19. package/dist/bridge/a2a/mapper.test.js +35 -9
  20. package/dist/bridge/a2a/mapper.test.js.map +1 -1
  21. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  22. package/dist/bridge/sse/mapper.js +60 -8
  23. package/dist/bridge/sse/mapper.js.map +1 -1
  24. package/dist/bridge/sse/mapper.test.js +123 -16
  25. package/dist/bridge/sse/mapper.test.js.map +1 -1
  26. package/dist/compaction/verifier.d.ts.map +1 -1
  27. package/dist/compaction/verifier.js +3 -2
  28. package/dist/compaction/verifier.js.map +1 -1
  29. package/dist/config/runtime.d.ts +14 -14
  30. package/dist/config/runtime.js +1 -1
  31. package/dist/config/runtime.js.map +1 -1
  32. package/dist/contracts/api.d.ts +1 -1
  33. package/dist/contracts/api.d.ts.map +1 -1
  34. package/dist/contracts/schemas.js +1 -1
  35. package/dist/contracts/schemas.js.map +1 -1
  36. package/dist/gateway/local.d.ts +1 -1
  37. package/dist/gateway/local.d.ts.map +1 -1
  38. package/dist/gateway/local.js +1 -0
  39. package/dist/gateway/local.js.map +1 -1
  40. package/dist/manager/agent/__tests__/lifecycle.test.js +2 -2
  41. package/dist/provider/collect.d.ts +25 -0
  42. package/dist/provider/collect.d.ts.map +1 -0
  43. package/dist/provider/collect.js +82 -0
  44. package/dist/provider/collect.js.map +1 -0
  45. package/dist/provider/collect.test.d.ts +22 -0
  46. package/dist/provider/collect.test.d.ts.map +1 -0
  47. package/dist/provider/collect.test.js +123 -0
  48. package/dist/provider/collect.test.js.map +1 -0
  49. package/dist/provider/instrumentation.d.ts.map +1 -1
  50. package/dist/provider/instrumentation.js +10 -43
  51. package/dist/provider/instrumentation.js.map +1 -1
  52. package/dist/provider/instrumentation.test.d.ts +15 -0
  53. package/dist/provider/instrumentation.test.d.ts.map +1 -1
  54. package/dist/provider/instrumentation.test.js +73 -87
  55. package/dist/provider/instrumentation.test.js.map +1 -1
  56. package/dist/provider/mock.d.ts +1 -2
  57. package/dist/provider/mock.d.ts.map +1 -1
  58. package/dist/provider/mock.js +2 -5
  59. package/dist/provider/mock.js.map +1 -1
  60. package/dist/public-runtime.d.ts +1 -0
  61. package/dist/public-runtime.d.ts.map +1 -1
  62. package/dist/public-runtime.js +5 -0
  63. package/dist/public-runtime.js.map +1 -1
  64. package/dist/run/LimitChecker.test.d.ts +2 -0
  65. package/dist/run/LimitChecker.test.d.ts.map +1 -0
  66. package/dist/run/LimitChecker.test.js +26 -0
  67. package/dist/run/LimitChecker.test.js.map +1 -0
  68. package/dist/run/reporter.d.ts.map +1 -1
  69. package/dist/run/reporter.js +10 -6
  70. package/dist/run/reporter.js.map +1 -1
  71. package/dist/runtime/query/__tests__/prompt.test.d.ts +2 -0
  72. package/dist/runtime/query/__tests__/prompt.test.d.ts.map +1 -0
  73. package/dist/runtime/query/__tests__/prompt.test.js +35 -0
  74. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -0
  75. package/dist/runtime/query/context-cache.d.ts +2 -0
  76. package/dist/runtime/query/context-cache.d.ts.map +1 -1
  77. package/dist/runtime/query/context-cache.js +3 -0
  78. package/dist/runtime/query/context-cache.js.map +1 -1
  79. package/dist/runtime/query/events.d.ts +2 -0
  80. package/dist/runtime/query/events.d.ts.map +1 -1
  81. package/dist/runtime/query/events.js +48 -1
  82. package/dist/runtime/query/events.js.map +1 -1
  83. package/dist/runtime/query/executor.d.ts.map +1 -1
  84. package/dist/runtime/query/executor.js +55 -5
  85. package/dist/runtime/query/executor.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +2 -1
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +2 -0
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  91. package/dist/runtime/query/iteration/index.js +245 -13
  92. package/dist/runtime/query/iteration/index.js.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  94. package/dist/runtime/query/iteration/phases/compaction.js +2 -0
  95. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  96. package/dist/runtime/query/prompt.d.ts +2 -0
  97. package/dist/runtime/query/prompt.d.ts.map +1 -1
  98. package/dist/runtime/query/prompt.js +35 -13
  99. package/dist/runtime/query/prompt.js.map +1 -1
  100. package/dist/session/__tests__/integration/e2e-spawn.test.js +2 -2
  101. package/dist/session/__tests__/integration/event-stream-ordering.test.d.ts +1 -1
  102. package/dist/session/__tests__/integration/event-stream-ordering.test.js +7 -7
  103. package/dist/streaming/coalesce.d.ts +28 -0
  104. package/dist/streaming/coalesce.d.ts.map +1 -0
  105. package/dist/streaming/coalesce.js +75 -0
  106. package/dist/streaming/coalesce.js.map +1 -0
  107. package/dist/streaming/coalesce.test.d.ts +19 -0
  108. package/dist/streaming/coalesce.test.d.ts.map +1 -0
  109. package/dist/streaming/coalesce.test.js +120 -0
  110. package/dist/streaming/coalesce.test.js.map +1 -0
  111. package/dist/tools/coordinator/index.d.ts +2 -0
  112. package/dist/tools/coordinator/index.d.ts.map +1 -1
  113. package/dist/tools/coordinator/index.js +1 -0
  114. package/dist/tools/coordinator/index.js.map +1 -1
  115. package/dist/types/agent/base.d.ts +7 -0
  116. package/dist/types/agent/base.d.ts.map +1 -1
  117. package/dist/types/agent/gateway.d.ts +2 -1
  118. package/dist/types/agent/gateway.d.ts.map +1 -1
  119. package/dist/types/ids/index.d.ts +10 -0
  120. package/dist/types/ids/index.d.ts.map +1 -1
  121. package/dist/types/ids/index.js.map +1 -1
  122. package/dist/types/provider/interface.d.ts +26 -2
  123. package/dist/types/provider/interface.d.ts.map +1 -1
  124. package/dist/types/provider/stream.d.ts +18 -0
  125. package/dist/types/provider/stream.d.ts.map +1 -1
  126. package/dist/types/run/events.d.ts +58 -8
  127. package/dist/types/run/events.d.ts.map +1 -1
  128. package/dist/types/run/events.js +23 -1
  129. package/dist/types/run/events.js.map +1 -1
  130. package/dist/types/run/schema-version.d.ts +7 -1
  131. package/dist/types/run/schema-version.d.ts.map +1 -1
  132. package/dist/types/run/schema-version.js +7 -1
  133. package/dist/types/run/schema-version.js.map +1 -1
  134. package/dist/types/run/stop-reason.d.ts +9 -0
  135. package/dist/types/run/stop-reason.d.ts.map +1 -1
  136. package/package.json +1 -1
  137. package/src/advisory/executor.test.ts +37 -15
  138. package/src/advisory/executor.ts +10 -7
  139. package/src/agents/ReactiveAgent.ts +1 -0
  140. package/src/agents/RouterAgent.ts +9 -6
  141. package/src/agents/SupervisorAgent.ts +2 -0
  142. package/src/bridge/a2a/mapper.test.ts +35 -9
  143. package/src/bridge/a2a/mapper.ts +23 -9
  144. package/src/bridge/sse/mapper.test.ts +152 -24
  145. package/src/bridge/sse/mapper.ts +66 -9
  146. package/src/compaction/verifier.ts +9 -6
  147. package/src/config/runtime.ts +1 -1
  148. package/src/contracts/api.ts +7 -0
  149. package/src/contracts/schemas.ts +1 -1
  150. package/src/gateway/local.ts +3 -2
  151. package/src/manager/agent/__tests__/lifecycle.test.ts +2 -2
  152. package/src/provider/collect.test.ts +142 -0
  153. package/src/provider/collect.ts +85 -0
  154. package/src/provider/instrumentation.test.ts +81 -100
  155. package/src/provider/instrumentation.ts +11 -53
  156. package/src/provider/mock.ts +2 -6
  157. package/src/public-runtime.ts +6 -0
  158. package/src/run/LimitChecker.test.ts +32 -0
  159. package/src/run/reporter.ts +10 -7
  160. package/src/runtime/query/__tests__/prompt.test.ts +38 -0
  161. package/src/runtime/query/context-cache.ts +5 -0
  162. package/src/runtime/query/events.ts +52 -1
  163. package/src/runtime/query/executor.ts +54 -5
  164. package/src/runtime/query/index.ts +5 -1
  165. package/src/runtime/query/iteration/index.ts +301 -26
  166. package/src/runtime/query/iteration/phases/compaction.ts +2 -0
  167. package/src/runtime/query/prompt.ts +45 -17
  168. package/src/session/__tests__/integration/e2e-spawn.test.ts +2 -2
  169. package/src/session/__tests__/integration/event-stream-ordering.test.ts +7 -7
  170. package/src/streaming/coalesce.test.ts +132 -0
  171. package/src/streaming/coalesce.ts +89 -0
  172. package/src/tools/coordinator/index.ts +3 -0
  173. package/src/types/agent/base.ts +9 -0
  174. package/src/types/agent/gateway.ts +3 -1
  175. package/src/types/ids/index.ts +10 -0
  176. package/src/types/provider/interface.ts +28 -3
  177. package/src/types/provider/stream.ts +18 -0
  178. package/src/types/run/events.ts +105 -9
  179. package/src/types/run/schema-version.ts +7 -1
  180. package/src/types/run/stop-reason.ts +17 -0
@@ -67,39 +67,43 @@ describe('mapRunToStreamEvent — mapped variants', () => {
67
67
  expect(b).toEqual({ wire: 'iteration.completed', data: { run_id: RID, iteration: 2 } })
68
68
  })
69
69
 
70
- it('llm_response message.delta with content + has_tool_calls', () => {
71
- const r = mapRunToStreamEvent(
72
- { type: 'llm_response', runId: RID, content: 'hi', hasToolCalls: false },
73
- RID,
74
- )
75
- expect(r).toEqual({
76
- wire: 'message.delta',
77
- data: { run_id: RID, content: 'hi', has_tool_calls: false },
78
- })
79
- })
80
-
81
- it('llm_response with null content → content: null', () => {
82
- const r = mapRunToStreamEvent(
83
- { type: 'llm_response', runId: RID, content: null, hasToolCalls: true },
84
- RID,
85
- )
86
- expect(r?.data).toMatchObject({ content: null, has_tool_calls: true })
87
- })
88
-
89
- it('tool_executing / tool_completed carry tool_name + input/result', () => {
70
+ it('tool_executing / tool_completed carry tool_use_id, tool_name, input/result, is_error', () => {
71
+ const TUID = 'toolu_x'
90
72
  const exec = mapRunToStreamEvent(
91
- { type: 'tool_executing', runId: RID, toolName: 'read_file', input: { path: '/a' } },
73
+ {
74
+ type: 'tool_executing',
75
+ runId: RID,
76
+ toolUseId: TUID,
77
+ toolName: 'read_file',
78
+ input: { path: '/a' },
79
+ },
92
80
  RID,
93
81
  )
94
82
  expect(exec?.wire).toBe('tool.executing')
95
- expect(exec?.data).toMatchObject({ tool_name: 'read_file', input: { path: '/a' } })
83
+ expect(exec?.data).toMatchObject({
84
+ tool_use_id: TUID,
85
+ tool_name: 'read_file',
86
+ input: { path: '/a' },
87
+ })
96
88
 
97
89
  const done = mapRunToStreamEvent(
98
- { type: 'tool_completed', runId: RID, toolName: 'read_file', result: 'ok' },
90
+ {
91
+ type: 'tool_completed',
92
+ runId: RID,
93
+ toolUseId: TUID,
94
+ toolName: 'read_file',
95
+ result: 'ok',
96
+ isError: false,
97
+ },
99
98
  RID,
100
99
  )
101
100
  expect(done?.wire).toBe('tool.completed')
102
- expect(done?.data).toMatchObject({ tool_name: 'read_file', result: 'ok' })
101
+ expect(done?.data).toMatchObject({
102
+ tool_use_id: TUID,
103
+ tool_name: 'read_file',
104
+ result: 'ok',
105
+ is_error: false,
106
+ })
103
107
  })
104
108
 
105
109
  it('tool_review_requested / tool_review_completed carry review fields', () => {
@@ -410,6 +414,130 @@ describe('mapRunToStreamEvent — explicit null set', () => {
410
414
  })
411
415
  })
412
416
 
417
+ describe('mapRunToStreamEvent — v3 message and tool-input lifecycle', () => {
418
+ const MID = 'msg_1' as `msg_${string}`
419
+ const TUID = 'toolu_a'
420
+
421
+ it('message_started → message.created', () => {
422
+ const r = mapRunToStreamEvent(
423
+ { type: 'message_started', runId: RID, iteration: 0, messageId: MID },
424
+ RID,
425
+ )
426
+ expect(r?.wire).toBe('message.created')
427
+ expect(r?.data).toMatchObject({ run_id: RID, iteration: 0, message_id: MID })
428
+ })
429
+
430
+ it('text_delta → message.delta carries raw text fragment', () => {
431
+ const r = mapRunToStreamEvent(
432
+ {
433
+ type: 'text_delta',
434
+ runId: RID,
435
+ iteration: 0,
436
+ messageId: MID,
437
+ text: 'hel',
438
+ },
439
+ RID,
440
+ )
441
+ expect(r?.wire).toBe('message.delta')
442
+ expect(r?.data).toMatchObject({ message_id: MID, text: 'hel' })
443
+ })
444
+
445
+ it('message_completed → message.completed carries stop reason and usage', () => {
446
+ const usage = {
447
+ promptTokens: 10,
448
+ completionTokens: 5,
449
+ totalTokens: 15,
450
+ cachedTokens: 0,
451
+ cacheWriteTokens: 0,
452
+ }
453
+ const r = mapRunToStreamEvent(
454
+ {
455
+ type: 'message_completed',
456
+ runId: RID,
457
+ iteration: 0,
458
+ messageId: MID,
459
+ stopReason: 'end_turn',
460
+ usage,
461
+ },
462
+ RID,
463
+ )
464
+ expect(r?.wire).toBe('message.completed')
465
+ expect(r?.data).toMatchObject({
466
+ message_id: MID,
467
+ stop_reason: 'end_turn',
468
+ usage,
469
+ })
470
+ })
471
+
472
+ it('message_completed without usage → usage: null (defensive against dropped message_stop)', () => {
473
+ const r = mapRunToStreamEvent(
474
+ {
475
+ type: 'message_completed',
476
+ runId: RID,
477
+ iteration: 0,
478
+ messageId: MID,
479
+ stopReason: 'tool_use',
480
+ },
481
+ RID,
482
+ )
483
+ expect(r?.data).toMatchObject({ usage: null })
484
+ })
485
+
486
+ it('tool_input_started → tool.input_started carries toolUseId + toolName', () => {
487
+ const r = mapRunToStreamEvent(
488
+ {
489
+ type: 'tool_input_started',
490
+ runId: RID,
491
+ iteration: 0,
492
+ messageId: MID,
493
+ toolUseId: TUID,
494
+ toolName: 'Read',
495
+ },
496
+ RID,
497
+ )
498
+ expect(r?.wire).toBe('tool.input_started')
499
+ expect(r?.data).toMatchObject({
500
+ tool_use_id: TUID,
501
+ tool_name: 'Read',
502
+ message_id: MID,
503
+ })
504
+ })
505
+
506
+ it('tool_input_delta → tool.input_delta carries raw partial JSON fragment', () => {
507
+ const r = mapRunToStreamEvent(
508
+ {
509
+ type: 'tool_input_delta',
510
+ runId: RID,
511
+ toolUseId: TUID,
512
+ partialJson: '{"file_path":"',
513
+ },
514
+ RID,
515
+ )
516
+ expect(r?.wire).toBe('tool.input_delta')
517
+ expect(r?.data).toMatchObject({
518
+ tool_use_id: TUID,
519
+ partial_json: '{"file_path":"',
520
+ })
521
+ })
522
+
523
+ it('tool_input_completed → tool.input_completed carries parsed input object', () => {
524
+ const r = mapRunToStreamEvent(
525
+ {
526
+ type: 'tool_input_completed',
527
+ runId: RID,
528
+ toolUseId: TUID,
529
+ input: { file_path: '/etc/passwd' },
530
+ },
531
+ RID,
532
+ )
533
+ expect(r?.wire).toBe('tool.input_completed')
534
+ expect(r?.data).toMatchObject({
535
+ tool_use_id: TUID,
536
+ input: { file_path: '/etc/passwd' },
537
+ })
538
+ })
539
+ })
540
+
413
541
  describe('mapSessionToStreamEvent (deprecated alias)', () => {
414
542
  it('is the same function reference as mapRunToStreamEvent', () => {
415
543
  // Identity check is deterministic. toEqual on paired calls
@@ -33,19 +33,11 @@ const MAPPING: {
33
33
  transform: (e, runId) => ({ run_id: runId, iteration: e.iteration }),
34
34
  },
35
35
 
36
- llm_response: {
37
- wire: 'message.delta',
38
- transform: (e, runId) => ({
39
- run_id: runId,
40
- content: e.content ?? null,
41
- has_tool_calls: !!e.hasToolCalls,
42
- }),
43
- },
44
-
45
36
  tool_executing: {
46
37
  wire: 'tool.executing',
47
38
  transform: (e, runId) => ({
48
39
  run_id: runId,
40
+ tool_use_id: e.toolUseId,
49
41
  tool_name: e.toolName,
50
42
  input: e.input,
51
43
  }),
@@ -55,8 +47,10 @@ const MAPPING: {
55
47
  wire: 'tool.completed',
56
48
  transform: (e, runId) => ({
57
49
  run_id: runId,
50
+ tool_use_id: e.toolUseId,
58
51
  tool_name: e.toolName,
59
52
  result: e.result,
53
+ is_error: e.isError,
60
54
  }),
61
55
  },
62
56
 
@@ -279,6 +273,69 @@ const MAPPING: {
279
273
  subsession_spawned: null,
280
274
  subsession_messaged: null,
281
275
  subsession_idled: null,
276
+
277
+ // v3 message + tool-input lifecycle (ses_001-tool-stream-events). Additive
278
+ // today; the orchestrator does not yet emit these. Phase 4 of the
279
+ // migration switches the orchestrator over and removes `llm_response`
280
+ // from this map.
281
+ message_started: {
282
+ wire: 'message.created',
283
+ transform: (e, runId) => ({
284
+ run_id: runId,
285
+ iteration: e.iteration,
286
+ message_id: e.messageId,
287
+ }),
288
+ },
289
+
290
+ text_delta: {
291
+ wire: 'message.delta',
292
+ transform: (e, runId) => ({
293
+ run_id: runId,
294
+ iteration: e.iteration,
295
+ message_id: e.messageId,
296
+ text: e.text,
297
+ }),
298
+ },
299
+
300
+ message_completed: {
301
+ wire: 'message.completed',
302
+ transform: (e, runId) => ({
303
+ run_id: runId,
304
+ iteration: e.iteration,
305
+ message_id: e.messageId,
306
+ stop_reason: e.stopReason,
307
+ usage: e.usage ?? null,
308
+ }),
309
+ },
310
+
311
+ tool_input_started: {
312
+ wire: 'tool.input_started',
313
+ transform: (e, runId) => ({
314
+ run_id: runId,
315
+ iteration: e.iteration,
316
+ message_id: e.messageId,
317
+ tool_use_id: e.toolUseId,
318
+ tool_name: e.toolName,
319
+ }),
320
+ },
321
+
322
+ tool_input_delta: {
323
+ wire: 'tool.input_delta',
324
+ transform: (e, runId) => ({
325
+ run_id: runId,
326
+ tool_use_id: e.toolUseId,
327
+ partial_json: e.partialJson,
328
+ }),
329
+ },
330
+
331
+ tool_input_completed: {
332
+ wire: 'tool.input_completed',
333
+ transform: (e, runId) => ({
334
+ run_id: runId,
335
+ tool_use_id: e.toolUseId,
336
+ input: e.input,
337
+ }),
338
+ },
282
339
  }
283
340
 
284
341
  export function mapRunToStreamEvent(event: RunEvent, runId: RunId): MappedStreamEvent | null {
@@ -1,4 +1,5 @@
1
1
  import type { CompactionConfig } from '../config/runtime.js'
2
+ import { collect } from '../provider/collect.js'
2
3
  import type { Message } from '../types/message/index.js'
3
4
  import type { LLMProvider } from '../types/provider/interface.js'
4
5
  import type { WorkingStateManager } from './manager.js'
@@ -74,12 +75,14 @@ export async function buildVerifiedSummary(
74
75
  },
75
76
  ]
76
77
 
77
- const response = await provider.chat({
78
- model: '',
79
- messages: verificationMessages,
80
- maxTokens: config.llmVerificationMaxTokens,
81
- temperature: 0,
82
- })
78
+ const response = await collect(
79
+ provider.chatStream({
80
+ model: '',
81
+ messages: verificationMessages,
82
+ maxTokens: config.llmVerificationMaxTokens,
83
+ temperature: 0,
84
+ }),
85
+ )
83
86
 
84
87
  const responseText = response.message.content?.trim() ?? ''
85
88
 
@@ -63,7 +63,7 @@ export type PluginRuntimeConfig = z.infer<typeof PluginRuntimeConfigSchema>
63
63
  export const RuntimeConfigSchema = z.object({
64
64
  model: z.string().default('qwen/qwen3.6-plus:free'),
65
65
  temperature: z.number().min(0).max(2).default(0.3),
66
- tokenBudget: z.number().positive().default(100_000),
66
+ tokenBudget: z.number().nonnegative().default(100_000),
67
67
  maxResponseTokens: z.number().positive().default(8192),
68
68
  timeoutMs: z.number().positive().default(600_000),
69
69
  maxIterations: z.number().positive().default(200),
@@ -145,6 +145,13 @@ export type StreamEventType =
145
145
  | 'tool.executing'
146
146
  | 'tool.completed'
147
147
  | 'tool.error'
148
+ // v3 tool input lifecycle (ses_001-tool-stream-events). Additive; phase 4
149
+ // of the migration removes `tool.error` and folds the boolean into
150
+ // `tool.completed`. Until then both surfaces are wire-supported so
151
+ // adapters can roll forward independently.
152
+ | 'tool.input_started'
153
+ | 'tool.input_delta'
154
+ | 'tool.input_completed'
148
155
  | 'token.usage'
149
156
  | 'message.created'
150
157
  | 'message.delta'
@@ -8,7 +8,7 @@ export const RunConfigSchema = z
8
8
  .object({
9
9
  model: z.string().min(1).optional(),
10
10
  temperature: z.number().min(0).max(2).optional(),
11
- tokenBudget: z.number().int().positive().optional(),
11
+ tokenBudget: z.number().int().nonnegative().optional(),
12
12
  maxResponseTokens: z.number().int().positive().optional(),
13
13
  timeoutMs: z.number().int().positive().max(3_600_000).optional(),
14
14
  permissionMode: z.enum(['plan', 'auto']).optional(),
@@ -14,7 +14,7 @@ export class LocalTaskGateway implements TaskGateway {
14
14
  private listener: RunEventListener | undefined
15
15
  private trackedTaskIds: Set<TaskId> = new Set()
16
16
 
17
- private parentInput?: Pick<AgentInput, 'taskStore' | 'runtimeToolOverrides'>
17
+ private parentInput?: Pick<AgentInput, 'taskStore' | 'runtimeToolOverrides' | 'runtimeContext'>
18
18
 
19
19
  private completionListeners: Set<(handle: TaskHandle) => void> = new Set()
20
20
 
@@ -22,7 +22,7 @@ export class LocalTaskGateway implements TaskGateway {
22
22
  agentManager: AgentManagerContract,
23
23
  taskContext: AgentTaskContext,
24
24
  listener?: RunEventListener,
25
- parentInput?: Pick<AgentInput, 'taskStore' | 'runtimeToolOverrides'>,
25
+ parentInput?: Pick<AgentInput, 'taskStore' | 'runtimeToolOverrides' | 'runtimeContext'>,
26
26
  ) {
27
27
  this.agentManager = agentManager
28
28
  this.taskContext = taskContext
@@ -39,6 +39,7 @@ export class LocalTaskGateway implements TaskGateway {
39
39
  workingDirectory: options.workingDirectory,
40
40
  taskStore: this.parentInput?.taskStore,
41
41
  runtimeToolOverrides: this.parentInput?.runtimeToolOverrides,
42
+ runtimeContext: options.runtimeContext ?? this.parentInput?.runtimeContext,
42
43
  },
43
44
  // Phase 6: spawn scope propagates from the gateway's task context.
44
45
  // The caller built it at SupervisorAgent boundary (§12.1).
@@ -258,14 +258,14 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
258
258
  expect(spawned.lineage.parentSessionId).toBe(harness.parentSession.id)
259
259
  expect(spawned.lineage.rootSessionId).toBe(harness.parentSession.id)
260
260
  expect(spawned.lineage.depth).toBe(1)
261
- expect(spawned.schemaVersion).toBe(2)
261
+ expect(spawned.schemaVersion).toBe(3)
262
262
  }
263
263
 
264
264
  const idled = events.find((e) => e.type === 'subsession_idled')
265
265
  expect(idled).toBeDefined()
266
266
  if (idled && 'lineage' in idled) {
267
267
  expect(idled.lineage.depth).toBe(1)
268
- expect(idled.schemaVersion).toBe(2)
268
+ expect(idled.schemaVersion).toBe(3)
269
269
  }
270
270
  })
271
271
 
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Behavioural contract for `collect()` (ses_001-tool-stream-events phase 1A):
3
+ *
4
+ * - Drains a `StreamChunk` async iterable into a single
5
+ * `ChatCompletionResponse` matching the legacy `provider.chat()` shape.
6
+ * - Concatenates `delta.content` in arrival order; null when no text
7
+ * chunks ever arrive.
8
+ * - Buckets tool-call argument fragments by `index`; emits `toolCalls`
9
+ * sorted by index. `id` and `function.name` come from the first chunk
10
+ * that supplies them; `function.arguments` is the concatenation of all
11
+ * `arguments` fragments for that index.
12
+ * - Latest-wins for `finishReason` and `usage`; defaults
13
+ * `finishReason: 'stop'` and zero usage if the provider omits them
14
+ * (defensive — see anthropics/anthropic-sdk-typescript#842).
15
+ * - Throws if any chunk surfaces a `chunk.error`.
16
+ *
17
+ * Phase 2 swaps every internal `provider.chat()` call site for
18
+ * `collect(provider.chatStream())`; the response shape parity guarded
19
+ * here is what makes that swap safe.
20
+ */
21
+
22
+ import { describe, expect, it } from 'vitest'
23
+
24
+ import type { StreamChunk } from '../types/provider/stream.js'
25
+
26
+ import { collect } from './collect.js'
27
+
28
+ async function* fromArray(chunks: StreamChunk[]): AsyncIterable<StreamChunk> {
29
+ for (const chunk of chunks) yield chunk
30
+ }
31
+
32
+ describe('collect()', () => {
33
+ it('aggregates text-only stream into single content string', async () => {
34
+ const result = await collect(
35
+ fromArray([
36
+ { id: 'm1', delta: { content: 'hel' } },
37
+ { id: 'm1', delta: { content: 'lo' } },
38
+ { id: 'm1', delta: { content: ' world' } },
39
+ { id: 'm1', delta: {}, finishReason: 'stop' },
40
+ ]),
41
+ )
42
+ expect(result.message.content).toBe('hello world')
43
+ expect(result.message.toolCalls).toBeUndefined()
44
+ expect(result.finishReason).toBe('stop')
45
+ expect(result.id).toBe('m1')
46
+ })
47
+
48
+ it('returns content: null when no text chunks arrive', async () => {
49
+ const result = await collect(fromArray([{ id: 'm', delta: {}, finishReason: 'stop' }]))
50
+ expect(result.message.content).toBeNull()
51
+ })
52
+
53
+ it('buckets parallel tool calls by index, preserves order', async () => {
54
+ const result = await collect(
55
+ fromArray([
56
+ {
57
+ id: 'm',
58
+ delta: {
59
+ toolCalls: [
60
+ { index: 0, id: 'toolu_a', function: { name: 'Read' } },
61
+ { index: 1, id: 'toolu_b', function: { name: 'WebSearch' } },
62
+ ],
63
+ },
64
+ },
65
+ {
66
+ id: 'm',
67
+ delta: {
68
+ toolCalls: [{ index: 1, function: { arguments: '{"query":"x"}' } }],
69
+ },
70
+ },
71
+ {
72
+ id: 'm',
73
+ delta: {
74
+ toolCalls: [
75
+ { index: 0, function: { arguments: '{"file_path":' } },
76
+ { index: 0, function: { arguments: '"/a"}' } },
77
+ ],
78
+ },
79
+ },
80
+ { id: 'm', delta: {}, finishReason: 'tool_calls' },
81
+ ]),
82
+ )
83
+ expect(result.message.toolCalls).toEqual([
84
+ {
85
+ id: 'toolu_a',
86
+ type: 'function',
87
+ function: { name: 'Read', arguments: '{"file_path":"/a"}' },
88
+ },
89
+ {
90
+ id: 'toolu_b',
91
+ type: 'function',
92
+ function: { name: 'WebSearch', arguments: '{"query":"x"}' },
93
+ },
94
+ ])
95
+ expect(result.finishReason).toBe('tool_calls')
96
+ })
97
+
98
+ it('latest finishReason and usage win', async () => {
99
+ const result = await collect(
100
+ fromArray([
101
+ { id: 'm', delta: {}, finishReason: 'stop' },
102
+ {
103
+ id: 'm',
104
+ delta: {},
105
+ finishReason: 'length',
106
+ usage: {
107
+ promptTokens: 100,
108
+ completionTokens: 50,
109
+ totalTokens: 150,
110
+ cachedTokens: 0,
111
+ cacheWriteTokens: 0,
112
+ },
113
+ },
114
+ ]),
115
+ )
116
+ expect(result.finishReason).toBe('length')
117
+ expect(result.usage.totalTokens).toBe(150)
118
+ })
119
+
120
+ it('defaults finishReason to stop and usage to zero when provider omits them', async () => {
121
+ const result = await collect(fromArray([{ id: 'm', delta: { content: 'hi' } }]))
122
+ expect(result.finishReason).toBe('stop')
123
+ expect(result.usage).toEqual({
124
+ promptTokens: 0,
125
+ completionTokens: 0,
126
+ totalTokens: 0,
127
+ cachedTokens: 0,
128
+ cacheWriteTokens: 0,
129
+ })
130
+ })
131
+
132
+ it('throws on chunk.error', async () => {
133
+ await expect(
134
+ collect(
135
+ fromArray([
136
+ { id: 'm', delta: { content: 'hi' } },
137
+ { id: 'm', delta: {}, error: 'rate limited' },
138
+ ]),
139
+ ),
140
+ ).rejects.toThrow('rate limited')
141
+ })
142
+ })
@@ -0,0 +1,85 @@
1
+ import type { ChatCompletionResponse } from '../types/provider/chat.js'
2
+ import type { StreamChunk } from '../types/provider/stream.js'
3
+
4
+ /**
5
+ * Drains a {@link StreamChunk} async iterable into the equivalent
6
+ * non-streaming {@link ChatCompletionResponse}.
7
+ *
8
+ * Phase 2 of ses_001-tool-stream-events removes `LLMProvider.chat()`; the
9
+ * four internal callers that genuinely need the aggregated view (advisory
10
+ * executor, RouterAgent's deterministic routing decision, compaction's
11
+ * verifier, the instrumentation wrapper) replace `provider.chat(p)` with
12
+ * `collect(provider.chatStream(p))`.
13
+ *
14
+ * Behaviour matches the pre-removal `chat()` contract:
15
+ * - text content is concatenated in delta order;
16
+ * - tool calls are bucketed by `index` into the existing
17
+ * `Array<{ id, function: { name, arguments } }>` shape;
18
+ * - usage and finishReason fall back to safe defaults when the provider
19
+ * omits them (defensive — see anthropics/anthropic-sdk-typescript#842
20
+ * where `message_stop` is occasionally dropped on connection close).
21
+ *
22
+ * The orchestrator does NOT call this helper — it consumes the stream
23
+ * directly so it can emit per-delta `RunEvent`s.
24
+ */
25
+ export async function collect(stream: AsyncIterable<StreamChunk>): Promise<ChatCompletionResponse> {
26
+ let id = ''
27
+ const model = ''
28
+ let content = ''
29
+ let finishReason: ChatCompletionResponse['finishReason'] = 'stop'
30
+ let usage: ChatCompletionResponse['usage'] = {
31
+ promptTokens: 0,
32
+ completionTokens: 0,
33
+ totalTokens: 0,
34
+ cachedTokens: 0,
35
+ cacheWriteTokens: 0,
36
+ }
37
+
38
+ const toolBuckets = new Map<number, { id: string; name: string; argsBuf: string }>()
39
+
40
+ for await (const chunk of stream) {
41
+ if (chunk.error) {
42
+ throw new Error(chunk.error)
43
+ }
44
+ if (!id && chunk.id) id = chunk.id
45
+
46
+ if (chunk.delta.content) {
47
+ content += chunk.delta.content
48
+ }
49
+
50
+ for (const tc of chunk.delta.toolCalls ?? []) {
51
+ const bucket = toolBuckets.get(tc.index) ?? {
52
+ id: '',
53
+ name: '',
54
+ argsBuf: '',
55
+ }
56
+ if (tc.id) bucket.id = tc.id
57
+ if (tc.function?.name) bucket.name = tc.function.name
58
+ if (tc.function?.arguments) bucket.argsBuf += tc.function.arguments
59
+ toolBuckets.set(tc.index, bucket)
60
+ }
61
+
62
+ if (chunk.finishReason) finishReason = chunk.finishReason
63
+ if (chunk.usage) usage = chunk.usage
64
+ }
65
+
66
+ const toolCalls = [...toolBuckets.entries()]
67
+ .sort(([a], [b]) => a - b)
68
+ .map(([, b]) => ({
69
+ id: b.id,
70
+ type: 'function' as const,
71
+ function: { name: b.name, arguments: b.argsBuf },
72
+ }))
73
+
74
+ return {
75
+ id,
76
+ model,
77
+ message: {
78
+ role: 'assistant',
79
+ content: content.length > 0 ? content : null,
80
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
81
+ },
82
+ finishReason,
83
+ usage,
84
+ }
85
+ }