@namzu/sdk 0.6.0 → 1.0.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 +362 -0
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +9 -2
- package/dist/advisory/executor.js.map +1 -1
- package/dist/advisory/executor.test.d.ts +2 -1
- package/dist/advisory/executor.test.d.ts.map +1 -1
- package/dist/advisory/executor.test.js +7 -4
- package/dist/advisory/executor.test.js.map +1 -1
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +2 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +7 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/bridge/sse/mapper.test.js +2 -2
- package/dist/constants/compaction/index.d.ts.map +1 -1
- package/dist/constants/compaction/index.js +8 -3
- package/dist/constants/compaction/index.js.map +1 -1
- package/dist/constants/sandbox/index.d.ts +21 -0
- package/dist/constants/sandbox/index.d.ts.map +1 -1
- package/dist/constants/sandbox/index.js +30 -0
- package/dist/constants/sandbox/index.js.map +1 -1
- package/dist/constants/tools/index.d.ts.map +1 -1
- package/dist/constants/tools/index.js +33 -2
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +35 -5
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/persona/assembler.d.ts +1 -0
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +28 -6
- package/dist/persona/assembler.js.map +1 -1
- package/dist/provider/collect.test.js +2 -2
- package/dist/public-runtime.d.ts +5 -4
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +2 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +87 -5
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/execute.test.d.ts +4 -2
- package/dist/registry/tool/execute.test.d.ts.map +1 -1
- package/dist/registry/tool/execute.test.js +112 -3
- package/dist/registry/tool/execute.test.js.map +1 -1
- package/dist/registry/toolset/catalog.d.ts +42 -0
- package/dist/registry/toolset/catalog.d.ts.map +1 -0
- package/dist/registry/toolset/catalog.js +217 -0
- package/dist/registry/toolset/catalog.js.map +1 -0
- package/dist/registry/toolset/catalog.test.d.ts +2 -0
- package/dist/registry/toolset/catalog.test.d.ts.map +1 -0
- package/dist/registry/toolset/catalog.test.js +85 -0
- package/dist/registry/toolset/catalog.test.js.map +1 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.js +147 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.js +98 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.js.map +1 -0
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +38 -3
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -1
- package/dist/runtime/query/__tests__/prompt.test.js +47 -2
- package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
- package/dist/runtime/query/__tests__/stream-recovery.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.js +126 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -0
- package/dist/runtime/query/continuation.d.ts +16 -0
- package/dist/runtime/query/continuation.d.ts.map +1 -0
- package/dist/runtime/query/continuation.js +16 -0
- package/dist/runtime/query/continuation.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +3 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +71 -3
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +19 -3
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +22 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +227 -60
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +21 -1
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +32 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/workspace/__tests__/shared-run.test.d.ts +2 -0
- package/dist/session/workspace/__tests__/shared-run.test.d.ts.map +1 -0
- package/dist/session/workspace/__tests__/shared-run.test.js +147 -0
- package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -0
- package/dist/session/workspace/index.d.ts +2 -0
- package/dist/session/workspace/index.d.ts.map +1 -1
- package/dist/session/workspace/index.js +1 -0
- package/dist/session/workspace/index.js.map +1 -1
- package/dist/session/workspace/shared-run.d.ts +81 -0
- package/dist/session/workspace/shared-run.d.ts.map +1 -0
- package/dist/session/workspace/shared-run.js +251 -0
- package/dist/session/workspace/shared-run.js.map +1 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +36 -6
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/loader.test.d.ts +2 -0
- package/dist/skills/loader.test.d.ts.map +1 -0
- package/dist/skills/loader.test.js +65 -0
- package/dist/skills/loader.test.js.map +1 -0
- package/dist/streaming/coalesce.test.js +1 -1
- package/dist/tools/builtins/__tests__/edit.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +38 -0
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.js +22 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/read-file.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/read-file.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/read-file.test.js +24 -0
- package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.js +52 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/write-file.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/write-file.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/write-file.test.js +74 -0
- package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +40 -7
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts +5 -2
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +114 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +13 -13
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +23 -8
- 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 +4 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/verify-outputs.d.ts +5 -0
- package/dist/tools/builtins/verify-outputs.d.ts.map +1 -0
- package/dist/tools/builtins/verify-outputs.js +103 -0
- package/dist/tools/builtins/verify-outputs.js.map +1 -0
- package/dist/tools/builtins/write-file.d.ts +3 -2
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +72 -12
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/agent.test.d.ts +15 -0
- package/dist/tools/coordinator/__tests__/agent.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/agent.test.js +142 -0
- package/dist/tools/coordinator/__tests__/agent.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/task-list.test.d.ts +13 -0
- package/dist/tools/coordinator/__tests__/task-list.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js +162 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -0
- package/dist/tools/coordinator/agent.d.ts +34 -0
- package/dist/tools/coordinator/agent.d.ts.map +1 -0
- package/dist/tools/coordinator/agent.js +107 -0
- package/dist/tools/coordinator/agent.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +7 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +111 -21
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/base.d.ts +8 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/reactive.d.ts +23 -0
- package/dist/types/agent/reactive.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +14 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/message/index.d.ts +22 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +7 -2
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +2 -9
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +6 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +193 -0
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/skills/index.d.ts +2 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +22 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/toolset/index.d.ts +71 -0
- package/dist/types/toolset/index.d.ts.map +1 -0
- package/dist/types/toolset/index.js +2 -0
- package/dist/types/toolset/index.js.map +1 -0
- package/dist/types/workspace/index.d.ts +1 -0
- package/dist/types/workspace/index.d.ts.map +1 -1
- package/dist/types/workspace/shared-run.d.ts +61 -0
- package/dist/types/workspace/shared-run.d.ts.map +1 -0
- package/dist/types/workspace/shared-run.js +2 -0
- package/dist/types/workspace/shared-run.js.map +1 -0
- package/dist/verification/index.d.ts +1 -0
- package/dist/verification/index.d.ts.map +1 -1
- package/dist/verification/index.js +1 -0
- package/dist/verification/index.js.map +1 -1
- package/dist/verification/presets.d.ts +53 -0
- package/dist/verification/presets.d.ts.map +1 -0
- package/dist/verification/presets.js +70 -0
- package/dist/verification/presets.js.map +1 -0
- package/dist/verification/presets.test.d.ts +16 -0
- package/dist/verification/presets.test.d.ts.map +1 -0
- package/dist/verification/presets.test.js +79 -0
- package/dist/verification/presets.test.js.map +1 -0
- package/package.json +3 -2
- package/src/advisory/executor.test.ts +7 -4
- package/src/advisory/executor.ts +11 -2
- package/src/agents/ReactiveAgent.ts +2 -0
- package/src/agents/SupervisorAgent.ts +7 -0
- package/src/bridge/sse/mapper.test.ts +2 -2
- package/src/constants/compaction/index.ts +8 -3
- package/src/constants/sandbox/index.ts +37 -0
- package/src/constants/tools/index.ts +33 -2
- package/src/manager/run/persistence.ts +34 -6
- package/src/persona/assembler.ts +31 -8
- package/src/provider/collect.test.ts +2 -2
- package/src/public-runtime.ts +14 -1
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +7 -0
- package/src/registry/index.ts +7 -0
- package/src/registry/tool/execute.test.ts +132 -3
- package/src/registry/tool/execute.ts +94 -9
- package/src/registry/toolset/catalog.test.ts +97 -0
- package/src/registry/toolset/catalog.ts +283 -0
- package/src/runtime/query/__tests__/deferred-tools.test.ts +183 -0
- package/src/runtime/query/__tests__/executor-concurrency.test.ts +122 -0
- package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +48 -3
- package/src/runtime/query/__tests__/prompt.test.ts +51 -2
- package/src/runtime/query/__tests__/stream-recovery.test.ts +156 -0
- package/src/runtime/query/continuation.ts +16 -0
- package/src/runtime/query/executor.ts +82 -13
- package/src/runtime/query/index.ts +24 -3
- package/src/runtime/query/iteration/index.ts +263 -68
- package/src/runtime/query/iteration/phases/context.ts +10 -0
- package/src/runtime/query/prompt.ts +17 -1
- package/src/runtime/query/tooling.ts +2 -0
- package/src/sandbox/provider/local.ts +33 -0
- package/src/session/workspace/__tests__/shared-run.test.ts +181 -0
- package/src/session/workspace/index.ts +6 -0
- package/src/session/workspace/shared-run.ts +316 -0
- package/src/skills/loader.test.ts +89 -0
- package/src/skills/loader.ts +37 -6
- package/src/streaming/coalesce.test.ts +1 -1
- package/src/tools/builtins/__tests__/edit.test.ts +57 -0
- package/src/tools/builtins/__tests__/payload-budget.test.ts +29 -0
- package/src/tools/builtins/__tests__/read-file.test.ts +31 -0
- package/src/tools/builtins/__tests__/verify-outputs.test.ts +71 -0
- package/src/tools/builtins/__tests__/write-file.test.ts +97 -0
- package/src/tools/builtins/bash.ts +48 -7
- package/src/tools/builtins/edit.ts +162 -27
- package/src/tools/builtins/index.ts +13 -13
- package/src/tools/builtins/read-file.ts +31 -8
- package/src/tools/builtins/search-tools.ts +5 -1
- package/src/tools/builtins/verify-outputs.ts +126 -0
- package/src/tools/builtins/write-file.ts +83 -14
- package/src/tools/coordinator/__tests__/agent.test.ts +172 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +182 -0
- package/src/tools/coordinator/agent.ts +157 -0
- package/src/tools/coordinator/index.ts +128 -22
- package/src/types/agent/base.ts +8 -0
- package/src/types/agent/reactive.ts +25 -0
- package/src/types/agent/supervisor.ts +16 -0
- package/src/types/message/index.ts +32 -2
- package/src/types/provider/chat.ts +2 -9
- package/src/types/run/events.ts +6 -0
- package/src/types/sandbox/index.ts +219 -0
- package/src/types/skills/index.ts +4 -0
- package/src/types/tool/index.ts +24 -0
- package/src/types/toolset/index.ts +86 -0
- package/src/types/workspace/index.ts +9 -0
- package/src/types/workspace/shared-run.ts +65 -0
- package/src/verification/index.ts +1 -0
- package/src/verification/presets.test.ts +112 -0
- package/src/verification/presets.ts +72 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
5
|
+
import { ToolRegistry } from '../tool/execute.js'
|
|
6
|
+
import { ToolCatalog, createToolCatalogFromRegistry } from './catalog.js'
|
|
7
|
+
|
|
8
|
+
function makeTool(name: string, description = `${name} tool`): ToolDefinition {
|
|
9
|
+
return {
|
|
10
|
+
name,
|
|
11
|
+
description,
|
|
12
|
+
inputSchema: z.object({ query: z.string().optional() }),
|
|
13
|
+
async execute() {
|
|
14
|
+
return { success: true, output: `${name}-ran` }
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ToolCatalog', () => {
|
|
20
|
+
it('keeps sources, toolsets, and tools as separate records', () => {
|
|
21
|
+
const catalog = new ToolCatalog()
|
|
22
|
+
catalog.registerSource({
|
|
23
|
+
id: 'mcp:microsoft-learn',
|
|
24
|
+
kind: 'mcp_server',
|
|
25
|
+
name: 'Microsoft Learn',
|
|
26
|
+
mcpServer: {
|
|
27
|
+
name: 'microsoft-learn',
|
|
28
|
+
url: 'https://learn.microsoft.com/api/mcp',
|
|
29
|
+
transport: 'streamable_http',
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
catalog.registerToolset({
|
|
33
|
+
id: 'mcp-toolset:microsoft-learn',
|
|
34
|
+
sourceId: 'mcp:microsoft-learn',
|
|
35
|
+
name: 'Microsoft Learn toolset',
|
|
36
|
+
defaultPolicy: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
loading: 'deferred',
|
|
39
|
+
preferred: true,
|
|
40
|
+
permissionPolicy: 'always_allow',
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
catalog.registerTool({
|
|
44
|
+
name: 'microsoft_docs_search',
|
|
45
|
+
description: 'Search Microsoft documentation',
|
|
46
|
+
sourceId: 'mcp:microsoft-learn',
|
|
47
|
+
toolsetId: 'mcp-toolset:microsoft-learn',
|
|
48
|
+
policy: { enabled: true, loading: 'deferred', preferred: true },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(catalog.listSources()).toHaveLength(1)
|
|
52
|
+
expect(catalog.listToolsets()).toHaveLength(1)
|
|
53
|
+
expect(catalog.getToolsByLoading(['deferred']).map((t) => t.name)).toEqual([
|
|
54
|
+
'microsoft_docs_search',
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('searches deferred tools by name, description, source, and toolset', () => {
|
|
59
|
+
const registry = new ToolRegistry()
|
|
60
|
+
registry.register(makeTool('github_search_issues', 'Search repository issues'), 'deferred')
|
|
61
|
+
registry.register(makeTool('slack_search_messages', 'Search team messages'), 'deferred')
|
|
62
|
+
registry.register(makeTool('bash', 'Run a shell command'))
|
|
63
|
+
|
|
64
|
+
const catalog = createToolCatalogFromRegistry(registry, {
|
|
65
|
+
source: {
|
|
66
|
+
id: 'host',
|
|
67
|
+
kind: 'host_tool',
|
|
68
|
+
name: 'Host runtime',
|
|
69
|
+
description: 'Local shell and collaboration tools',
|
|
70
|
+
},
|
|
71
|
+
toolset: {
|
|
72
|
+
id: 'default-host',
|
|
73
|
+
name: 'Default host tools',
|
|
74
|
+
defaultPolicy: { enabled: true, loading: 'eager' },
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
catalog.searchTools('repository', { loading: ['deferred'] }).map((r) => r.tool.name),
|
|
80
|
+
).toEqual(['github_search_issues'])
|
|
81
|
+
expect(catalog.toLLMTools().map((t) => t.function.name)).toEqual(['bash'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('preserves registry availability as catalog loading policy', () => {
|
|
85
|
+
const registry = new ToolRegistry()
|
|
86
|
+
registry.register(makeTool('read_file'))
|
|
87
|
+
registry.register(makeTool('web_search'), 'deferred')
|
|
88
|
+
registry.register(makeTool('write_file'), 'suspended')
|
|
89
|
+
|
|
90
|
+
const catalog = createToolCatalogFromRegistry(registry)
|
|
91
|
+
|
|
92
|
+
expect(catalog.getTool('read_file')?.policy.loading).toBe('eager')
|
|
93
|
+
expect(catalog.getTool('web_search')?.policy.loading).toBe('deferred')
|
|
94
|
+
expect(catalog.getTool('write_file')?.policy.loading).toBe('suspended')
|
|
95
|
+
expect(catalog.toLLMTools({ loading: ['suspended'] })).toEqual([])
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
2
|
+
import type { ToolRegistryContract } from '../../types/tool/index.js'
|
|
3
|
+
import type { LLMToolSchema, ToolAvailability, ToolDefinition } from '../../types/tool/index.js'
|
|
4
|
+
import type {
|
|
5
|
+
ToolCatalogEntry,
|
|
6
|
+
ToolCatalogSearchResult,
|
|
7
|
+
ToolCatalogSnapshot,
|
|
8
|
+
ToolLoadingMode,
|
|
9
|
+
ToolSource,
|
|
10
|
+
ToolsetDefinition,
|
|
11
|
+
ToolsetPolicy,
|
|
12
|
+
} from '../../types/toolset/index.js'
|
|
13
|
+
|
|
14
|
+
export interface ToolCatalogSearchOptions {
|
|
15
|
+
readonly loading?: readonly ToolLoadingMode[]
|
|
16
|
+
readonly limit?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ToolCatalogFromRegistryOptions {
|
|
20
|
+
readonly source?: ToolSource
|
|
21
|
+
readonly toolset?: Omit<ToolsetDefinition, 'sourceId'> & {
|
|
22
|
+
readonly sourceId?: string
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_HOST_SOURCE: ToolSource = {
|
|
27
|
+
id: 'host-tools',
|
|
28
|
+
kind: 'host_tool',
|
|
29
|
+
name: 'Host tools',
|
|
30
|
+
description: 'Tools executed by the host runtime.',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_HOST_TOOLSET: ToolsetDefinition = {
|
|
34
|
+
id: 'host-tools',
|
|
35
|
+
sourceId: DEFAULT_HOST_SOURCE.id,
|
|
36
|
+
name: 'Host tools',
|
|
37
|
+
defaultPolicy: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
loading: 'eager',
|
|
40
|
+
permissionPolicy: 'default',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ToolCatalog {
|
|
45
|
+
private sources = new Map<string, ToolSource>()
|
|
46
|
+
private toolsets = new Map<string, ToolsetDefinition>()
|
|
47
|
+
private tools = new Map<string, ToolCatalogEntry>()
|
|
48
|
+
|
|
49
|
+
registerSource(source: ToolSource): void {
|
|
50
|
+
this.sources.set(source.id, source)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerToolset(toolset: ToolsetDefinition): void {
|
|
54
|
+
if (!this.sources.has(toolset.sourceId)) {
|
|
55
|
+
throw new Error(`Toolset "${toolset.id}" references unknown source "${toolset.sourceId}"`)
|
|
56
|
+
}
|
|
57
|
+
this.toolsets.set(toolset.id, toolset)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerTool(tool: ToolCatalogEntry): void {
|
|
61
|
+
if (!this.sources.has(tool.sourceId)) {
|
|
62
|
+
throw new Error(`Tool "${tool.name}" references unknown source "${tool.sourceId}"`)
|
|
63
|
+
}
|
|
64
|
+
if (!this.toolsets.has(tool.toolsetId)) {
|
|
65
|
+
throw new Error(`Tool "${tool.name}" references unknown toolset "${tool.toolsetId}"`)
|
|
66
|
+
}
|
|
67
|
+
this.tools.set(tool.name, tool)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getSource(id: string): ToolSource | undefined {
|
|
71
|
+
return this.sources.get(id)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getToolset(id: string): ToolsetDefinition | undefined {
|
|
75
|
+
return this.toolsets.get(id)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getTool(name: string): ToolCatalogEntry | undefined {
|
|
79
|
+
return this.tools.get(name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
listSources(): ToolSource[] {
|
|
83
|
+
return [...this.sources.values()]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
listToolsets(): ToolsetDefinition[] {
|
|
87
|
+
return [...this.toolsets.values()]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
listTools(): ToolCatalogEntry[] {
|
|
91
|
+
return [...this.tools.values()]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
snapshot(): ToolCatalogSnapshot {
|
|
95
|
+
return {
|
|
96
|
+
sources: this.listSources(),
|
|
97
|
+
toolsets: this.listToolsets(),
|
|
98
|
+
tools: this.listTools(),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getToolsByLoading(loading: readonly ToolLoadingMode[]): ToolCatalogEntry[] {
|
|
103
|
+
const wanted = new Set(loading)
|
|
104
|
+
return this.listTools().filter((tool) => wanted.has(resolveToolLoading(tool.policy)))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
searchTools(query: string, options: ToolCatalogSearchOptions = {}): ToolCatalogSearchResult[] {
|
|
108
|
+
const normalized = query.trim().toLowerCase()
|
|
109
|
+
const terms = normalized.split(/\s+/).filter(Boolean)
|
|
110
|
+
const loading = options.loading ? new Set(options.loading) : null
|
|
111
|
+
const results: ToolCatalogSearchResult[] = []
|
|
112
|
+
|
|
113
|
+
for (const tool of this.listTools()) {
|
|
114
|
+
if (loading && !loading.has(resolveToolLoading(tool.policy))) continue
|
|
115
|
+
if (tool.policy.enabled === false) continue
|
|
116
|
+
|
|
117
|
+
const source = this.sources.get(tool.sourceId)
|
|
118
|
+
const toolset = this.toolsets.get(tool.toolsetId)
|
|
119
|
+
if (!source || !toolset) continue
|
|
120
|
+
|
|
121
|
+
const scored = scoreToolSearch({
|
|
122
|
+
terms,
|
|
123
|
+
tool,
|
|
124
|
+
source,
|
|
125
|
+
toolset,
|
|
126
|
+
})
|
|
127
|
+
if (scored.score <= 0) continue
|
|
128
|
+
|
|
129
|
+
results.push({
|
|
130
|
+
tool,
|
|
131
|
+
source,
|
|
132
|
+
toolset,
|
|
133
|
+
score: scored.score,
|
|
134
|
+
matched: scored.matched,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
results.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name))
|
|
139
|
+
return results.slice(0, options.limit ?? 5)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
toLLMTools(options: { readonly loading?: readonly ToolLoadingMode[] } = {}): LLMToolSchema[] {
|
|
143
|
+
const loading = options.loading ?? ['eager']
|
|
144
|
+
return this.getToolsByLoading(loading)
|
|
145
|
+
.filter((tool) => tool.policy.enabled !== false)
|
|
146
|
+
.map((tool) => tool.llmSchema ?? toolDefinitionToLLMTool(tool.definition))
|
|
147
|
+
.filter((tool): tool is LLMToolSchema => Boolean(tool))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createToolCatalogFromRegistry(
|
|
152
|
+
registry: ToolRegistryContract,
|
|
153
|
+
options: ToolCatalogFromRegistryOptions = {},
|
|
154
|
+
): ToolCatalog {
|
|
155
|
+
const source = options.source ?? DEFAULT_HOST_SOURCE
|
|
156
|
+
const toolset: ToolsetDefinition = {
|
|
157
|
+
...DEFAULT_HOST_TOOLSET,
|
|
158
|
+
...options.toolset,
|
|
159
|
+
sourceId: options.toolset?.sourceId ?? source.id,
|
|
160
|
+
}
|
|
161
|
+
const catalog = new ToolCatalog()
|
|
162
|
+
catalog.registerSource(source)
|
|
163
|
+
catalog.registerToolset(toolset)
|
|
164
|
+
|
|
165
|
+
for (const definition of registry.getAll()) {
|
|
166
|
+
const availability = registry.getAvailability(definition.name)
|
|
167
|
+
catalog.registerTool(
|
|
168
|
+
toolDefinitionToCatalogEntry(definition, {
|
|
169
|
+
availability,
|
|
170
|
+
sourceId: source.id,
|
|
171
|
+
toolsetId: toolset.id,
|
|
172
|
+
toolsetPolicy: toolset.defaultPolicy,
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return catalog
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function toolDefinitionToCatalogEntry(
|
|
181
|
+
definition: ToolDefinition,
|
|
182
|
+
input: {
|
|
183
|
+
readonly availability?: ToolAvailability
|
|
184
|
+
readonly sourceId: string
|
|
185
|
+
readonly toolsetId: string
|
|
186
|
+
readonly toolsetPolicy?: ToolsetPolicy
|
|
187
|
+
},
|
|
188
|
+
): ToolCatalogEntry {
|
|
189
|
+
const loading = loadingFromAvailability(input.availability ?? 'active')
|
|
190
|
+
return {
|
|
191
|
+
name: definition.name,
|
|
192
|
+
description: definition.description,
|
|
193
|
+
sourceId: input.sourceId,
|
|
194
|
+
toolsetId: input.toolsetId,
|
|
195
|
+
definition,
|
|
196
|
+
permissions: definition.permissions,
|
|
197
|
+
category: definition.category,
|
|
198
|
+
policy: {
|
|
199
|
+
...input.toolsetPolicy,
|
|
200
|
+
enabled: input.toolsetPolicy?.enabled === false ? false : loading !== 'suspended',
|
|
201
|
+
loading,
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function loadingFromAvailability(availability: ToolAvailability): ToolLoadingMode {
|
|
207
|
+
switch (availability) {
|
|
208
|
+
case 'deferred':
|
|
209
|
+
return 'deferred'
|
|
210
|
+
case 'suspended':
|
|
211
|
+
return 'suspended'
|
|
212
|
+
default:
|
|
213
|
+
return 'eager'
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveToolLoading(policy: ToolsetPolicy): ToolLoadingMode {
|
|
218
|
+
if (policy.enabled === false) return 'disabled'
|
|
219
|
+
return policy.loading ?? 'eager'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function toolDefinitionToLLMTool(definition: ToolDefinition | undefined): LLMToolSchema | null {
|
|
223
|
+
if (!definition) return null
|
|
224
|
+
return {
|
|
225
|
+
type: 'function',
|
|
226
|
+
function: {
|
|
227
|
+
name: definition.name,
|
|
228
|
+
description: definition.description,
|
|
229
|
+
parameters: zodToJsonSchema(definition.inputSchema, {
|
|
230
|
+
target: 'jsonSchema7',
|
|
231
|
+
$refStrategy: 'none',
|
|
232
|
+
}) as Record<string, unknown>,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function scoreToolSearch(input: {
|
|
238
|
+
readonly terms: readonly string[]
|
|
239
|
+
readonly tool: ToolCatalogEntry
|
|
240
|
+
readonly source: ToolSource
|
|
241
|
+
readonly toolset: ToolsetDefinition
|
|
242
|
+
}): { score: number; matched: string[] } {
|
|
243
|
+
const terms = input.terms.length > 0 ? input.terms : ['']
|
|
244
|
+
const matched = new Set<string>()
|
|
245
|
+
let score = 0
|
|
246
|
+
|
|
247
|
+
for (const term of terms) {
|
|
248
|
+
if (!term) continue
|
|
249
|
+
|
|
250
|
+
const toolName = input.tool.name.toLowerCase()
|
|
251
|
+
const description = input.tool.description.toLowerCase()
|
|
252
|
+
const sourceText =
|
|
253
|
+
`${input.source.name} ${input.source.description ?? ''} ${input.source.kind}`.toLowerCase()
|
|
254
|
+
const toolsetText = `${input.toolset.name} ${input.toolset.description ?? ''}`.toLowerCase()
|
|
255
|
+
|
|
256
|
+
if (toolName === term) {
|
|
257
|
+
score += 12
|
|
258
|
+
matched.add('name')
|
|
259
|
+
} else if (toolName.includes(term)) {
|
|
260
|
+
score += 8
|
|
261
|
+
matched.add('name')
|
|
262
|
+
}
|
|
263
|
+
if (description.includes(term)) {
|
|
264
|
+
score += 5
|
|
265
|
+
matched.add('description')
|
|
266
|
+
}
|
|
267
|
+
if (sourceText.includes(term)) {
|
|
268
|
+
score += 3
|
|
269
|
+
matched.add('source')
|
|
270
|
+
}
|
|
271
|
+
if (toolsetText.includes(term)) {
|
|
272
|
+
score += 2
|
|
273
|
+
matched.add('toolset')
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (input.tool.policy.preferred && score > 0) score += 1
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
score,
|
|
281
|
+
matched: [...matched],
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
8
|
+
import { SearchToolsTool } from '../../../tools/builtins/search-tools.js'
|
|
9
|
+
import type { RunId, SessionId, TenantId } from '../../../types/ids/index.js'
|
|
10
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
11
|
+
import type {
|
|
12
|
+
ChatCompletionParams,
|
|
13
|
+
LLMProvider,
|
|
14
|
+
StreamChunk,
|
|
15
|
+
} from '../../../types/provider/index.js'
|
|
16
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
17
|
+
import { drainQuery } from '../index.js'
|
|
18
|
+
|
|
19
|
+
const ZERO_USAGE = {
|
|
20
|
+
promptTokens: 0,
|
|
21
|
+
completionTokens: 0,
|
|
22
|
+
totalTokens: 0,
|
|
23
|
+
cachedTokens: 0,
|
|
24
|
+
cacheWriteTokens: 0,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class CapturingProvider implements LLMProvider {
|
|
28
|
+
readonly id = 'capturing'
|
|
29
|
+
readonly name = 'Capturing Provider'
|
|
30
|
+
lastParams?: ChatCompletionParams
|
|
31
|
+
|
|
32
|
+
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
33
|
+
this.lastParams = params
|
|
34
|
+
yield {
|
|
35
|
+
id: 'msg_1',
|
|
36
|
+
delta: { content: 'done' },
|
|
37
|
+
}
|
|
38
|
+
yield {
|
|
39
|
+
id: 'msg_1',
|
|
40
|
+
delta: {},
|
|
41
|
+
finishReason: 'stop',
|
|
42
|
+
usage: ZERO_USAGE,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function registerDeferredDocumentTool(tools: ToolRegistry, name = 'generate_document'): void {
|
|
48
|
+
tools.register(
|
|
49
|
+
{
|
|
50
|
+
name,
|
|
51
|
+
description: 'Generate a project document by document id.',
|
|
52
|
+
inputSchema: z.object({
|
|
53
|
+
documentId: z.string(),
|
|
54
|
+
}),
|
|
55
|
+
execute: async () => ({ success: true, output: 'generated' }),
|
|
56
|
+
},
|
|
57
|
+
'deferred',
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('query deferred tool discovery', () => {
|
|
62
|
+
let workdirs: string[] = []
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
66
|
+
workdirs = []
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('auto-exposes search_tools when deferred tools are registered', async () => {
|
|
70
|
+
const provider = new CapturingProvider()
|
|
71
|
+
const tools = new ToolRegistry()
|
|
72
|
+
registerDeferredDocumentTool(tools)
|
|
73
|
+
|
|
74
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-deferred-tools-'))
|
|
75
|
+
workdirs.push(workingDirectory)
|
|
76
|
+
|
|
77
|
+
const run = await drainQuery({
|
|
78
|
+
provider,
|
|
79
|
+
tools,
|
|
80
|
+
runConfig: {
|
|
81
|
+
model: 'mock-model',
|
|
82
|
+
timeoutMs: 5_000,
|
|
83
|
+
tokenBudget: 100_000,
|
|
84
|
+
maxIterations: 1,
|
|
85
|
+
maxResponseTokens: 256,
|
|
86
|
+
},
|
|
87
|
+
agentId: 'agent_test',
|
|
88
|
+
agentName: 'Test Agent',
|
|
89
|
+
messages: [createUserMessage('what tools can you use?')],
|
|
90
|
+
workingDirectory,
|
|
91
|
+
sessionId: 'ses_deferred_tools' as SessionId,
|
|
92
|
+
threadId: 'thd_deferred_tools' as ThreadId,
|
|
93
|
+
projectId: 'prj_deferred_tools' as ProjectId,
|
|
94
|
+
tenantId: 'tnt_deferred_tools' as TenantId,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(run.status).toBe('completed')
|
|
98
|
+
expect(tools.has(SearchToolsTool.name)).toBe(true)
|
|
99
|
+
expect(tools.getAvailability(SearchToolsTool.name)).toBe('active')
|
|
100
|
+
expect(tools.getAvailability('generate_document')).toBe('deferred')
|
|
101
|
+
|
|
102
|
+
const toolNames = provider.lastParams?.tools?.map((tool) => tool.function.name).sort() ?? []
|
|
103
|
+
expect(toolNames).toEqual([SearchToolsTool.name])
|
|
104
|
+
|
|
105
|
+
const systemPrompt = (provider.lastParams?.messages ?? [])
|
|
106
|
+
.filter((message) => message.role === 'system')
|
|
107
|
+
.map((message) => message.content)
|
|
108
|
+
.join('\n')
|
|
109
|
+
expect(systemPrompt).toContain('Use search_tools to load these before use:')
|
|
110
|
+
expect(systemPrompt).toContain('- generate_document')
|
|
111
|
+
expect(systemPrompt).not.toContain(
|
|
112
|
+
'Deferred tools are discoverable but not executable until the runtime activates them',
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('keeps search_tools executable when allowedTools names a deferred tool', async () => {
|
|
117
|
+
const provider = new CapturingProvider()
|
|
118
|
+
const tools = new ToolRegistry()
|
|
119
|
+
registerDeferredDocumentTool(tools)
|
|
120
|
+
|
|
121
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-deferred-tools-'))
|
|
122
|
+
workdirs.push(workingDirectory)
|
|
123
|
+
|
|
124
|
+
const run = await drainQuery({
|
|
125
|
+
provider,
|
|
126
|
+
tools,
|
|
127
|
+
allowedTools: ['generate_document'],
|
|
128
|
+
runConfig: {
|
|
129
|
+
model: 'mock-model',
|
|
130
|
+
timeoutMs: 5_000,
|
|
131
|
+
tokenBudget: 100_000,
|
|
132
|
+
maxIterations: 1,
|
|
133
|
+
maxResponseTokens: 256,
|
|
134
|
+
},
|
|
135
|
+
agentId: 'agent_test',
|
|
136
|
+
agentName: 'Test Agent',
|
|
137
|
+
messages: [createUserMessage('generate D-01')],
|
|
138
|
+
workingDirectory,
|
|
139
|
+
sessionId: 'ses_deferred_allowed_tools' as SessionId,
|
|
140
|
+
threadId: 'thd_deferred_allowed_tools' as ThreadId,
|
|
141
|
+
projectId: 'prj_deferred_allowed_tools' as ProjectId,
|
|
142
|
+
tenantId: 'tnt_deferred_allowed_tools' as TenantId,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(run.status).toBe('completed')
|
|
146
|
+
expect(tools.getAvailability('generate_document')).toBe('deferred')
|
|
147
|
+
|
|
148
|
+
const toolNames = provider.lastParams?.tools?.map((tool) => tool.function.name).sort() ?? []
|
|
149
|
+
expect(toolNames).toEqual([SearchToolsTool.name])
|
|
150
|
+
|
|
151
|
+
const systemPrompt = (provider.lastParams?.messages ?? [])
|
|
152
|
+
.filter((message) => message.role === 'system')
|
|
153
|
+
.map((message) => message.content)
|
|
154
|
+
.join('\n')
|
|
155
|
+
expect(systemPrompt).toContain('Use search_tools to load these before use:')
|
|
156
|
+
expect(systemPrompt).toContain('- generate_document')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('does not let search_tools reveal or activate deferred tools outside allowedTools', async () => {
|
|
160
|
+
const tools = new ToolRegistry()
|
|
161
|
+
registerDeferredDocumentTool(tools)
|
|
162
|
+
registerDeferredDocumentTool(tools, 'dangerous_delete_document')
|
|
163
|
+
|
|
164
|
+
const result = await SearchToolsTool.execute(
|
|
165
|
+
{ query: 'delete' },
|
|
166
|
+
{
|
|
167
|
+
runId: 'run_deferred_allowed_tools' as RunId,
|
|
168
|
+
workingDirectory: '/tmp',
|
|
169
|
+
abortSignal: new AbortController().signal,
|
|
170
|
+
env: {},
|
|
171
|
+
log: () => undefined,
|
|
172
|
+
toolRegistry: tools,
|
|
173
|
+
allowedTools: ['generate_document', SearchToolsTool.name],
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
expect(result.success).toBe(true)
|
|
178
|
+
expect(result.output).toContain('No deferred tools matching "delete"')
|
|
179
|
+
expect(result.output).not.toContain('dangerous_delete_document')
|
|
180
|
+
expect(tools.getAvailability('generate_document')).toBe('deferred')
|
|
181
|
+
expect(tools.getAvailability('dangerous_delete_document')).toBe('deferred')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ActivityStore } from '../../../store/activity/memory.js'
|
|
3
|
+
import type { RunId } from '../../../types/ids/index.js'
|
|
4
|
+
import type { ChatCompletionResponse } from '../../../types/provider/index.js'
|
|
5
|
+
import type { RunEvent } from '../../../types/run/index.js'
|
|
6
|
+
import type { ToolRegistryContract } from '../../../types/tool/index.js'
|
|
7
|
+
import type { Logger } from '../../../utils/logger.js'
|
|
8
|
+
import { ToolExecutor } from '../executor.js'
|
|
9
|
+
|
|
10
|
+
const mockRunId = 'run_test' as RunId
|
|
11
|
+
|
|
12
|
+
function makeLogger(): Logger {
|
|
13
|
+
const stub = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
|
14
|
+
return { ...stub, child: vi.fn(() => ({ ...stub, child: vi.fn() })) } as unknown as Logger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
18
|
+
|
|
19
|
+
/** Two tool calls in one assistant message (one batch). */
|
|
20
|
+
function twoCallResponse(name: string, a: object, b: object): ChatCompletionResponse {
|
|
21
|
+
return {
|
|
22
|
+
message: {
|
|
23
|
+
role: 'assistant',
|
|
24
|
+
content: null,
|
|
25
|
+
toolCalls: [
|
|
26
|
+
{ id: 'c1', type: 'function', function: { name, arguments: JSON.stringify(a) } },
|
|
27
|
+
{ id: 'c2', type: 'function', function: { name, arguments: JSON.stringify(b) } },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
finishReason: 'tool_calls',
|
|
31
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
32
|
+
} as ChatCompletionResponse
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('ToolExecutor — concurrencySafe batching', () => {
|
|
36
|
+
let activityStore: ActivityStore
|
|
37
|
+
let emitEvent: (e: RunEvent) => Promise<void>
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
activityStore = new ActivityStore(mockRunId, {
|
|
41
|
+
enabled: true,
|
|
42
|
+
trackToolCalls: true,
|
|
43
|
+
trackLlmTurns: true,
|
|
44
|
+
})
|
|
45
|
+
emitEvent = async () => {}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('serializes concurrency-unsafe tools so read-modify-write does not race', async () => {
|
|
49
|
+
// Shared mutable state, mutated via read → await → write (like `edit`).
|
|
50
|
+
let file = 'A'
|
|
51
|
+
const execute = vi.fn(async (_name: string, input: unknown) => {
|
|
52
|
+
const current = file // read
|
|
53
|
+
await delay(10) // window a parallel run would exploit
|
|
54
|
+
file = current + (input as { add: string }).add // write
|
|
55
|
+
return { success: true, output: 'ok' }
|
|
56
|
+
})
|
|
57
|
+
const tools = {
|
|
58
|
+
register: vi.fn(),
|
|
59
|
+
unregister: vi.fn(),
|
|
60
|
+
execute,
|
|
61
|
+
// edit/write/bash declare concurrencySafe:false → isConcurrencySafe()=>false
|
|
62
|
+
get: vi.fn(() => ({ isConcurrencySafe: () => false })),
|
|
63
|
+
has: vi.fn(() => true),
|
|
64
|
+
listNames: vi.fn(() => []),
|
|
65
|
+
getAvailability: vi.fn(),
|
|
66
|
+
} as unknown as ToolRegistryContract
|
|
67
|
+
|
|
68
|
+
const exec = new ToolExecutor(
|
|
69
|
+
{
|
|
70
|
+
tools,
|
|
71
|
+
runId: mockRunId,
|
|
72
|
+
workingDirectory: '/tmp',
|
|
73
|
+
permissionMode: 'auto',
|
|
74
|
+
env: {},
|
|
75
|
+
abortSignal: new AbortController().signal,
|
|
76
|
+
},
|
|
77
|
+
activityStore,
|
|
78
|
+
emitEvent,
|
|
79
|
+
makeLogger(),
|
|
80
|
+
)
|
|
81
|
+
await exec.executeBatch(twoCallResponse('edit', { add: 'B' }, { add: 'C' }))
|
|
82
|
+
// Serialized: A→AB→ABC. A racing run would lose one append (e.g. 'AC').
|
|
83
|
+
expect(file).toBe('ABC')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('runs concurrency-safe tools in parallel', async () => {
|
|
87
|
+
let active = 0
|
|
88
|
+
let maxActive = 0
|
|
89
|
+
const execute = vi.fn(async () => {
|
|
90
|
+
active++
|
|
91
|
+
maxActive = Math.max(maxActive, active)
|
|
92
|
+
await delay(10)
|
|
93
|
+
active--
|
|
94
|
+
return { success: true, output: 'ok' }
|
|
95
|
+
})
|
|
96
|
+
const tools = {
|
|
97
|
+
register: vi.fn(),
|
|
98
|
+
unregister: vi.fn(),
|
|
99
|
+
execute,
|
|
100
|
+
get: vi.fn(() => ({ isConcurrencySafe: () => true })),
|
|
101
|
+
has: vi.fn(() => true),
|
|
102
|
+
listNames: vi.fn(() => []),
|
|
103
|
+
getAvailability: vi.fn(),
|
|
104
|
+
} as unknown as ToolRegistryContract
|
|
105
|
+
|
|
106
|
+
const exec = new ToolExecutor(
|
|
107
|
+
{
|
|
108
|
+
tools,
|
|
109
|
+
runId: mockRunId,
|
|
110
|
+
workingDirectory: '/tmp',
|
|
111
|
+
permissionMode: 'auto',
|
|
112
|
+
env: {},
|
|
113
|
+
abortSignal: new AbortController().signal,
|
|
114
|
+
},
|
|
115
|
+
activityStore,
|
|
116
|
+
emitEvent,
|
|
117
|
+
makeLogger(),
|
|
118
|
+
)
|
|
119
|
+
await exec.executeBatch(twoCallResponse('grep', { p: '1' }, { p: '2' }))
|
|
120
|
+
expect(maxActive).toBe(2) // both ran at once
|
|
121
|
+
})
|
|
122
|
+
})
|