@namzu/sdk 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +177 -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 +4 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -2
- 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__/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/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 +96 -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/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 +13 -1
- 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/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 +8 -0
- 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__/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/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +103 -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/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +14 -1
- 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/toolset/index.ts +22 -0
|
@@ -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
|
})
|
|
@@ -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
|
+
*/
|
|
@@ -0,0 +1,180 @@
|
|
|
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 { MockLLMProvider } from '../../../provider/mock.js'
|
|
8
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
9
|
+
import type { SessionId, TenantId } from '../../../types/ids/index.js'
|
|
10
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
11
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
12
|
+
import { drainQuery } from '../index.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Forcing a tool is the one step-shaping knob that can hang an agent.
|
|
16
|
+
*
|
|
17
|
+
* A forced choice that PERSISTS makes the model call a tool, read the
|
|
18
|
+
* result, and be forced again — forever. The peer SDK that puts
|
|
19
|
+
* `tool_choice` on persistent model settings carries three moving parts to
|
|
20
|
+
* undo that: a tool-use tracker, an opt-out flag, and a reset applied at
|
|
21
|
+
* two separate call sites. Its flag defaults to on precisely because
|
|
22
|
+
* turning it off hangs the agent.
|
|
23
|
+
*
|
|
24
|
+
* Putting the knob on `prepareStep` removes the failure instead of
|
|
25
|
+
* managing it: the next step is prepared from scratch, so a force cannot
|
|
26
|
+
* outlive the step that asked for it. There is no flag to get wrong.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
let workdirs: string[] = []
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
|
|
33
|
+
workdirs = []
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
async function mkWorkdir(): Promise<string> {
|
|
37
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-tool-choice-'))
|
|
38
|
+
workdirs.push(dir)
|
|
39
|
+
return dir
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registerEcho(tools: ToolRegistry): void {
|
|
43
|
+
tools.register({
|
|
44
|
+
name: 'echo',
|
|
45
|
+
description: 'Echo the text back.',
|
|
46
|
+
inputSchema: z.object({ text: z.string().optional() }),
|
|
47
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function baseParams(provider: MockLLMProvider, tools: ToolRegistry) {
|
|
52
|
+
return {
|
|
53
|
+
provider,
|
|
54
|
+
tools,
|
|
55
|
+
runConfig: {
|
|
56
|
+
model: 'mock-model',
|
|
57
|
+
timeoutMs: 30_000,
|
|
58
|
+
tokenBudget: 100_000,
|
|
59
|
+
maxIterations: 4,
|
|
60
|
+
maxResponseTokens: 256,
|
|
61
|
+
},
|
|
62
|
+
agentId: 'agent_tc',
|
|
63
|
+
agentName: 'Tool Choice Agent',
|
|
64
|
+
workingDirectory: await mkWorkdir(),
|
|
65
|
+
sessionId: 'ses_tc' as SessionId,
|
|
66
|
+
threadId: 'thd_tc' as ThreadId,
|
|
67
|
+
projectId: 'prj_tc' as ProjectId,
|
|
68
|
+
tenantId: 'tnt_tc' as TenantId,
|
|
69
|
+
messages: [createUserMessage('go')],
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('a step can force the model to call a tool', () => {
|
|
74
|
+
it('puts the caller choice on that step request', async () => {
|
|
75
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
76
|
+
const tools = new ToolRegistry()
|
|
77
|
+
registerEcho(tools)
|
|
78
|
+
|
|
79
|
+
await drainQuery({
|
|
80
|
+
...(await baseParams(provider, tools)),
|
|
81
|
+
prepareStep: () => ({ toolChoice: 'required' as const }),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(provider.requests.at(0)?.toolChoice).toBe('required')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('carries a named function through unchanged', async () => {
|
|
88
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
89
|
+
const tools = new ToolRegistry()
|
|
90
|
+
registerEcho(tools)
|
|
91
|
+
|
|
92
|
+
await drainQuery({
|
|
93
|
+
...(await baseParams(provider, tools)),
|
|
94
|
+
prepareStep: () => ({
|
|
95
|
+
toolChoice: { type: 'function' as const, function: { name: 'echo' } },
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(provider.requests.at(0)?.toolChoice).toEqual({
|
|
100
|
+
type: 'function',
|
|
101
|
+
function: { name: 'echo' },
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('leaves the request alone when no step asks', async () => {
|
|
106
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
107
|
+
const tools = new ToolRegistry()
|
|
108
|
+
registerEcho(tools)
|
|
109
|
+
|
|
110
|
+
await drainQuery(await baseParams(provider, tools))
|
|
111
|
+
|
|
112
|
+
expect(provider.requests.at(0)?.toolChoice).toBeUndefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('says nothing when there are no tools to choose between', async () => {
|
|
116
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
|
|
117
|
+
|
|
118
|
+
await drainQuery({
|
|
119
|
+
...(await baseParams(provider, new ToolRegistry())),
|
|
120
|
+
prepareStep: () => ({ toolChoice: 'required' as const }),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// `tool_choice` alongside an absent tool list is rejected by the
|
|
124
|
+
// providers, so a forced choice with nothing registered has to drop.
|
|
125
|
+
expect(provider.requests.at(0)?.toolChoice).toBeUndefined()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* NOT covered here, and deliberately not faked: the forced-final turn takes
|
|
131
|
+
* precedence over a step's choice (`forceFinalize` wins in
|
|
132
|
+
* `iteration/index.ts`), so a budget-exhausted run can still stop asking
|
|
133
|
+
* for tools and answer. Reaching that branch needs the guard to raise a
|
|
134
|
+
* budget warning, which needs real usage the mock provider does not report.
|
|
135
|
+
* The precedence is implemented and read in review; it is not pinned.
|
|
136
|
+
*/
|
|
137
|
+
describe('a forced choice cannot outlive the step that asked for it', () => {
|
|
138
|
+
it('applies to the named step only, leaving later steps free', async () => {
|
|
139
|
+
const provider = new MockLLMProvider({
|
|
140
|
+
turns: [
|
|
141
|
+
{ toolCalls: [{ id: 'c1', name: 'echo', rawArguments: '{}' }] },
|
|
142
|
+
{ text: 'finished' },
|
|
143
|
+
],
|
|
144
|
+
})
|
|
145
|
+
const tools = new ToolRegistry()
|
|
146
|
+
registerEcho(tools)
|
|
147
|
+
|
|
148
|
+
await drainQuery({
|
|
149
|
+
...(await baseParams(provider, tools)),
|
|
150
|
+
// Only the first step is forced. Without per-step scoping this
|
|
151
|
+
// would keep forcing and the run would never reach a text answer.
|
|
152
|
+
prepareStep: ({ stepNumber }) =>
|
|
153
|
+
stepNumber === 1 ? { toolChoice: 'required' as const } : {},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(provider.requests.at(0)?.toolChoice).toBe('required')
|
|
157
|
+
expect(provider.requests.at(1)?.toolChoice).toBeUndefined()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('a run whose every step forces a tool still ends, bounded by the loop', async () => {
|
|
161
|
+
const provider = new MockLLMProvider({
|
|
162
|
+
turns: [
|
|
163
|
+
{ toolCalls: [{ id: 'c1', name: 'echo', rawArguments: '{}' }] },
|
|
164
|
+
{ toolCalls: [{ id: 'c2', name: 'echo', rawArguments: '{}' }] },
|
|
165
|
+
{ text: 'finished' },
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
const tools = new ToolRegistry()
|
|
169
|
+
registerEcho(tools)
|
|
170
|
+
|
|
171
|
+
const run = await drainQuery({
|
|
172
|
+
...(await baseParams(provider, tools)),
|
|
173
|
+
prepareStep: () => ({ toolChoice: 'required' as const }),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// The knob cannot hang a run on its own: the iteration cap is still
|
|
177
|
+
// the backstop, and it settles rather than spinning.
|
|
178
|
+
expect(['completed', 'failed']).toContain(run.status)
|
|
179
|
+
})
|
|
180
|
+
})
|