@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -0,0 +1,191 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { AdvisorDefinition } from '../../types/advisory/index.js'
4
+ import type { Message } from '../../types/message/index.js'
5
+ import { AdvisoryContext } from '../context.js'
6
+ import { TriggerEvaluator } from '../evaluator.js'
7
+ import { AdvisoryExecutor } from '../executor.js'
8
+ import { ADVISORY_RESPONSE_CONTRACT } from '../parse.js'
9
+ import { AdvisorRegistry } from '../registry.js'
10
+
11
+ /**
12
+ * An advisor consulted BY THE MODEL saw the question and nothing else.
13
+ *
14
+ * Two paths reach `AdvisoryExecutor.consult`. The trigger path
15
+ * (`iteration/phases/advisory.ts`) has always passed the live messages, the
16
+ * working state and the tool catalogue. The tool path passed
17
+ * `{ messages: [], iteration: 0 }` — a literal empty context — so the
18
+ * model's own `include_context: true` had nothing to include, and the
19
+ * advisor answered a question about a situation it could not see.
20
+ */
21
+
22
+ function recordingProvider(): { provider: AdvisorDefinition['provider']; calls: Message[][] } {
23
+ const calls: Message[][] = []
24
+ const provider = {
25
+ chatStream: async function* (params: { messages: Message[] }) {
26
+ calls.push(params.messages)
27
+ yield { id: 'a1', delta: { content: 'ADVICE: do the thing' } }
28
+ yield {
29
+ id: 'a1',
30
+ delta: {},
31
+ finishReason: 'stop',
32
+ usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
33
+ }
34
+ },
35
+ } as unknown as AdvisorDefinition['provider']
36
+ return { provider, calls }
37
+ }
38
+
39
+ function advisor(over: Partial<AdvisorDefinition> = {}): AdvisorDefinition {
40
+ return {
41
+ id: 'adv_1',
42
+ name: 'Reviewer',
43
+ provider: recordingProvider().provider,
44
+ model: 'mock-model',
45
+ ...over,
46
+ } as AdvisorDefinition
47
+ }
48
+
49
+ const log = {
50
+ debug: vi.fn(),
51
+ info: vi.fn(),
52
+ warn: vi.fn(),
53
+ error: vi.fn(),
54
+ child: () => log,
55
+ } as never
56
+
57
+ function contextFor(a: AdvisorDefinition): AdvisoryContext {
58
+ return new AdvisoryContext(
59
+ new AdvisorRegistry([a]),
60
+ new AdvisoryExecutor(log),
61
+ new TriggerEvaluator([]),
62
+ )
63
+ }
64
+
65
+ describe('the context a tool-initiated consultation is given', () => {
66
+ it('is empty until the runtime supplies one', () => {
67
+ const ctx = contextFor(advisor())
68
+
69
+ // The fallback exists for a context built without a runtime, and it is
70
+ // what every tool-initiated call used to get.
71
+ expect(ctx.callContext()).toEqual({ messages: [], iteration: 0 })
72
+ })
73
+
74
+ it('is the live run once the runtime wires it', () => {
75
+ const ctx = contextFor(advisor())
76
+ const messages: Message[] = [{ role: 'user', content: 'the situation', timestamp: 1 }]
77
+
78
+ ctx.setCallContextProvider(() => ({ messages, iteration: 7 }))
79
+
80
+ expect(ctx.callContext().messages).toBe(messages)
81
+ expect(ctx.callContext().iteration).toBe(7)
82
+ })
83
+
84
+ it('is read at call time, not at construction', () => {
85
+ const ctx = contextFor(advisor())
86
+ let iteration = 1
87
+ ctx.setCallContextProvider(() => ({ messages: [], iteration }))
88
+
89
+ iteration = 4
90
+
91
+ // The tool is built once per run and called at an unknown later point.
92
+ // A snapshot would hand every advisor the state the run started with.
93
+ expect(ctx.callContext().iteration).toBe(4)
94
+ })
95
+ })
96
+
97
+ describe('what the advisor actually receives', () => {
98
+ it('sees the conversation when context is included', async () => {
99
+ const { provider, calls } = recordingProvider()
100
+ const executor = new AdvisoryExecutor(log)
101
+
102
+ await executor.consult(
103
+ advisor({ provider }),
104
+ { advisorId: 'adv_1', question: 'what next?', includeContext: true },
105
+ { messages: [{ role: 'user', content: 'deploy is failing', timestamp: 1 }], iteration: 2 },
106
+ )
107
+
108
+ const sent = JSON.stringify(calls[0])
109
+ expect(sent).toContain('deploy is failing')
110
+ })
111
+
112
+ it('sees none of it when the caller says not to', async () => {
113
+ const { provider, calls } = recordingProvider()
114
+ const executor = new AdvisoryExecutor(log)
115
+
116
+ await executor.consult(
117
+ advisor({ provider }),
118
+ { advisorId: 'adv_1', question: 'what next?', includeContext: false },
119
+ { messages: [{ role: 'user', content: 'deploy is failing', timestamp: 1 }], iteration: 2 },
120
+ )
121
+
122
+ expect(JSON.stringify(calls[0])).not.toContain('deploy is failing')
123
+ })
124
+
125
+ it('is told when the caller marked the request urgent', async () => {
126
+ const { provider, calls } = recordingProvider()
127
+ const executor = new AdvisoryExecutor(log)
128
+
129
+ await executor.consult(
130
+ advisor({ provider }),
131
+ { advisorId: 'adv_1', question: 'what next?', urgency: 'high' },
132
+ { messages: [], iteration: 1 },
133
+ )
134
+
135
+ // The value used to reach exactly one debug log line, so 'high' and
136
+ // 'low' produced byte-identical requests.
137
+ expect(String(calls[0]?.[0]?.content)).toContain('URGENT')
138
+ })
139
+
140
+ it('is told when there is room to consider alternatives', async () => {
141
+ const { provider, calls } = recordingProvider()
142
+ const executor = new AdvisoryExecutor(log)
143
+
144
+ await executor.consult(
145
+ advisor({ provider }),
146
+ { advisorId: 'adv_1', question: 'what next?', urgency: 'low' },
147
+ { messages: [], iteration: 1 },
148
+ )
149
+
150
+ expect(String(calls[0]?.[0]?.content)).toContain('low urgency')
151
+ })
152
+
153
+ it("appends nothing at all for 'normal', which is the point of not stating it", async () => {
154
+ const { provider, calls } = recordingProvider()
155
+ const executor = new AdvisoryExecutor(log)
156
+ const a = advisor({ provider })
157
+
158
+ await executor.consult(
159
+ a,
160
+ { advisorId: 'adv_1', question: 'what next?', urgency: 'normal' },
161
+ { messages: [], iteration: 1 },
162
+ )
163
+ await executor.consult(
164
+ a,
165
+ { advisorId: 'adv_1', question: 'what next?', urgency: 'high' },
166
+ { messages: [], iteration: 1 },
167
+ )
168
+
169
+ // Asserting "does not contain the other two phrases" is too weak — it
170
+ // would let ANY new sentence in. The response contract is the last
171
+ // thing the prompt says when urgency contributes nothing, so ending on
172
+ // it is what "appended nothing" actually means.
173
+ expect(String(calls[0]?.[0]?.content).trimEnd().endsWith(ADVISORY_RESPONSE_CONTRACT)).toBe(true)
174
+ expect(String(calls[1]?.[0]?.content).trimEnd().endsWith(ADVISORY_RESPONSE_CONTRACT)).toBe(
175
+ false,
176
+ )
177
+ })
178
+
179
+ it('says nothing about urgency when the caller did not', async () => {
180
+ const { provider, calls } = recordingProvider()
181
+ const executor = new AdvisoryExecutor(log)
182
+
183
+ await executor.consult(
184
+ advisor({ provider }),
185
+ { advisorId: 'adv_1', question: 'what next?' },
186
+ { messages: [], iteration: 1 },
187
+ )
188
+
189
+ expect(String(calls[0]?.[0]?.content).trimEnd().endsWith(ADVISORY_RESPONSE_CONTRACT)).toBe(true)
190
+ })
191
+ })
@@ -1,8 +1,20 @@
1
1
  import type { AdvisoryBudget, AdvisoryCallRecord } from '../types/advisory/index.js'
2
2
  import type { TriggerEvaluator } from './evaluator.js'
3
+ import type { AdvisoryCallContext } from './executor.js'
3
4
  import type { AdvisoryExecutor } from './executor.js'
4
5
  import type { AdvisorRegistry } from './registry.js'
5
6
 
7
+ /**
8
+ * What the run looks like right now, for an advisory call that did not come
9
+ * from the iteration loop.
10
+ *
11
+ * A function rather than a snapshot because the tool is built once per run
12
+ * and called at an unknown later point: capturing the context at
13
+ * construction would hand every advisor the state the run had before it
14
+ * started.
15
+ */
16
+ export type AdvisoryCallContextProvider = () => AdvisoryCallContext
17
+
6
18
  export class AdvisoryContext {
7
19
  readonly registry: AdvisorRegistry
8
20
  readonly executor: AdvisoryExecutor
@@ -10,6 +22,7 @@ export class AdvisoryContext {
10
22
  readonly callHistory: AdvisoryCallRecord[] = []
11
23
 
12
24
  private readonly budget: AdvisoryBudget | undefined
25
+ private callContextProvider: AdvisoryCallContextProvider | undefined
13
26
 
14
27
  constructor(
15
28
  registry: AdvisorRegistry,
@@ -23,6 +36,25 @@ export class AdvisoryContext {
23
36
  this.budget = budget
24
37
  }
25
38
 
39
+ /** Wired by the runtime once the run exists. */
40
+ setCallContextProvider(provider: AdvisoryCallContextProvider): void {
41
+ this.callContextProvider = provider
42
+ }
43
+
44
+ /**
45
+ * The call context for a tool-initiated consultation.
46
+ *
47
+ * The trigger path (`iteration/phases/advisory.ts`) has always passed the
48
+ * live messages, working state and tool catalogue. The TOOL path passed
49
+ * `{ messages: [], iteration: 0 }` — a literal empty context — so an
50
+ * advisor consulted by the model saw the question and nothing else, and
51
+ * the model's `include_context` had nothing to include either way. The
52
+ * empty fallback survives only for a context built without a runtime.
53
+ */
54
+ callContext(): AdvisoryCallContext {
55
+ return this.callContextProvider?.() ?? { messages: [], iteration: 0 }
56
+ }
57
+
26
58
  recordCall(record: AdvisoryCallRecord): void {
27
59
  this.callHistory.push(record)
28
60
  }
@@ -11,6 +11,19 @@ import { calculateCost } from '../utils/cost.js'
11
11
  import { type Logger, getRootLogger } from '../utils/logger.js'
12
12
  import { ADVISORY_RESPONSE_CONTRACT, parseAdvisoryResponse } from './parse.js'
13
13
 
14
+ /**
15
+ * What the advisor is told about how urgent the caller said this is.
16
+ *
17
+ * `'normal'` says nothing on purpose. A sentence asserting the ordinary case
18
+ * is prompt weight that changes no answer, and stating it on every call
19
+ * would make the two that matter harder to notice.
20
+ */
21
+ const URGENCY_DIRECTION: Record<'low' | 'normal' | 'high', string | undefined> = {
22
+ high: 'This request is marked URGENT. Lead with the single most important action and keep the reasoning to what is needed to justify it.',
23
+ normal: undefined,
24
+ low: 'This request is marked low urgency. There is room to note secondary considerations and alternatives worth weighing.',
25
+ }
26
+
14
27
  export interface AdvisoryCallContext {
15
28
  readonly messages: Message[]
16
29
  readonly workingStateSummary?: string
@@ -53,7 +66,7 @@ export class AdvisoryExecutor {
53
66
  ): Promise<AdvisoryExecutionResult> {
54
67
  const startMs = Date.now()
55
68
 
56
- const systemPrompt = this.buildSystemPrompt(advisor)
69
+ const systemPrompt = this.buildSystemPrompt(advisor, request.urgency)
57
70
  const contextMessages = this.buildContext(advisor, request, callCtx)
58
71
 
59
72
  const messages: Message[] = [
@@ -100,11 +113,25 @@ export class AdvisoryExecutor {
100
113
  }
101
114
  }
102
115
 
103
- private buildSystemPrompt(advisor: AdvisorDefinition): string {
116
+ private buildSystemPrompt(
117
+ advisor: AdvisorDefinition,
118
+ urgency?: AdvisoryRequest['urgency'],
119
+ ): string {
104
120
  // The contract is appended to every branch, not folded into the
105
121
  // default: an advisor with its own prompt or a persona is still read
106
122
  // back by the same parser, and used to be the one never told so.
107
- return `${this.describeAdvisor(advisor)}\n\n${ADVISORY_RESPONSE_CONTRACT}`
123
+ const parts = [this.describeAdvisor(advisor), ADVISORY_RESPONSE_CONTRACT]
124
+
125
+ // The caller is invited to say how urgent this is, and the value used
126
+ // to reach exactly one debug log line — `urgency: 'high'` and
127
+ // `urgency: 'low'` produced byte-identical requests. Telling the
128
+ // ADVISOR is the honest minimum: it is the party that can act on the
129
+ // answer, and it costs one sentence rather than a routing policy this
130
+ // kernel has no business inventing.
131
+ const direction = URGENCY_DIRECTION[urgency ?? 'normal']
132
+ if (direction) parts.push(direction)
133
+
134
+ return parts.join('\n\n')
108
135
  }
109
136
 
110
137
  private describeAdvisor(advisor: AdvisorDefinition): string {
@@ -0,0 +1,239 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { AssistantMessage, Message, ToolMessage } from '../../types/message/index.js'
4
+ import { createSlidingWindowReducer } from '../reducer.js'
5
+ import type { ContextReduction } from '../reducer.js'
6
+
7
+ /**
8
+ * `strategy: 'sliding-window'` was accepted by the config schema and then
9
+ * ignored — the runtime asked only whether the strategy was `'disabled'`, so
10
+ * a host who chose the cheap non-LLM path silently got the expensive LLM one,
11
+ * summarization calls and all. These pin the behaviour the name always
12
+ * claimed, and the three invariants that make a shorter history usable rather
13
+ * than merely shorter.
14
+ */
15
+
16
+ const sys = (content: string): Message => ({ role: 'system', content, timestamp: 1 })
17
+ const user = (content: string): Message => ({ role: 'user', content, timestamp: 1 })
18
+ const asst = (content: string): Message => ({ role: 'assistant', content, timestamp: 1 })
19
+
20
+ const callsTool = (id: string): AssistantMessage => ({
21
+ role: 'assistant',
22
+ content: null,
23
+ timestamp: 1,
24
+ toolCalls: [{ id, type: 'function', function: { name: 'echo', arguments: '{}' } }],
25
+ })
26
+
27
+ const toolResult = (id: string): ToolMessage => ({
28
+ role: 'tool',
29
+ content: 'ok',
30
+ timestamp: 1,
31
+ toolCallId: id,
32
+ })
33
+
34
+ function reduction(
35
+ messages: readonly Message[],
36
+ over?: Partial<ContextReduction>,
37
+ ): ContextReduction {
38
+ return {
39
+ messages,
40
+ reason: 'threshold',
41
+ estimatedTokens: 1_000,
42
+ contextWindowTokens: 2_000,
43
+ model: 'mock-model',
44
+ keepRecentMessages: 4,
45
+ ...over,
46
+ }
47
+ }
48
+
49
+ describe('the sliding window keeps recent turns and drops the rest', () => {
50
+ it('keeps the configured tail', async () => {
51
+ const reduce = createSlidingWindowReducer()
52
+ const messages = [sys('prompt'), ...Array.from({ length: 10 }, (_, i) => user(`m${i}`))]
53
+
54
+ const next = await reduce(reduction(messages))
55
+
56
+ expect(next).toBeDefined()
57
+ expect(next?.length).toBe(5) // the system floor plus keepRecentMessages
58
+ expect(next?.at(-1)).toBe(messages.at(-1))
59
+ })
60
+
61
+ it('cuts harder when the provider already rejected the prompt', async () => {
62
+ const reduce = createSlidingWindowReducer()
63
+ const messages = [sys('prompt'), ...Array.from({ length: 10 }, (_, i) => user(`m${i}`))]
64
+
65
+ const threshold = await reduce(reduction(messages))
66
+ const overflow = await reduce(reduction(messages, { reason: 'overflow' }))
67
+
68
+ // An overflow means the ordinary window was ALREADY too big. Cutting to
69
+ // the same size would shed nothing and the caller would retry an
70
+ // identical prompt.
71
+ expect(overflow?.length).toBeLessThan(threshold?.length ?? 0)
72
+ })
73
+
74
+ it('summarizes nothing — every survivor is the original object', async () => {
75
+ const reduce = createSlidingWindowReducer()
76
+ const messages = [sys('prompt'), ...Array.from({ length: 8 }, (_, i) => user(`m${i}`))]
77
+
78
+ const next = await reduce(reduction(messages))
79
+
80
+ for (const message of next ?? []) expect(messages).toContain(message)
81
+ })
82
+ })
83
+
84
+ describe('the three invariants a shorter history has to keep', () => {
85
+ it('never drops the leading system floor', async () => {
86
+ const reduce = createSlidingWindowReducer()
87
+ const messages = [
88
+ sys('prompt'),
89
+ sys('working memory'),
90
+ ...Array.from({ length: 8 }, (_, i) => user(`m${i}`)),
91
+ ]
92
+
93
+ const next = await reduce(reduction(messages))
94
+
95
+ expect(next?.[0]).toBe(messages[0])
96
+ expect(next?.[1]).toBe(messages[1])
97
+ })
98
+
99
+ it('does not split a tool_use from its tool_result', async () => {
100
+ const reduce = createSlidingWindowReducer()
101
+ // The naive cut at length-3 lands between the assistant call and its
102
+ // result, which is a provider 400 on the next turn.
103
+ const messages = [
104
+ sys('prompt'),
105
+ user('go'),
106
+ asst('thinking'),
107
+ callsTool('c1'),
108
+ toolResult('c1'),
109
+ user('again'),
110
+ asst('done'),
111
+ user('more'),
112
+ ]
113
+
114
+ const next = await reduce(reduction(messages, { keepRecentMessages: 5 }))
115
+
116
+ const kept = new Set(next ?? [])
117
+ // Cutting at 3 would keep the call and its result together; cutting at
118
+ // 4 would orphan the result. The pair is never split either way.
119
+ expect(kept.has(messages[4] as Message)).toBe(kept.has(messages[3] as Message))
120
+ })
121
+
122
+ it('takes a cut above the requested window when nothing below is safe', async () => {
123
+ const reduce = createSlidingWindowReducer()
124
+ // A multi-step turn: the user is silent while the agent works, so every
125
+ // boundary inside the recent window opens on an assistant or tool
126
+ // message. A backwards-only search would decline here — exactly when
127
+ // the history is longest.
128
+ const messages = [
129
+ sys('prompt'),
130
+ user('go'),
131
+ callsTool('c1'),
132
+ toolResult('c1'),
133
+ asst('working'),
134
+ callsTool('c2'),
135
+ toolResult('c2'),
136
+ // The only user turn after the opening one, and it sits ABOVE the
137
+ // requested cut — so every candidate below opens on an assistant or
138
+ // tool message and the backwards search comes back empty.
139
+ user('next'),
140
+ callsTool('c3'),
141
+ toolResult('c3'),
142
+ ]
143
+
144
+ const next = await reduce(reduction(messages, { keepRecentMessages: 5 }))
145
+
146
+ expect(next).toBeDefined()
147
+ expect(next?.length).toBeLessThan(messages.length)
148
+ // It kept LESS than asked rather than declining, and what it kept opens
149
+ // on a user turn.
150
+ expect(next?.[1]?.role).toBe('user')
151
+ })
152
+
153
+ it('keeps a pinned turn from the middle of the conversation', async () => {
154
+ const reduce = createSlidingWindowReducer()
155
+ const pinned: Message = { ...user('the account id is X'), retain: true }
156
+ const messages = [
157
+ sys('prompt'),
158
+ user('m0'),
159
+ pinned,
160
+ ...Array.from({ length: 8 }, (_, i) => user(`m${i + 1}`)),
161
+ ]
162
+
163
+ const next = await reduce(reduction(messages))
164
+
165
+ // Recency is not the only thing that matters, which is what `retain`
166
+ // exists to say.
167
+ expect(next).toContain(pinned)
168
+ expect(next).not.toContain(messages[1])
169
+ })
170
+ })
171
+
172
+ describe('refusing beats a reduction that would break the next turn', () => {
173
+ it('declines when there is nothing but the system floor to cut', async () => {
174
+ const reduce = createSlidingWindowReducer()
175
+
176
+ expect(await reduce(reduction([sys('prompt'), user('go')]))).toBeUndefined()
177
+ })
178
+
179
+ it('declines when every candidate cut splits one assistant fan-out', async () => {
180
+ const reduce = createSlidingWindowReducer()
181
+ // Six results answering one call: no cut inside the block is safe, and
182
+ // the condition clears itself on the next assistant message.
183
+ const messages = [
184
+ sys('prompt'),
185
+ user('go'),
186
+ {
187
+ role: 'assistant',
188
+ content: null,
189
+ timestamp: 1,
190
+ toolCalls: Array.from({ length: 6 }, (_, i) => ({
191
+ id: `c${i}`,
192
+ type: 'function' as const,
193
+ function: { name: 'echo', arguments: '{}' },
194
+ })),
195
+ } as AssistantMessage,
196
+ ...Array.from({ length: 6 }, (_, i) => toolResult(`c${i}`)),
197
+ ]
198
+
199
+ expect(await reduce(reduction(messages, { keepRecentMessages: 2 }))).toBeUndefined()
200
+ })
201
+
202
+ it('declines rather than reporting a reduction that removed nothing', async () => {
203
+ const reduce = createSlidingWindowReducer()
204
+ // Every non-system message is pinned, so the survivor set is the input.
205
+ const messages = [
206
+ sys('prompt'),
207
+ ...Array.from({ length: 8 }, (_, i) => ({ ...user(`m${i}`), retain: true })),
208
+ ]
209
+
210
+ // Saying "reduced" here would have the overflow path retry an identical
211
+ // prompt and burn a model call to be told the same thing.
212
+ expect(await reduce(reduction(messages))).toBeUndefined()
213
+ })
214
+ })
215
+
216
+ describe('the knob a caller reaches for', () => {
217
+ it('lets an explicit window override the run config', async () => {
218
+ const reduce = createSlidingWindowReducer({ keepRecentMessages: 2 })
219
+ const messages = [sys('prompt'), ...Array.from({ length: 10 }, (_, i) => user(`m${i}`))]
220
+
221
+ const next = await reduce(reduction(messages, { keepRecentMessages: 8 }))
222
+
223
+ expect(next?.length).toBe(3)
224
+ })
225
+
226
+ it('is a plain function, so a host can wrap one', async () => {
227
+ const inner = createSlidingWindowReducer()
228
+ const seen = vi.fn()
229
+ const reduce = async (input: ContextReduction) => {
230
+ seen(input.reason)
231
+ return inner(input)
232
+ }
233
+ const messages = [sys('prompt'), ...Array.from({ length: 8 }, (_, i) => user(`m${i}`))]
234
+
235
+ await reduce(reduction(messages, { reason: 'overflow' }))
236
+
237
+ expect(seen).toHaveBeenCalledWith('overflow')
238
+ })
239
+ })
@@ -20,6 +20,13 @@ import type { CompactionStrategy } from './types.js'
20
20
  * const trimmed = manager.applyManagement(messages)
21
21
  * ```
22
22
  */
23
+ /**
24
+ * @deprecated Nothing calls this. Select a strategy with
25
+ * `compactionConfig.strategy`, or pass a `contextReducer` to `query()`.
26
+ *
27
+ * The factory produced managers the runtime had no seam to drive, so its
28
+ * return value was always dropped on the floor. See {@link ConversationManager}.
29
+ */
23
30
  export function createConversationManager(
24
31
  strategy: CompactionStrategy,
25
32
  config: CompactionConfig,
@@ -11,6 +11,14 @@ export type { DanglingResult } from './dangling.js'
11
11
 
12
12
  export type { ConversationManager } from './interface.js'
13
13
 
14
+ export { createSlidingWindowReducer } from './reducer.js'
15
+ export type {
16
+ ContextReducer,
17
+ ContextReduction,
18
+ ContextReductionReason,
19
+ SlidingWindowOptions,
20
+ } from './reducer.js'
21
+
14
22
  export { WorkingStateManager } from './manager.js'
15
23
 
16
24
  export { serializeState } from './serializer.js'
@@ -7,6 +7,19 @@ import type { Message } from '../types/message/index.js'
7
7
  * A manager applies two strategies:
8
8
  * 1. **Routine management** (applyManagement): Called after each iteration to proactively optimize context.
9
9
  * 2. **Overflow reduction** (reduceContext): Called when the LLM reports context window exceeded.
10
+ *
11
+ * @deprecated Use {@link ContextReducer}, which the runtime actually drives.
12
+ *
13
+ * This interface cannot be implemented correctly. `reduceContext` is
14
+ * documented as reducing the history, but it takes `Message[]` and returns
15
+ * `boolean` — the only way to honour the contract is to mutate the argument
16
+ * in place, and neither shipped implementation does. Both build a shorter
17
+ * array locally, discard it, and return `true`. Nothing in the runtime ever
18
+ * called any of it, which is why an unfulfillable contract survived.
19
+ *
20
+ * `ContextReducer` returns the new history, may be async so a reducer can
21
+ * call a model, and is told whether it was asked speculatively or after the
22
+ * provider rejected the prompt. Kept exported until the next major.
10
23
  */
11
24
  export interface ConversationManager {
12
25
  /** Unique name for this manager (e.g., 'structured', 'sliding-window', 'disabled') */
@@ -5,6 +5,9 @@ import type { ConversationManager } from '../interface.js'
5
5
  * No-op conversation manager implementation.
6
6
  * Never modifies messages, useful for testing or when context management is disabled.
7
7
  */
8
+ /**
9
+ * @deprecated Use `strategy: 'disabled'`, which the runtime reads.
10
+ */
8
11
  export class NullManager implements ConversationManager {
9
12
  readonly name = 'null'
10
13
 
@@ -20,6 +20,12 @@ export interface SlidingWindowManagerConfig {
20
20
  * 2. Use findSafeTrimIndex to ensure tool call/result pairs remain atomic
21
21
  * 3. applyManagement runs proactively each iteration; reduceContext on overflow
22
22
  */
23
+ /**
24
+ * @deprecated Use `createSlidingWindowReducer()`, or `strategy: 'sliding-window'`.
25
+ *
26
+ * Never reachable through the runtime, and `reduceContext` here computes a
27
+ * trim index and returns a boolean without trimming anything.
28
+ */
23
29
  export class SlidingWindowManager implements ConversationManager {
24
30
  readonly name = 'sliding-window'
25
31
  private readonly keepRecentMessages: number
@@ -22,6 +22,16 @@ import { serializeState } from '../serializer.js'
22
22
  *
23
23
  * Provides maximum context preservation but more computational overhead.
24
24
  */
25
+ /**
26
+ * @deprecated Not the structured strategy. `strategy: 'structured'` runs the
27
+ * live path in `runtime/query/iteration/phases/compaction.ts`, which this
28
+ * class predates and does not share code with.
29
+ *
30
+ * What is here is a weaker parallel copy: no LLM-verified summary, no stale
31
+ * tool-result clearing, no retention pins, no events — and a `reduceContext`
32
+ * that builds a shorter history into a local array and then returns a
33
+ * boolean, discarding it.
34
+ */
25
35
  export class StructuredCompactionManager implements ConversationManager {
26
36
  readonly name = 'structured'
27
37
  private readonly config: CompactionConfig