@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
@@ -1,51 +1,78 @@
1
+ /**
2
+ * Phase 2 of ses_001-tool-stream-events removed `chat()` from
3
+ * `LLMProvider`; this suite now exercises the streaming-only wrapper.
4
+ *
5
+ * Invariants under test:
6
+ * - `wrapProviderWithProbes(provider)` returns an object that
7
+ * forwards `chatStream` to the inner provider while emitting
8
+ * `provider_call_start` before iteration and either
9
+ * `provider_call_completed` (after the iterator drains cleanly,
10
+ * carrying any aggregated `usage` from the last chunk that
11
+ * supplied one) or `provider_call_failed` (on a thrown error).
12
+ * - `callId` is unique per call and correlates start/completed/failed.
13
+ * - Optional methods (`listModels`, `healthCheck`, `doctorCheck`)
14
+ * are forwarded when present on the inner provider.
15
+ */
16
+
1
17
  import { describe, expect, it, vi } from 'vitest'
2
18
 
3
19
  import { buildProbeContext } from '../probe/context.js'
4
20
  import { createProbeRegistry } from '../probe/registry.js'
5
21
  import type { AgentBusEvent } from '../types/bus/index.js'
6
- import type { ChatCompletionParams, ChatCompletionResponse } from '../types/provider/chat.js'
22
+ import type { TokenUsage } from '../types/common/index.js'
23
+ import type { ChatCompletionParams } from '../types/provider/chat.js'
7
24
  import type { LLMProvider } from '../types/provider/interface.js'
8
25
  import type { StreamChunk } from '../types/provider/stream.js'
9
26
 
10
27
  import { wrapProviderWithProbes } from './instrumentation.js'
11
28
 
29
+ const STREAM_USAGE: TokenUsage = {
30
+ promptTokens: 10,
31
+ completionTokens: 5,
32
+ totalTokens: 15,
33
+ cachedTokens: 0,
34
+ cacheWriteTokens: 0,
35
+ }
36
+
12
37
  function makeFakeProvider(
13
38
  overrides: Partial<{
14
- chat: LLMProvider['chat']
15
39
  chatStream: LLMProvider['chatStream']
16
40
  }> = {},
17
41
  ): LLMProvider {
18
- const defaultChat: LLMProvider['chat'] = async (
19
- _params: ChatCompletionParams,
20
- ): Promise<ChatCompletionResponse> => {
21
- return {
22
- content: 'ok',
23
- usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
24
- } as unknown as ChatCompletionResponse
25
- }
26
42
  const defaultStream: LLMProvider['chatStream'] = async function* (
27
43
  _params: ChatCompletionParams,
28
44
  ): AsyncIterable<StreamChunk> {
29
- yield { delta: 'hi' } as unknown as StreamChunk
45
+ yield { id: 'm', delta: { content: 'hi' } }
46
+ yield {
47
+ id: 'm',
48
+ delta: {},
49
+ finishReason: 'stop',
50
+ usage: STREAM_USAGE,
51
+ }
30
52
  }
31
53
  return {
32
54
  id: 'p1',
33
55
  name: 'Provider 1',
34
- chat: overrides.chat ?? defaultChat,
35
56
  chatStream: overrides.chatStream ?? defaultStream,
36
57
  }
37
58
  }
38
59
 
39
60
  const params: ChatCompletionParams = { model: 'm1', messages: [] } as ChatCompletionParams
40
61
 
41
- describe('wrapProviderWithProbes chat', () => {
42
- it('emits provider_call_start before the chat call and provider_call_completed after', async () => {
62
+ async function drain(iter: AsyncIterable<StreamChunk>): Promise<StreamChunk[]> {
63
+ const out: StreamChunk[] = []
64
+ for await (const c of iter) out.push(c)
65
+ return out
66
+ }
67
+
68
+ describe('wrapProviderWithProbes — chatStream', () => {
69
+ it('emits provider_call_start before iteration and provider_call_completed after drain', async () => {
43
70
  const reg = createProbeRegistry()
44
71
  const seen: AgentBusEvent[] = []
45
72
  reg.onAny((event) => seen.push(event as AgentBusEvent))
46
73
 
47
74
  const wrapped = wrapProviderWithProbes(makeFakeProvider(), { probes: reg })
48
- await wrapped.chat(params)
75
+ await drain(wrapped.chatStream(params))
49
76
 
50
77
  expect(seen.map((e) => e.type)).toEqual(['provider_call_start', 'provider_call_completed'])
51
78
  const start = seen[0] as AgentBusEvent & { type: 'provider_call_start' }
@@ -53,39 +80,55 @@ describe('wrapProviderWithProbes — chat', () => {
53
80
  expect(start.providerId).toBe('p1')
54
81
  expect(start.model).toBe('m1')
55
82
  expect(completed.callId).toBe(start.callId)
56
- expect(completed.usage).toEqual({ inputTokens: 10, outputTokens: 5, totalTokens: 15 })
57
83
  expect(completed.durationMs).toBeGreaterThanOrEqual(0)
58
84
  })
59
85
 
60
- it('emits provider_call_failed and re-throws when chat throws', async () => {
86
+ it('captures usage from the last chunk that carries it', async () => {
87
+ const reg = createProbeRegistry()
88
+ const seen: AgentBusEvent[] = []
89
+ reg.onAny((event) => seen.push(event as AgentBusEvent))
90
+
91
+ const wrapped = wrapProviderWithProbes(makeFakeProvider(), { probes: reg })
92
+ await drain(wrapped.chatStream(params))
93
+
94
+ const completed = seen[1] as AgentBusEvent & { type: 'provider_call_completed' }
95
+ expect(completed.usage).toMatchObject({
96
+ inputTokens: STREAM_USAGE.promptTokens,
97
+ outputTokens: STREAM_USAGE.completionTokens,
98
+ totalTokens: STREAM_USAGE.totalTokens,
99
+ })
100
+ })
101
+
102
+ it('emits provider_call_failed and re-throws when chatStream throws mid-iteration', async () => {
61
103
  const reg = createProbeRegistry()
62
104
  const seen: AgentBusEvent[] = []
63
105
  reg.onAny((event) => seen.push(event as AgentBusEvent))
64
106
 
65
107
  const failing = makeFakeProvider({
66
- chat: async () => {
108
+ chatStream: async function* () {
109
+ yield { id: 'm', delta: { content: 'partial' } }
67
110
  throw new Error('boom')
68
111
  },
69
112
  })
70
113
  const wrapped = wrapProviderWithProbes(failing, { probes: reg })
71
114
 
72
- await expect(wrapped.chat(params)).rejects.toThrow('boom')
115
+ await expect(drain(wrapped.chatStream(params))).rejects.toThrow('boom')
73
116
  expect(seen.map((e) => e.type)).toEqual(['provider_call_start', 'provider_call_failed'])
74
117
  const failed = seen[1] as AgentBusEvent & { type: 'provider_call_failed' }
75
118
  expect(failed.error).toBe('boom')
76
119
  })
77
120
 
78
- it('correlates start and completed by callId', async () => {
121
+ it('correlates start and completed by callId across multiple calls', async () => {
79
122
  const reg = createProbeRegistry()
80
123
  const ids: string[] = []
81
124
  reg.on('provider_call_start', (event) => ids.push(`s:${event.callId}`))
82
125
  reg.on('provider_call_completed', (event) => ids.push(`c:${event.callId}`))
83
126
 
84
127
  const wrapped = wrapProviderWithProbes(makeFakeProvider(), { probes: reg })
85
- await wrapped.chat(params)
86
- await wrapped.chat(params)
128
+ await drain(wrapped.chatStream(params))
129
+ await drain(wrapped.chatStream(params))
87
130
 
88
- expect(ids.length).toBe(4)
131
+ expect(ids).toHaveLength(4)
89
132
  expect(ids[0]?.split(':')[1]).toBe(ids[1]?.split(':')[1])
90
133
  expect(ids[2]?.split(':')[1]).toBe(ids[3]?.split(':')[1])
91
134
  expect(ids[0]).not.toBe(ids[2])
@@ -99,94 +142,32 @@ describe('wrapProviderWithProbes — chat', () => {
99
142
 
100
143
  wrapped.listModels?.()
101
144
  wrapped.healthCheck?.()
102
- expect(listModels).toHaveBeenCalledTimes(1)
103
- expect(healthCheck).toHaveBeenCalledTimes(1)
104
- })
105
- })
106
-
107
- describe('wrapProviderWithProbes — chatStream', () => {
108
- it('emits provider_call_start before iteration and provider_call_completed after', async () => {
109
- const reg = createProbeRegistry()
110
- const seen: AgentBusEvent[] = []
111
- reg.onAny((event) => seen.push(event as AgentBusEvent))
112
-
113
- const wrapped = wrapProviderWithProbes(makeFakeProvider(), { probes: reg })
114
- const chunks: StreamChunk[] = []
115
- for await (const chunk of wrapped.chatStream(params)) {
116
- chunks.push(chunk)
117
- }
118
145
 
119
- expect(chunks.length).toBe(1)
120
- expect(seen.map((e) => e.type)).toEqual(['provider_call_start', 'provider_call_completed'])
146
+ expect(listModels).toHaveBeenCalled()
147
+ expect(healthCheck).toHaveBeenCalled()
121
148
  })
122
149
 
123
- it('emits provider_call_failed when the underlying stream throws mid-iteration', async () => {
124
- const reg = createProbeRegistry()
125
- const seen: AgentBusEvent[] = []
126
- reg.onAny((event) => seen.push(event as AgentBusEvent))
127
-
128
- const failing = makeFakeProvider({
129
- chatStream: async function* (_params: ChatCompletionParams): AsyncIterable<StreamChunk> {
130
- yield { delta: 'a' } as unknown as StreamChunk
131
- throw new Error('stream-boom')
132
- },
133
- })
134
- const wrapped = wrapProviderWithProbes(failing, { probes: reg })
135
-
136
- await expect(async () => {
137
- for await (const _chunk of wrapped.chatStream(params)) {
138
- // noop
139
- }
140
- }).rejects.toThrow('stream-boom')
141
-
142
- expect(seen.map((e) => e.type)).toEqual(['provider_call_start', 'provider_call_failed'])
150
+ it('omits optional methods when inner provider does not declare them', () => {
151
+ const wrapped = wrapProviderWithProbes(makeFakeProvider())
152
+ expect(wrapped.listModels).toBeUndefined()
153
+ expect(wrapped.healthCheck).toBeUndefined()
143
154
  })
144
- })
145
155
 
146
- describe('wrapProviderWithProbes runId propagation', () => {
147
- it('attaches runId to each emitted event when supplied', async () => {
156
+ it('uses the configured probe context (runId)', async () => {
148
157
  const reg = createProbeRegistry()
149
- let observedRunId: string | undefined
150
- reg.on('provider_call_start', (event, ctx) => {
151
- observedRunId = event.runId ?? ctx.runId
158
+ const ctx = buildProbeContext({ runId: 'run_42' as `run_${string}` })
159
+ const seen: AgentBusEvent[] = []
160
+ reg.onAny((event, c) => {
161
+ seen.push(event as AgentBusEvent)
162
+ expect(c.runId).toBe(ctx.runId)
152
163
  })
153
164
 
154
165
  const wrapped = wrapProviderWithProbes(makeFakeProvider(), {
155
166
  probes: reg,
156
- runId: 'run_42' as never,
157
- })
158
- await wrapped.chat(params)
159
- expect(observedRunId).toBe('run_42')
160
- })
161
- })
162
-
163
- describe('wrapProviderWithProbes — uses singleton when no probes opt provided', () => {
164
- it('still wraps successfully without throwing (smoke)', async () => {
165
- // Use a fresh inner provider; we just want to verify the default path
166
- // instantiates and runs. Singleton dispatch is exercised in registry tests.
167
- const wrapped = wrapProviderWithProbes(makeFakeProvider())
168
- await expect(wrapped.chat(params)).resolves.toBeDefined()
169
- })
170
- })
171
-
172
- describe('wrapProviderWithProbes — context still flows through buildProbeContext', () => {
173
- it('handler receives a frozen ctx', async () => {
174
- const reg = createProbeRegistry()
175
- let captured: Readonly<{ isReplay: boolean }> | undefined
176
- reg.on('provider_call_start', (_event, ctx) => {
177
- captured = ctx
167
+ runId: ctx.runId,
178
168
  })
169
+ await drain(wrapped.chatStream(params))
179
170
 
180
- const wrapped = wrapProviderWithProbes(makeFakeProvider(), { probes: reg })
181
- await wrapped.chat(params)
182
- expect(captured).toBeDefined()
183
- expect(Object.isFrozen(captured)).toBe(true)
184
- expect(captured?.isReplay).toBe(false)
185
- })
186
-
187
- it('buildProbeContext used internally returns a frozen ProbeContext (sanity check)', () => {
188
- const ctx = buildProbeContext({ isReplay: true })
189
- expect(ctx.isReplay).toBe(true)
190
- expect(Object.isFrozen(ctx)).toBe(true)
171
+ expect(seen.map((e) => e.type)).toEqual(['provider_call_start', 'provider_call_completed'])
191
172
  })
192
173
  })
@@ -1,8 +1,9 @@
1
1
  import { buildProbeContext } from '../probe/context.js'
2
2
  import { type ProbeRegistry, probe as defaultProbeRegistry } from '../probe/registry.js'
3
3
  import type { ProviderCallId, ProviderCallUsage } from '../types/bus/index.js'
4
+ import type { TokenUsage } from '../types/common/index.js'
4
5
  import type { RunId } from '../types/ids/index.js'
5
- import type { ChatCompletionParams, ChatCompletionResponse } from '../types/provider/chat.js'
6
+ import type { ChatCompletionParams } from '../types/provider/chat.js'
6
7
  import type { LLMProvider } from '../types/provider/interface.js'
7
8
  import type { StreamChunk } from '../types/provider/stream.js'
8
9
 
@@ -18,14 +19,14 @@ function nextCallId(): ProviderCallId {
18
19
  return `pcall_${Date.now().toString(36)}${providerCallCounter.toString(36)}` as ProviderCallId
19
20
  }
20
21
 
21
- function extractUsage(response: ChatCompletionResponse): ProviderCallUsage | undefined {
22
- const usage = (response as { usage?: ProviderCallUsage }).usage
22
+ function extractStreamUsage(usage: TokenUsage | undefined): ProviderCallUsage | undefined {
23
23
  if (!usage) return undefined
24
+ const u = usage as TokenUsage & Partial<ProviderCallUsage>
24
25
  return {
25
- inputTokens: usage.inputTokens,
26
- outputTokens: usage.outputTokens,
27
- totalTokens: usage.totalTokens,
28
- costUsd: usage.costUsd,
26
+ inputTokens: u.inputTokens ?? u.promptTokens,
27
+ outputTokens: u.outputTokens ?? u.completionTokens,
28
+ totalTokens: u.totalTokens,
29
+ costUsd: u.costUsd,
29
30
  }
30
31
  }
31
32
 
@@ -42,52 +43,6 @@ export function wrapProviderWithProbes(
42
43
  listModels: provider.listModels?.bind(provider),
43
44
  healthCheck: provider.healthCheck?.bind(provider),
44
45
 
45
- async chat(params: ChatCompletionParams): Promise<ChatCompletionResponse> {
46
- const callId = nextCallId()
47
- const ctx = buildProbeContext({ runId })
48
- const startedAt = Date.now()
49
- probes.dispatch(
50
- {
51
- type: 'provider_call_start',
52
- providerId: provider.id,
53
- model: params.model,
54
- callId,
55
- runId,
56
- },
57
- ctx,
58
- )
59
- try {
60
- const response = await provider.chat(params)
61
- probes.dispatch(
62
- {
63
- type: 'provider_call_completed',
64
- providerId: provider.id,
65
- model: params.model,
66
- callId,
67
- runId,
68
- durationMs: Date.now() - startedAt,
69
- usage: extractUsage(response),
70
- },
71
- ctx,
72
- )
73
- return response
74
- } catch (error) {
75
- probes.dispatch(
76
- {
77
- type: 'provider_call_failed',
78
- providerId: provider.id,
79
- model: params.model,
80
- callId,
81
- runId,
82
- durationMs: Date.now() - startedAt,
83
- error: error instanceof Error ? error.message : String(error),
84
- },
85
- ctx,
86
- )
87
- throw error
88
- }
89
- },
90
-
91
46
  async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
92
47
  const callId = nextCallId()
93
48
  const ctx = buildProbeContext({ runId })
@@ -103,7 +58,9 @@ export function wrapProviderWithProbes(
103
58
  ctx,
104
59
  )
105
60
  try {
61
+ let lastUsage: TokenUsage | undefined
106
62
  for await (const chunk of provider.chatStream(params)) {
63
+ if (chunk.usage) lastUsage = chunk.usage
107
64
  yield chunk
108
65
  }
109
66
  probes.dispatch(
@@ -114,6 +71,7 @@ export function wrapProviderWithProbes(
114
71
  callId,
115
72
  runId,
116
73
  durationMs: Date.now() - startedAt,
74
+ usage: extractStreamUsage(lastUsage),
117
75
  },
118
76
  ctx,
119
77
  )
@@ -50,13 +50,9 @@ export class MockLLMProvider implements LLMProvider {
50
50
  }
51
51
  }
52
52
 
53
- async chat(params: ChatCompletionParams): Promise<ChatCompletionResponse> {
54
- await this.delay()
55
- return this.normalizeResponse(params, this.responseText)
56
- }
57
-
58
53
  async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
59
- const response = await this.chat(params)
54
+ await this.delay()
55
+ const response = this.normalizeResponse(params, this.responseText)
60
56
  const content = response.message.content ?? ''
61
57
  const chunkSize = 8
62
58
 
@@ -235,10 +235,16 @@ export {
235
235
 
236
236
  export { wrapProviderWithProbes } from './provider/instrumentation.js'
237
237
  export type { ProviderInstrumentationOptions } from './provider/instrumentation.js'
238
+ export { collect } from './provider/collect.js'
238
239
 
239
240
  export { wrapVaultWithProbes } from './vault/instrumentation.js'
240
241
  export type { VaultInstrumentationOptions } from './vault/instrumentation.js'
241
242
 
243
+ // Doctor runtime moved to @namzu/cli in 0.5.0. SDK keeps only the
244
+ // protocol types under `types/doctor/` (re-exported via public-types.ts)
245
+ // + `LLMProvider.doctorCheck?()` hook on the provider interface.
246
+ // Operators run `npx @namzu/cli doctor`; embedded usage lives there too.
247
+
242
248
  // ─── session runtime — explicit named lists, no `export *` ───────────────
243
249
  // See §1.5 + §4.2 of design.md. Types flow through public-types.ts.
244
250
 
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { RuntimeConfigSchema } from '../config/runtime.js'
4
+ import { RunConfigSchema } from '../contracts/schemas.js'
5
+ import { checkLimitsDetailed } from './LimitChecker.js'
6
+
7
+ describe('token budget limits', () => {
8
+ it('treats tokenBudget 0 as unlimited at runtime', () => {
9
+ const result = checkLimitsDetailed(
10
+ {
11
+ tokenBudget: 0,
12
+ timeoutMs: 60_000,
13
+ maxIterations: 10,
14
+ budgetWarningThreshold: 0.9,
15
+ },
16
+ {
17
+ aborted: false,
18
+ totalTokens: 10_000_000,
19
+ totalCost: 0,
20
+ currentIteration: 1,
21
+ startTime: Date.now(),
22
+ },
23
+ )
24
+
25
+ expect(result).toEqual({ type: 'ok' })
26
+ })
27
+
28
+ it('accepts tokenBudget 0 in public runtime config schemas', () => {
29
+ expect(RuntimeConfigSchema.parse({ tokenBudget: 0 }).tokenBudget).toBe(0)
30
+ expect(RunConfigSchema.parse({ tokenBudget: 0 }).tokenBudget).toBe(0)
31
+ })
32
+ })
@@ -60,13 +60,6 @@ export function createRunReporter(parentLogger?: Logger): RunReporter {
60
60
  })
61
61
  break
62
62
 
63
- case 'llm_response':
64
- log.info('LLM response received', {
65
- runId: event.runId,
66
- hasToolCalls: event.hasToolCalls,
67
- })
68
- break
69
-
70
63
  case 'token_usage_updated':
71
64
  log.info('Token usage updated', {
72
65
  runId: event.runId,
@@ -99,6 +92,16 @@ export function createRunReporter(parentLogger?: Logger): RunReporter {
99
92
  case 'checkpoint_created':
100
93
  case 'run_paused':
101
94
  case 'run_resuming':
95
+ // v3 message + tool-input lifecycle (ses_001-tool-stream-events).
96
+ // The reporter is a debug log surface; per-delta lines would be
97
+ // too noisy. Phase 4 may add structured logging at the
98
+ // message_completed boundary if signal proves useful.
99
+ case 'message_started':
100
+ case 'text_delta':
101
+ case 'message_completed':
102
+ case 'tool_input_started':
103
+ case 'tool_input_delta':
104
+ case 'tool_input_completed':
102
105
  break
103
106
 
104
107
  case 'agent_pending':
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import type { ToolRegistryContract } from '../../../types/tool/index.js'
3
+ import { PromptBuilder } from '../prompt.js'
4
+
5
+ function makeToolRegistry(): ToolRegistryContract {
6
+ return {
7
+ register: vi.fn(),
8
+ unregister: vi.fn(),
9
+ execute: vi.fn(),
10
+ get: vi.fn(() => undefined),
11
+ has: vi.fn(() => false),
12
+ listNames: vi.fn(() => []),
13
+ getAvailability: vi.fn(),
14
+ toPromptSection: vi.fn(() => ''),
15
+ toTierGuidance: vi.fn(() => ''),
16
+ } as unknown as ToolRegistryContract
17
+ }
18
+
19
+ describe('PromptBuilder runtime context', () => {
20
+ it('includes output contract even when no filesystem tool is registered', () => {
21
+ const prompt = new PromptBuilder({
22
+ systemPrompt: 'You are a worker.',
23
+ tools: makeToolRegistry(),
24
+ runtimeContext: {
25
+ label: 'test runtime',
26
+ outputDirectory: 'outputs/',
27
+ outputFileMarker: 'OUTPUT_FILE: <filename> - <description>',
28
+ notes: ['Mirror generated files after the turn.'],
29
+ },
30
+ }).build('full', '/tmp/work')
31
+
32
+ expect(prompt).toContain('Runtime: test runtime')
33
+ expect(prompt).toContain('Working directory: /tmp/work')
34
+ expect(prompt).toContain('Output directory: outputs/')
35
+ expect(prompt).toContain('OUTPUT_FILE: <filename> - <description>')
36
+ expect(prompt).toContain('Mirror generated files after the turn.')
37
+ })
38
+ })
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
+ import type { AgentRuntimeContext } from '../../types/agent/base.js'
2
3
  import type { AgentContextLevel } from '../../types/agent/factory.js'
3
4
  import type { AgentPersona } from '../../types/persona/index.js'
4
5
  import type { ProjectId } from '../../types/session/ids.js'
@@ -18,6 +19,7 @@ export interface PromptCacheInput {
18
19
  basePrompt?: string
19
20
  tools: ToolRegistryContract
20
21
  allowedTools?: string[]
22
+ runtimeContext?: AgentRuntimeContext
21
23
  }
22
24
 
23
25
  export class ContextCache {
@@ -48,6 +50,7 @@ export class ContextCache {
48
50
  basePrompt: input.basePrompt,
49
51
  tools: input.tools,
50
52
  allowedTools: input.allowedTools,
53
+ runtimeContext: input.runtimeContext,
51
54
  })
52
55
 
53
56
  this.cachedPrompt = builder.build()
@@ -78,6 +81,7 @@ export class ContextCache {
78
81
  basePrompt: input.basePrompt,
79
82
  tools: input.tools,
80
83
  allowedTools: input.allowedTools,
84
+ runtimeContext: input.runtimeContext,
81
85
  })
82
86
 
83
87
  const segments = builder.buildSegmented(contextLevel, workingDirectory)
@@ -124,6 +128,7 @@ export class ContextCache {
124
128
  input.basePrompt ?? '',
125
129
  ...(input.skills?.map((s) => s.metadata.name) ?? []),
126
130
  ...(input.allowedTools ?? []),
131
+ JSON.stringify(input.runtimeContext ?? {}),
127
132
  ]
128
133
 
129
134
  return createHash('sha256').update(parts.join('\0')).digest('hex').slice(0, 16)
@@ -4,15 +4,33 @@ import { buildProbeContext } from '../../probe/context.js'
4
4
  import { type ProbeRegistry, probe as defaultProbeRegistry } from '../../probe/registry.js'
5
5
  import type { ActivityEvent, ActivityStore } from '../../store/activity/memory.js'
6
6
  import type { RunId } from '../../types/ids/index.js'
7
+ import { isEphemeralEvent } from '../../types/run/events.js'
7
8
  import type { RunEvent } from '../../types/run/index.js'
8
9
  import type { TaskEvent, TaskStore } from '../../types/task/index.js'
10
+ import { getRootLogger } from '../../utils/logger.js'
9
11
 
10
12
  export type EmitEvent = (event: RunEvent) => Promise<void>
11
13
 
14
+ /**
15
+ * Soft cap on the in-memory pending-event queue. When the queue exceeds
16
+ * this size and a new ephemeral event arrives, the oldest ephemeral
17
+ * event is dropped to make room. Lifecycle events are never dropped —
18
+ * they carry state transitions consumers cannot reconstruct.
19
+ *
20
+ * Sized for ~5–10 seconds of worst-case provider delta cadence
21
+ * (100 deltas/s sustained) before pressure kicks in. Tune via
22
+ * empirical evidence; not a hard guarantee, just a safety net.
23
+ *
24
+ * Codex D2 (ses_001-tool-stream-events).
25
+ */
26
+ const PENDING_EVENT_SOFT_CAP = 1000
27
+
12
28
  export class EventTranslator {
13
29
  private pendingEvents: RunEvent[] = []
14
30
  private runMgr: RunPersistence
15
31
  private probes: ProbeRegistry
32
+ private droppedDeltaCount = 0
33
+ private readonly log = getRootLogger().child({ component: 'EventTranslator' })
16
34
 
17
35
  constructor(runMgr: RunPersistence, probeRegistry: ProbeRegistry = defaultProbeRegistry) {
18
36
  this.runMgr = runMgr
@@ -21,8 +39,41 @@ export class EventTranslator {
21
39
 
22
40
  readonly emitEvent: EmitEvent = async (event: RunEvent): Promise<void> => {
23
41
  this.probes.dispatch(event, buildProbeContext({ runId: event.runId }))
42
+
43
+ // D2: bound the queue. Drop oldest ephemeral events under
44
+ // pressure rather than letting unbounded growth swamp a slow
45
+ // consumer (or lock the orchestrator on awaitable disk I/O).
46
+ // Lifecycle events are sacred — they carry state transitions a
47
+ // consumer cannot reconstruct from neighbouring events.
48
+ if (this.pendingEvents.length >= PENDING_EVENT_SOFT_CAP) {
49
+ const dropIdx = this.pendingEvents.findIndex(isEphemeralEvent)
50
+ if (dropIdx !== -1) {
51
+ this.pendingEvents.splice(dropIdx, 1)
52
+ this.droppedDeltaCount += 1
53
+ if (this.droppedDeltaCount === 1 || this.droppedDeltaCount % 100 === 0) {
54
+ this.log.warn('Dropped ephemeral RunEvent under bus pressure', {
55
+ runId: event.runId,
56
+ droppedCount: this.droppedDeltaCount,
57
+ queueSize: this.pendingEvents.length,
58
+ })
59
+ }
60
+ }
61
+ // If no ephemeral events are buffered the lifecycle events
62
+ // themselves are the queue's contents — accept the overflow
63
+ // and rely on consumer drain catching up. Better to grow
64
+ // briefly than to drop a state transition.
65
+ }
66
+
24
67
  this.pendingEvents.push(event)
25
- await this.runMgr.getRunStore().appendEvent(event)
68
+
69
+ // D1 middle path: ephemeral events never enter `transcript.jsonl`.
70
+ // They live only on the in-memory bus for live UI rendering.
71
+ // Replay (`runtime/query/replay/prepare.ts`) reads checkpoints
72
+ // not transcripts, so this preserves replay fidelity while
73
+ // eliminating the durable bloat codex flagged.
74
+ if (!isEphemeralEvent(event)) {
75
+ await this.runMgr.getRunStore().appendEvent(event)
76
+ }
26
77
  };
27
78
 
28
79
  *drainPending(): Generator<RunEvent> {