@namzu/sdk 1.0.0 → 1.1.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 (215) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +5 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts +22 -1
  6. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  7. package/dist/agents/SupervisorAgent.js +47 -14
  8. package/dist/agents/SupervisorAgent.js.map +1 -1
  9. package/dist/agents/__tests__/supervisor-ledger.test.d.ts +2 -0
  10. package/dist/agents/__tests__/supervisor-ledger.test.d.ts.map +1 -0
  11. package/dist/agents/__tests__/supervisor-ledger.test.js +131 -0
  12. package/dist/agents/__tests__/supervisor-ledger.test.js.map +1 -0
  13. package/dist/config/runtime.d.ts +24 -0
  14. package/dist/config/runtime.d.ts.map +1 -1
  15. package/dist/config/runtime.js +9 -0
  16. package/dist/config/runtime.js.map +1 -1
  17. package/dist/connector/builtins/webhook.test.js +17 -0
  18. package/dist/connector/builtins/webhook.test.js.map +1 -1
  19. package/dist/connector/index.d.ts +1 -0
  20. package/dist/connector/index.d.ts.map +1 -1
  21. package/dist/connector/index.js +1 -0
  22. package/dist/connector/index.js.map +1 -1
  23. package/dist/connector/mcp/client.d.ts.map +1 -1
  24. package/dist/connector/mcp/client.js +4 -0
  25. package/dist/connector/mcp/client.js.map +1 -1
  26. package/dist/connector/mcp/index.d.ts +1 -0
  27. package/dist/connector/mcp/index.d.ts.map +1 -1
  28. package/dist/connector/mcp/index.js +1 -0
  29. package/dist/connector/mcp/index.js.map +1 -1
  30. package/dist/connector/mcp/streamable-http.d.ts +22 -0
  31. package/dist/connector/mcp/streamable-http.d.ts.map +1 -0
  32. package/dist/connector/mcp/streamable-http.js +128 -0
  33. package/dist/connector/mcp/streamable-http.js.map +1 -0
  34. package/dist/connector/mcp/streamable-http.test.d.ts +2 -0
  35. package/dist/connector/mcp/streamable-http.test.d.ts.map +1 -0
  36. package/dist/connector/mcp/streamable-http.test.js +132 -0
  37. package/dist/connector/mcp/streamable-http.test.js.map +1 -0
  38. package/dist/persona/assembler.d.ts.map +1 -1
  39. package/dist/persona/assembler.js +18 -9
  40. package/dist/persona/assembler.js.map +1 -1
  41. package/dist/provider/collect.d.ts.map +1 -1
  42. package/dist/provider/collect.js +4 -1
  43. package/dist/provider/collect.js.map +1 -1
  44. package/dist/public-runtime.d.ts +1 -1
  45. package/dist/public-runtime.d.ts.map +1 -1
  46. package/dist/public-runtime.js +1 -1
  47. package/dist/public-runtime.js.map +1 -1
  48. package/dist/registry/tool/execute.d.ts +27 -0
  49. package/dist/registry/tool/execute.d.ts.map +1 -1
  50. package/dist/registry/tool/execute.js +124 -21
  51. package/dist/registry/tool/execute.js.map +1 -1
  52. package/dist/registry/tool/execute.test.d.ts +9 -6
  53. package/dist/registry/tool/execute.test.d.ts.map +1 -1
  54. package/dist/registry/tool/execute.test.js +72 -10
  55. package/dist/registry/tool/execute.test.js.map +1 -1
  56. package/dist/registry/toolset/catalog.test.js +62 -0
  57. package/dist/registry/toolset/catalog.test.js.map +1 -1
  58. package/dist/runtime/query/__tests__/deferred-tools.test.js +43 -5
  59. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
  60. package/dist/runtime/query/__tests__/long-document-flow.test.d.ts +2 -0
  61. package/dist/runtime/query/__tests__/long-document-flow.test.d.ts.map +1 -0
  62. package/dist/runtime/query/__tests__/long-document-flow.test.js +154 -0
  63. package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -0
  64. package/dist/runtime/query/__tests__/prompt.test.js +15 -11
  65. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
  66. package/dist/runtime/query/index.d.ts +9 -0
  67. package/dist/runtime/query/index.d.ts.map +1 -1
  68. package/dist/runtime/query/index.js +23 -2
  69. package/dist/runtime/query/index.js.map +1 -1
  70. package/dist/runtime/query/iteration/index.d.ts +2 -0
  71. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  72. package/dist/runtime/query/iteration/index.js +99 -6
  73. package/dist/runtime/query/iteration/index.js.map +1 -1
  74. package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
  75. package/dist/runtime/query/iteration/phases/checkpoint.js +2 -2
  76. package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
  77. package/dist/runtime/query/iteration/phases/compaction.d.ts +8 -0
  78. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/phases/compaction.js +84 -3
  80. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  81. package/dist/runtime/query/iteration/phases/context.d.ts +12 -0
  82. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  83. package/dist/runtime/query/iteration/phases/context.js +53 -0
  84. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  85. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts +2 -0
  86. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts.map +1 -0
  87. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js +73 -0
  88. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js.map +1 -0
  89. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts +2 -0
  90. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts.map +1 -0
  91. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js +120 -0
  92. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js.map +1 -0
  93. package/dist/runtime/query/iteration/phases/tool-review.d.ts +1 -1
  94. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/tool-review.js +7 -2
  96. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts +19 -0
  98. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts.map +1 -0
  99. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js +177 -0
  100. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js.map +1 -0
  101. package/dist/runtime/query/iteration/phases/working-memory.d.ts +31 -0
  102. package/dist/runtime/query/iteration/phases/working-memory.d.ts.map +1 -0
  103. package/dist/runtime/query/iteration/phases/working-memory.js +81 -0
  104. package/dist/runtime/query/iteration/phases/working-memory.js.map +1 -0
  105. package/dist/session/workspace/__tests__/shared-run.test.js +1 -1
  106. package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -1
  107. package/dist/session/workspace/shared-run.js +1 -0
  108. package/dist/session/workspace/shared-run.js.map +1 -1
  109. package/dist/skills/loader.test.js +8 -8
  110. package/dist/skills/loader.test.js.map +1 -1
  111. package/dist/streaming/coalesce.test.js.map +1 -1
  112. package/dist/tools/builtins/__tests__/payload-budget.test.js +48 -0
  113. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
  114. package/dist/tools/builtins/__tests__/read-file.test.js +9 -0
  115. package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -1
  116. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  117. package/dist/tools/builtins/read-file.js +50 -2
  118. package/dist/tools/builtins/read-file.js.map +1 -1
  119. package/dist/tools/builtins/search-tools.d.ts.map +1 -1
  120. package/dist/tools/builtins/search-tools.js +32 -7
  121. package/dist/tools/builtins/search-tools.js.map +1 -1
  122. package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts +2 -0
  123. package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts.map +1 -0
  124. package/dist/tools/coordinator/__tests__/approve-plan.test.js +114 -0
  125. package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -0
  126. package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts +2 -0
  127. package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts.map +1 -0
  128. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +309 -0
  129. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -0
  130. package/dist/tools/coordinator/index.d.ts +8 -0
  131. package/dist/tools/coordinator/index.d.ts.map +1 -1
  132. package/dist/tools/coordinator/index.js +195 -15
  133. package/dist/tools/coordinator/index.js.map +1 -1
  134. package/dist/types/agent/index.d.ts +1 -0
  135. package/dist/types/agent/index.d.ts.map +1 -1
  136. package/dist/types/agent/index.js +1 -0
  137. package/dist/types/agent/index.js.map +1 -1
  138. package/dist/types/agent/reactive.d.ts +15 -0
  139. package/dist/types/agent/reactive.d.ts.map +1 -1
  140. package/dist/types/agent/supervisor.d.ts +18 -0
  141. package/dist/types/agent/supervisor.d.ts.map +1 -1
  142. package/dist/types/agent/working-memory.d.ts +29 -0
  143. package/dist/types/agent/working-memory.d.ts.map +1 -0
  144. package/dist/types/agent/working-memory.js +2 -0
  145. package/dist/types/agent/working-memory.js.map +1 -0
  146. package/dist/types/common/index.d.ts +10 -0
  147. package/dist/types/common/index.d.ts.map +1 -1
  148. package/dist/types/common/index.js +18 -0
  149. package/dist/types/common/index.js.map +1 -1
  150. package/dist/types/common/usage-merge.test.d.ts +2 -0
  151. package/dist/types/common/usage-merge.test.d.ts.map +1 -0
  152. package/dist/types/common/usage-merge.test.js +48 -0
  153. package/dist/types/common/usage-merge.test.js.map +1 -0
  154. package/dist/types/connector/mcp.d.ts +9 -2
  155. package/dist/types/connector/mcp.d.ts.map +1 -1
  156. package/dist/types/hitl/index.d.ts +42 -0
  157. package/dist/types/hitl/index.d.ts.map +1 -1
  158. package/dist/types/hitl/index.js +11 -0
  159. package/dist/types/hitl/index.js.map +1 -1
  160. package/dist/types/provider/chat.d.ts +9 -0
  161. package/dist/types/provider/chat.d.ts.map +1 -1
  162. package/dist/version.d.ts.map +1 -1
  163. package/dist/version.js +25 -4
  164. package/dist/version.js.map +1 -1
  165. package/package.json +1 -1
  166. package/src/agents/ReactiveAgent.ts +5 -0
  167. package/src/agents/SupervisorAgent.ts +56 -16
  168. package/src/agents/__tests__/supervisor-ledger.test.ts +185 -0
  169. package/src/config/runtime.ts +9 -0
  170. package/src/connector/builtins/webhook.test.ts +21 -0
  171. package/src/connector/index.ts +1 -0
  172. package/src/connector/mcp/client.ts +4 -0
  173. package/src/connector/mcp/index.ts +1 -0
  174. package/src/connector/mcp/streamable-http.test.ts +169 -0
  175. package/src/connector/mcp/streamable-http.ts +152 -0
  176. package/src/persona/assembler.ts +18 -6
  177. package/src/provider/collect.ts +4 -1
  178. package/src/public-runtime.ts +1 -0
  179. package/src/registry/tool/execute.test.ts +85 -10
  180. package/src/registry/tool/execute.ts +124 -21
  181. package/src/registry/toolset/catalog.test.ts +68 -0
  182. package/src/runtime/query/__tests__/deferred-tools.test.ts +52 -5
  183. package/src/runtime/query/__tests__/long-document-flow.test.ts +180 -0
  184. package/src/runtime/query/__tests__/prompt.test.ts +15 -11
  185. package/src/runtime/query/index.ts +34 -2
  186. package/src/runtime/query/iteration/index.ts +98 -6
  187. package/src/runtime/query/iteration/phases/checkpoint.ts +7 -2
  188. package/src/runtime/query/iteration/phases/compaction.ts +89 -3
  189. package/src/runtime/query/iteration/phases/context.ts +58 -0
  190. package/src/runtime/query/iteration/phases/hitl-abort-race.test.ts +86 -0
  191. package/src/runtime/query/iteration/phases/hitl-answer-question.test.ts +139 -0
  192. package/src/runtime/query/iteration/phases/tool-review.ts +7 -3
  193. package/src/runtime/query/iteration/phases/working-memory-compaction.test.ts +215 -0
  194. package/src/runtime/query/iteration/phases/working-memory.ts +86 -0
  195. package/src/session/workspace/__tests__/shared-run.test.ts +1 -1
  196. package/src/session/workspace/shared-run.ts +1 -0
  197. package/src/skills/loader.test.ts +8 -8
  198. package/src/streaming/coalesce.test.ts +3 -3
  199. package/src/tools/builtins/__tests__/payload-budget.test.ts +63 -0
  200. package/src/tools/builtins/__tests__/read-file.test.ts +12 -0
  201. package/src/tools/builtins/read-file.ts +50 -2
  202. package/src/tools/builtins/search-tools.ts +38 -7
  203. package/src/tools/coordinator/__tests__/approve-plan.test.ts +139 -0
  204. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +370 -0
  205. package/src/tools/coordinator/index.ts +227 -17
  206. package/src/types/agent/index.ts +1 -0
  207. package/src/types/agent/reactive.ts +17 -0
  208. package/src/types/agent/supervisor.ts +20 -0
  209. package/src/types/agent/working-memory.ts +29 -0
  210. package/src/types/common/index.ts +19 -0
  211. package/src/types/common/usage-merge.test.ts +53 -0
  212. package/src/types/connector/mcp.ts +14 -2
  213. package/src/types/hitl/index.ts +52 -1
  214. package/src/types/provider/chat.ts +10 -0
  215. package/src/version.ts +26 -4
@@ -0,0 +1,169 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { MCPJsonRpcMessage } from '../../types/connector/index.js'
4
+ import { MCPClient } from './client.js'
5
+
6
+ afterEach(() => {
7
+ vi.unstubAllGlobals()
8
+ })
9
+
10
+ describe('Streamable HTTP MCP transport', () => {
11
+ it('connects through MCPClient, preserves session headers, and parses SSE responses', async () => {
12
+ const fetchMock = vi.fn<typeof fetch>()
13
+ fetchMock
14
+ .mockResolvedValueOnce(
15
+ jsonResponse(
16
+ {
17
+ jsonrpc: '2.0',
18
+ id: 1,
19
+ result: {
20
+ protocolVersion: '2024-11-05',
21
+ capabilities: { tools: { listChanged: true } },
22
+ serverInfo: { name: 'github' },
23
+ },
24
+ },
25
+ { 'mcp-session-id': 'sid_123' },
26
+ ),
27
+ )
28
+ .mockResolvedValueOnce(new Response('', { status: 202 }))
29
+ .mockResolvedValueOnce(
30
+ sseResponse([
31
+ { jsonrpc: '2.0', method: 'notifications/progress', params: { pct: 50 } },
32
+ {
33
+ jsonrpc: '2.0',
34
+ id: 2,
35
+ result: {
36
+ tools: [
37
+ {
38
+ name: 'search_repositories',
39
+ description: 'Search repositories',
40
+ inputSchema: { type: 'object' },
41
+ },
42
+ ],
43
+ },
44
+ },
45
+ ]),
46
+ )
47
+ vi.stubGlobal('fetch', fetchMock)
48
+
49
+ const client = new MCPClient({
50
+ serverName: 'github',
51
+ transport: {
52
+ type: 'streamable_http',
53
+ url: 'https://mcp.example.test/mcp',
54
+ headers: { Authorization: 'Bearer token' },
55
+ },
56
+ })
57
+ const notifications: string[] = []
58
+ client.onNotification((method) => notifications.push(method))
59
+
60
+ await client.connect()
61
+ const tools = await client.listTools()
62
+
63
+ expect(tools.map((tool) => tool.name)).toEqual(['search_repositories'])
64
+ expect(notifications).toEqual(['notifications/progress'])
65
+ expect(fetchMock).toHaveBeenCalledTimes(3)
66
+
67
+ const initialize = requestAt(fetchMock, 0)
68
+ expect(initialize.input).toBe('https://mcp.example.test/mcp')
69
+ expect(initialize.body.method).toBe('initialize')
70
+ expect(initialize.headers.Authorization).toBe('Bearer token')
71
+ expect(initialize.headers.Accept).toBe('application/json, text/event-stream')
72
+
73
+ const initialized = requestAt(fetchMock, 1)
74
+ expect(initialized.body.method).toBe('notifications/initialized')
75
+ expect(initialized.headers['Mcp-Session-Id']).toBe('sid_123')
76
+
77
+ const listTools = requestAt(fetchMock, 2)
78
+ expect(listTools.body.method).toBe('tools/list')
79
+ expect(listTools.headers['Mcp-Session-Id']).toBe('sid_123')
80
+ })
81
+
82
+ it('supports the streamable-http alias without non-standard hint headers', async () => {
83
+ const fetchMock = vi.fn<typeof fetch>()
84
+ fetchMock
85
+ .mockResolvedValueOnce(
86
+ jsonResponse(
87
+ {
88
+ jsonrpc: '2.0',
89
+ id: 1,
90
+ result: {
91
+ protocolVersion: '2024-11-05',
92
+ capabilities: { tools: {} },
93
+ serverInfo: { name: 'linear' },
94
+ },
95
+ },
96
+ { 'Mcp-Session-Id': 'sid_alias' },
97
+ ),
98
+ )
99
+ .mockResolvedValueOnce(new Response(null, { status: 204 }))
100
+ .mockResolvedValueOnce(
101
+ jsonResponse({
102
+ jsonrpc: '2.0',
103
+ id: 2,
104
+ result: { content: [{ type: 'text', text: 'ok' }] },
105
+ }),
106
+ )
107
+ vi.stubGlobal('fetch', fetchMock)
108
+
109
+ const client = new MCPClient({
110
+ serverName: 'linear',
111
+ transport: {
112
+ type: 'streamable-http',
113
+ url: 'https://mcp.example.test/linear',
114
+ },
115
+ })
116
+
117
+ await client.connect()
118
+ const result = await client.callTool('create_issue', { title: 'Bug' })
119
+
120
+ expect(result.content).toEqual([{ type: 'text', text: 'ok' }])
121
+ const callTool = requestAt(fetchMock, 2)
122
+ expect(callTool.body.method).toBe('tools/call')
123
+ expect(callTool.headers).toEqual({
124
+ 'Content-Type': 'application/json',
125
+ Accept: 'application/json, text/event-stream',
126
+ 'Mcp-Session-Id': 'sid_alias',
127
+ })
128
+ })
129
+ })
130
+
131
+ function jsonResponse(body: MCPJsonRpcMessage, headers?: Record<string, string>): Response {
132
+ return new Response(JSON.stringify(body), {
133
+ status: 200,
134
+ headers: {
135
+ 'content-type': 'application/json',
136
+ ...headers,
137
+ },
138
+ })
139
+ }
140
+
141
+ function sseResponse(messages: MCPJsonRpcMessage[]): Response {
142
+ return new Response(messages.map((message) => `data: ${JSON.stringify(message)}\n\n`).join(''), {
143
+ status: 200,
144
+ headers: {
145
+ 'content-type': 'text/event-stream',
146
+ },
147
+ })
148
+ }
149
+
150
+ function requestAt(
151
+ fetchMock: ReturnType<typeof vi.fn<typeof fetch>>,
152
+ index: number,
153
+ ): {
154
+ input: Parameters<typeof fetch>[0]
155
+ headers: Record<string, string>
156
+ body: MCPJsonRpcMessage
157
+ } {
158
+ const call = fetchMock.mock.calls[index]
159
+ if (!call) {
160
+ throw new Error(`No fetch call at index ${index}`)
161
+ }
162
+ const [input, init] = call
163
+ if (!init) {
164
+ throw new Error(`Fetch call at index ${index} had no init`)
165
+ }
166
+ const headers = init.headers as Record<string, string>
167
+ const body = JSON.parse(String(init.body)) as MCPJsonRpcMessage
168
+ return { input, headers, body }
169
+ }
@@ -0,0 +1,152 @@
1
+ import type {
2
+ MCPJsonRpcMessage,
3
+ MCPStreamableHttpTransportConfig,
4
+ MCPTransport,
5
+ } from '../../types/connector/index.js'
6
+ import { type Logger, getRootLogger } from '../../utils/logger.js'
7
+
8
+ const DEFAULT_TIMEOUT_MS = 30_000
9
+
10
+ export class StreamableHttpTransport implements MCPTransport {
11
+ private messageHandlers: Array<(message: MCPJsonRpcMessage) => void> = []
12
+ private closeHandlers: Array<() => void> = []
13
+ private errorHandlers: Array<(error: Error) => void> = []
14
+ private connected = false
15
+ private sessionId: string | null = null
16
+ private log: Logger
17
+
18
+ constructor(private readonly config: MCPStreamableHttpTransportConfig) {
19
+ this.log = getRootLogger().child({ component: 'StreamableHttpTransport' })
20
+ }
21
+
22
+ async connect(): Promise<void> {
23
+ if (this.connected) return
24
+ this.connected = true
25
+ this.log.info(`StreamableHttpTransport connected to ${this.config.url}`)
26
+ }
27
+
28
+ async close(): Promise<void> {
29
+ if (!this.connected) return
30
+ this.connected = false
31
+ for (const handler of this.closeHandlers) handler()
32
+ }
33
+
34
+ async send(message: MCPJsonRpcMessage): Promise<void> {
35
+ if (!this.connected) {
36
+ throw new Error('StreamableHttpTransport: not connected')
37
+ }
38
+
39
+ const controller = new AbortController()
40
+ const timeout = setTimeout(
41
+ () => controller.abort(),
42
+ this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
43
+ )
44
+
45
+ try {
46
+ const response = await fetch(this.config.url, {
47
+ method: 'POST',
48
+ headers: this.buildHeaders(),
49
+ body: JSON.stringify(message),
50
+ signal: controller.signal,
51
+ })
52
+
53
+ this.captureSessionId(response)
54
+
55
+ if (!response.ok) {
56
+ throw new Error(`StreamableHttpTransport: HTTP ${response.status}: ${response.statusText}`)
57
+ }
58
+
59
+ await this.dispatchResponseMessages(response)
60
+ } catch (err) {
61
+ const error = err instanceof Error ? err : new Error(String(err))
62
+ for (const handler of this.errorHandlers) handler(error)
63
+ throw error
64
+ } finally {
65
+ clearTimeout(timeout)
66
+ }
67
+ }
68
+
69
+ onMessage(handler: (message: MCPJsonRpcMessage) => void): void {
70
+ this.messageHandlers.push(handler)
71
+ }
72
+
73
+ onClose(handler: () => void): void {
74
+ this.closeHandlers.push(handler)
75
+ }
76
+
77
+ onError(handler: (error: Error) => void): void {
78
+ this.errorHandlers.push(handler)
79
+ }
80
+
81
+ isConnected(): boolean {
82
+ return this.connected
83
+ }
84
+
85
+ private buildHeaders(): Record<string, string> {
86
+ const headers: Record<string, string> = {
87
+ 'Content-Type': 'application/json',
88
+ Accept: 'application/json, text/event-stream',
89
+ ...this.config.headers,
90
+ }
91
+
92
+ if (this.sessionId) {
93
+ headers['Mcp-Session-Id'] = this.sessionId
94
+ }
95
+
96
+ return headers
97
+ }
98
+
99
+ private captureSessionId(response: Response): void {
100
+ const sessionId =
101
+ response.headers.get('mcp-session-id') ?? response.headers.get('Mcp-Session-Id')
102
+ if (sessionId && sessionId.length > 0) {
103
+ this.sessionId = sessionId
104
+ }
105
+ }
106
+
107
+ private async dispatchResponseMessages(response: Response): Promise<void> {
108
+ const text = await response.text()
109
+ if (text.trim().length === 0) return
110
+
111
+ const contentType = response.headers.get('content-type') ?? ''
112
+ const messages = contentType.includes('text/event-stream')
113
+ ? parseSseMessages(text)
114
+ : parseJsonMessages(text)
115
+
116
+ for (const message of messages) {
117
+ for (const handler of this.messageHandlers) handler(message)
118
+ }
119
+ }
120
+ }
121
+
122
+ function parseJsonMessages(raw: string): MCPJsonRpcMessage[] {
123
+ const parsed = JSON.parse(raw) as MCPJsonRpcMessage | MCPJsonRpcMessage[]
124
+ return Array.isArray(parsed) ? parsed : [parsed]
125
+ }
126
+
127
+ function parseSseMessages(raw: string): MCPJsonRpcMessage[] {
128
+ const normalized = raw.replace(/\r\n/g, '\n')
129
+ const events = normalized.split(/\n\n+/)
130
+ const messages: MCPJsonRpcMessage[] = []
131
+
132
+ for (const event of events) {
133
+ const dataLines = event
134
+ .split('\n')
135
+ .filter((line) => line.startsWith('data:'))
136
+ .map((line) => line.slice('data:'.length).trimStart())
137
+
138
+ if (dataLines.length === 0) continue
139
+
140
+ const data = dataLines.join('\n').trim()
141
+ if (data.length === 0 || data === '[DONE]') continue
142
+
143
+ const parsed = JSON.parse(data) as MCPJsonRpcMessage | MCPJsonRpcMessage[]
144
+ if (Array.isArray(parsed)) {
145
+ messages.push(...parsed)
146
+ } else {
147
+ messages.push(parsed)
148
+ }
149
+ }
150
+
151
+ return messages
152
+ }
@@ -48,17 +48,29 @@ export function renderSkillsSection(skills?: Skill[]): string | null {
48
48
 
49
49
  const available = skills
50
50
  .map((s) => {
51
- const details = [`description: ${s.metadata.description.trim()}`]
52
- if (s.metadata.compatibility) details.push(`compatibility: ${s.metadata.compatibility}`)
53
- if (s.metadata.license) details.push(`license: ${s.metadata.license}`)
54
- if (s.metadata.allowedTools) details.push(`allowed-tools: ${s.metadata.allowedTools}`)
55
- return `- ${s.metadata.name} (${details.join('; ')})\n directory: ${s.dirPath}`
51
+ const lines = [
52
+ '<skill>',
53
+ `<name>${s.metadata.name}</name>`,
54
+ `<description>${s.metadata.description.trim()}</description>`,
55
+ `<location>${s.dirPath}/SKILL.md</location>`,
56
+ ]
57
+ if (s.metadata.compatibility) {
58
+ lines.push(`<compatibility>${s.metadata.compatibility}</compatibility>`)
59
+ }
60
+ if (s.metadata.license) {
61
+ lines.push(`<license>${s.metadata.license}</license>`)
62
+ }
63
+ if (s.metadata.allowedTools) {
64
+ lines.push(`<allowed_tools>${s.metadata.allowedTools}</allowed_tools>`)
65
+ }
66
+ lines.push('</skill>')
67
+ return lines.join('\n')
56
68
  })
57
69
  .join('\n')
58
70
 
59
71
  const loadedSkills = skills.filter((s) => s.body)
60
72
  const sections = [
61
- `## Available Skills\nThese Agent Skills are available through progressive disclosure. Use a skill only when the task matches its description. If a skill is not already loaded below, activate/read its SKILL.md from the listed directory when the runtime provides filesystem or skill-loading access.\n\n${available}`,
73
+ `## Available Skills\nThe following block is a manifest, not the full skill content. Skill metadata is always visible; SKILL.md bodies are loaded only when the user's task matches the skill description.\n\n<available_skills>\n${available}\n</available_skills>\n\nSkill usage protocol:\n- Plain questions do not require a skill.\n- When a matching skill is already listed under Loaded Skills, apply its loaded instructions.\n- When a matching skill is not loaded and the runtime exposes filesystem or skill-loading tools, read the SKILL.md at its <location> before writing code, creating files, running shell commands for that workflow, or calling mutation tools guided by the skill.\n- Do not claim to have read a SKILL.md until its content is actually present in the prompt or returned by a tool.\n- Tool schemas and runtime permissions remain authoritative; skills provide guidance, not hidden tools.`,
62
74
  ]
63
75
 
64
76
  if (loadedSkills.length > 0) {
@@ -1,3 +1,4 @@
1
+ import { mergeTokenUsage } from '../types/common/index.js'
1
2
  import type { ChatCompletionResponse } from '../types/provider/chat.js'
2
3
  import type { StreamChunk } from '../types/provider/stream.js'
3
4
 
@@ -60,7 +61,9 @@ export async function collect(stream: AsyncIterable<StreamChunk>): Promise<ChatC
60
61
  }
61
62
 
62
63
  if (chunk.finishReason) finishReason = chunk.finishReason
63
- if (chunk.usage) usage = chunk.usage
64
+ // Merge (per-field max), not last-write-wins: a late frame that omits
65
+ // input/cache tokens must not zero the counts captured earlier in the stream.
66
+ if (chunk.usage) usage = mergeTokenUsage(usage, chunk.usage)
64
67
  }
65
68
 
66
69
  const toolCalls = [...toolBuckets.entries()]
@@ -194,6 +194,7 @@ export {
194
194
  RemoteExecutionContext,
195
195
  ScopedConnectorRegistry,
196
196
  StdioTransport,
197
+ StreamableHttpTransport,
197
198
  TenantConnectorManager,
198
199
  toolDefinitionToMCPTool,
199
200
  toolResultToMCPToolResult,
@@ -20,18 +20,21 @@
20
20
  * reports true iff at least one tool is suspended.
21
21
  * - `getAvailability(name)` returns 'active' as a default even for
22
22
  * unknown names (this is non-obvious but is the current behavior).
23
- * - `searchDeferred(q)` filters DEFERRED tools: a useful whole query
24
- * matches name OR description (case-insensitive); a batched multi-term
25
- * query matches meaningful tokens against the NAME only. Generic/short
26
- * tokens (`clawtool`, `tool`, …) are ignored so they can't over-activate.
23
+ * - `searchDeferred(q)` is a RANKED weighted search over DEFERRED tools:
24
+ * per meaningful term (≥3 chars, not a stop token) the score is
25
+ * exact-name 12 / name-substring 8 / description 5 / argument-name 3;
26
+ * results sort score-descending with name ties broken alphabetically.
27
+ * Generic/short tokens (`clawtool`, `tool`, CRUD verbs like `list`,
28
+ * `read`, …) are stopped so they can't over-activate the catalog.
27
29
  * - `assignTiers(mapping)` mutates `tool.tier` on existing tools;
28
30
  * throws via `getOrThrow` on unknown name; throws if the tier id
29
31
  * is not in `tierConfig.tiers`.
30
32
  * - `toTierGuidance` returns null without a guidanceTemplate; calls
31
33
  * the template with every defined tier otherwise.
32
34
  * - `toPromptSection`: returns '' when no active or deferred tools;
33
- * otherwise produces `<available_tools>` + `<deferred_tools>`
34
- * fragments.
35
+ * otherwise produces `<available_tools>` (name-only — descriptions
36
+ * already ride the runtime tools param) + `<deferred_tools>` (name +
37
+ * first-sentence hint ≤100 chars) fragments.
35
38
  * - `toLLMTools`: converts active + suspended tools (filtered by
36
39
  * `toolNames` if provided) into LLM tool schemas via
37
40
  * `zodToJsonSchema`. When `tierConfig.labelInDescription` is true,
@@ -198,15 +201,59 @@ describe('ToolRegistry — searchDeferred', () => {
198
201
  r.register([makeTool('clawtool_PeerRegister')], 'deferred')
199
202
  r.register([makeTool('clawtool_PeerList')], 'deferred')
200
203
  r.register([makeTool('clawtool_Unrelated')], 'deferred')
201
- // A whole-phrase substring match would find none of these.
204
+ // A whole-phrase substring match would find none of these. Equal
205
+ // scores tie-break alphabetically by name (deterministic ranking).
202
206
  expect(r.searchDeferred('A2aCard PeerRegister PeerList').map((t) => t.name)).toEqual([
203
207
  'clawtool_A2aCard',
204
- 'clawtool_PeerRegister',
205
208
  'clawtool_PeerList',
209
+ 'clawtool_PeerRegister',
206
210
  ])
207
211
  expect(r.searchDeferred(' ')).toEqual([])
208
212
  })
209
213
 
214
+ it('ranks exact name above name substring above description-only matches', () => {
215
+ const r = new ToolRegistry()
216
+ r.register([makeTool('release_notes', { description: 'Write deploy notes.' })], 'deferred')
217
+ r.register([makeTool('deploy_preview', { description: 'Stage a preview build.' })], 'deferred')
218
+ r.register([makeTool('deploy', { description: 'Ship the current build.' })], 'deferred')
219
+ expect(r.searchDeferred('deploy').map((t) => t.name)).toEqual([
220
+ 'deploy', // exact name → 12
221
+ 'deploy_preview', // name substring → 8
222
+ 'release_notes', // description only → 5
223
+ ])
224
+ })
225
+
226
+ it('indexes argument names so a parameter term can locate a tool', () => {
227
+ const r = new ToolRegistry()
228
+ r.register(
229
+ [
230
+ makeTool('send_invoice', {
231
+ inputSchema: z.object({ customerEmail: z.string(), amount: z.number() }),
232
+ }),
233
+ ],
234
+ 'deferred',
235
+ )
236
+ r.register([makeTool('send_reminder')], 'deferred')
237
+ expect(r.searchDeferred('customerEmail').map((t) => t.name)).toEqual(['send_invoice'])
238
+ })
239
+
240
+ it('stops generic CRUD verbs so they cannot activate catalog slices', () => {
241
+ const r = new ToolRegistry()
242
+ r.register(
243
+ [makeTool('list_deals', { description: 'List the deals in an account.' })],
244
+ 'deferred',
245
+ )
246
+ r.register([makeTool('list_workflows', { description: 'List workflows.' })], 'deferred')
247
+ r.register([makeTool('read_document', { description: 'Read a document body.' })], 'deferred')
248
+ // Bare CRUD verbs identify nothing.
249
+ expect(r.searchDeferred('list')).toEqual([])
250
+ expect(r.searchDeferred('read')).toEqual([])
251
+ expect(r.searchDeferred('create update delete')).toEqual([])
252
+ // The non-generic remainder carries the query.
253
+ expect(r.searchDeferred('list deals').map((t) => t.name)).toEqual(['list_deals'])
254
+ expect(r.searchDeferred('read document').map((t) => t.name)).toEqual(['read_document'])
255
+ })
256
+
210
257
  it('does not over-activate on generic/shared tokens', () => {
211
258
  const r = new ToolRegistry()
212
259
  r.register([makeTool('clawtool_A2aCard', { description: 'peer card' })], 'deferred')
@@ -270,10 +317,38 @@ describe('ToolRegistry — toPromptSection + toLLMTools', () => {
270
317
  expect(s).toContain('<tool_runtime_contract>')
271
318
  expect(s).toContain('runtime tools parameter')
272
319
  expect(s).toContain('<available_tools>')
273
- expect(s).toContain('- a: a tool')
320
+ // Active entries are NAME-ONLY: the description already rides the
321
+ // runtime tools param every request — repeating it double-bills.
322
+ expect(s).toContain('- a\n')
323
+ expect(s).not.toContain('- a: a tool')
274
324
  expect(s).toContain('<deferred_tools>')
275
325
  expect(s).toContain('Deferred tools are discoverable')
276
- expect(s).toContain('- b')
326
+ // Deferred entries carry a one-line hint — their schema is off the
327
+ // wire, so the hint is the model's only capability signal.
328
+ expect(s).toContain('- b: b tool')
329
+ })
330
+
331
+ it('toPromptSection truncates deferred hints to the first sentence, capped at 100 chars', () => {
332
+ const r = new ToolRegistry()
333
+ r.register(
334
+ [
335
+ makeTool('two_sentences', {
336
+ description: 'Reads the document. Second sentence that must not appear.',
337
+ }),
338
+ makeTool('long_sentence', {
339
+ description: `${'x'.repeat(150)} end`,
340
+ }),
341
+ ],
342
+ 'deferred',
343
+ )
344
+ const s = r.toPromptSection()
345
+ expect(s).toContain('- two_sentences: Reads the document.')
346
+ expect(s).not.toContain('Second sentence')
347
+ const longLine = s.split('\n').find((line) => line.startsWith('- long_sentence:'))
348
+ expect(longLine).toBeDefined()
349
+ const hint = (longLine ?? '').replace('- long_sentence: ', '')
350
+ expect(hint.length).toBeLessThanOrEqual(100)
351
+ expect(hint.endsWith('…')).toBe(true)
277
352
  })
278
353
 
279
354
  it('toPromptSection references search_tools only when it is active', () => {