@namzu/sdk 3.0.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 (307) hide show
  1. package/CHANGELOG.md +223 -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 +5 -3
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +11 -3
  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__/capability-negotiation.test.js +59 -0
  121. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  122. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  125. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  128. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  129. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  132. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  133. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  134. package/dist/runtime/query/executor.d.ts +1 -0
  135. package/dist/runtime/query/executor.d.ts.map +1 -1
  136. package/dist/runtime/query/executor.js +10 -1
  137. package/dist/runtime/query/executor.js.map +1 -1
  138. package/dist/runtime/query/index.d.ts +10 -0
  139. package/dist/runtime/query/index.d.ts.map +1 -1
  140. package/dist/runtime/query/index.js +26 -0
  141. package/dist/runtime/query/index.js.map +1 -1
  142. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  143. package/dist/runtime/query/iteration/index.js +118 -34
  144. package/dist/runtime/query/iteration/index.js.map +1 -1
  145. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  146. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  147. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  148. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  149. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  150. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  151. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  153. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  154. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  155. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  156. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  157. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  158. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  159. package/dist/runtime/query/resume-run.d.ts +70 -0
  160. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  161. package/dist/runtime/query/resume-run.js +46 -0
  162. package/dist/runtime/query/resume-run.js.map +1 -0
  163. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  164. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  165. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  166. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  167. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  168. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  169. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  170. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  171. package/dist/tools/advisory/index.js +1 -1
  172. package/dist/tools/advisory/index.js.map +1 -1
  173. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
  174. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
  175. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
  176. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
  177. package/dist/tools/builtins/__tests__/edit.test.js +242 -1
  178. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  179. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
  180. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
  181. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
  182. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
  183. package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
  184. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
  185. package/dist/tools/builtins/atomic-write-file.js +31 -2
  186. package/dist/tools/builtins/atomic-write-file.js.map +1 -1
  187. package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
  188. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
  189. package/dist/tools/builtins/content-fingerprint.js +30 -0
  190. package/dist/tools/builtins/content-fingerprint.js.map +1 -0
  191. package/dist/tools/builtins/edit.d.ts.map +1 -1
  192. package/dist/tools/builtins/edit.js +172 -31
  193. package/dist/tools/builtins/edit.js.map +1 -1
  194. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  195. package/dist/tools/builtins/read-file.js +5 -2
  196. package/dist/tools/builtins/read-file.js.map +1 -1
  197. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  198. package/dist/tools/builtins/write-file.js +75 -27
  199. package/dist/tools/builtins/write-file.js.map +1 -1
  200. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
  201. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
  202. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  203. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  204. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  205. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  206. package/dist/tools/coordinator/index.d.ts.map +1 -1
  207. package/dist/tools/coordinator/index.js +75 -5
  208. package/dist/tools/coordinator/index.js.map +1 -1
  209. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  210. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  211. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  212. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  213. package/dist/types/agent/supervisor.d.ts +15 -0
  214. package/dist/types/agent/supervisor.d.ts.map +1 -1
  215. package/dist/types/connector/core.d.ts +34 -0
  216. package/dist/types/connector/core.d.ts.map +1 -1
  217. package/dist/types/connector/definition.d.ts +10 -0
  218. package/dist/types/connector/definition.d.ts.map +1 -1
  219. package/dist/types/connector/mcp.d.ts +13 -0
  220. package/dist/types/connector/mcp.d.ts.map +1 -1
  221. package/dist/types/rag/retrieval.d.ts +16 -0
  222. package/dist/types/rag/retrieval.d.ts.map +1 -1
  223. package/dist/types/rag/storage.d.ts +9 -0
  224. package/dist/types/rag/storage.d.ts.map +1 -1
  225. package/dist/types/rag/vector.d.ts +11 -0
  226. package/dist/types/rag/vector.d.ts.map +1 -1
  227. package/dist/types/router/task-router.d.ts +19 -0
  228. package/dist/types/router/task-router.d.ts.map +1 -1
  229. package/dist/types/run/prepare-step.d.ts +56 -3
  230. package/dist/types/run/prepare-step.d.ts.map +1 -1
  231. package/dist/types/tool/index.d.ts +17 -1
  232. package/dist/types/tool/index.d.ts.map +1 -1
  233. package/dist/types/tool/index.js.map +1 -1
  234. package/dist/types/toolset/index.d.ts +22 -0
  235. package/dist/types/toolset/index.d.ts.map +1 -1
  236. package/package.json +1 -1
  237. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  238. package/src/advisory/context.ts +32 -0
  239. package/src/advisory/executor.ts +30 -3
  240. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  241. package/src/compaction/factory.ts +7 -0
  242. package/src/compaction/index.ts +8 -0
  243. package/src/compaction/interface.ts +13 -0
  244. package/src/compaction/managers/null.ts +3 -0
  245. package/src/compaction/managers/slidingWindow.ts +6 -0
  246. package/src/compaction/managers/structured.ts +10 -0
  247. package/src/compaction/reducer.ts +154 -0
  248. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  249. package/src/connector/builtins/http.test.ts +28 -2
  250. package/src/connector/builtins/http.ts +26 -2
  251. package/src/connector/index.ts +6 -2
  252. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  253. package/src/connector/mcp/client.ts +95 -0
  254. package/src/connector/mcp/discovery.ts +19 -4
  255. package/src/connector/mcp/index.ts +6 -2
  256. package/src/connector/mcp/server.ts +101 -3
  257. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  258. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  259. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  260. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  261. package/src/plugin/lifecycle.ts +57 -1
  262. package/src/plugin/loader.ts +57 -3
  263. package/src/public-runtime.ts +16 -1
  264. package/src/public-types.ts +5 -0
  265. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  266. package/src/rag/ingestion.ts +1 -0
  267. package/src/rag/retriever.ts +2 -0
  268. package/src/rag/vector-store.ts +5 -0
  269. package/src/registry/tool/execute.ts +123 -119
  270. package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
  271. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  272. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  273. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  274. package/src/runtime/query/executor.ts +9 -1
  275. package/src/runtime/query/index.ts +39 -0
  276. package/src/runtime/query/iteration/index.ts +130 -34
  277. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  278. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  279. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  280. package/src/runtime/query/iteration/phases/context.ts +11 -0
  281. package/src/runtime/query/resume-run.ts +93 -0
  282. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  283. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  284. package/src/tools/advisory/index.ts +1 -1
  285. package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
  286. package/src/tools/builtins/__tests__/edit.test.ts +324 -1
  287. package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
  288. package/src/tools/builtins/atomic-write-file.ts +30 -2
  289. package/src/tools/builtins/content-fingerprint.ts +33 -0
  290. package/src/tools/builtins/edit.ts +188 -36
  291. package/src/tools/builtins/read-file.ts +5 -2
  292. package/src/tools/builtins/write-file.ts +79 -28
  293. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
  294. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  295. package/src/tools/coordinator/index.ts +116 -42
  296. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  297. package/src/types/agent/supervisor.ts +15 -0
  298. package/src/types/connector/core.ts +34 -0
  299. package/src/types/connector/definition.ts +10 -0
  300. package/src/types/connector/mcp.ts +14 -0
  301. package/src/types/rag/retrieval.ts +16 -0
  302. package/src/types/rag/storage.ts +9 -0
  303. package/src/types/rag/vector.ts +11 -0
  304. package/src/types/router/task-router.ts +19 -0
  305. package/src/types/run/prepare-step.ts +58 -3
  306. package/src/types/tool/index.ts +17 -1
  307. package/src/types/toolset/index.ts +22 -0
@@ -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
@@ -0,0 +1,154 @@
1
+ import type { Message } from '../types/message/index.js'
2
+ import { findSafeTrimIndex } from './dangling.js'
3
+ import { findRetainedIndices } from './retention.js'
4
+
5
+ /**
6
+ * Why the runtime is asking for a shorter history.
7
+ *
8
+ * The two cases want different behaviour and a reducer that cannot tell
9
+ * them apart has to guess. `'threshold'` is speculative — the estimate says
10
+ * the window is filling, nothing has failed, and declining costs only some
11
+ * headroom. `'overflow'` is the provider having already rejected the
12
+ * prompt: declining there ends the run.
13
+ */
14
+ export type ContextReductionReason = 'threshold' | 'overflow'
15
+
16
+ /** Everything a reducer needs to decide, and nothing it cannot act on. */
17
+ export interface ContextReduction {
18
+ /** The live history, oldest first. Treat as read-only and return a new array. */
19
+ readonly messages: readonly Message[]
20
+ readonly reason: ContextReductionReason
21
+ /** Estimated size of `messages`. Derived from the provider's last prompt count when there is one, else measured. */
22
+ readonly estimatedTokens: number
23
+ /** What it has to fit in. Resolved from the model when the host did not say. */
24
+ readonly contextWindowTokens: number
25
+ /** The model this run is calling, for a reducer that varies by model. */
26
+ readonly model: string
27
+ /** The run's configured recent-window size, as a starting point. */
28
+ readonly keepRecentMessages: number
29
+ }
30
+
31
+ /**
32
+ * Replace the run's history with a shorter one.
33
+ *
34
+ * Returning `undefined` means "I could not shorten this" and is a first-class
35
+ * answer, not a failure — on `'threshold'` the run simply continues, and on
36
+ * `'overflow'` it fails with an irreducible-prompt error instead of retrying
37
+ * a prompt nothing changed. That is deliberate: a reducer that returned the
38
+ * input unchanged while reporting success would send the same rejected
39
+ * prompt again and burn a call to learn the same thing.
40
+ *
41
+ * A reducer OWNS reduction for the run it governs. The built-in structured
42
+ * pass — LLM-verified summarization, stale tool-result clearing, working
43
+ * state slots — does not also run, because two mechanisms editing the same
44
+ * history in one pass cannot both be reasoned about.
45
+ *
46
+ * Invariants a reducer is expected to keep, all three enforced by the
47
+ * built-in one and none of them checkable from here:
48
+ *
49
+ * 1. The leading system messages stay. They carry the system prompt and the
50
+ * working-memory slot; dropping them changes who the agent is.
51
+ * 2. `tool_use` and its `tool_result` stay together. A split pair is a 400
52
+ * from the provider, so a reducer that splits one turns a context problem
53
+ * into a dead run. {@link findSafeTrimIndex} is exported for this.
54
+ * 3. Messages marked `retain` survive. That marker is how a caller says a
55
+ * fact in the middle of the conversation outranks recency.
56
+ */
57
+ export type ContextReducer = (
58
+ reduction: ContextReduction,
59
+ ) => readonly Message[] | undefined | Promise<readonly Message[] | undefined>
60
+
61
+ export interface SlidingWindowOptions {
62
+ /**
63
+ * How many trailing messages to keep. Defaults to the run's configured
64
+ * `keepRecentMessages`, so the same knob governs both strategies.
65
+ */
66
+ readonly keepRecentMessages?: number
67
+
68
+ /**
69
+ * Fraction of the window to keep when relieving an overflow, applied to
70
+ * the recent count. Defaults to 0.5.
71
+ *
72
+ * An overflow means the ordinary window was already too big, so cutting
73
+ * to the same size again would shed nothing and the caller would retry
74
+ * an identical prompt.
75
+ */
76
+ readonly overflowFactor?: number
77
+ }
78
+
79
+ /**
80
+ * Keep the last N turns, drop what precedes them, summarize nothing.
81
+ *
82
+ * The honest counterpart to the structured strategy rather than a lesser
83
+ * version of it. Structured compaction pays a summarization call and
84
+ * paraphrases the agent's own reasoning to buy context; this pays nothing
85
+ * and admits the older history is gone. For a long-running agent whose
86
+ * state lives outside the transcript — a task queue, a file it keeps
87
+ * editing, a working-memory block the host renders each turn — that is the
88
+ * better trade, and the paraphrase was only ever cost.
89
+ *
90
+ * Until now `strategy: 'sliding-window'` was accepted by the config schema
91
+ * and then ignored: the runtime asked only whether the strategy was
92
+ * `'disabled'`, so choosing the cheap non-LLM path silently ran the
93
+ * expensive LLM one. This is the implementation that name always claimed.
94
+ */
95
+ export function createSlidingWindowReducer(options: SlidingWindowOptions = {}): ContextReducer {
96
+ const factor = options.overflowFactor ?? 0.5
97
+
98
+ return ({ messages, reason, keepRecentMessages }) => {
99
+ const configured = options.keepRecentMessages ?? keepRecentMessages
100
+ const keep =
101
+ reason === 'overflow' ? Math.max(1, Math.floor(configured * factor)) : Math.max(1, configured)
102
+
103
+ let leadingSystem = 0
104
+ while (messages[leadingSystem]?.role === 'system') leadingSystem++
105
+
106
+ const naive = messages.length - keep
107
+ if (naive <= leadingSystem) return undefined
108
+
109
+ // `findSafeTrimIndex` moves a cut forward until the kept tail is a
110
+ // history a provider will accept — no `tool_result` without its
111
+ // `tool_use`, and never opening on an assistant turn, because the tail
112
+ // becomes the start of the wire conversation once the older half is
113
+ // gone. So a candidate is safe exactly when it is already its own
114
+ // answer.
115
+ //
116
+ // Walk BACKWARDS first: that lands on the safe cut nearest the
117
+ // requested window, keeping a little more than asked rather than a lot
118
+ // less.
119
+ const trimmable = [...messages]
120
+ let cut = -1
121
+ for (let candidate = naive; candidate > leadingSystem; candidate--) {
122
+ if (findSafeTrimIndex(trimmable, candidate) === candidate) {
123
+ cut = candidate
124
+ break
125
+ }
126
+ }
127
+
128
+ // Nothing safe below the request. Take the answer ABOVE it instead of
129
+ // refusing: in a multi-step turn — the agent working through tool calls
130
+ // with the user silent — every boundary in the recent window lands on
131
+ // an assistant or tool message, so a backwards-only search declines
132
+ // exactly when the history is longest and shedding matters most. The
133
+ // forward cut keeps less than asked, which is a smaller history than
134
+ // the caller wanted but still a correct one.
135
+ if (cut < 0) {
136
+ const forward = findSafeTrimIndex(trimmable, naive)
137
+ // `messages.length` means "keep nothing", which is not a history.
138
+ if (forward > leadingSystem && forward < messages.length) cut = forward
139
+ }
140
+ if (cut < 0) return undefined
141
+
142
+ const retained = findRetainedIndices(messages)
143
+ const survivors: Message[] = []
144
+ for (let i = 0; i < messages.length; i++) {
145
+ const message = messages[i]
146
+ if (!message) continue
147
+ if (i < leadingSystem || i >= cut || retained.has(i)) survivors.push(message)
148
+ }
149
+
150
+ // Nothing went. Reporting that as a reduction would have the overflow
151
+ // path retry an identical prompt.
152
+ return survivors.length === messages.length ? undefined : survivors
153
+ }
154
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { AuthConfig } from '../../../types/connector/index.js'
4
+ import { HttpConnector } from '../http.js'
5
+
6
+ /**
7
+ * `'oauth2'` was grouped with `'none'` and `'custom'`, returning no headers.
8
+ *
9
+ * So a connector configured for OAuth2 sent an UNAUTHENTICATED request.
10
+ * Every other auth type throws on a missing credential; this one quietly
11
+ * did not, and the upstream's 401 reads as a bad token rather than as no
12
+ * token at all — which sends whoever is debugging to look at the token.
13
+ */
14
+
15
+ function headersFor(auth: AuthConfig): Record<string, string> {
16
+ const connector = new HttpConnector()
17
+ // The resolver is private by design; reaching it directly is what keeps
18
+ // this test about the header decision rather than about fetch.
19
+ return (
20
+ connector as unknown as { resolveAuthHeaders(a: AuthConfig): Record<string, string> }
21
+ ).resolveAuthHeaders(auth)
22
+ }
23
+
24
+ describe('an oauth2 connector does not send an unauthenticated request', () => {
25
+ it('sends the access token it was given', () => {
26
+ const headers = headersFor({
27
+ type: 'oauth2',
28
+ credentials: { accessToken: 'tok_abc' },
29
+ } as AuthConfig)
30
+
31
+ expect(headers.Authorization).toBe('Bearer tok_abc')
32
+ })
33
+
34
+ it('accepts the token under either name', () => {
35
+ const headers = headersFor({ type: 'oauth2', credentials: { token: 'tok_xyz' } } as AuthConfig)
36
+
37
+ expect(headers.Authorization).toBe('Bearer tok_xyz')
38
+ })
39
+
40
+ it('refuses when there is no token, rather than sending nothing', () => {
41
+ // The whole defect: an empty header set went out and the request
42
+ // reached the upstream with no credential at all.
43
+ expect(() => headersFor({ type: 'oauth2', credentials: {} } as AuthConfig)).toThrow(
44
+ /accessToken/,
45
+ )
46
+ })
47
+
48
+ it('refuses when credentials are absent entirely', () => {
49
+ expect(() => headersFor({ type: 'oauth2' } as AuthConfig)).toThrow(/unauthenticated/)
50
+ })
51
+ })
52
+
53
+ describe('the other schemes are unchanged', () => {
54
+ it("still sends nothing for 'none'", () => {
55
+ expect(headersFor({ type: 'none' } as AuthConfig)).toEqual({})
56
+ })
57
+
58
+ it("still sends nothing for 'custom', which is the host's job", () => {
59
+ // Deliberately empty, unlike oauth2: `custom` means the host attaches
60
+ // its own headers, so there is nothing to omit and nothing to refuse.
61
+ expect(headersFor({ type: 'custom' } as AuthConfig)).toEqual({})
62
+ })
63
+
64
+ it('still sends a bearer token', () => {
65
+ expect(
66
+ headersFor({ type: 'bearer', credentials: { token: 't' } } as AuthConfig).Authorization,
67
+ ).toBe('Bearer t')
68
+ })
69
+
70
+ it('still refuses a bearer with no token', () => {
71
+ expect(() => headersFor({ type: 'bearer', credentials: {} } as AuthConfig)).toThrow(/token/)
72
+ })
73
+ })
@@ -140,8 +140,12 @@ describe('HttpConnector', () => {
140
140
  expect(headers.Authorization).toBe(`Basic ${btoa('alice:pw')}`)
141
141
  })
142
142
 
143
- it('none / oauth2 / custom add no auth headers', async () => {
144
- for (const type of ['none', 'oauth2', 'custom'] as const) {
143
+ it('none / custom add no auth headers', async () => {
144
+ // `oauth2` was in this list, and that is what the defect was: it
145
+ // meant a connector configured for OAuth2 reached the upstream with
146
+ // no credential at all. `none` and `custom` belong here — the first
147
+ // asks for no auth, the second says the host attaches its own.
148
+ for (const type of ['none', 'custom'] as const) {
145
149
  const c = new HttpConnector()
146
150
  await c.connect({ baseUrl: 'https://api.example.com' }, { type, credentials: {} })
147
151
  fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
@@ -152,6 +156,28 @@ describe('HttpConnector', () => {
152
156
  }
153
157
  })
154
158
 
159
+ it('oauth2 refuses rather than reaching the upstream unauthenticated', async () => {
160
+ const c = new HttpConnector()
161
+
162
+ await expect(
163
+ c.connect({ baseUrl: 'https://api.example.com' }, { type: 'oauth2', credentials: {} }),
164
+ ).rejects.toThrow(/accessToken/)
165
+ })
166
+
167
+ it('oauth2 sends the access token it was given', async () => {
168
+ const c = new HttpConnector()
169
+ await c.connect(
170
+ { baseUrl: 'https://api.example.com' },
171
+ { type: 'oauth2', credentials: { accessToken: 'tok_1' } },
172
+ )
173
+ fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
174
+
175
+ await c.execute('request', { method: 'GET', path: 'x' })
176
+
177
+ const headers = fetchMock.mock.calls.at(-1)?.[1].headers as Record<string, string>
178
+ expect(headers.Authorization).toBe('Bearer tok_1')
179
+ })
180
+
155
181
  it('api_key throws when apiKey is missing', async () => {
156
182
  const c = new HttpConnector()
157
183
  await expect(
@@ -188,9 +188,33 @@ export class HttpConnector extends BaseConnector<HttpConnectorConfig> {
188
188
  const encoded = btoa(`${username}:${password}`)
189
189
  return { Authorization: `Basic ${encoded}` }
190
190
  }
191
- case 'none':
192
- case 'oauth2':
191
+ case 'oauth2': {
192
+ // Grouped with `none` until now, so a connector configured for
193
+ // OAuth2 sent an UNAUTHENTICATED request — every other auth type
194
+ // throws on a missing credential, and this one quietly did not.
195
+ // The upstream answers 401 and the failure reads as a bad token
196
+ // rather than as no token at all.
197
+ //
198
+ // The token exchange itself is not implemented here: a client
199
+ // credentials or authorization-code flow needs a token endpoint,
200
+ // refresh handling and a place to keep the result, none of which
201
+ // belong in a request-header helper. What is supported is an
202
+ // access token the host already holds, which is the case a
203
+ // connector config can actually express today.
204
+ const token = creds.accessToken ?? creds.token
205
+ if (!token) {
206
+ throw new Error(
207
+ 'AuthConfig oauth2: missing required credential "accessToken". This connector holds no token endpoint, so an access token obtained elsewhere has to be supplied — sending the request without one would reach the upstream unauthenticated.',
208
+ )
209
+ }
210
+ return { Authorization: `Bearer ${token}` }
211
+ }
193
212
  case 'custom':
213
+ // Deliberately empty, unlike `oauth2` above: `custom` means the
214
+ // host attaches its own headers, so there is nothing here to
215
+ // omit and nothing to refuse.
216
+ return {}
217
+ case 'none':
194
218
  return {}
195
219
  default: {
196
220
  const _exhaustive: never = auth.type
@@ -51,5 +51,9 @@ export { MCPToolDiscovery } from './mcp/discovery.js'
51
51
 
52
52
  export { MCPConnectorBridge } from '../bridge/mcp/connector/adapter.js'
53
53
 
54
- export { MCPServer } from './mcp/server.js'
55
- export type { MCPServerToolProvider, MCPServerResourceProvider } from './mcp/server.js'
54
+ export { MCPServer, MCPMethodNotFound } from './mcp/server.js'
55
+ export type {
56
+ MCPServerToolProvider,
57
+ MCPServerResourceProvider,
58
+ MCPServerPromptProvider,
59
+ } from './mcp/server.js'