@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,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'
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { JSON_RPC_METHOD_NOT_FOUND } from '../../../constants/mcp/index.js'
4
+ import type {
5
+ MCPJsonRpcMessage,
6
+ MCPPromptDefinition,
7
+ MCPPromptMessage,
8
+ MCPTransport,
9
+ } from '../../../types/connector/index.js'
10
+ import { MCPClient } from '../client.js'
11
+ import { MCPServer } from '../server.js'
12
+ import type { MCPServerPromptProvider, MCPServerToolProvider } from '../server.js'
13
+
14
+ /**
15
+ * The half of MCP that is not tools.
16
+ *
17
+ * `MCPPromptDefinition` and `MCPPromptArgument` were declared when the types
18
+ * were written; no client method ever asked for a prompt and no server branch
19
+ * ever served one. `MCPLifecycleEvent` and `MCPEventListener` were declared
20
+ * beside them and nothing ever emitted one, so a host could learn that a
21
+ * server had died only by noticing calls had started failing.
22
+ */
23
+
24
+ function fakeTransport(): {
25
+ transport: MCPTransport
26
+ sent: MCPJsonRpcMessage[]
27
+ deliver: (m: MCPJsonRpcMessage) => void
28
+ } {
29
+ const sent: MCPJsonRpcMessage[] = []
30
+ let onMessage: ((m: MCPJsonRpcMessage) => void) | undefined
31
+ const transport: MCPTransport = {
32
+ connect: async () => undefined,
33
+ close: async () => undefined,
34
+ send: async (m: MCPJsonRpcMessage) => {
35
+ sent.push(m)
36
+ },
37
+ onMessage: (h: (m: MCPJsonRpcMessage) => void) => {
38
+ onMessage = h
39
+ },
40
+ onClose: () => undefined,
41
+ onError: () => undefined,
42
+ } as unknown as MCPTransport
43
+ return { transport, sent, deliver: (m) => onMessage?.(m) }
44
+ }
45
+
46
+ const toolProvider: MCPServerToolProvider = {
47
+ listTools: () => [],
48
+ callTool: async () => ({ content: [{ type: 'text', text: 'ok' }] }),
49
+ }
50
+
51
+ function promptProvider(over: Partial<MCPServerPromptProvider> = {}): MCPServerPromptProvider {
52
+ const prompts: MCPPromptDefinition[] = [
53
+ {
54
+ name: 'summarize',
55
+ description: 'Summarize a file',
56
+ arguments: [
57
+ { name: 'path', required: true },
58
+ { name: 'style', required: false },
59
+ ],
60
+ },
61
+ ]
62
+ const messages: MCPPromptMessage[] = [
63
+ { role: 'user', content: { type: 'text', text: 'summarize it' } },
64
+ ]
65
+ return {
66
+ listPrompts: () => prompts,
67
+ getPrompt: async () => ({ description: 'Summarize a file', messages }),
68
+ ...over,
69
+ }
70
+ }
71
+
72
+ async function ask(
73
+ t: ReturnType<typeof fakeTransport>,
74
+ method: string,
75
+ params: Record<string, unknown> = {},
76
+ ) {
77
+ t.deliver({ jsonrpc: '2.0', id: 1, method, params } as MCPJsonRpcMessage)
78
+ await new Promise((r) => setTimeout(r, 0))
79
+ return t.sent.at(-1) as MCPJsonRpcMessage & {
80
+ result?: unknown
81
+ error?: { code: number; message: string }
82
+ }
83
+ }
84
+
85
+ describe('a server serves the prompts it publishes', () => {
86
+ it('lists them', async () => {
87
+ const t = fakeTransport()
88
+ const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
89
+ await server.start(t.transport)
90
+
91
+ const reply = await ask(t, 'prompts/list')
92
+
93
+ expect((reply.result as { prompts: MCPPromptDefinition[] }).prompts[0]?.name).toBe('summarize')
94
+ })
95
+
96
+ it('returns the messages the server composed', async () => {
97
+ const t = fakeTransport()
98
+ const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
99
+ await server.start(t.transport)
100
+
101
+ const reply = await ask(t, 'prompts/get', {
102
+ name: 'summarize',
103
+ arguments: { path: 'a.ts' },
104
+ })
105
+
106
+ expect((reply.result as { messages: MCPPromptMessage[] }).messages[0]?.content).toEqual({
107
+ type: 'text',
108
+ text: 'summarize it',
109
+ })
110
+ })
111
+
112
+ it('advertises the capability only when something is behind it', async () => {
113
+ const withPrompts = fakeTransport()
114
+ const without = fakeTransport()
115
+ const a = new MCPServer({ name: 'a' }, toolProvider, undefined, promptProvider())
116
+ const b = new MCPServer({ name: 'b' }, toolProvider)
117
+ await a.start(withPrompts.transport)
118
+ await b.start(without.transport)
119
+
120
+ const withCap = await ask(withPrompts, 'initialize')
121
+ const withoutCap = await ask(without, 'initialize')
122
+
123
+ // A capability advertised without a provider is a promise the next
124
+ // call breaks.
125
+ expect(
126
+ (withCap.result as { capabilities: Record<string, unknown> }).capabilities.prompts,
127
+ ).toBeDefined()
128
+ expect(
129
+ (withoutCap.result as { capabilities: Record<string, unknown> }).capabilities.prompts,
130
+ ).toBeUndefined()
131
+ })
132
+
133
+ it('refuses a required argument the caller omitted', async () => {
134
+ const t = fakeTransport()
135
+ const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
136
+ await server.start(t.transport)
137
+
138
+ const reply = await ask(t, 'prompts/get', { name: 'summarize', arguments: {} })
139
+
140
+ // Checked from the declaration, so every provider does not have to
141
+ // re-implement it or forget to.
142
+ expect(reply.error?.message).toContain('path')
143
+ })
144
+
145
+ it('accepts an optional argument being absent', async () => {
146
+ const t = fakeTransport()
147
+ const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
148
+ await server.start(t.transport)
149
+
150
+ const reply = await ask(t, 'prompts/get', {
151
+ name: 'summarize',
152
+ arguments: { path: 'a.ts' },
153
+ })
154
+
155
+ expect(reply.error).toBeUndefined()
156
+ })
157
+
158
+ it('refuses a prompt it does not publish', async () => {
159
+ const t = fakeTransport()
160
+ const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
161
+ await server.start(t.transport)
162
+
163
+ const reply = await ask(t, 'prompts/get', { name: 'nonexistent' })
164
+
165
+ expect(reply.error?.message).toContain('nonexistent')
166
+ })
167
+ })
168
+
169
+ describe('"none" and "not here" are different answers', () => {
170
+ it('says method-not-found for prompts when there is no provider', async () => {
171
+ const t = fakeTransport()
172
+ const server = new MCPServer({ name: 'srv' }, toolProvider)
173
+ await server.start(t.transport)
174
+
175
+ const reply = await ask(t, 'prompts/list')
176
+
177
+ expect(reply.error?.code).toBe(JSON_RPC_METHOD_NOT_FOUND)
178
+ })
179
+
180
+ it('says method-not-found for resources when there is no provider', async () => {
181
+ const t = fakeTransport()
182
+ const server = new MCPServer({ name: 'srv' }, toolProvider)
183
+ await server.start(t.transport)
184
+
185
+ const reply = await ask(t, 'resources/list')
186
+
187
+ // This used to answer `{ resources: [] }` for a capability
188
+ // `initialize` never advertised — telling a client "none" where the
189
+ // truth is "not here". The two send a client in opposite directions.
190
+ expect(reply.error?.code).toBe(JSON_RPC_METHOD_NOT_FOUND)
191
+ })
192
+
193
+ it('uses the protocol code for an unknown method, not a generic failure', async () => {
194
+ const t = fakeTransport()
195
+ const server = new MCPServer({ name: 'srv' }, toolProvider)
196
+ await server.start(t.transport)
197
+
198
+ const reply = await ask(t, 'completely/unknown')
199
+
200
+ expect(reply.error?.code).toBe(JSON_RPC_METHOD_NOT_FOUND)
201
+ })
202
+
203
+ it('still reports a provider failure as an internal error', async () => {
204
+ const t = fakeTransport()
205
+ const server = new MCPServer(
206
+ { name: 'srv' },
207
+ toolProvider,
208
+ undefined,
209
+ promptProvider({
210
+ listPrompts: () => {
211
+ throw new Error('provider exploded')
212
+ },
213
+ }),
214
+ )
215
+ await server.start(t.transport)
216
+
217
+ const reply = await ask(t, 'prompts/list')
218
+
219
+ // A broken provider is not the same as an unimplemented method, and
220
+ // collapsing the two would tell a client to stop asking for something
221
+ // that works tomorrow.
222
+ expect(reply.error?.code).toBe(-32603)
223
+ })
224
+ })
225
+
226
+ describe('a client says out loud what it already knew', () => {
227
+ function failingClient() {
228
+ // `connect` failing is the simplest path that actually emits. A
229
+ // never-connected client's `disconnect` returns early and emits
230
+ // nothing — an easy way to write a test that proves nothing, which is
231
+ // what the first version of these did.
232
+ return new MCPClient({
233
+ serverName: 'srv',
234
+ transport: { type: 'stdio', command: 'definitely-not-a-real-binary-xyz', args: [] },
235
+ })
236
+ }
237
+
238
+ /**
239
+ * Covered here: the transport-error emitter. `connect`'s own catch emits
240
+ * the same event and is NOT pinned — a spawn failure surfaces through the
241
+ * transport first, so the catch is the second line of defence rather than
242
+ * the path this reaches. Reaching it needs a connection that opens and
243
+ * then fails negotiation, which this fake cannot stage; said rather than
244
+ * left for a mutation run to discover.
245
+ */
246
+ it('reports a failure to whoever is listening', async () => {
247
+ const client = failingClient()
248
+ const seen: string[] = []
249
+ client.onLifecycle((e) => seen.push(e.type))
250
+
251
+ await client.connect().catch(() => undefined)
252
+
253
+ expect(seen).toContain('mcp_client_error')
254
+ })
255
+
256
+ it('stops reporting once the listener unsubscribes', async () => {
257
+ const client = failingClient()
258
+ const seen = vi.fn()
259
+
260
+ const off = client.onLifecycle(seen)
261
+ off()
262
+ await client.connect().catch(() => undefined)
263
+
264
+ // `onNotification` returns nothing, so a listener registered there
265
+ // cannot be removed and keeps a disposed host object alive for as
266
+ // long as the client. This is that bug not repeated.
267
+ expect(seen).not.toHaveBeenCalled()
268
+ })
269
+
270
+ it('leaves other listeners working when one throws', async () => {
271
+ const client = failingClient()
272
+ const survivor = vi.fn()
273
+ client.onLifecycle(() => {
274
+ throw new Error('observer bug')
275
+ })
276
+ client.onLifecycle(survivor)
277
+
278
+ await client.connect().catch(() => undefined)
279
+
280
+ // These fire from inside transport callbacks and from `connect`'s
281
+ // failure path, so an escaping exception would surface as a connection
282
+ // error — blaming the server for a bug in the host's own observer, and
283
+ // silencing every listener registered after the broken one.
284
+ expect(survivor).toHaveBeenCalled()
285
+ })
286
+ })