@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.
Files changed (257) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
  3. package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
  4. package/dist/advisory/__tests__/consultation-context.test.js +124 -0
  5. package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
  6. package/dist/advisory/context.d.ts +25 -0
  7. package/dist/advisory/context.d.ts.map +1 -1
  8. package/dist/advisory/context.js +18 -0
  9. package/dist/advisory/context.js.map +1 -1
  10. package/dist/advisory/executor.d.ts.map +1 -1
  11. package/dist/advisory/executor.js +25 -3
  12. package/dist/advisory/executor.js.map +1 -1
  13. package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
  14. package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
  15. package/dist/compaction/__tests__/context-reducer.test.js +197 -0
  16. package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
  17. package/dist/compaction/factory.d.ts +7 -0
  18. package/dist/compaction/factory.d.ts.map +1 -1
  19. package/dist/compaction/factory.js +7 -0
  20. package/dist/compaction/factory.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -0
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js +1 -0
  24. package/dist/compaction/index.js.map +1 -1
  25. package/dist/compaction/interface.d.ts +13 -0
  26. package/dist/compaction/interface.d.ts.map +1 -1
  27. package/dist/compaction/managers/null.d.ts +3 -0
  28. package/dist/compaction/managers/null.d.ts.map +1 -1
  29. package/dist/compaction/managers/null.js +3 -0
  30. package/dist/compaction/managers/null.js.map +1 -1
  31. package/dist/compaction/managers/slidingWindow.d.ts +6 -0
  32. package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
  33. package/dist/compaction/managers/slidingWindow.js +6 -0
  34. package/dist/compaction/managers/slidingWindow.js.map +1 -1
  35. package/dist/compaction/managers/structured.d.ts +10 -0
  36. package/dist/compaction/managers/structured.d.ts.map +1 -1
  37. package/dist/compaction/managers/structured.js +10 -0
  38. package/dist/compaction/managers/structured.js.map +1 -1
  39. package/dist/compaction/reducer.d.ts +86 -0
  40. package/dist/compaction/reducer.d.ts.map +1 -0
  41. package/dist/compaction/reducer.js +77 -0
  42. package/dist/compaction/reducer.js.map +1 -0
  43. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
  44. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
  45. package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
  46. package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
  47. package/dist/connector/builtins/http.d.ts.map +1 -1
  48. package/dist/connector/builtins/http.js +24 -2
  49. package/dist/connector/builtins/http.js.map +1 -1
  50. package/dist/connector/builtins/http.test.js +18 -2
  51. package/dist/connector/builtins/http.test.js.map +1 -1
  52. package/dist/connector/index.d.ts +2 -2
  53. package/dist/connector/index.d.ts.map +1 -1
  54. package/dist/connector/index.js +1 -1
  55. package/dist/connector/index.js.map +1 -1
  56. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
  57. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
  58. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
  59. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
  60. package/dist/connector/mcp/client.d.ts +52 -1
  61. package/dist/connector/mcp/client.d.ts.map +1 -1
  62. package/dist/connector/mcp/client.js +86 -0
  63. package/dist/connector/mcp/client.js.map +1 -1
  64. package/dist/connector/mcp/discovery.d.ts +12 -1
  65. package/dist/connector/mcp/discovery.d.ts.map +1 -1
  66. package/dist/connector/mcp/discovery.js +19 -4
  67. package/dist/connector/mcp/discovery.js.map +1 -1
  68. package/dist/connector/mcp/index.d.ts +2 -2
  69. package/dist/connector/mcp/index.d.ts.map +1 -1
  70. package/dist/connector/mcp/index.js +1 -1
  71. package/dist/connector/mcp/index.js.map +1 -1
  72. package/dist/connector/mcp/server.d.ts +42 -1
  73. package/dist/connector/mcp/server.d.ts.map +1 -1
  74. package/dist/connector/mcp/server.js +77 -4
  75. package/dist/connector/mcp/server.js.map +1 -1
  76. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
  77. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
  78. package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
  79. package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
  80. package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
  81. package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
  82. package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
  83. package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
  84. package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
  85. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
  86. package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
  87. package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
  88. package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
  89. package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
  90. package/dist/plugin/lifecycle.d.ts +41 -0
  91. package/dist/plugin/lifecycle.d.ts.map +1 -1
  92. package/dist/plugin/lifecycle.js +29 -1
  93. package/dist/plugin/lifecycle.js.map +1 -1
  94. package/dist/plugin/loader.d.ts +39 -3
  95. package/dist/plugin/loader.d.ts.map +1 -1
  96. package/dist/plugin/loader.js +37 -4
  97. package/dist/plugin/loader.js.map +1 -1
  98. package/dist/public-runtime.d.ts +4 -2
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +5 -2
  101. package/dist/public-runtime.js.map +1 -1
  102. package/dist/public-types.d.ts +2 -2
  103. package/dist/public-types.d.ts.map +1 -1
  104. package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
  105. package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
  106. package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
  107. package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
  108. package/dist/rag/ingestion.d.ts.map +1 -1
  109. package/dist/rag/ingestion.js +1 -0
  110. package/dist/rag/ingestion.js.map +1 -1
  111. package/dist/rag/retriever.d.ts.map +1 -1
  112. package/dist/rag/retriever.js +2 -0
  113. package/dist/rag/retriever.js.map +1 -1
  114. package/dist/rag/vector-store.d.ts.map +1 -1
  115. package/dist/rag/vector-store.js +6 -0
  116. package/dist/rag/vector-store.js.map +1 -1
  117. package/dist/registry/tool/execute.d.ts.map +1 -1
  118. package/dist/registry/tool/execute.js +113 -109
  119. package/dist/registry/tool/execute.js.map +1 -1
  120. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  121. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  122. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  125. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  128. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  129. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  132. package/dist/runtime/query/index.d.ts +10 -0
  133. package/dist/runtime/query/index.d.ts.map +1 -1
  134. package/dist/runtime/query/index.js +26 -0
  135. package/dist/runtime/query/index.js.map +1 -1
  136. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  137. package/dist/runtime/query/iteration/index.js +96 -34
  138. package/dist/runtime/query/iteration/index.js.map +1 -1
  139. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  140. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  141. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  142. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  143. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  144. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  145. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  147. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  148. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  149. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  150. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  151. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  153. package/dist/runtime/query/resume-run.d.ts +70 -0
  154. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  155. package/dist/runtime/query/resume-run.js +46 -0
  156. package/dist/runtime/query/resume-run.js.map +1 -0
  157. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  158. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  159. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  160. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  161. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  162. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  163. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  164. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  165. package/dist/tools/advisory/index.js +1 -1
  166. package/dist/tools/advisory/index.js.map +1 -1
  167. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  168. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  169. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  170. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  171. package/dist/tools/coordinator/index.d.ts.map +1 -1
  172. package/dist/tools/coordinator/index.js +13 -1
  173. package/dist/tools/coordinator/index.js.map +1 -1
  174. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  175. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  176. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  177. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  178. package/dist/types/agent/supervisor.d.ts +15 -0
  179. package/dist/types/agent/supervisor.d.ts.map +1 -1
  180. package/dist/types/connector/core.d.ts +34 -0
  181. package/dist/types/connector/core.d.ts.map +1 -1
  182. package/dist/types/connector/definition.d.ts +10 -0
  183. package/dist/types/connector/definition.d.ts.map +1 -1
  184. package/dist/types/connector/mcp.d.ts +13 -0
  185. package/dist/types/connector/mcp.d.ts.map +1 -1
  186. package/dist/types/rag/retrieval.d.ts +16 -0
  187. package/dist/types/rag/retrieval.d.ts.map +1 -1
  188. package/dist/types/rag/storage.d.ts +9 -0
  189. package/dist/types/rag/storage.d.ts.map +1 -1
  190. package/dist/types/rag/vector.d.ts +11 -0
  191. package/dist/types/rag/vector.d.ts.map +1 -1
  192. package/dist/types/router/task-router.d.ts +19 -0
  193. package/dist/types/router/task-router.d.ts.map +1 -1
  194. package/dist/types/run/prepare-step.d.ts +56 -3
  195. package/dist/types/run/prepare-step.d.ts.map +1 -1
  196. package/dist/types/toolset/index.d.ts +22 -0
  197. package/dist/types/toolset/index.d.ts.map +1 -1
  198. package/package.json +1 -1
  199. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  200. package/src/advisory/context.ts +32 -0
  201. package/src/advisory/executor.ts +30 -3
  202. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  203. package/src/compaction/factory.ts +7 -0
  204. package/src/compaction/index.ts +8 -0
  205. package/src/compaction/interface.ts +13 -0
  206. package/src/compaction/managers/null.ts +3 -0
  207. package/src/compaction/managers/slidingWindow.ts +6 -0
  208. package/src/compaction/managers/structured.ts +10 -0
  209. package/src/compaction/reducer.ts +154 -0
  210. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  211. package/src/connector/builtins/http.test.ts +28 -2
  212. package/src/connector/builtins/http.ts +26 -2
  213. package/src/connector/index.ts +6 -2
  214. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  215. package/src/connector/mcp/client.ts +95 -0
  216. package/src/connector/mcp/discovery.ts +19 -4
  217. package/src/connector/mcp/index.ts +6 -2
  218. package/src/connector/mcp/server.ts +101 -3
  219. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  220. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  221. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  222. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  223. package/src/plugin/lifecycle.ts +57 -1
  224. package/src/plugin/loader.ts +57 -3
  225. package/src/public-runtime.ts +8 -0
  226. package/src/public-types.ts +5 -0
  227. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  228. package/src/rag/ingestion.ts +1 -0
  229. package/src/rag/retriever.ts +2 -0
  230. package/src/rag/vector-store.ts +5 -0
  231. package/src/registry/tool/execute.ts +123 -119
  232. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  233. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  234. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  235. package/src/runtime/query/index.ts +39 -0
  236. package/src/runtime/query/iteration/index.ts +103 -34
  237. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  238. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  239. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  240. package/src/runtime/query/iteration/phases/context.ts +11 -0
  241. package/src/runtime/query/resume-run.ts +93 -0
  242. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  243. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  244. package/src/tools/advisory/index.ts +1 -1
  245. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  246. package/src/tools/coordinator/index.ts +14 -1
  247. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  248. package/src/types/agent/supervisor.ts +15 -0
  249. package/src/types/connector/core.ts +34 -0
  250. package/src/types/connector/definition.ts +10 -0
  251. package/src/types/connector/mcp.ts +14 -0
  252. package/src/types/rag/retrieval.ts +16 -0
  253. package/src/types/rag/storage.ts +9 -0
  254. package/src/types/rag/vector.ts +11 -0
  255. package/src/types/router/task-router.ts +19 -0
  256. package/src/types/run/prepare-step.ts +58 -3
  257. 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
- { messages: [], iteration: 0 },
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
- dependsOn: [],
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
  }