@namzu/sdk 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +11 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +118 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -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/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +242 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
- package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
- package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
- package/dist/tools/builtins/atomic-write-file.js +31 -2
- package/dist/tools/builtins/atomic-write-file.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
- package/dist/tools/builtins/content-fingerprint.js +30 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +172 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +5 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +75 -27
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +75 -5
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +17 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +16 -1
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/executor.ts +9 -1
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +130 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
- package/src/tools/builtins/__tests__/edit.test.ts +324 -1
- package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
- package/src/tools/builtins/atomic-write-file.ts +30 -2
- package/src/tools/builtins/content-fingerprint.ts +33 -0
- package/src/tools/builtins/edit.ts +188 -36
- package/src/tools/builtins/read-file.ts +5 -2
- package/src/tools/builtins/write-file.ts +79 -28
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +116 -42
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/tool/index.ts +17 -1
- package/src/types/toolset/index.ts +22 -0
package/src/public-types.ts
CHANGED
|
@@ -134,6 +134,7 @@ export type {
|
|
|
134
134
|
HybridExecutionContextOptions,
|
|
135
135
|
LocalExecutionContextOptions,
|
|
136
136
|
MCPServerResourceProvider,
|
|
137
|
+
MCPServerPromptProvider,
|
|
137
138
|
MCPServerToolProvider,
|
|
138
139
|
RemoteExecutionContextOptions,
|
|
139
140
|
TenantConnectorManagerConfig,
|
|
@@ -157,6 +158,10 @@ export type { ToolCallContext } from './verification/index.js'
|
|
|
157
158
|
export type { DiskSessionStoreConfig, LinkageView } from './store/session/index.js'
|
|
158
159
|
|
|
159
160
|
export type {
|
|
161
|
+
ContextReducer,
|
|
162
|
+
ContextReduction,
|
|
163
|
+
ContextReductionReason,
|
|
164
|
+
SlidingWindowOptions,
|
|
160
165
|
ConversationManager,
|
|
161
166
|
CompactionStrategy,
|
|
162
167
|
DanglingResult,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { EmbeddingProvider, TenantScope } from '../../types/rag/index.js'
|
|
4
|
+
import { DefaultIngestionPipeline } from '../ingestion.js'
|
|
5
|
+
import { DefaultRetriever } from '../retriever.js'
|
|
6
|
+
import { InMemoryVectorStore } from '../vector-store.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `TenantScope.namespace` and `KnowledgeBaseConfig.namespace` were declared
|
|
10
|
+
* from the start and neither reached storage. Ingestion copied
|
|
11
|
+
* `scope.tenantId` onto every chunk and dropped the namespace; the store
|
|
12
|
+
* filtered on tenant alone. So a partition a host asked for did not exist,
|
|
13
|
+
* and every namespace in a tenant saw every other one's documents.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Deterministic and content-derived, so similarity is stable across runs. */
|
|
17
|
+
const embedder: EmbeddingProvider = {
|
|
18
|
+
async embed(texts: string[]) {
|
|
19
|
+
return texts.map((t) => [t.length % 7, t.charCodeAt(0) % 5, 1])
|
|
20
|
+
},
|
|
21
|
+
async embedQuery(text: string) {
|
|
22
|
+
return [text.length % 7, text.charCodeAt(0) % 5, 1]
|
|
23
|
+
},
|
|
24
|
+
dimensions: 3,
|
|
25
|
+
model: 'test-embedder',
|
|
26
|
+
id: 'test-embedder',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tenant = 'tnt_one' as TenantScope['tenantId']
|
|
30
|
+
|
|
31
|
+
function scope(namespace?: string): TenantScope {
|
|
32
|
+
return { tenantId: tenant, ...(namespace !== undefined ? { namespace } : {}) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function seed() {
|
|
36
|
+
const store = new InMemoryVectorStore()
|
|
37
|
+
const ingestion = new DefaultIngestionPipeline(store, embedder)
|
|
38
|
+
const kb = 'kb_one' as never
|
|
39
|
+
|
|
40
|
+
await ingestion.ingest('alpha partition content', {}, scope('alpha'), kb)
|
|
41
|
+
await ingestion.ingest('beta partition content', {}, scope('beta'), kb)
|
|
42
|
+
await ingestion.ingest('default partition content', {}, scope(), kb)
|
|
43
|
+
|
|
44
|
+
return { store, retriever: new DefaultRetriever(store, embedder), kb }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const texts = (r: { chunks: { chunk: { content: string } }[] }) =>
|
|
48
|
+
r.chunks.map((c) => c.chunk.content)
|
|
49
|
+
|
|
50
|
+
describe('a namespace partitions what a query can see', () => {
|
|
51
|
+
it('returns only its own partition', async () => {
|
|
52
|
+
const { retriever, kb } = await seed()
|
|
53
|
+
|
|
54
|
+
const found = await retriever.retrieve({ text: 'partition content' }, scope('alpha'), kb)
|
|
55
|
+
|
|
56
|
+
expect(texts(found)).toEqual(['alpha partition content'])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('does not leak the other partition', async () => {
|
|
60
|
+
const { retriever, kb } = await seed()
|
|
61
|
+
|
|
62
|
+
const found = await retriever.retrieve({ text: 'partition content' }, scope('beta'), kb)
|
|
63
|
+
|
|
64
|
+
expect(texts(found).join()).not.toContain('alpha')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('treats an absent namespace as the default partition, not as no filter', async () => {
|
|
68
|
+
const { retriever, kb } = await seed()
|
|
69
|
+
|
|
70
|
+
const found = await retriever.retrieve({ text: 'partition content' }, scope(), kb)
|
|
71
|
+
|
|
72
|
+
// The whole point. Reading absence as "no filter" is how the boundary
|
|
73
|
+
// leaks: a caller who never asked for a namespace would see every
|
|
74
|
+
// namespaced chunk in the tenant.
|
|
75
|
+
expect(texts(found)).toEqual(['default partition content'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('keeps tenants apart as it always did', async () => {
|
|
79
|
+
const { retriever, kb } = await seed()
|
|
80
|
+
|
|
81
|
+
const found = await retriever.retrieve(
|
|
82
|
+
{ text: 'partition content' },
|
|
83
|
+
{
|
|
84
|
+
tenantId: 'tnt_other' as TenantScope['tenantId'],
|
|
85
|
+
namespace: 'alpha',
|
|
86
|
+
},
|
|
87
|
+
kb,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
expect(found.chunks).toEqual([])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('stamps the namespace onto the chunk at ingest', async () => {
|
|
94
|
+
const { store } = await seed()
|
|
95
|
+
|
|
96
|
+
const results = await store.search({
|
|
97
|
+
embedding: await embedder.embedQuery('partition content'),
|
|
98
|
+
topK: 10,
|
|
99
|
+
tenantId: tenant,
|
|
100
|
+
namespace: 'alpha',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Ingest is where it has to land — filtering at search against a value
|
|
104
|
+
// nothing writes returns zero rows, and "no results" reads as "nothing
|
|
105
|
+
// matched" rather than "this scope was never stored".
|
|
106
|
+
expect(results.every((r) => r.chunk.content.startsWith('alpha'))).toBe(true)
|
|
107
|
+
expect(results.length).toBeGreaterThan(0)
|
|
108
|
+
})
|
|
109
|
+
})
|
package/src/rag/ingestion.ts
CHANGED
|
@@ -48,6 +48,7 @@ export class DefaultIngestionPipeline implements IngestionPipeline {
|
|
|
48
48
|
documentId,
|
|
49
49
|
knowledgeBaseId,
|
|
50
50
|
tenantId: scope.tenantId,
|
|
51
|
+
...(scope.namespace !== undefined ? { namespace: scope.namespace } : {}),
|
|
51
52
|
content: cc.content,
|
|
52
53
|
index: cc.index,
|
|
53
54
|
tokenCount: estimateTokens(cc.content),
|
package/src/rag/retriever.ts
CHANGED
|
@@ -76,6 +76,7 @@ export class DefaultRetriever implements Retriever {
|
|
|
76
76
|
embedding,
|
|
77
77
|
topK: config.topK,
|
|
78
78
|
tenantId: scope.tenantId,
|
|
79
|
+
...(scope.namespace !== undefined ? { namespace: scope.namespace } : {}),
|
|
79
80
|
knowledgeBaseId,
|
|
80
81
|
minScore: config.minScore,
|
|
81
82
|
})
|
|
@@ -92,6 +93,7 @@ export class DefaultRetriever implements Retriever {
|
|
|
92
93
|
embedding,
|
|
93
94
|
topK: config.topK * 2,
|
|
94
95
|
tenantId: scope.tenantId,
|
|
96
|
+
...(scope.namespace !== undefined ? { namespace: scope.namespace } : {}),
|
|
95
97
|
knowledgeBaseId,
|
|
96
98
|
minScore: 0,
|
|
97
99
|
})
|
package/src/rag/vector-store.ts
CHANGED
|
@@ -20,6 +20,11 @@ export class InMemoryVectorStore implements VectorStore {
|
|
|
20
20
|
|
|
21
21
|
for (const chunk of this.chunks.values()) {
|
|
22
22
|
if (chunk.tenantId !== query.tenantId) continue
|
|
23
|
+
// Equality INCLUDING absence: an omitted namespace is the default
|
|
24
|
+
// partition, not the absence of a filter. Treating it as no filter
|
|
25
|
+
// is how the boundary leaks — a caller who never asked for a
|
|
26
|
+
// namespace would see every namespaced chunk in the tenant.
|
|
27
|
+
if (chunk.namespace !== query.namespace) continue
|
|
23
28
|
if (query.knowledgeBaseId && chunk.knowledgeBaseId !== query.knowledgeBaseId) continue
|
|
24
29
|
if (!chunk.embedding) continue
|
|
25
30
|
|
|
@@ -375,151 +375,155 @@ Executable tool names, descriptions, and JSON input schemas are attached through
|
|
|
375
375
|
: otelContext.active()
|
|
376
376
|
|
|
377
377
|
return tracer.startActiveSpan(toolSpanName(toolName), {}, parentCtx, async (span) => {
|
|
378
|
-
|
|
379
|
-
[GENAI.TOOL_NAME]: toolName,
|
|
380
|
-
[GENAI.TOOL_TYPE]: 'function',
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
const tool = this.getOrThrow(toolName)
|
|
384
|
-
|
|
385
|
-
const availability = this.getAvailability(toolName)
|
|
386
|
-
if (availability !== 'active') {
|
|
387
|
-
const msg = `Tool "${toolName}" is ${availability} and cannot be executed`
|
|
388
|
-
this.log.warn(msg)
|
|
378
|
+
try {
|
|
389
379
|
span.setAttributes({
|
|
390
|
-
[
|
|
391
|
-
[
|
|
380
|
+
[GENAI.TOOL_NAME]: toolName,
|
|
381
|
+
[GENAI.TOOL_TYPE]: 'function',
|
|
392
382
|
})
|
|
393
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: msg })
|
|
394
|
-
span.end()
|
|
395
|
-
return {
|
|
396
|
-
success: false,
|
|
397
|
-
output: '',
|
|
398
|
-
error: msg,
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
383
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
406
|
-
const msg = `
|
|
384
|
+
const tool = this.getOrThrow(toolName)
|
|
385
|
+
|
|
386
|
+
const availability = this.getAvailability(toolName)
|
|
387
|
+
if (availability !== 'active') {
|
|
388
|
+
const msg = `Tool "${toolName}" is ${availability} and cannot be executed`
|
|
389
|
+
this.log.warn(msg)
|
|
407
390
|
span.setAttributes({
|
|
408
391
|
[NAMZU.TOOL_SUCCESS]: false,
|
|
409
392
|
[NAMZU.TOOL_ERROR]: msg,
|
|
410
393
|
})
|
|
411
394
|
span.setStatus({ code: SpanStatusCode.ERROR, message: msg })
|
|
412
|
-
span.end()
|
|
413
395
|
return {
|
|
414
396
|
success: false,
|
|
415
397
|
output: '',
|
|
416
398
|
error: msg,
|
|
417
|
-
permissionDenied: true,
|
|
418
|
-
permissionMessage: msg,
|
|
419
399
|
}
|
|
420
400
|
}
|
|
421
|
-
}
|
|
422
401
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
rawInput === undefined ||
|
|
443
|
-
(typeof rawInput === 'object' &&
|
|
444
|
-
!Array.isArray(rawInput) &&
|
|
445
|
-
Object.keys(rawInput as Record<string, unknown>).length === 0)
|
|
446
|
-
|
|
447
|
-
const requiredHint = describeRequiredInput(tool.inputSchema)
|
|
448
|
-
// A conditional schema's required shape cannot be reconstructed
|
|
449
|
-
// from JSON Schema's top-level `required`, so the author gets to
|
|
450
|
-
// say what a valid retry looks like.
|
|
451
|
-
const recoveryHint = tool.validationErrorHint?.trim()
|
|
452
|
-
? ` ${tool.validationErrorHint.trim()}`
|
|
453
|
-
: ''
|
|
454
|
-
|
|
455
|
-
const enrichedMessage = isEmptyInput
|
|
456
|
-
? `Tool "${toolName}" was called with no arguments. ${requiredHint}${recoveryHint} Retry the call with the required parameters populated.`
|
|
457
|
-
: `Validation failed for "${toolName}": ${errorMessage}. ${requiredHint}${recoveryHint}`
|
|
458
|
-
|
|
459
|
-
this.log.error(`Tool input validation failed: ${toolName}`, {
|
|
460
|
-
errors: errorMessage,
|
|
461
|
-
empty: isEmptyInput,
|
|
462
|
-
})
|
|
402
|
+
const mode = context.permissionContext?.mode ?? 'auto'
|
|
403
|
+
if (mode === 'plan') {
|
|
404
|
+
const isReadOnly = tool.isReadOnly ? tool.isReadOnly(rawInput) : false
|
|
405
|
+
if (!isReadOnly) {
|
|
406
|
+
const msg = `plan mode: non-read-only tool "${toolName}" blocked`
|
|
407
|
+
span.setAttributes({
|
|
408
|
+
[NAMZU.TOOL_SUCCESS]: false,
|
|
409
|
+
[NAMZU.TOOL_ERROR]: msg,
|
|
410
|
+
})
|
|
411
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: msg })
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
output: '',
|
|
415
|
+
error: msg,
|
|
416
|
+
permissionDenied: true,
|
|
417
|
+
permissionMessage: msg,
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
463
421
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
422
|
+
const parseResult = tool.inputSchema.safeParse(rawInput)
|
|
423
|
+
if (!parseResult.success) {
|
|
424
|
+
const errorMessage = parseResult.error.issues
|
|
425
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
426
|
+
.join('; ')
|
|
427
|
+
|
|
428
|
+
// Distinguish "model sent an empty/no-arg call" from
|
|
429
|
+
// "model sent partial args" — the first is most often a
|
|
430
|
+
// streaming hiccup or a definition-test ping (a provider
|
|
431
|
+
// occasionally pings tool surfaces with `{}` while the
|
|
432
|
+
// schema is still loading), the second is a genuine
|
|
433
|
+
// programming mistake by the model. The model self-
|
|
434
|
+
// corrects MUCH more reliably when the error tells it
|
|
435
|
+
// (a) which fields are required, (b) their types, and
|
|
436
|
+
// (c) a minimal example call. Without these hints the
|
|
437
|
+
// downstream UI just shows a red "Failed" row and the
|
|
438
|
+
// model rarely retries with the right args.
|
|
439
|
+
const isEmptyInput =
|
|
440
|
+
rawInput === null ||
|
|
441
|
+
rawInput === undefined ||
|
|
442
|
+
(typeof rawInput === 'object' &&
|
|
443
|
+
!Array.isArray(rawInput) &&
|
|
444
|
+
Object.keys(rawInput as Record<string, unknown>).length === 0)
|
|
445
|
+
|
|
446
|
+
const requiredHint = describeRequiredInput(tool.inputSchema)
|
|
447
|
+
// A conditional schema's required shape cannot be reconstructed
|
|
448
|
+
// from JSON Schema's top-level `required`, so the author gets to
|
|
449
|
+
// say what a valid retry looks like.
|
|
450
|
+
const recoveryHint = tool.validationErrorHint?.trim()
|
|
451
|
+
? ` ${tool.validationErrorHint.trim()}`
|
|
452
|
+
: ''
|
|
453
|
+
|
|
454
|
+
const enrichedMessage = isEmptyInput
|
|
455
|
+
? `Tool "${toolName}" was called with no arguments. ${requiredHint}${recoveryHint} Retry the call with the required parameters populated.`
|
|
456
|
+
: `Validation failed for "${toolName}": ${errorMessage}. ${requiredHint}${recoveryHint}`
|
|
457
|
+
|
|
458
|
+
this.log.error(`Tool input validation failed: ${toolName}`, {
|
|
459
|
+
errors: errorMessage,
|
|
460
|
+
empty: isEmptyInput,
|
|
461
|
+
})
|
|
470
462
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
463
|
+
span.setAttributes({
|
|
464
|
+
[NAMZU.TOOL_SUCCESS]: false,
|
|
465
|
+
[NAMZU.TOOL_ERROR]: `Validation: ${errorMessage}`,
|
|
466
|
+
})
|
|
467
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
output: '',
|
|
472
|
+
error: enrichedMessage,
|
|
473
|
+
}
|
|
475
474
|
}
|
|
476
|
-
}
|
|
477
475
|
|
|
478
|
-
|
|
476
|
+
const finalInput = parseResult.data
|
|
479
477
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
478
|
+
try {
|
|
479
|
+
this.log.debug(`Executing tool: ${toolName}`)
|
|
480
|
+
const startedAt = Date.now()
|
|
481
|
+
const result = await tool.execute(finalInput, context)
|
|
482
|
+
const durationMs = Date.now() - startedAt
|
|
483
|
+
this.log.debug(`Tool completed: ${toolName}`, {
|
|
484
|
+
success: result.success,
|
|
485
|
+
})
|
|
488
486
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
487
|
+
span.setAttribute(NAMZU.TOOL_SUCCESS, result.success)
|
|
488
|
+
recordToolCall(
|
|
489
|
+
toolName,
|
|
490
|
+
result.success,
|
|
491
|
+
result.success ? undefined : result.error,
|
|
492
|
+
durationMs,
|
|
493
|
+
)
|
|
494
|
+
if (!result.success && result.error) {
|
|
495
|
+
span.setAttribute(NAMZU.TOOL_ERROR, result.error)
|
|
496
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error })
|
|
497
|
+
} else {
|
|
498
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
499
|
+
}
|
|
502
500
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
501
|
+
return result
|
|
502
|
+
} catch (err) {
|
|
503
|
+
const errorMessage = toErrorMessage(err)
|
|
504
|
+
this.log.error(`Tool execution error: ${toolName}`, {
|
|
505
|
+
error: errorMessage,
|
|
506
|
+
})
|
|
509
507
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
508
|
+
span.setAttributes({
|
|
509
|
+
[NAMZU.TOOL_SUCCESS]: false,
|
|
510
|
+
[NAMZU.TOOL_ERROR]: errorMessage,
|
|
511
|
+
})
|
|
512
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
|
|
513
|
+
span.recordException(err instanceof Error ? err : new Error(errorMessage))
|
|
516
514
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
output: '',
|
|
518
|
+
error: `Tool "${toolName}" execution failed: ${errorMessage}`,
|
|
519
|
+
}
|
|
521
520
|
}
|
|
522
521
|
} finally {
|
|
522
|
+
// The only place this span ends. It used to be ended at three
|
|
523
|
+
// early returns and in a finally that opened below them, so
|
|
524
|
+
// anything throwing before that try — `getOrThrow` on a name the
|
|
525
|
+
// registry does not hold, for one — left the span open and the
|
|
526
|
+
// tool's trace unclosed.
|
|
523
527
|
span.end()
|
|
524
528
|
}
|
|
525
529
|
})
|
|
@@ -44,6 +44,26 @@ const NO_VISION_CAPABILITIES: ProviderCapabilities = {
|
|
|
44
44
|
supportsVision: false,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A tool that opts in to constrained generation, which is the only thing
|
|
49
|
+
* that makes `enforceToolInputSchema` non-empty on the wire.
|
|
50
|
+
*/
|
|
51
|
+
function registerEnforcedTool(tools: ToolRegistry, name: string): void {
|
|
52
|
+
tools.register({
|
|
53
|
+
name,
|
|
54
|
+
description: `${name} tool`,
|
|
55
|
+
inputSchema: z.object({ new_string: z.string().optional(), newStr: z.string().optional() }),
|
|
56
|
+
modelInputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: { new_string: { type: 'string' } },
|
|
59
|
+
required: ['new_string'],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
enforceModelInput: true,
|
|
63
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
function registerEchoTool(tools: ToolRegistry): void {
|
|
48
68
|
tools.register({
|
|
49
69
|
name: 'echo',
|
|
@@ -229,4 +249,53 @@ describe('query() capability negotiation', () => {
|
|
|
229
249
|
}),
|
|
230
250
|
).rejects.toThrow(/supportsVision: false/)
|
|
231
251
|
})
|
|
252
|
+
|
|
253
|
+
it('names the enforced tools on the request, so a driver can constrain them', async () => {
|
|
254
|
+
const provider = capturingProvider()
|
|
255
|
+
const tools = new ToolRegistry()
|
|
256
|
+
registerEnforcedTool(tools, 'edit')
|
|
257
|
+
registerEnforcedTool(tools, 'write')
|
|
258
|
+
registerEchoTool(tools)
|
|
259
|
+
|
|
260
|
+
const run = await drainQuery({
|
|
261
|
+
...baseParams(provider, tools, await mkWorkdir()),
|
|
262
|
+
messages: [createUserMessage('hello')],
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
expect(run.status).toBe('completed')
|
|
266
|
+
// The producer was deleted, so this was undefined on every request and
|
|
267
|
+
// the three drivers that read it were reading a field nothing ever
|
|
268
|
+
// set — `enforceModelInput` on a tool meant nothing end to end.
|
|
269
|
+
expect(provider.requests.at(-1)?.enforceToolInputSchema).toEqual(['edit', 'write'])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('omits the field entirely when no tool opts in', async () => {
|
|
273
|
+
const provider = capturingProvider()
|
|
274
|
+
const tools = new ToolRegistry()
|
|
275
|
+
registerEchoTool(tools)
|
|
276
|
+
|
|
277
|
+
await drainQuery({
|
|
278
|
+
...baseParams(provider, tools, await mkWorkdir()),
|
|
279
|
+
messages: [createUserMessage('hello')],
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// An empty array would read as "enforce nothing" rather than "nothing
|
|
283
|
+
// asked", and a driver cannot tell those two apart.
|
|
284
|
+
expect(provider.requests.at(-1)?.enforceToolInputSchema).toBeUndefined()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('follows the allowed set rather than everything registered', async () => {
|
|
288
|
+
const provider = capturingProvider()
|
|
289
|
+
const tools = new ToolRegistry()
|
|
290
|
+
registerEnforcedTool(tools, 'edit')
|
|
291
|
+
registerEnforcedTool(tools, 'excluded')
|
|
292
|
+
|
|
293
|
+
await drainQuery({
|
|
294
|
+
...baseParams(provider, tools, await mkWorkdir()),
|
|
295
|
+
allowedTools: ['edit'],
|
|
296
|
+
messages: [createUserMessage('hello')],
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
expect(provider.requests.at(-1)?.enforceToolInputSchema).toEqual(['edit'])
|
|
300
|
+
})
|
|
232
301
|
})
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
|
|
6
|
+
import { MockLLMProvider } from '../../../provider/mock.js'
|
|
7
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
8
|
+
import type { SessionId, TenantId } from '../../../types/ids/index.js'
|
|
9
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
10
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
11
|
+
import type { Skill } from '../../../types/skills/index.js'
|
|
12
|
+
import { drainQuery } from '../index.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A run's skills are fixed at `query()` time and rendered into the cached
|
|
16
|
+
* system prefix, so every skill a run might ever need is paid for on every
|
|
17
|
+
* turn. A phased agent rarely needs them all at once.
|
|
18
|
+
*
|
|
19
|
+
* A peer runtime resolves instructions, model, tools, skills and subagents
|
|
20
|
+
* from context at run time. namzu had the first three; this is the fourth.
|
|
21
|
+
* The fifth is deliberately absent — see the note at the bottom.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
let workdirs: string[] = []
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
|
|
28
|
+
workdirs = []
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function skill(name: string, body?: string): Skill {
|
|
32
|
+
return {
|
|
33
|
+
metadata: { name, description: `${name} description` },
|
|
34
|
+
dirPath: `/skills/${name}`,
|
|
35
|
+
...(body !== undefined ? { body } : {}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function run(provider: MockLLMProvider, over: Record<string, unknown> = {}): Promise<void> {
|
|
40
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
|
|
41
|
+
workdirs.push(dir)
|
|
42
|
+
await drainQuery({
|
|
43
|
+
provider,
|
|
44
|
+
tools: new ToolRegistry(),
|
|
45
|
+
runConfig: {
|
|
46
|
+
model: 'mock-model',
|
|
47
|
+
timeoutMs: 30_000,
|
|
48
|
+
tokenBudget: 100_000,
|
|
49
|
+
maxIterations: 4,
|
|
50
|
+
maxResponseTokens: 256,
|
|
51
|
+
},
|
|
52
|
+
agentId: 'agent_sk',
|
|
53
|
+
agentName: 'Skill Agent',
|
|
54
|
+
workingDirectory: dir,
|
|
55
|
+
sessionId: 'ses_sk' as SessionId,
|
|
56
|
+
threadId: 'thd_sk' as ThreadId,
|
|
57
|
+
projectId: 'prj_sk' as ProjectId,
|
|
58
|
+
tenantId: 'tnt_sk' as TenantId,
|
|
59
|
+
messages: [createUserMessage('go')],
|
|
60
|
+
...over,
|
|
61
|
+
} as never)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sent = (provider: MockLLMProvider, index = 0): string =>
|
|
65
|
+
JSON.stringify(provider.requests.at(index)?.messages ?? [])
|
|
66
|
+
|
|
67
|
+
describe('a step can put a skill in front of the model', () => {
|
|
68
|
+
it('renders the skill it named', async () => {
|
|
69
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
70
|
+
|
|
71
|
+
await run(provider, { prepareStep: () => ({ skills: [skill('search-the-web')] }) })
|
|
72
|
+
|
|
73
|
+
expect(sent(provider)).toContain('search-the-web')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('sends nothing extra when the step names none', async () => {
|
|
77
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
78
|
+
|
|
79
|
+
await run(provider)
|
|
80
|
+
|
|
81
|
+
expect(sent(provider)).not.toContain('Available Skills')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('treats an empty list as naming none', async () => {
|
|
85
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
86
|
+
|
|
87
|
+
await run(provider, { prepareStep: () => ({ skills: [] }) })
|
|
88
|
+
|
|
89
|
+
// An empty list is a caller saying "no skills this step", not a
|
|
90
|
+
// request for an empty section header.
|
|
91
|
+
expect(sent(provider)).not.toContain('Available Skills')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('carries the step guidance alongside it', async () => {
|
|
95
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
96
|
+
|
|
97
|
+
await run(provider, {
|
|
98
|
+
prepareStep: () => ({ system: 'You are researching.', skills: [skill('search-the-web')] }),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Both ride the same ephemeral trailing message; neither should
|
|
102
|
+
// displace the other.
|
|
103
|
+
const body = sent(provider)
|
|
104
|
+
expect(body).toContain('You are researching.')
|
|
105
|
+
expect(body).toContain('search-the-web')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('a step skill does not outlive its step', () => {
|
|
110
|
+
it('is absent from the next step that does not ask for it', async () => {
|
|
111
|
+
const provider = new MockLLMProvider({
|
|
112
|
+
turns: [{ toolCalls: [] as never, text: 'thinking' }, { text: 'done' }],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await run(provider, {
|
|
116
|
+
prepareStep: ({ stepNumber }: { stepNumber: number }) =>
|
|
117
|
+
stepNumber === 1 ? { skills: [skill('search-the-web')] } : {},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(sent(provider, 0)).toContain('search-the-web')
|
|
121
|
+
if (provider.requests.length > 1) {
|
|
122
|
+
expect(sent(provider, 1)).not.toContain('search-the-web')
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does not accumulate across steps', async () => {
|
|
127
|
+
const provider = new MockLLMProvider({
|
|
128
|
+
turns: [{ text: 'still working' }, { text: 'done' }],
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await run(provider, {
|
|
132
|
+
maxIterations: 2,
|
|
133
|
+
prepareStep: () => ({ skills: [skill('search-the-web')] }),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Appended for the call, never retained. Were it written into the run's
|
|
137
|
+
// history the section would stack up turn after turn — the same
|
|
138
|
+
// message repeated, growing the prompt and invalidating the cached
|
|
139
|
+
// prefix every iteration.
|
|
140
|
+
const systemCounts = provider.requests.map(
|
|
141
|
+
(r) => (r.messages ?? []).filter((m) => m.role === 'system').length,
|
|
142
|
+
)
|
|
143
|
+
expect(new Set(systemCounts).size).toBe(1)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sub-agents are deliberately NOT per-step, and this is the note rather than
|
|
149
|
+
* a test that would pretend otherwise. Which agents `create_task` can reach
|
|
150
|
+
* is baked into that tool's input schema, so varying it per step would
|
|
151
|
+
* rebuild the tool catalogue every turn — a worse prompt-cache trade than
|
|
152
|
+
* moving tools around, for a narrowing a step can already express by
|
|
153
|
+
* withholding `create_task` through `activeTools`.
|
|
154
|
+
*/
|