@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.
- package/CHANGELOG.md +241 -0
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +3 -2
- package/dist/advisory/executor.js.map +1 -1
- package/dist/advisory/executor.test.js +36 -14
- package/dist/advisory/executor.test.js.map +1 -1
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +1 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/RouterAgent.d.ts.map +1 -1
- package/dist/agents/RouterAgent.js +3 -2
- package/dist/agents/RouterAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +2 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +23 -9
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/a2a/mapper.test.js +35 -9
- package/dist/bridge/a2a/mapper.test.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +60 -8
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.test.js +123 -16
- package/dist/bridge/sse/mapper.test.js.map +1 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +3 -2
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +14 -14
- package/dist/config/runtime.js +1 -1
- package/dist/config/runtime.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/contracts/schemas.js +1 -1
- package/dist/contracts/schemas.js.map +1 -1
- package/dist/gateway/local.d.ts +1 -1
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +1 -0
- package/dist/gateway/local.js.map +1 -1
- package/dist/manager/agent/__tests__/lifecycle.test.js +2 -2
- package/dist/provider/collect.d.ts +25 -0
- package/dist/provider/collect.d.ts.map +1 -0
- package/dist/provider/collect.js +82 -0
- package/dist/provider/collect.js.map +1 -0
- package/dist/provider/collect.test.d.ts +22 -0
- package/dist/provider/collect.test.d.ts.map +1 -0
- package/dist/provider/collect.test.js +123 -0
- package/dist/provider/collect.test.js.map +1 -0
- package/dist/provider/instrumentation.d.ts.map +1 -1
- package/dist/provider/instrumentation.js +10 -43
- package/dist/provider/instrumentation.js.map +1 -1
- package/dist/provider/instrumentation.test.d.ts +15 -0
- package/dist/provider/instrumentation.test.d.ts.map +1 -1
- package/dist/provider/instrumentation.test.js +73 -87
- package/dist/provider/instrumentation.test.js.map +1 -1
- package/dist/provider/mock.d.ts +1 -2
- package/dist/provider/mock.d.ts.map +1 -1
- package/dist/provider/mock.js +2 -5
- package/dist/provider/mock.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/LimitChecker.test.d.ts +2 -0
- package/dist/run/LimitChecker.test.d.ts.map +1 -0
- package/dist/run/LimitChecker.test.js +26 -0
- package/dist/run/LimitChecker.test.js.map +1 -0
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +10 -6
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/prompt.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/prompt.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/prompt.test.js +35 -0
- package/dist/runtime/query/__tests__/prompt.test.js.map +1 -0
- package/dist/runtime/query/context-cache.d.ts +2 -0
- package/dist/runtime/query/context-cache.d.ts.map +1 -1
- package/dist/runtime/query/context-cache.js +3 -0
- package/dist/runtime/query/context-cache.js.map +1 -1
- package/dist/runtime/query/events.d.ts +2 -0
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +48 -1
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +55 -5
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +2 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +2 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +245 -13
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +2 -0
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts +2 -0
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +35 -13
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/session/__tests__/integration/e2e-spawn.test.js +2 -2
- package/dist/session/__tests__/integration/event-stream-ordering.test.d.ts +1 -1
- package/dist/session/__tests__/integration/event-stream-ordering.test.js +7 -7
- package/dist/streaming/coalesce.d.ts +28 -0
- package/dist/streaming/coalesce.d.ts.map +1 -0
- package/dist/streaming/coalesce.js +75 -0
- package/dist/streaming/coalesce.js.map +1 -0
- package/dist/streaming/coalesce.test.d.ts +19 -0
- package/dist/streaming/coalesce.test.d.ts.map +1 -0
- package/dist/streaming/coalesce.test.js +120 -0
- package/dist/streaming/coalesce.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +2 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +1 -0
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/base.d.ts +7 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/gateway.d.ts +2 -1
- package/dist/types/agent/gateway.d.ts.map +1 -1
- package/dist/types/ids/index.d.ts +10 -0
- package/dist/types/ids/index.d.ts.map +1 -1
- package/dist/types/ids/index.js.map +1 -1
- package/dist/types/provider/interface.d.ts +26 -2
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +18 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +58 -8
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js +23 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/schema-version.d.ts +7 -1
- package/dist/types/run/schema-version.d.ts.map +1 -1
- package/dist/types/run/schema-version.js +7 -1
- package/dist/types/run/schema-version.js.map +1 -1
- package/dist/types/run/stop-reason.d.ts +9 -0
- package/dist/types/run/stop-reason.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/executor.test.ts +37 -15
- package/src/advisory/executor.ts +10 -7
- package/src/agents/ReactiveAgent.ts +1 -0
- package/src/agents/RouterAgent.ts +9 -6
- package/src/agents/SupervisorAgent.ts +2 -0
- package/src/bridge/a2a/mapper.test.ts +35 -9
- package/src/bridge/a2a/mapper.ts +23 -9
- package/src/bridge/sse/mapper.test.ts +152 -24
- package/src/bridge/sse/mapper.ts +66 -9
- package/src/compaction/verifier.ts +9 -6
- package/src/config/runtime.ts +1 -1
- package/src/contracts/api.ts +7 -0
- package/src/contracts/schemas.ts +1 -1
- package/src/gateway/local.ts +3 -2
- package/src/manager/agent/__tests__/lifecycle.test.ts +2 -2
- package/src/provider/collect.test.ts +142 -0
- package/src/provider/collect.ts +85 -0
- package/src/provider/instrumentation.test.ts +81 -100
- package/src/provider/instrumentation.ts +11 -53
- package/src/provider/mock.ts +2 -6
- package/src/public-runtime.ts +6 -0
- package/src/run/LimitChecker.test.ts +32 -0
- package/src/run/reporter.ts +10 -7
- package/src/runtime/query/__tests__/prompt.test.ts +38 -0
- package/src/runtime/query/context-cache.ts +5 -0
- package/src/runtime/query/events.ts +52 -1
- package/src/runtime/query/executor.ts +54 -5
- package/src/runtime/query/index.ts +5 -1
- package/src/runtime/query/iteration/index.ts +301 -26
- package/src/runtime/query/iteration/phases/compaction.ts +2 -0
- package/src/runtime/query/prompt.ts +45 -17
- package/src/session/__tests__/integration/e2e-spawn.test.ts +2 -2
- package/src/session/__tests__/integration/event-stream-ordering.test.ts +7 -7
- package/src/streaming/coalesce.test.ts +132 -0
- package/src/streaming/coalesce.ts +89 -0
- package/src/tools/coordinator/index.ts +3 -0
- package/src/types/agent/base.ts +9 -0
- package/src/types/agent/gateway.ts +3 -1
- package/src/types/ids/index.ts +10 -0
- package/src/types/provider/interface.ts +28 -3
- package/src/types/provider/stream.ts +18 -0
- package/src/types/run/events.ts +105 -9
- package/src/types/run/schema-version.ts +7 -1
- 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 {
|
|
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' }
|
|
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
|
-
|
|
42
|
-
|
|
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.
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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.
|
|
86
|
-
await wrapped.
|
|
128
|
+
await drain(wrapped.chatStream(params))
|
|
129
|
+
await drain(wrapped.chatStream(params))
|
|
87
130
|
|
|
88
|
-
expect(ids
|
|
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(
|
|
120
|
-
expect(
|
|
146
|
+
expect(listModels).toHaveBeenCalled()
|
|
147
|
+
expect(healthCheck).toHaveBeenCalled()
|
|
121
148
|
})
|
|
122
149
|
|
|
123
|
-
it('
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
26
|
-
outputTokens:
|
|
27
|
-
totalTokens:
|
|
28
|
-
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
|
)
|
package/src/provider/mock.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/public-runtime.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/run/reporter.ts
CHANGED
|
@@ -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
|
-
|
|
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> {
|