@namzu/sdk 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +177 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -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 +96 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +13 -1
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +8 -0
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +103 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +14 -1
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/toolset/index.ts +22 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* There was no span around the model call at all.
|
|
8
|
+
*
|
|
9
|
+
* `chatSpanName` shipped in the telemetry attributes with zero call sites,
|
|
10
|
+
* so a run's traces carried no LLM latency whatsoever — and the one thing
|
|
11
|
+
* anybody opens a trace to find, which turn was slow and why, was the one
|
|
12
|
+
* thing missing from it. The token counts landed on the iteration span
|
|
13
|
+
* instead of the operation that produced them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface Recorded {
|
|
17
|
+
name: string
|
|
18
|
+
attributes: Record<string, unknown>
|
|
19
|
+
ended: boolean
|
|
20
|
+
status?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const spans: Recorded[] = []
|
|
24
|
+
|
|
25
|
+
vi.mock('../runtime-accessors.js', () => ({
|
|
26
|
+
getTracer: () => ({
|
|
27
|
+
startSpan: (name: string) => {
|
|
28
|
+
const rec: Recorded = { name, attributes: {}, ended: false }
|
|
29
|
+
spans.push(rec)
|
|
30
|
+
return {
|
|
31
|
+
setAttributes: (a: Record<string, unknown>) => Object.assign(rec.attributes, a),
|
|
32
|
+
setAttribute: (k: string, v: unknown) => {
|
|
33
|
+
rec.attributes[k] = v
|
|
34
|
+
},
|
|
35
|
+
setStatus: (s: { code: number }) => {
|
|
36
|
+
rec.status = s.code
|
|
37
|
+
},
|
|
38
|
+
recordException: () => undefined,
|
|
39
|
+
addEvent: () => undefined,
|
|
40
|
+
end: () => {
|
|
41
|
+
rec.ended = true
|
|
42
|
+
},
|
|
43
|
+
spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
|
|
44
|
+
isRecording: () => true,
|
|
45
|
+
updateName: () => undefined,
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
startActiveSpan: (_n: string, _o: unknown, _c: unknown, fn: (s: unknown) => unknown) =>
|
|
49
|
+
fn({
|
|
50
|
+
setAttributes: () => undefined,
|
|
51
|
+
setStatus: () => undefined,
|
|
52
|
+
recordException: () => undefined,
|
|
53
|
+
end: () => undefined,
|
|
54
|
+
spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
|
|
55
|
+
isRecording: () => true,
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
getMeter: () => ({
|
|
59
|
+
createCounter: () => ({ add: () => undefined }),
|
|
60
|
+
createHistogram: () => ({ record: () => undefined }),
|
|
61
|
+
}),
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
let workdirs: string[] = []
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
spans.length = 0
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
|
|
72
|
+
workdirs = []
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
async function runOnce(turns: { text?: string }[]): Promise<void> {
|
|
76
|
+
const { MockLLMProvider } = await import('../../provider/mock.js')
|
|
77
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
78
|
+
const { drainQuery } = await import('../../runtime/query/index.js')
|
|
79
|
+
const { createUserMessage } = await import('../../types/message/index.js')
|
|
80
|
+
|
|
81
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-chatspan-'))
|
|
82
|
+
workdirs.push(dir)
|
|
83
|
+
|
|
84
|
+
await drainQuery({
|
|
85
|
+
provider: new MockLLMProvider({ turns: turns as never }),
|
|
86
|
+
tools: new ToolRegistry(),
|
|
87
|
+
runConfig: {
|
|
88
|
+
model: 'mock-model',
|
|
89
|
+
timeoutMs: 30_000,
|
|
90
|
+
tokenBudget: 100_000,
|
|
91
|
+
maxIterations: 3,
|
|
92
|
+
maxResponseTokens: 256,
|
|
93
|
+
temperature: 0.3,
|
|
94
|
+
},
|
|
95
|
+
agentId: 'agent_cs',
|
|
96
|
+
agentName: 'Chat Span Agent',
|
|
97
|
+
workingDirectory: dir,
|
|
98
|
+
sessionId: 'ses_cs',
|
|
99
|
+
threadId: 'thd_cs',
|
|
100
|
+
projectId: 'prj_cs',
|
|
101
|
+
tenantId: 'tnt_cs',
|
|
102
|
+
messages: [createUserMessage('go')],
|
|
103
|
+
} as never)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chatSpans = () => spans.filter((s) => s.name.startsWith('chat '))
|
|
107
|
+
|
|
108
|
+
describe('the model call has a span of its own', () => {
|
|
109
|
+
it('opens one named for the model', async () => {
|
|
110
|
+
await runOnce([{ text: 'done' }])
|
|
111
|
+
|
|
112
|
+
// One per model call. A run makes at least one, and a forced-final
|
|
113
|
+
// turn makes another — both are model calls and both deserve a span.
|
|
114
|
+
expect(chatSpans().length).toBeGreaterThanOrEqual(1)
|
|
115
|
+
for (const s of chatSpans()) expect(s.name).toBe('chat mock-model')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('closes it', async () => {
|
|
119
|
+
await runOnce([{ text: 'done' }])
|
|
120
|
+
|
|
121
|
+
for (const s of chatSpans()) expect(s.ended).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('carries the request parameters that were actually used', async () => {
|
|
125
|
+
await runOnce([{ text: 'done' }])
|
|
126
|
+
|
|
127
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
128
|
+
expect(attrs['gen_ai.operation.name']).toBe('chat')
|
|
129
|
+
expect(attrs['gen_ai.request.model']).toBe('mock-model')
|
|
130
|
+
expect(attrs['gen_ai.request.temperature']).toBe(0.3)
|
|
131
|
+
expect(attrs['gen_ai.request.max_tokens']).toBe(256)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('carries the usage on the call that produced it', async () => {
|
|
135
|
+
await runOnce([{ text: 'done' }])
|
|
136
|
+
|
|
137
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
138
|
+
// These previously landed only on the iteration span — one level up
|
|
139
|
+
// from the operation that spent the tokens.
|
|
140
|
+
expect(attrs).toHaveProperty('gen_ai.usage.input_tokens')
|
|
141
|
+
expect(attrs).toHaveProperty('gen_ai.usage.output_tokens')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('records what the response said it was, not only what was asked for', async () => {
|
|
145
|
+
await runOnce([{ text: 'done' }])
|
|
146
|
+
|
|
147
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
148
|
+
// A provider may answer on a different model than the alias asked
|
|
149
|
+
// for, and the response id is how a trace is matched to a provider's
|
|
150
|
+
// own logs. Both constants existed and neither was ever set.
|
|
151
|
+
expect(attrs['gen_ai.response.model']).toBe('mock-model')
|
|
152
|
+
expect(attrs['gen_ai.response.id']).toBeDefined()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('records the finish reason as an array, per the convention', async () => {
|
|
156
|
+
await runOnce([{ text: 'done' }])
|
|
157
|
+
|
|
158
|
+
// One call can finish several ways when a provider returns more than
|
|
159
|
+
// one choice, which is why the convention makes it a list.
|
|
160
|
+
expect(Array.isArray(chatSpans()[0]?.attributes['gen_ai.response.finish_reasons'])).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('reports cache tokens, which had constants and no producer', async () => {
|
|
164
|
+
await runOnce([{ text: 'done' }])
|
|
165
|
+
|
|
166
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
167
|
+
expect(attrs).toHaveProperty('namzu.cache.read_tokens')
|
|
168
|
+
expect(attrs).toHaveProperty('namzu.cache.write_tokens')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('opens one per turn, not one per run', async () => {
|
|
172
|
+
await runOnce([{ text: 'first' }, { text: 'second' }])
|
|
173
|
+
|
|
174
|
+
// A run with several turns is exactly the case a trace is opened for.
|
|
175
|
+
expect(chatSpans().length).toBeGreaterThanOrEqual(1)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('nests under the iteration span rather than emitting as a root', async () => {
|
|
179
|
+
await runOnce([{ text: 'done' }])
|
|
180
|
+
|
|
181
|
+
// The iteration span is created first; the chat span is parented to it
|
|
182
|
+
// explicitly because this body is an async generator and the ambient
|
|
183
|
+
// context at resume time belongs to the consumer.
|
|
184
|
+
const iterationIndex = spans.findIndex((s) => s.name.includes('iteration'))
|
|
185
|
+
const chatIndex = spans.findIndex((s) => s.name.startsWith('chat '))
|
|
186
|
+
expect(iterationIndex).toBeGreaterThanOrEqual(0)
|
|
187
|
+
expect(chatIndex).toBeGreaterThan(iterationIndex)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A span that never ends is a trace that never closes, and the export is
|
|
6
|
+
* incomplete for exactly the run that failed.
|
|
7
|
+
*
|
|
8
|
+
* Both sites had the same shape: an `end()` call at every exit the author
|
|
9
|
+
* could see. The iteration loop had seventeen of them and the tool executor
|
|
10
|
+
* three plus a `finally` that opened below them. That is a rule every future
|
|
11
|
+
* edit has to remember, and it was already broken — anything throwing between
|
|
12
|
+
* the span's creation and the `try` left it open, and a generator abandoned
|
|
13
|
+
* by its consumer reached no exit at all.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const ended: string[] = []
|
|
17
|
+
const started: string[] = []
|
|
18
|
+
|
|
19
|
+
function fakeSpan(name: string) {
|
|
20
|
+
return {
|
|
21
|
+
setAttributes: () => undefined,
|
|
22
|
+
setAttribute: () => undefined,
|
|
23
|
+
setStatus: () => undefined,
|
|
24
|
+
recordException: () => undefined,
|
|
25
|
+
addEvent: () => undefined,
|
|
26
|
+
end: () => ended.push(name),
|
|
27
|
+
spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
|
|
28
|
+
isRecording: () => true,
|
|
29
|
+
updateName: () => undefined,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
vi.mock('../runtime-accessors.js', () => ({
|
|
34
|
+
getTracer: () => ({
|
|
35
|
+
startSpan: (name: string) => {
|
|
36
|
+
started.push(name)
|
|
37
|
+
return fakeSpan(name)
|
|
38
|
+
},
|
|
39
|
+
startActiveSpan: (
|
|
40
|
+
name: string,
|
|
41
|
+
_opts: unknown,
|
|
42
|
+
_ctx: unknown,
|
|
43
|
+
fn: (span: ReturnType<typeof fakeSpan>) => unknown,
|
|
44
|
+
) => {
|
|
45
|
+
started.push(name)
|
|
46
|
+
return fn(fakeSpan(name))
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
getMeter: () => ({
|
|
50
|
+
createCounter: () => ({ add: () => undefined }),
|
|
51
|
+
createHistogram: () => ({ record: () => undefined }),
|
|
52
|
+
}),
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
function toolContext() {
|
|
56
|
+
return {
|
|
57
|
+
runId: 'run_span',
|
|
58
|
+
workingDirectory: '.',
|
|
59
|
+
abortSignal: new AbortController().signal,
|
|
60
|
+
env: {},
|
|
61
|
+
log: () => undefined,
|
|
62
|
+
} as never
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
started.length = 0
|
|
67
|
+
ended.length = 0
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
vi.clearAllMocks()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('a tool span closes however the call leaves', () => {
|
|
75
|
+
it('closes on the ordinary path', async () => {
|
|
76
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
77
|
+
const tools = new ToolRegistry()
|
|
78
|
+
tools.register({
|
|
79
|
+
name: 'echo',
|
|
80
|
+
description: 'echo',
|
|
81
|
+
inputSchema: z.object({}),
|
|
82
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await tools.execute('echo', {}, toolContext())
|
|
86
|
+
|
|
87
|
+
expect(started).toHaveLength(1)
|
|
88
|
+
expect(ended).toEqual(started)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('closes when the tool throws', async () => {
|
|
92
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
93
|
+
const tools = new ToolRegistry()
|
|
94
|
+
tools.register({
|
|
95
|
+
name: 'boom',
|
|
96
|
+
description: 'boom',
|
|
97
|
+
inputSchema: z.object({}),
|
|
98
|
+
execute: async () => {
|
|
99
|
+
throw new Error('kaboom')
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await tools.execute('boom', {}, toolContext())
|
|
104
|
+
|
|
105
|
+
expect(ended).toEqual(started)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('closes when input validation refuses the call', async () => {
|
|
109
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
110
|
+
const tools = new ToolRegistry()
|
|
111
|
+
tools.register({
|
|
112
|
+
name: 'strict',
|
|
113
|
+
description: 'strict',
|
|
114
|
+
inputSchema: z.object({ required: z.string() }),
|
|
115
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await tools.execute('strict', { wrong: 1 }, toolContext())
|
|
119
|
+
|
|
120
|
+
expect(ended).toEqual(started)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('closes when the tool is not active', async () => {
|
|
124
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
125
|
+
const tools = new ToolRegistry()
|
|
126
|
+
tools.register(
|
|
127
|
+
{
|
|
128
|
+
name: 'later',
|
|
129
|
+
description: 'later',
|
|
130
|
+
inputSchema: z.object({}),
|
|
131
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
132
|
+
},
|
|
133
|
+
'deferred',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
await tools.execute('later', {}, toolContext())
|
|
137
|
+
|
|
138
|
+
expect(ended).toEqual(started)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('closes when the registry does not hold the name at all', async () => {
|
|
142
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
143
|
+
const tools = new ToolRegistry()
|
|
144
|
+
|
|
145
|
+
// `getOrThrow` sat OUTSIDE the try that owned the finally, so this
|
|
146
|
+
// path — the one where the model invents a tool name — opened a span
|
|
147
|
+
// and never closed it.
|
|
148
|
+
await expect(tools.execute('nonexistent', {}, toolContext())).rejects.toThrow()
|
|
149
|
+
|
|
150
|
+
expect(started).toHaveLength(1)
|
|
151
|
+
expect(ended).toEqual(started)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -104,7 +104,7 @@ export function buildAdvisoryTools(opts: AdvisoryToolsOptions): ToolDefinition[]
|
|
|
104
104
|
const executionResult = await advisoryCtx.executor.consult(
|
|
105
105
|
advisor,
|
|
106
106
|
{ advisorId: advisor.id, question, domain, urgency, includeContext: include_context },
|
|
107
|
-
|
|
107
|
+
advisoryCtx.callContext(),
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
advisoryCtx.recordCall({
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { resolvePlanDependencies } from '../plan-dependencies.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The model is shown `depends_on` on every plan step and told it means
|
|
7
|
+
* "Step descriptions this depends on". `approve_plan` then passed
|
|
8
|
+
* `dependsOn: []` for every step, so the ordering it declared was discarded
|
|
9
|
+
* at the only place it entered the system.
|
|
10
|
+
*
|
|
11
|
+
* The cost is not scheduling — the dependency gate in `PlanManager` has no
|
|
12
|
+
* callers — it is the approval. `dependsOn` is serialized into the
|
|
13
|
+
* `plan_approval` payload a human reads before saying yes, so the reviewer
|
|
14
|
+
* saw a plan whose steps all looked independent however carefully the model
|
|
15
|
+
* had ordered them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const id = (index: number) => `step_${index + 1}`
|
|
19
|
+
|
|
20
|
+
describe('what the model described becomes what the plan holds', () => {
|
|
21
|
+
it('resolves a description to the step that carries it', () => {
|
|
22
|
+
const result = resolvePlanDependencies(
|
|
23
|
+
[
|
|
24
|
+
{ description: 'Read the config' },
|
|
25
|
+
{ description: 'Write the report', depends_on: ['Read the config'] },
|
|
26
|
+
],
|
|
27
|
+
id,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ ok: true, dependsOn: [[], ['step_1']] })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('resolves several dependencies on one step', () => {
|
|
34
|
+
const result = resolvePlanDependencies(
|
|
35
|
+
[
|
|
36
|
+
{ description: 'Read the config' },
|
|
37
|
+
{ description: 'Read the code' },
|
|
38
|
+
{ description: 'Write the report', depends_on: ['Read the config', 'Read the code'] },
|
|
39
|
+
],
|
|
40
|
+
id,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(result.ok && result.dependsOn[2]).toEqual(['step_1', 'step_2'])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('resolves a forward dependency, since order of declaration is not order of execution', () => {
|
|
47
|
+
const result = resolvePlanDependencies(
|
|
48
|
+
[
|
|
49
|
+
{ description: 'Write the report', depends_on: ['Read the config'] },
|
|
50
|
+
{ description: 'Read the config' },
|
|
51
|
+
],
|
|
52
|
+
id,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(result.ok && result.dependsOn[0]).toEqual(['step_2'])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('leaves a step with no dependencies empty', () => {
|
|
59
|
+
const result = resolvePlanDependencies([{ description: 'Read the config' }], id)
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual({ ok: true, dependsOn: [[]] })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('forgives whitespace and casing a model did not keep identical', () => {
|
|
65
|
+
const result = resolvePlanDependencies(
|
|
66
|
+
[
|
|
67
|
+
{ description: 'Read the config' },
|
|
68
|
+
{ description: 'Write', depends_on: ['read the CONFIG '] },
|
|
69
|
+
],
|
|
70
|
+
id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Rejecting this would teach the model nothing, for a plan that was
|
|
74
|
+
// right.
|
|
75
|
+
expect(result.ok && result.dependsOn[1]).toEqual(['step_1'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('collapses the same dependency named twice', () => {
|
|
79
|
+
const result = resolvePlanDependencies(
|
|
80
|
+
[{ description: 'Read' }, { description: 'Write', depends_on: ['Read', 'Read'] }],
|
|
81
|
+
id,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(result.ok && result.dependsOn[1]).toEqual(['step_1'])
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('a dependency that cannot mean anything is refused, not dropped', () => {
|
|
89
|
+
it('refuses a dependency naming no step', () => {
|
|
90
|
+
const result = resolvePlanDependencies(
|
|
91
|
+
[{ description: 'Write', depends_on: ['Read the config'] }],
|
|
92
|
+
id,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(result.ok).toBe(false)
|
|
96
|
+
// The model has to be able to act on it, so the offending text is named.
|
|
97
|
+
expect(!result.ok && result.error).toContain('Read the config')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('refuses a dependency two steps could answer', () => {
|
|
101
|
+
const result = resolvePlanDependencies(
|
|
102
|
+
[
|
|
103
|
+
{ description: 'Review' },
|
|
104
|
+
{ description: 'Review' },
|
|
105
|
+
{ description: 'Report', depends_on: ['Review'] },
|
|
106
|
+
],
|
|
107
|
+
id,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// Picking either is a coin flip whose result a human then approves as
|
|
111
|
+
// if it were the model's intent.
|
|
112
|
+
expect(result.ok).toBe(false)
|
|
113
|
+
expect(!result.ok && result.error).toContain('2 steps share that description')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('refuses a step that depends on itself', () => {
|
|
117
|
+
const result = resolvePlanDependencies([{ description: 'Loop', depends_on: ['Loop'] }], id)
|
|
118
|
+
|
|
119
|
+
expect(result.ok).toBe(false)
|
|
120
|
+
expect(!result.ok && result.error).toContain('depends on itself')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('refuses a two-step cycle', () => {
|
|
124
|
+
const result = resolvePlanDependencies(
|
|
125
|
+
[
|
|
126
|
+
{ description: 'A', depends_on: ['B'] },
|
|
127
|
+
{ description: 'B', depends_on: ['A'] },
|
|
128
|
+
],
|
|
129
|
+
id,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false)
|
|
133
|
+
expect(!result.ok && result.error).toContain('loop')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('refuses a cycle several steps long', () => {
|
|
137
|
+
const result = resolvePlanDependencies(
|
|
138
|
+
[
|
|
139
|
+
{ description: 'A', depends_on: ['C'] },
|
|
140
|
+
{ description: 'B', depends_on: ['A'] },
|
|
141
|
+
{ description: 'C', depends_on: ['B'] },
|
|
142
|
+
],
|
|
143
|
+
id,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// This is the failure worth catching hardest: no step in a loop can
|
|
147
|
+
// start, so the plan does not error — it simply stops.
|
|
148
|
+
expect(result.ok).toBe(false)
|
|
149
|
+
expect(!result.ok && result.error).toContain('loop')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('names every step in the loop so it can be broken', () => {
|
|
153
|
+
const result = resolvePlanDependencies(
|
|
154
|
+
[
|
|
155
|
+
{ description: 'Alpha', depends_on: ['Gamma'] },
|
|
156
|
+
{ description: 'Beta' },
|
|
157
|
+
{ description: 'Gamma', depends_on: ['Alpha'] },
|
|
158
|
+
],
|
|
159
|
+
id,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect(result.ok).toBe(false)
|
|
163
|
+
if (result.ok) return
|
|
164
|
+
expect(result.error).toContain('Alpha')
|
|
165
|
+
expect(result.error).toContain('Gamma')
|
|
166
|
+
// Beta is not in the loop and must not be blamed for it.
|
|
167
|
+
expect(result.error).not.toContain('Beta')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('accepts a diamond, which is not a cycle', () => {
|
|
171
|
+
const result = resolvePlanDependencies(
|
|
172
|
+
[
|
|
173
|
+
{ description: 'Start' },
|
|
174
|
+
{ description: 'Left', depends_on: ['Start'] },
|
|
175
|
+
{ description: 'Right', depends_on: ['Start'] },
|
|
176
|
+
{ description: 'Join', depends_on: ['Left', 'Right'] },
|
|
177
|
+
],
|
|
178
|
+
id,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Two paths reaching one step is an ordinary plan shape, and a naive
|
|
182
|
+
// visited-set cycle check calls it a loop.
|
|
183
|
+
expect(result.ok).toBe(true)
|
|
184
|
+
expect(result.ok && result.dependsOn[3]).toEqual(['step_2', 'step_3'])
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -8,6 +8,7 @@ import type { RunId, TaskId } from '../../types/ids/index.js'
|
|
|
8
8
|
import type { TaskStore } from '../../types/task/index.js'
|
|
9
9
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
10
10
|
import { defineTool } from '../defineTool.js'
|
|
11
|
+
import { resolvePlanDependencies } from './plan-dependencies.js'
|
|
11
12
|
|
|
12
13
|
export type TaskLaunchedCallback = (
|
|
13
14
|
agentTaskId: TaskId,
|
|
@@ -437,6 +438,15 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
437
438
|
}
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
// Resolve BEFORE touching the plan manager. A refusal here has to
|
|
442
|
+
// leave no half-built plan behind: `startGenerating` replaces the
|
|
443
|
+
// current plan, so failing after it would discard a plan that was
|
|
444
|
+
// fine in favour of one that never completes.
|
|
445
|
+
const dependencies = resolvePlanDependencies(steps, (index) => `step_${index + 1}`)
|
|
446
|
+
if (!dependencies.ok) {
|
|
447
|
+
return { success: false, output: '', error: dependencies.error }
|
|
448
|
+
}
|
|
449
|
+
|
|
440
450
|
pm.startGenerating(title)
|
|
441
451
|
for (let i = 0; i < steps.length; i++) {
|
|
442
452
|
const step = steps[i]
|
|
@@ -445,7 +455,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
445
455
|
id: `step_${i + 1}`,
|
|
446
456
|
description: step.description,
|
|
447
457
|
toolName: step.agent_id ? 'create_task' : undefined,
|
|
448
|
-
|
|
458
|
+
// Was `[]` unconditionally, which dropped every ordering
|
|
459
|
+
// constraint the model was invited to express — and put an
|
|
460
|
+
// empty dependency list in front of the human approving it.
|
|
461
|
+
dependsOn: [...(dependencies.dependsOn[i] ?? [])],
|
|
449
462
|
order: i + 1,
|
|
450
463
|
})
|
|
451
464
|
}
|