@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.
- package/CHANGELOG.md +167 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +5 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts +22 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +47 -14
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/supervisor-ledger.test.d.ts +2 -0
- package/dist/agents/__tests__/supervisor-ledger.test.d.ts.map +1 -0
- package/dist/agents/__tests__/supervisor-ledger.test.js +131 -0
- package/dist/agents/__tests__/supervisor-ledger.test.js.map +1 -0
- package/dist/config/runtime.d.ts +24 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +9 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/builtins/webhook.test.js +17 -0
- package/dist/connector/builtins/webhook.test.js.map +1 -1
- package/dist/connector/index.d.ts +1 -0
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -0
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +4 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +1 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +22 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -0
- package/dist/connector/mcp/streamable-http.js +128 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -0
- package/dist/connector/mcp/streamable-http.test.d.ts +2 -0
- package/dist/connector/mcp/streamable-http.test.d.ts.map +1 -0
- package/dist/connector/mcp/streamable-http.test.js +132 -0
- package/dist/connector/mcp/streamable-http.test.js.map +1 -0
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +18 -9
- package/dist/persona/assembler.js.map +1 -1
- package/dist/provider/collect.d.ts.map +1 -1
- package/dist/provider/collect.js +4 -1
- package/dist/provider/collect.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +27 -0
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +124 -21
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/execute.test.d.ts +9 -6
- package/dist/registry/tool/execute.test.d.ts.map +1 -1
- package/dist/registry/tool/execute.test.js +72 -10
- package/dist/registry/tool/execute.test.js.map +1 -1
- package/dist/registry/toolset/catalog.test.js +62 -0
- package/dist/registry/toolset/catalog.test.js.map +1 -1
- package/dist/runtime/query/__tests__/deferred-tools.test.js +43 -5
- package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
- package/dist/runtime/query/__tests__/long-document-flow.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/long-document-flow.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/long-document-flow.test.js +154 -0
- package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -0
- package/dist/runtime/query/__tests__/prompt.test.js +15 -11
- package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
- package/dist/runtime/query/index.d.ts +9 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +23 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +2 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +99 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.js +2 -2
- package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +8 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +84 -3
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +12 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +53 -0
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js +73 -0
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js +120 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/tool-review.d.ts +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +7 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts +19 -0
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js +177 -0
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/working-memory.d.ts +31 -0
- package/dist/runtime/query/iteration/phases/working-memory.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/working-memory.js +81 -0
- package/dist/runtime/query/iteration/phases/working-memory.js.map +1 -0
- package/dist/session/workspace/__tests__/shared-run.test.js +1 -1
- package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -1
- package/dist/session/workspace/shared-run.js +1 -0
- package/dist/session/workspace/shared-run.js.map +1 -1
- package/dist/skills/loader.test.js +8 -8
- package/dist/skills/loader.test.js.map +1 -1
- package/dist/streaming/coalesce.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/payload-budget.test.js +48 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/read-file.test.js +9 -0
- package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +50 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/search-tools.d.ts.map +1 -1
- package/dist/tools/builtins/search-tools.js +32 -7
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.js +114 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +309 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +8 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +195 -15
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/index.d.ts +1 -0
- package/dist/types/agent/index.d.ts.map +1 -1
- package/dist/types/agent/index.js +1 -0
- package/dist/types/agent/index.js.map +1 -1
- package/dist/types/agent/reactive.d.ts +15 -0
- package/dist/types/agent/reactive.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +18 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/agent/working-memory.d.ts +29 -0
- package/dist/types/agent/working-memory.d.ts.map +1 -0
- package/dist/types/agent/working-memory.js +2 -0
- package/dist/types/agent/working-memory.js.map +1 -0
- package/dist/types/common/index.d.ts +10 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +18 -0
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/common/usage-merge.test.d.ts +2 -0
- package/dist/types/common/usage-merge.test.d.ts.map +1 -0
- package/dist/types/common/usage-merge.test.js +48 -0
- package/dist/types/common/usage-merge.test.js.map +1 -0
- package/dist/types/connector/mcp.d.ts +9 -2
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +42 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js +11 -0
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +9 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +25 -4
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +5 -0
- package/src/agents/SupervisorAgent.ts +56 -16
- package/src/agents/__tests__/supervisor-ledger.test.ts +185 -0
- package/src/config/runtime.ts +9 -0
- package/src/connector/builtins/webhook.test.ts +21 -0
- package/src/connector/index.ts +1 -0
- package/src/connector/mcp/client.ts +4 -0
- package/src/connector/mcp/index.ts +1 -0
- package/src/connector/mcp/streamable-http.test.ts +169 -0
- package/src/connector/mcp/streamable-http.ts +152 -0
- package/src/persona/assembler.ts +18 -6
- package/src/provider/collect.ts +4 -1
- package/src/public-runtime.ts +1 -0
- package/src/registry/tool/execute.test.ts +85 -10
- package/src/registry/tool/execute.ts +124 -21
- package/src/registry/toolset/catalog.test.ts +68 -0
- package/src/runtime/query/__tests__/deferred-tools.test.ts +52 -5
- package/src/runtime/query/__tests__/long-document-flow.test.ts +180 -0
- package/src/runtime/query/__tests__/prompt.test.ts +15 -11
- package/src/runtime/query/index.ts +34 -2
- package/src/runtime/query/iteration/index.ts +98 -6
- package/src/runtime/query/iteration/phases/checkpoint.ts +7 -2
- package/src/runtime/query/iteration/phases/compaction.ts +89 -3
- package/src/runtime/query/iteration/phases/context.ts +58 -0
- package/src/runtime/query/iteration/phases/hitl-abort-race.test.ts +86 -0
- package/src/runtime/query/iteration/phases/hitl-answer-question.test.ts +139 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +7 -3
- package/src/runtime/query/iteration/phases/working-memory-compaction.test.ts +215 -0
- package/src/runtime/query/iteration/phases/working-memory.ts +86 -0
- package/src/session/workspace/__tests__/shared-run.test.ts +1 -1
- package/src/session/workspace/shared-run.ts +1 -0
- package/src/skills/loader.test.ts +8 -8
- package/src/streaming/coalesce.test.ts +3 -3
- package/src/tools/builtins/__tests__/payload-budget.test.ts +63 -0
- package/src/tools/builtins/__tests__/read-file.test.ts +12 -0
- package/src/tools/builtins/read-file.ts +50 -2
- package/src/tools/builtins/search-tools.ts +38 -7
- package/src/tools/coordinator/__tests__/approve-plan.test.ts +139 -0
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +370 -0
- package/src/tools/coordinator/index.ts +227 -17
- package/src/types/agent/index.ts +1 -0
- package/src/types/agent/reactive.ts +17 -0
- package/src/types/agent/supervisor.ts +20 -0
- package/src/types/agent/working-memory.ts +29 -0
- package/src/types/common/index.ts +19 -0
- package/src/types/common/usage-merge.test.ts +53 -0
- package/src/types/connector/mcp.ts +14 -2
- package/src/types/hitl/index.ts +52 -1
- package/src/types/provider/chat.ts +10 -0
- 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
|
+
}
|
package/src/persona/assembler.ts
CHANGED
|
@@ -48,17 +48,29 @@ export function renderSkillsSection(skills?: Skill[]): string | null {
|
|
|
48
48
|
|
|
49
49
|
const available = skills
|
|
50
50
|
.map((s) => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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\
|
|
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) {
|
package/src/provider/collect.ts
CHANGED
|
@@ -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
|
-
|
|
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()]
|
package/src/public-runtime.ts
CHANGED
|
@@ -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)`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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>`
|
|
34
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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', () => {
|