@qodo/sdk 0.13.3 → 2.0.0-next.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/LICENSE +31 -118
- package/README.md +133 -121
- package/bin/qodo-skills.mjs +13 -0
- package/bundled-skills/code-review/SKILL.md +41 -0
- package/bundled-skills/pr-summary/SKILL.md +59 -0
- package/bundled-skills/test-gen/SKILL.md +47 -0
- package/dist/auth/index.browser.d.ts +38 -0
- package/dist/auth/index.browser.d.ts.map +1 -0
- package/dist/auth/index.browser.js +62 -0
- package/dist/auth/index.browser.js.map +1 -0
- package/dist/auth/index.d.ts +44 -30
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +57 -110
- package/dist/auth/index.js.map +1 -1
- package/dist/client/AgentsClient.d.ts +33 -0
- package/dist/client/AgentsClient.d.ts.map +1 -0
- package/dist/client/AgentsClient.js +40 -0
- package/dist/client/AgentsClient.js.map +1 -0
- package/dist/client/ArtifactsClient.d.ts +43 -0
- package/dist/client/ArtifactsClient.d.ts.map +1 -0
- package/dist/client/ArtifactsClient.js +54 -0
- package/dist/client/ArtifactsClient.js.map +1 -0
- package/dist/client/BulletinClient.d.ts +45 -0
- package/dist/client/BulletinClient.d.ts.map +1 -0
- package/dist/client/BulletinClient.js +51 -0
- package/dist/client/BulletinClient.js.map +1 -0
- package/dist/client/InfoClient.d.ts +58 -0
- package/dist/client/InfoClient.d.ts.map +1 -0
- package/dist/client/InfoClient.js +135 -0
- package/dist/client/InfoClient.js.map +1 -0
- package/dist/client/PipelineClient.d.ts +162 -0
- package/dist/client/PipelineClient.d.ts.map +1 -0
- package/dist/client/PipelineClient.js +340 -0
- package/dist/client/PipelineClient.js.map +1 -0
- package/dist/client/QarRegistryClient.d.ts +396 -0
- package/dist/client/QarRegistryClient.d.ts.map +1 -0
- package/dist/client/QarRegistryClient.js +536 -0
- package/dist/client/QarRegistryClient.js.map +1 -0
- package/dist/client/QodoClient.d.ts +296 -0
- package/dist/client/QodoClient.d.ts.map +1 -0
- package/dist/client/QodoClient.js +803 -0
- package/dist/client/QodoClient.js.map +1 -0
- package/dist/client/SpecsClient.d.ts +121 -0
- package/dist/client/SpecsClient.d.ts.map +1 -0
- package/dist/client/SpecsClient.js +252 -0
- package/dist/client/SpecsClient.js.map +1 -0
- package/dist/client/StateClient.d.ts +35 -0
- package/dist/client/StateClient.d.ts.map +1 -0
- package/dist/client/StateClient.js +36 -0
- package/dist/client/StateClient.js.map +1 -0
- package/dist/client/TaskClient.d.ts +706 -0
- package/dist/client/TaskClient.d.ts.map +1 -0
- package/dist/client/TaskClient.js +2522 -0
- package/dist/client/TaskClient.js.map +1 -0
- package/dist/client/ToolClient.d.ts +278 -0
- package/dist/client/ToolClient.d.ts.map +1 -0
- package/dist/client/ToolClient.js +1115 -0
- package/dist/client/ToolClient.js.map +1 -0
- package/dist/client/a2a/index.d.ts +10 -0
- package/dist/client/a2a/index.d.ts.map +1 -0
- package/dist/client/a2a/index.js +9 -0
- package/dist/client/a2a/index.js.map +1 -0
- package/dist/client/a2a/registerA2A.d.ts +170 -0
- package/dist/client/a2a/registerA2A.d.ts.map +1 -0
- package/dist/client/a2a/registerA2A.js +85 -0
- package/dist/client/a2a/registerA2A.js.map +1 -0
- package/dist/client/connection.d.ts +800 -0
- package/dist/client/connection.d.ts.map +1 -0
- package/dist/client/connection.js +2020 -0
- package/dist/client/connection.js.map +1 -0
- package/dist/client/errors.d.ts +735 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +921 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/index.d.ts +26 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +20 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/inlineGraph.d.ts +66 -0
- package/dist/client/inlineGraph.d.ts.map +1 -0
- package/dist/client/inlineGraph.js +500 -0
- package/dist/client/inlineGraph.js.map +1 -0
- package/dist/client/internal/thenable.d.ts +27 -0
- package/dist/client/internal/thenable.d.ts.map +1 -0
- package/dist/client/internal/thenable.js +31 -0
- package/dist/client/internal/thenable.js.map +1 -0
- package/dist/client/iterator.d.ts +32 -0
- package/dist/client/iterator.d.ts.map +1 -0
- package/dist/client/iterator.js +73 -0
- package/dist/client/iterator.js.map +1 -0
- package/dist/client/mcp/McpClientPool.browser.d.ts +76 -0
- package/dist/client/mcp/McpClientPool.browser.d.ts.map +1 -0
- package/dist/client/mcp/McpClientPool.browser.js +78 -0
- package/dist/client/mcp/McpClientPool.browser.js.map +1 -0
- package/dist/client/mcp/McpClientPool.d.ts +236 -0
- package/dist/client/mcp/McpClientPool.d.ts.map +1 -0
- package/dist/client/mcp/McpClientPool.js +585 -0
- package/dist/client/mcp/McpClientPool.js.map +1 -0
- package/dist/client/mcp/projection.d.ts +109 -0
- package/dist/client/mcp/projection.d.ts.map +1 -0
- package/dist/client/mcp/projection.js +446 -0
- package/dist/client/mcp/projection.js.map +1 -0
- package/dist/client/mcp/substituteEnv.browser.d.ts +18 -0
- package/dist/client/mcp/substituteEnv.browser.d.ts.map +1 -0
- package/dist/client/mcp/substituteEnv.browser.js +20 -0
- package/dist/client/mcp/substituteEnv.browser.js.map +1 -0
- package/dist/client/mcp/substituteEnv.d.ts +45 -0
- package/dist/client/mcp/substituteEnv.d.ts.map +1 -0
- package/dist/client/mcp/substituteEnv.js +63 -0
- package/dist/client/mcp/substituteEnv.js.map +1 -0
- package/dist/client/observers.d.ts +57 -0
- package/dist/client/observers.d.ts.map +1 -0
- package/dist/client/observers.js +203 -0
- package/dist/client/observers.js.map +1 -0
- package/dist/client/options.d.ts +269 -0
- package/dist/client/options.d.ts.map +1 -0
- package/dist/client/options.js +9 -0
- package/dist/client/options.js.map +1 -0
- package/dist/client/tools/_readlineApprovalPrompt.browser.d.ts +17 -0
- package/dist/client/tools/_readlineApprovalPrompt.browser.d.ts.map +1 -0
- package/dist/client/tools/_readlineApprovalPrompt.browser.js +24 -0
- package/dist/client/tools/_readlineApprovalPrompt.browser.js.map +1 -0
- package/dist/client/tools/_readlineApprovalPrompt.d.ts +33 -0
- package/dist/client/tools/_readlineApprovalPrompt.d.ts.map +1 -0
- package/dist/client/tools/_readlineApprovalPrompt.js +90 -0
- package/dist/client/tools/_readlineApprovalPrompt.js.map +1 -0
- package/dist/client/tools/approval.d.ts +280 -0
- package/dist/client/tools/approval.d.ts.map +1 -0
- package/dist/client/tools/approval.js +229 -0
- package/dist/client/tools/approval.js.map +1 -0
- package/dist/client/tools/bindFunctionToolDefs.d.ts +156 -0
- package/dist/client/tools/bindFunctionToolDefs.d.ts.map +1 -0
- package/dist/client/tools/bindFunctionToolDefs.js +360 -0
- package/dist/client/tools/bindFunctionToolDefs.js.map +1 -0
- package/dist/client/tools/defineFunctionTool.d.ts +277 -0
- package/dist/client/tools/defineFunctionTool.d.ts.map +1 -0
- package/dist/client/tools/defineFunctionTool.js +190 -0
- package/dist/client/tools/defineFunctionTool.js.map +1 -0
- package/dist/client/transport.browser.d.ts +20 -0
- package/dist/client/transport.browser.d.ts.map +1 -0
- package/dist/client/transport.browser.js +29 -0
- package/dist/client/transport.browser.js.map +1 -0
- package/dist/client/transport.d.ts +47 -0
- package/dist/client/transport.d.ts.map +1 -0
- package/dist/client/transport.js +102 -0
- package/dist/client/transport.js.map +1 -0
- package/dist/client/transport.shared.d.ts +30 -0
- package/dist/client/transport.shared.d.ts.map +1 -0
- package/dist/client/transport.shared.js +40 -0
- package/dist/client/transport.shared.js.map +1 -0
- package/dist/client/uuid.d.ts +32 -0
- package/dist/client/uuid.d.ts.map +1 -0
- package/dist/client/uuid.js +65 -0
- package/dist/client/uuid.js.map +1 -0
- package/dist/index.d.ts +88 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +166 -43
- package/dist/index.js.map +1 -1
- package/dist/observability/attributes.d.ts +136 -0
- package/dist/observability/attributes.d.ts.map +1 -0
- package/dist/observability/attributes.js +184 -0
- package/dist/observability/attributes.js.map +1 -0
- package/dist/observability/index.d.ts +14 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +11 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/resolveOTel.browser.d.ts +13 -0
- package/dist/observability/resolveOTel.browser.d.ts.map +1 -0
- package/dist/observability/resolveOTel.browser.js +14 -0
- package/dist/observability/resolveOTel.browser.js.map +1 -0
- package/dist/observability/resolveOTel.d.ts +28 -0
- package/dist/observability/resolveOTel.d.ts.map +1 -0
- package/dist/observability/resolveOTel.js +74 -0
- package/dist/observability/resolveOTel.js.map +1 -0
- package/dist/observability/spans.d.ts +198 -0
- package/dist/observability/spans.d.ts.map +1 -0
- package/dist/observability/spans.js +300 -0
- package/dist/observability/spans.js.map +1 -0
- package/dist/observability/traceContext.d.ts +51 -0
- package/dist/observability/traceContext.d.ts.map +1 -0
- package/dist/observability/traceContext.js +151 -0
- package/dist/observability/traceContext.js.map +1 -0
- package/dist/observability/transportMetrics.d.ts +58 -0
- package/dist/observability/transportMetrics.d.ts.map +1 -0
- package/dist/observability/transportMetrics.js +55 -0
- package/dist/observability/transportMetrics.js.map +1 -0
- package/dist/qar/agentSpec.d.ts +93 -0
- package/dist/qar/agentSpec.d.ts.map +1 -0
- package/dist/qar/agentSpec.js +184 -0
- package/dist/qar/agentSpec.js.map +1 -0
- package/dist/qar/clientEvents.d.ts +86 -0
- package/dist/qar/clientEvents.d.ts.map +1 -0
- package/dist/qar/clientEvents.js +36 -0
- package/dist/qar/clientEvents.js.map +1 -0
- package/dist/qar/envelopes.d.ts +227 -0
- package/dist/qar/envelopes.d.ts.map +1 -0
- package/dist/qar/envelopes.js +67 -0
- package/dist/qar/envelopes.js.map +1 -0
- package/dist/qar/generated/envelope.d.ts +332 -0
- package/dist/qar/generated/envelope.d.ts.map +1 -0
- package/dist/qar/generated/envelope.js +15 -0
- package/dist/qar/generated/envelope.js.map +1 -0
- package/dist/qar/generated/qar-info.d.ts +76 -0
- package/dist/qar/generated/qar-info.d.ts.map +1 -0
- package/dist/qar/generated/qar-info.js +15 -0
- package/dist/qar/generated/qar-info.js.map +1 -0
- package/dist/qar/generated/qodo-task-start-payload.d.ts +54 -0
- package/dist/qar/generated/qodo-task-start-payload.d.ts.map +1 -0
- package/dist/qar/generated/qodo-task-start-payload.js +15 -0
- package/dist/qar/generated/qodo-task-start-payload.js.map +1 -0
- package/dist/qar/ids.d.ts +19 -0
- package/dist/qar/ids.d.ts.map +1 -0
- package/dist/qar/ids.js +11 -0
- package/dist/qar/ids.js.map +1 -0
- package/dist/qar/index.d.ts +24 -0
- package/dist/qar/index.d.ts.map +1 -0
- package/dist/qar/index.js +16 -0
- package/dist/qar/index.js.map +1 -0
- package/dist/qar/info.d.ts +37 -0
- package/dist/qar/info.d.ts.map +1 -0
- package/dist/qar/info.js +17 -0
- package/dist/qar/info.js.map +1 -0
- package/dist/qar/json.d.ts +14 -0
- package/dist/qar/json.d.ts.map +1 -0
- package/dist/qar/json.js +9 -0
- package/dist/qar/json.js.map +1 -0
- package/dist/qar/payloads.d.ts +480 -0
- package/dist/qar/payloads.d.ts.map +1 -0
- package/dist/qar/payloads.js +37 -0
- package/dist/qar/payloads.js.map +1 -0
- package/dist/qar/specs.d.ts +604 -0
- package/dist/qar/specs.d.ts.map +1 -0
- package/dist/qar/specs.js +29 -0
- package/dist/qar/specs.js.map +1 -0
- package/dist/qar/taskEvents.d.ts +25 -0
- package/dist/qar/taskEvents.d.ts.map +1 -0
- package/dist/qar/taskEvents.js +22 -0
- package/dist/qar/taskEvents.js.map +1 -0
- package/dist/qar/trace.d.ts +12 -0
- package/dist/qar/trace.d.ts.map +1 -0
- package/dist/qar/trace.js +12 -0
- package/dist/qar/trace.js.map +1 -0
- package/dist/skills/activation.d.ts +177 -0
- package/dist/skills/activation.d.ts.map +1 -0
- package/dist/skills/activation.js +428 -0
- package/dist/skills/activation.js.map +1 -0
- package/dist/skills/cli/index.browser.d.ts +18 -0
- package/dist/skills/cli/index.browser.d.ts.map +1 -0
- package/dist/skills/cli/index.browser.js +27 -0
- package/dist/skills/cli/index.browser.js.map +1 -0
- package/dist/skills/cli/index.d.ts +37 -0
- package/dist/skills/cli/index.d.ts.map +1 -0
- package/dist/skills/cli/index.js +494 -0
- package/dist/skills/cli/index.js.map +1 -0
- package/dist/skills/events.d.ts +255 -0
- package/dist/skills/events.d.ts.map +1 -0
- package/dist/skills/events.js +224 -0
- package/dist/skills/events.js.map +1 -0
- package/dist/skills/index.d.ts +45 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +34 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/inject.d.ts +57 -0
- package/dist/skills/inject.d.ts.map +1 -0
- package/dist/skills/inject.js +162 -0
- package/dist/skills/inject.js.map +1 -0
- package/dist/skills/lockfile.browser.d.ts +56 -0
- package/dist/skills/lockfile.browser.d.ts.map +1 -0
- package/dist/skills/lockfile.browser.js +55 -0
- package/dist/skills/lockfile.browser.js.map +1 -0
- package/dist/skills/lockfile.d.ts +137 -0
- package/dist/skills/lockfile.d.ts.map +1 -0
- package/dist/skills/lockfile.js +423 -0
- package/dist/skills/lockfile.js.map +1 -0
- package/dist/skills/manager.browser.d.ts +94 -0
- package/dist/skills/manager.browser.d.ts.map +1 -0
- package/dist/skills/manager.browser.js +159 -0
- package/dist/skills/manager.browser.js.map +1 -0
- package/dist/skills/manager.d.ts +362 -0
- package/dist/skills/manager.d.ts.map +1 -0
- package/dist/skills/manager.js +1386 -0
- package/dist/skills/manager.js.map +1 -0
- package/dist/skills/mcp/index.d.ts +15 -0
- package/dist/skills/mcp/index.d.ts.map +1 -0
- package/dist/skills/mcp/index.js +12 -0
- package/dist/skills/mcp/index.js.map +1 -0
- package/dist/skills/mcp/path.browser.d.ts +27 -0
- package/dist/skills/mcp/path.browser.d.ts.map +1 -0
- package/dist/skills/mcp/path.browser.js +33 -0
- package/dist/skills/mcp/path.browser.js.map +1 -0
- package/dist/skills/mcp/path.d.ts +57 -0
- package/dist/skills/mcp/path.d.ts.map +1 -0
- package/dist/skills/mcp/path.js +150 -0
- package/dist/skills/mcp/path.js.map +1 -0
- package/dist/skills/mcp/server.browser.d.ts +32 -0
- package/dist/skills/mcp/server.browser.d.ts.map +1 -0
- package/dist/skills/mcp/server.browser.js +53 -0
- package/dist/skills/mcp/server.browser.js.map +1 -0
- package/dist/skills/mcp/server.d.ts +144 -0
- package/dist/skills/mcp/server.d.ts.map +1 -0
- package/dist/skills/mcp/server.js +841 -0
- package/dist/skills/mcp/server.js.map +1 -0
- package/dist/skills/mcp/types.d.ts +72 -0
- package/dist/skills/mcp/types.d.ts.map +1 -0
- package/dist/skills/mcp/types.js +20 -0
- package/dist/skills/mcp/types.js.map +1 -0
- package/dist/skills/mcp/wireDefs.d.ts +58 -0
- package/dist/skills/mcp/wireDefs.d.ts.map +1 -0
- package/dist/skills/mcp/wireDefs.js +141 -0
- package/dist/skills/mcp/wireDefs.js.map +1 -0
- package/dist/skills/parser.d.ts +63 -0
- package/dist/skills/parser.d.ts.map +1 -0
- package/dist/skills/parser.js +755 -0
- package/dist/skills/parser.js.map +1 -0
- package/dist/skills/prefilter.d.ts +104 -0
- package/dist/skills/prefilter.d.ts.map +1 -0
- package/dist/skills/prefilter.js +398 -0
- package/dist/skills/prefilter.js.map +1 -0
- package/dist/skills/preprocess.d.ts +169 -0
- package/dist/skills/preprocess.d.ts.map +1 -0
- package/dist/skills/preprocess.js +535 -0
- package/dist/skills/preprocess.js.map +1 -0
- package/dist/skills/render.d.ts +83 -0
- package/dist/skills/render.d.ts.map +1 -0
- package/dist/skills/render.js +397 -0
- package/dist/skills/render.js.map +1 -0
- package/dist/skills/sources/index.browser.d.ts +29 -0
- package/dist/skills/sources/index.browser.d.ts.map +1 -0
- package/dist/skills/sources/index.browser.js +16 -0
- package/dist/skills/sources/index.browser.js.map +1 -0
- package/dist/skills/sources/index.d.ts +59 -0
- package/dist/skills/sources/index.d.ts.map +1 -0
- package/dist/skills/sources/index.js +471 -0
- package/dist/skills/sources/index.js.map +1 -0
- package/dist/skills/sources/walk.browser.d.ts +17 -0
- package/dist/skills/sources/walk.browser.d.ts.map +1 -0
- package/dist/skills/sources/walk.browser.js +19 -0
- package/dist/skills/sources/walk.browser.js.map +1 -0
- package/dist/skills/sources/walk.d.ts +68 -0
- package/dist/skills/sources/walk.d.ts.map +1 -0
- package/dist/skills/sources/walk.js +264 -0
- package/dist/skills/sources/walk.js.map +1 -0
- package/dist/skills/substitute.d.ts +87 -0
- package/dist/skills/substitute.d.ts.map +1 -0
- package/dist/skills/substitute.js +322 -0
- package/dist/skills/substitute.js.map +1 -0
- package/dist/skills/testing/SkillKit.browser.d.ts +62 -0
- package/dist/skills/testing/SkillKit.browser.d.ts.map +1 -0
- package/dist/skills/testing/SkillKit.browser.js +41 -0
- package/dist/skills/testing/SkillKit.browser.js.map +1 -0
- package/dist/skills/testing/SkillKit.d.ts +130 -0
- package/dist/skills/testing/SkillKit.d.ts.map +1 -0
- package/dist/skills/testing/SkillKit.js +316 -0
- package/dist/skills/testing/SkillKit.js.map +1 -0
- package/dist/skills/testing/index.d.ts +9 -0
- package/dist/skills/testing/index.d.ts.map +1 -0
- package/dist/skills/testing/index.js +8 -0
- package/dist/skills/testing/index.js.map +1 -0
- package/dist/skills/trust.d.ts +72 -0
- package/dist/skills/trust.d.ts.map +1 -0
- package/dist/skills/trust.js +183 -0
- package/dist/skills/trust.js.map +1 -0
- package/dist/skills/types.d.ts +627 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +85 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/skills/validator.d.ts +95 -0
- package/dist/skills/validator.d.ts.map +1 -0
- package/dist/skills/validator.js +486 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/tracing/PipelineTracer.d.ts +35 -22
- package/dist/tracing/PipelineTracer.d.ts.map +1 -1
- package/dist/tracing/PipelineTracer.js +106 -61
- package/dist/tracing/PipelineTracer.js.map +1 -1
- package/dist/tracing/SdkTracer.d.ts +63 -61
- package/dist/tracing/SdkTracer.d.ts.map +1 -1
- package/dist/tracing/SdkTracer.js +185 -177
- package/dist/tracing/SdkTracer.js.map +1 -1
- package/dist/tracing/index.d.ts +10 -1
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +9 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/tracing/types.d.ts +89 -16
- package/dist/tracing/types.d.ts.map +1 -1
- package/dist/tracing/types.js +17 -4
- package/dist/tracing/types.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -20
- package/dist/version.js.map +1 -1
- package/package.json +53 -39
- package/.claude/skills/qodo-agent/SKILL.md +0 -974
- package/.claude/skills/qodo-agent/assets/programmatic-agent.ts +0 -407
- package/.claude/skills/qodo-agent/references/builtin-tools.md +0 -342
- package/.claude/skills/qodo-agent/references/common-issues.md +0 -537
- package/bin/rg +0 -0
- package/dist/api/agent.d.ts +0 -104
- package/dist/api/agent.d.ts.map +0 -1
- package/dist/api/agent.js +0 -939
- package/dist/api/agent.js.map +0 -1
- package/dist/api/analytics.d.ts +0 -43
- package/dist/api/analytics.d.ts.map +0 -1
- package/dist/api/analytics.js +0 -163
- package/dist/api/analytics.js.map +0 -1
- package/dist/api/http.d.ts +0 -5
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/http.js +0 -62
- package/dist/api/http.js.map +0 -1
- package/dist/api/index.d.ts +0 -12
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -17
- package/dist/api/index.js.map +0 -1
- package/dist/api/taskTracking.d.ts +0 -54
- package/dist/api/taskTracking.d.ts.map +0 -1
- package/dist/api/taskTracking.js +0 -208
- package/dist/api/taskTracking.js.map +0 -1
- package/dist/api/types.d.ts +0 -93
- package/dist/api/types.d.ts.map +0 -1
- package/dist/api/types.js +0 -2
- package/dist/api/types.js.map +0 -1
- package/dist/api/utils.d.ts +0 -8
- package/dist/api/utils.d.ts.map +0 -1
- package/dist/api/utils.js +0 -63
- package/dist/api/utils.js.map +0 -1
- package/dist/api/websocket.d.ts +0 -203
- package/dist/api/websocket.d.ts.map +0 -1
- package/dist/api/websocket.js +0 -1166
- package/dist/api/websocket.js.map +0 -1
- package/dist/bin/install-skill.d.ts +0 -14
- package/dist/bin/install-skill.d.ts.map +0 -1
- package/dist/bin/install-skill.js +0 -125
- package/dist/bin/install-skill.js.map +0 -1
- package/dist/bin/run-helpers.d.ts +0 -34
- package/dist/bin/run-helpers.d.ts.map +0 -1
- package/dist/bin/run-helpers.js +0 -186
- package/dist/bin/run-helpers.js.map +0 -1
- package/dist/bin/run.d.ts +0 -13
- package/dist/bin/run.d.ts.map +0 -1
- package/dist/bin/run.js +0 -57
- package/dist/bin/run.js.map +0 -1
- package/dist/clients/index.d.ts +0 -10
- package/dist/clients/index.d.ts.map +0 -1
- package/dist/clients/index.js +0 -8
- package/dist/clients/index.js.map +0 -1
- package/dist/clients/info/InfoClient.d.ts +0 -37
- package/dist/clients/info/InfoClient.d.ts.map +0 -1
- package/dist/clients/info/InfoClient.js +0 -69
- package/dist/clients/info/InfoClient.js.map +0 -1
- package/dist/clients/info/index.d.ts +0 -4
- package/dist/clients/info/index.d.ts.map +0 -1
- package/dist/clients/info/index.js +0 -2
- package/dist/clients/info/index.js.map +0 -1
- package/dist/clients/info/types.d.ts +0 -21
- package/dist/clients/info/types.d.ts.map +0 -1
- package/dist/clients/info/types.js +0 -2
- package/dist/clients/info/types.js.map +0 -1
- package/dist/clients/sessions/SessionsClient.d.ts +0 -34
- package/dist/clients/sessions/SessionsClient.d.ts.map +0 -1
- package/dist/clients/sessions/SessionsClient.js +0 -71
- package/dist/clients/sessions/SessionsClient.js.map +0 -1
- package/dist/clients/sessions/index.d.ts +0 -4
- package/dist/clients/sessions/index.d.ts.map +0 -1
- package/dist/clients/sessions/index.js +0 -2
- package/dist/clients/sessions/index.js.map +0 -1
- package/dist/clients/sessions/types.d.ts +0 -20
- package/dist/clients/sessions/types.d.ts.map +0 -1
- package/dist/clients/sessions/types.js +0 -2
- package/dist/clients/sessions/types.js.map +0 -1
- package/dist/clients/tools/ToolsClient.d.ts +0 -39
- package/dist/clients/tools/ToolsClient.d.ts.map +0 -1
- package/dist/clients/tools/ToolsClient.js +0 -95
- package/dist/clients/tools/ToolsClient.js.map +0 -1
- package/dist/clients/tools/index.d.ts +0 -4
- package/dist/clients/tools/index.d.ts.map +0 -1
- package/dist/clients/tools/index.js +0 -2
- package/dist/clients/tools/index.js.map +0 -1
- package/dist/clients/tools/types.d.ts +0 -14
- package/dist/clients/tools/types.d.ts.map +0 -1
- package/dist/clients/tools/types.js +0 -2
- package/dist/clients/tools/types.js.map +0 -1
- package/dist/config/ConfigManager.d.ts +0 -43
- package/dist/config/ConfigManager.d.ts.map +0 -1
- package/dist/config/ConfigManager.js +0 -472
- package/dist/config/ConfigManager.js.map +0 -1
- package/dist/config/index.d.ts +0 -6
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -7
- package/dist/config/index.js.map +0 -1
- package/dist/config/urlConfig.d.ts +0 -15
- package/dist/config/urlConfig.d.ts.map +0 -1
- package/dist/config/urlConfig.js +0 -75
- package/dist/config/urlConfig.js.map +0 -1
- package/dist/constants/errors.d.ts +0 -2
- package/dist/constants/errors.d.ts.map +0 -1
- package/dist/constants/errors.js +0 -2
- package/dist/constants/errors.js.map +0 -1
- package/dist/constants/index.d.ts +0 -7
- package/dist/constants/index.d.ts.map +0 -1
- package/dist/constants/index.js +0 -11
- package/dist/constants/index.js.map +0 -1
- package/dist/constants/tools.d.ts +0 -4
- package/dist/constants/tools.d.ts.map +0 -1
- package/dist/constants/tools.js +0 -4
- package/dist/constants/tools.js.map +0 -1
- package/dist/constants/versions.d.ts +0 -2
- package/dist/constants/versions.d.ts.map +0 -1
- package/dist/constants/versions.js +0 -2
- package/dist/constants/versions.js.map +0 -1
- package/dist/context/buildUserContext.d.ts +0 -18
- package/dist/context/buildUserContext.d.ts.map +0 -1
- package/dist/context/buildUserContext.js +0 -34
- package/dist/context/buildUserContext.js.map +0 -1
- package/dist/context/index.d.ts +0 -9
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -9
- package/dist/context/index.js.map +0 -1
- package/dist/context/messageManager.d.ts +0 -42
- package/dist/context/messageManager.d.ts.map +0 -1
- package/dist/context/messageManager.js +0 -322
- package/dist/context/messageManager.js.map +0 -1
- package/dist/context/taskFocus.d.ts +0 -2
- package/dist/context/taskFocus.d.ts.map +0 -1
- package/dist/context/taskFocus.js +0 -26
- package/dist/context/taskFocus.js.map +0 -1
- package/dist/context/userInput.d.ts +0 -3
- package/dist/context/userInput.d.ts.map +0 -1
- package/dist/context/userInput.js +0 -20
- package/dist/context/userInput.js.map +0 -1
- package/dist/mcp/MCPManager.d.ts +0 -109
- package/dist/mcp/MCPManager.d.ts.map +0 -1
- package/dist/mcp/MCPManager.js +0 -592
- package/dist/mcp/MCPManager.js.map +0 -1
- package/dist/mcp/approvedTools.d.ts +0 -4
- package/dist/mcp/approvedTools.d.ts.map +0 -1
- package/dist/mcp/approvedTools.js +0 -19
- package/dist/mcp/approvedTools.js.map +0 -1
- package/dist/mcp/baseServer.d.ts +0 -75
- package/dist/mcp/baseServer.d.ts.map +0 -1
- package/dist/mcp/baseServer.js +0 -107
- package/dist/mcp/baseServer.js.map +0 -1
- package/dist/mcp/builtinServers.d.ts +0 -15
- package/dist/mcp/builtinServers.d.ts.map +0 -1
- package/dist/mcp/builtinServers.js +0 -141
- package/dist/mcp/builtinServers.js.map +0 -1
- package/dist/mcp/dynamicBEServer.d.ts +0 -20
- package/dist/mcp/dynamicBEServer.d.ts.map +0 -1
- package/dist/mcp/dynamicBEServer.js +0 -52
- package/dist/mcp/dynamicBEServer.js.map +0 -1
- package/dist/mcp/index.d.ts +0 -18
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -23
- package/dist/mcp/index.js.map +0 -1
- package/dist/mcp/mcpInitialization.d.ts +0 -2
- package/dist/mcp/mcpInitialization.d.ts.map +0 -1
- package/dist/mcp/mcpInitialization.js +0 -56
- package/dist/mcp/mcpInitialization.js.map +0 -1
- package/dist/mcp/servers/filesystem.d.ts +0 -44
- package/dist/mcp/servers/filesystem.d.ts.map +0 -1
- package/dist/mcp/servers/filesystem.js +0 -776
- package/dist/mcp/servers/filesystem.js.map +0 -1
- package/dist/mcp/servers/git.d.ts +0 -18
- package/dist/mcp/servers/git.d.ts.map +0 -1
- package/dist/mcp/servers/git.js +0 -441
- package/dist/mcp/servers/git.js.map +0 -1
- package/dist/mcp/servers/ripgrep.d.ts +0 -39
- package/dist/mcp/servers/ripgrep.d.ts.map +0 -1
- package/dist/mcp/servers/ripgrep.js +0 -550
- package/dist/mcp/servers/ripgrep.js.map +0 -1
- package/dist/mcp/servers/shell.d.ts +0 -20
- package/dist/mcp/servers/shell.d.ts.map +0 -1
- package/dist/mcp/servers/shell.js +0 -519
- package/dist/mcp/servers/shell.js.map +0 -1
- package/dist/mcp/serversRegistry.d.ts +0 -55
- package/dist/mcp/serversRegistry.d.ts.map +0 -1
- package/dist/mcp/serversRegistry.js +0 -416
- package/dist/mcp/serversRegistry.js.map +0 -1
- package/dist/mcp/toolProcessor.d.ts +0 -82
- package/dist/mcp/toolProcessor.d.ts.map +0 -1
- package/dist/mcp/toolProcessor.js +0 -392
- package/dist/mcp/toolProcessor.js.map +0 -1
- package/dist/mcp/types.d.ts +0 -29
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/mcp/types.js +0 -2
- package/dist/mcp/types.js.map +0 -1
- package/dist/messages/index.d.ts +0 -8
- package/dist/messages/index.d.ts.map +0 -1
- package/dist/messages/index.js +0 -7
- package/dist/messages/index.js.map +0 -1
- package/dist/messages/openai.d.ts +0 -26
- package/dist/messages/openai.d.ts.map +0 -1
- package/dist/messages/openai.js +0 -55
- package/dist/messages/openai.js.map +0 -1
- package/dist/messages/types.d.ts +0 -73
- package/dist/messages/types.d.ts.map +0 -1
- package/dist/messages/types.js +0 -78
- package/dist/messages/types.js.map +0 -1
- package/dist/parser/index.d.ts +0 -72
- package/dist/parser/index.d.ts.map +0 -1
- package/dist/parser/index.js +0 -967
- package/dist/parser/index.js.map +0 -1
- package/dist/parser/types.d.ts +0 -153
- package/dist/parser/types.d.ts.map +0 -1
- package/dist/parser/types.js +0 -6
- package/dist/parser/types.js.map +0 -1
- package/dist/parser/utils.d.ts +0 -18
- package/dist/parser/utils.d.ts.map +0 -1
- package/dist/parser/utils.js +0 -64
- package/dist/parser/utils.js.map +0 -1
- package/dist/sdk/QodoSDK.d.ts +0 -218
- package/dist/sdk/QodoSDK.d.ts.map +0 -1
- package/dist/sdk/QodoSDK.js +0 -1115
- package/dist/sdk/QodoSDK.js.map +0 -1
- package/dist/sdk/artifacts.d.ts +0 -156
- package/dist/sdk/artifacts.d.ts.map +0 -1
- package/dist/sdk/artifacts.js +0 -166
- package/dist/sdk/artifacts.js.map +0 -1
- package/dist/sdk/bootstrap.d.ts +0 -16
- package/dist/sdk/bootstrap.d.ts.map +0 -1
- package/dist/sdk/bootstrap.js +0 -28
- package/dist/sdk/bootstrap.js.map +0 -1
- package/dist/sdk/builders.d.ts +0 -54
- package/dist/sdk/builders.d.ts.map +0 -1
- package/dist/sdk/builders.js +0 -117
- package/dist/sdk/builders.js.map +0 -1
- package/dist/sdk/defaults.d.ts +0 -11
- package/dist/sdk/defaults.d.ts.map +0 -1
- package/dist/sdk/defaults.js +0 -39
- package/dist/sdk/defaults.js.map +0 -1
- package/dist/sdk/discovery.d.ts +0 -2
- package/dist/sdk/discovery.d.ts.map +0 -1
- package/dist/sdk/discovery.js +0 -25
- package/dist/sdk/discovery.js.map +0 -1
- package/dist/sdk/events.d.ts +0 -269
- package/dist/sdk/events.d.ts.map +0 -1
- package/dist/sdk/events.js +0 -69
- package/dist/sdk/events.js.map +0 -1
- package/dist/sdk/exit-expression.d.ts +0 -13
- package/dist/sdk/exit-expression.d.ts.map +0 -1
- package/dist/sdk/exit-expression.js +0 -35
- package/dist/sdk/exit-expression.js.map +0 -1
- package/dist/sdk/index.d.ts +0 -17
- package/dist/sdk/index.d.ts.map +0 -1
- package/dist/sdk/index.js +0 -17
- package/dist/sdk/index.js.map +0 -1
- package/dist/sdk/middleware.d.ts +0 -59
- package/dist/sdk/middleware.d.ts.map +0 -1
- package/dist/sdk/middleware.js +0 -69
- package/dist/sdk/middleware.js.map +0 -1
- package/dist/sdk/pipeline/PipelineBuilder.d.ts +0 -79
- package/dist/sdk/pipeline/PipelineBuilder.d.ts.map +0 -1
- package/dist/sdk/pipeline/PipelineBuilder.js +0 -129
- package/dist/sdk/pipeline/PipelineBuilder.js.map +0 -1
- package/dist/sdk/pipeline/PipelineRunner.d.ts +0 -28
- package/dist/sdk/pipeline/PipelineRunner.d.ts.map +0 -1
- package/dist/sdk/pipeline/PipelineRunner.js +0 -326
- package/dist/sdk/pipeline/PipelineRunner.js.map +0 -1
- package/dist/sdk/pipeline/compiler.d.ts +0 -24
- package/dist/sdk/pipeline/compiler.d.ts.map +0 -1
- package/dist/sdk/pipeline/compiler.js +0 -199
- package/dist/sdk/pipeline/compiler.js.map +0 -1
- package/dist/sdk/pipeline/declarative.d.ts +0 -34
- package/dist/sdk/pipeline/declarative.d.ts.map +0 -1
- package/dist/sdk/pipeline/declarative.js +0 -9
- package/dist/sdk/pipeline/declarative.js.map +0 -1
- package/dist/sdk/pipeline/index.d.ts +0 -20
- package/dist/sdk/pipeline/index.d.ts.map +0 -1
- package/dist/sdk/pipeline/index.js +0 -19
- package/dist/sdk/pipeline/index.js.map +0 -1
- package/dist/sdk/pipeline/types.d.ts +0 -93
- package/dist/sdk/pipeline/types.d.ts.map +0 -1
- package/dist/sdk/pipeline/types.js +0 -10
- package/dist/sdk/pipeline/types.js.map +0 -1
- package/dist/sdk/policies.d.ts +0 -163
- package/dist/sdk/policies.d.ts.map +0 -1
- package/dist/sdk/policies.js +0 -243
- package/dist/sdk/policies.js.map +0 -1
- package/dist/sdk/runner/AgentRunner.d.ts +0 -22
- package/dist/sdk/runner/AgentRunner.d.ts.map +0 -1
- package/dist/sdk/runner/AgentRunner.js +0 -222
- package/dist/sdk/runner/AgentRunner.js.map +0 -1
- package/dist/sdk/runner/finalize.d.ts +0 -56
- package/dist/sdk/runner/finalize.d.ts.map +0 -1
- package/dist/sdk/runner/finalize.js +0 -155
- package/dist/sdk/runner/finalize.js.map +0 -1
- package/dist/sdk/runner/formats.d.ts +0 -7
- package/dist/sdk/runner/formats.d.ts.map +0 -1
- package/dist/sdk/runner/formats.js +0 -76
- package/dist/sdk/runner/formats.js.map +0 -1
- package/dist/sdk/runner/index.d.ts +0 -9
- package/dist/sdk/runner/index.d.ts.map +0 -1
- package/dist/sdk/runner/index.js +0 -9
- package/dist/sdk/runner/index.js.map +0 -1
- package/dist/sdk/runner/progress.d.ts +0 -3
- package/dist/sdk/runner/progress.d.ts.map +0 -1
- package/dist/sdk/runner/progress.js +0 -16
- package/dist/sdk/runner/progress.js.map +0 -1
- package/dist/sdk/schemas.d.ts +0 -72
- package/dist/sdk/schemas.d.ts.map +0 -1
- package/dist/sdk/schemas.js +0 -282
- package/dist/sdk/schemas.js.map +0 -1
- package/dist/sdk/trigger-context.d.ts +0 -24
- package/dist/sdk/trigger-context.d.ts.map +0 -1
- package/dist/sdk/trigger-context.js +0 -136
- package/dist/sdk/trigger-context.js.map +0 -1
- package/dist/session/SessionContext.d.ts +0 -89
- package/dist/session/SessionContext.d.ts.map +0 -1
- package/dist/session/SessionContext.js +0 -410
- package/dist/session/SessionContext.js.map +0 -1
- package/dist/session/environment.d.ts +0 -52
- package/dist/session/environment.d.ts.map +0 -1
- package/dist/session/environment.js +0 -27
- package/dist/session/environment.js.map +0 -1
- package/dist/session/history.d.ts +0 -18
- package/dist/session/history.d.ts.map +0 -1
- package/dist/session/history.js +0 -68
- package/dist/session/history.js.map +0 -1
- package/dist/session/index.d.ts +0 -10
- package/dist/session/index.d.ts.map +0 -1
- package/dist/session/index.js +0 -9
- package/dist/session/index.js.map +0 -1
- package/dist/session/serverData.d.ts +0 -38
- package/dist/session/serverData.d.ts.map +0 -1
- package/dist/session/serverData.js +0 -261
- package/dist/session/serverData.js.map +0 -1
- package/dist/tracing/pipelineHelpers.d.ts +0 -29
- package/dist/tracing/pipelineHelpers.d.ts.map +0 -1
- package/dist/tracing/pipelineHelpers.js +0 -224
- package/dist/tracing/pipelineHelpers.js.map +0 -1
- package/dist/tracking/Tracker.d.ts +0 -55
- package/dist/tracking/Tracker.d.ts.map +0 -1
- package/dist/tracking/Tracker.js +0 -217
- package/dist/tracking/Tracker.js.map +0 -1
- package/dist/tracking/index.d.ts +0 -8
- package/dist/tracking/index.d.ts.map +0 -1
- package/dist/tracking/index.js +0 -8
- package/dist/tracking/index.js.map +0 -1
- package/dist/tracking/schemas.d.ts +0 -292
- package/dist/tracking/schemas.d.ts.map +0 -1
- package/dist/tracking/schemas.js +0 -91
- package/dist/tracking/schemas.js.map +0 -1
- package/dist/utils/extractSetFlags.d.ts +0 -6
- package/dist/utils/extractSetFlags.d.ts.map +0 -1
- package/dist/utils/extractSetFlags.js +0 -16
- package/dist/utils/extractSetFlags.js.map +0 -1
- package/dist/utils/formatTimeAgo.d.ts +0 -2
- package/dist/utils/formatTimeAgo.d.ts.map +0 -1
- package/dist/utils/formatTimeAgo.js +0 -20
- package/dist/utils/formatTimeAgo.js.map +0 -1
- package/dist/utils/index.d.ts +0 -12
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -12
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/machineId.d.ts +0 -14
- package/dist/utils/machineId.d.ts.map +0 -1
- package/dist/utils/machineId.js +0 -66
- package/dist/utils/machineId.js.map +0 -1
- package/dist/utils/pathUtils.d.ts +0 -22
- package/dist/utils/pathUtils.d.ts.map +0 -1
- package/dist/utils/pathUtils.js +0 -54
- package/dist/utils/pathUtils.js.map +0 -1
- package/scripts/download-ripgrep.js +0 -269
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ToolClient` — surfaces inbound `tool.request` envelopes
|
|
3
|
+
* (parallel-batched) and accepts outbound `tool.response` envelopes
|
|
4
|
+
* (same shape).
|
|
5
|
+
*
|
|
6
|
+
* The parallel-batched envelope shape:
|
|
7
|
+
*
|
|
8
|
+
* - `tool.request.payload` carries `calls: ToolCall[]` (always a list, even
|
|
9
|
+
* for a single call — modern LLM `parallel_tool_use`) and a required
|
|
10
|
+
* `node_name`. The handler receives the full envelope; `req.payload.calls`
|
|
11
|
+
* is the call list, `req.payload.node_name` is the originating node.
|
|
12
|
+
* - `tool.response.payload` carries `responses: ToolResponseItem[]` (same
|
|
13
|
+
* length / membership as the request's `calls`) and the same `node_name`.
|
|
14
|
+
*
|
|
15
|
+
* Handler return shapes:
|
|
16
|
+
*
|
|
17
|
+
* - returns `ToolResponseResult` (single SDK union) → auto-batched
|
|
18
|
+
* into `responses: [resp]` against `calls[0]`. Requires `calls.length === 1`;
|
|
19
|
+
* otherwise a `QodoWireValidationError` surfaces. The most common shape
|
|
20
|
+
* after a 1.x → 2.0 migration (where every request was single-call).
|
|
21
|
+
* - returns `readonly ToolResponseResult[]` (one per call) → auto-batched
|
|
22
|
+
* positionally; length must equal `calls.length`.
|
|
23
|
+
* - returns `undefined` → fall through
|
|
24
|
+
* to the next handler in the stack; if every handler returns `undefined`
|
|
25
|
+
* the consumer is expected to call `respond()` later (approval flow / async
|
|
26
|
+
* tool). With zero handlers, the SDK emits a `denied` envelope so QAR's
|
|
27
|
+
* agent loop doesn't hang.
|
|
28
|
+
*
|
|
29
|
+
* `respond(nodeName, response | responses)` is the manual escape hatch. It
|
|
30
|
+
* builds a `tool.response` envelope and writes it through the connection. No
|
|
31
|
+
* ack: QAR's `task.delta` / `task.done` chain advances on its own once the
|
|
32
|
+
* response lands.
|
|
33
|
+
*
|
|
34
|
+
* Concurrency: each `tool.request` envelope carries N calls but is dispatched
|
|
35
|
+
* as a single handler invocation. QAR may emit multiple `tool.request`
|
|
36
|
+
* envelopes per task (separate dispatch rounds); each runs through the stack
|
|
37
|
+
* independently. The wire is keyed on `tool_call_id` for each individual call.
|
|
38
|
+
*
|
|
39
|
+
* Error mapping (per-call):
|
|
40
|
+
* - handler throws `ToolDeniedError` → `status: 'error'`, `error.code: 'denied'`
|
|
41
|
+
* - handler throws anything else → `status: 'error'`, `error.code: 'error'`
|
|
42
|
+
* - returns malformed result → `status: 'error'`, generic message
|
|
43
|
+
* (handler internals never leak to QAR)
|
|
44
|
+
*
|
|
45
|
+
* Error/reason strings are truncated so a stack trace can't blow up the wire
|
|
46
|
+
* payload.
|
|
47
|
+
*/
|
|
48
|
+
import { QodoWireValidationError, ToolDeniedError } from './errors.js';
|
|
49
|
+
import { isThenable } from './internal/thenable.js';
|
|
50
|
+
import { toolOutcomeFor } from '../observability/spans.js';
|
|
51
|
+
import { QAR_TOOL_OUTCOME } from '../observability/attributes.js';
|
|
52
|
+
import { FunctionToolRouter } from './tools/bindFunctionToolDefs.js';
|
|
53
|
+
import { defaultApprovalHandler, } from './tools/approval.js';
|
|
54
|
+
/** Cap on `error.message` strings serialized into `tool.response` items. */
|
|
55
|
+
const MAX_ERROR_LEN = 1024;
|
|
56
|
+
export class ToolClient {
|
|
57
|
+
resolveConnection;
|
|
58
|
+
spanRecorder;
|
|
59
|
+
handlers = [];
|
|
60
|
+
/**
|
|
61
|
+
* SDK-attached auto-bridge handlers (qodo-skills, remote-MCP). Distinct
|
|
62
|
+
* from {@link handlers} (consumer-registered via `onRequest`) so the
|
|
63
|
+
* dispatcher's deterministic-deny path can fire when only SDK fallbacks
|
|
64
|
+
* are in the stack and no consumer-side handler exists.
|
|
65
|
+
*
|
|
66
|
+
* Without this split, the bridge-being-attached defeats the
|
|
67
|
+
* zero-primary-handler deny fallback. Walking primaries first,
|
|
68
|
+
* fallbacks second, with deny keyed on `primaries.length === 0`
|
|
69
|
+
* (regardless of fallback count) gives consumers the
|
|
70
|
+
* deterministic-deny ergonomic they expect from "I didn't register
|
|
71
|
+
* a handler" + lets the SDK auto-bridges still route MCP / skills
|
|
72
|
+
* calls when the consumer DOES register handlers later.
|
|
73
|
+
*/
|
|
74
|
+
fallbackHandlers = [];
|
|
75
|
+
wireValidationHandlers = [];
|
|
76
|
+
dispatcher = null;
|
|
77
|
+
dispatcherConnection = null;
|
|
78
|
+
/**
|
|
79
|
+
* `defineFunctionTool` router — owns the per-tool-name handler map
|
|
80
|
+
* and a single `onRequest` subscription that dispatches inbound
|
|
81
|
+
* `tool.request` calls by name. Lazily attached the first time a
|
|
82
|
+
* function-tool handler is registered.
|
|
83
|
+
*/
|
|
84
|
+
functionToolRouter = new FunctionToolRouter();
|
|
85
|
+
/**
|
|
86
|
+
* S4 — consumer-overridable approval resolver for
|
|
87
|
+
* `requireApproval`-gated function tools. Defaults to the
|
|
88
|
+
* stdin-prompt-when-TTY-else-deny handler from
|
|
89
|
+
* `src/client/tools/approval.ts`. Production apps register a custom
|
|
90
|
+
* handler (Slack, web UI, pager) via {@link setApprovalHandler}.
|
|
91
|
+
*
|
|
92
|
+
* Mutable so a `setApprovalHandler` call AFTER the router attaches
|
|
93
|
+
* still applies to subsequent batches — the router captures a
|
|
94
|
+
* getter, not the value, at attach time.
|
|
95
|
+
*/
|
|
96
|
+
approvalHandler = defaultApprovalHandler;
|
|
97
|
+
/**
|
|
98
|
+
* @param resolveConnection Returns the live `Connection` or throws if the
|
|
99
|
+
* client isn't connected yet.
|
|
100
|
+
* @param spanRecorder Recorder for `qar.client.tool.*` spans. No-op
|
|
101
|
+
* when OTel isn't configured.
|
|
102
|
+
*/
|
|
103
|
+
constructor(resolveConnection, spanRecorder) {
|
|
104
|
+
this.resolveConnection = resolveConnection;
|
|
105
|
+
this.spanRecorder = spanRecorder;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Register a handler for inbound `tool.request` events.
|
|
109
|
+
*
|
|
110
|
+
* Stacking semantics: the first handler that returns a non-`undefined`
|
|
111
|
+
* value (single result or array) claims the entire request; its responses
|
|
112
|
+
* are auto-sent. If every handler returns `undefined`, the consumer must
|
|
113
|
+
* call `respond()` explicitly within QAR's per-task tool-response timeout.
|
|
114
|
+
*
|
|
115
|
+
* `QodoClient.connect()` already attaches the fan-out dispatcher on the
|
|
116
|
+
* live connection (so a `tool.request` arriving with zero handlers
|
|
117
|
+
* deterministically emits `status: 'error'` / `error.code: 'denied'`
|
|
118
|
+
* instead of leaving QAR paused on the call set). `onRequest` just adds to
|
|
119
|
+
* the stack.
|
|
120
|
+
*
|
|
121
|
+
* If `attachDispatcher()` throws — e.g. the caller registered a handler
|
|
122
|
+
* before `connect()` and we have no connection to attach to — we roll
|
|
123
|
+
* back the handler push so consumers don't end up with an orphaned
|
|
124
|
+
* registration they can't `unsubscribe()`.
|
|
125
|
+
*/
|
|
126
|
+
onRequest(handler) {
|
|
127
|
+
this.handlers.push(handler);
|
|
128
|
+
try {
|
|
129
|
+
this.attachDispatcher();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// Roll back the just-added handler so the failed call leaves no trace.
|
|
133
|
+
const idx = this.handlers.lastIndexOf(handler);
|
|
134
|
+
if (idx >= 0)
|
|
135
|
+
this.handlers.splice(idx, 1);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
unsubscribe: () => {
|
|
140
|
+
const idx = this.handlers.indexOf(handler);
|
|
141
|
+
if (idx >= 0)
|
|
142
|
+
this.handlers.splice(idx, 1);
|
|
143
|
+
// Don't detach the dispatcher when the stack empties — the bound
|
|
144
|
+
// dispatcher is the deterministic-deny path for `tool.request`s
|
|
145
|
+
// that arrive while no handler is registered. Detach only on
|
|
146
|
+
// disconnect (handled by the dispatcher's `onConnectionClosed`).
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Register `defineFunctionTool`-produced entries against the
|
|
152
|
+
* client's internal router. Any tool in `tools` carrying the
|
|
153
|
+
* `defineFunctionTool` handler symbol gets added to the router's
|
|
154
|
+
* name → handler table; tools without the symbol (e.g. hand-authored
|
|
155
|
+
* `FunctionToolDef` literals) are skipped.
|
|
156
|
+
*
|
|
157
|
+
* Idempotent — re-registering the same tool name replaces the
|
|
158
|
+
* existing handler in-place. The router's `onRequest` subscription
|
|
159
|
+
* is attached lazily on first registration; it stays attached for
|
|
160
|
+
* the lifetime of the client.
|
|
161
|
+
*
|
|
162
|
+
* `tasks.startWithAgent` / `tasks.startWithGraph` auto-call this
|
|
163
|
+
* with `agent.tools` so the common path "drop helpers into
|
|
164
|
+
* `tools[]`" needs no manual binding. The explicit method is
|
|
165
|
+
* available for consumers using `tasks.start` (server-defined
|
|
166
|
+
* agent_id) who still want type-safe handler organization by tool
|
|
167
|
+
* name, or for any pre-task registration flow.
|
|
168
|
+
*
|
|
169
|
+
* @returns Number of newly registered handlers from this call.
|
|
170
|
+
*/
|
|
171
|
+
bindFunctionTools(tools) {
|
|
172
|
+
const added = this.functionToolRouter.register(tools);
|
|
173
|
+
if (added > 0)
|
|
174
|
+
this.functionToolRouter.attach(this);
|
|
175
|
+
return added;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Register the resolver invoked when a `requireApproval`-gated
|
|
179
|
+
* function tool is about to dispatch (S4). The handler decides
|
|
180
|
+
* approve / deny; the SDK runs the function-tool handler on
|
|
181
|
+
* approve and returns `outcome: 'denied'` to QAR on deny.
|
|
182
|
+
*
|
|
183
|
+
* The default handler prompts on stdin when `process.stdin.isTTY`
|
|
184
|
+
* is true; in non-interactive contexts it deterministic-denies
|
|
185
|
+
* with a typed error message pointing at the missing registration.
|
|
186
|
+
* Production apps that route approvals to Slack / a web UI / a
|
|
187
|
+
* pager should register a custom handler at startup.
|
|
188
|
+
*
|
|
189
|
+
* Late binding is fine — a handler registered AFTER the router
|
|
190
|
+
* has attached still applies to subsequent inbound batches.
|
|
191
|
+
*
|
|
192
|
+
* @see {@link ApprovalHandler} for the resolver contract.
|
|
193
|
+
* @see `src/client/tools/approval.ts` for the default behavior.
|
|
194
|
+
*/
|
|
195
|
+
setApprovalHandler(handler) {
|
|
196
|
+
if (typeof handler !== 'function') {
|
|
197
|
+
throw new TypeError('ToolClient.setApprovalHandler: handler must be a function');
|
|
198
|
+
}
|
|
199
|
+
this.approvalHandler = handler;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* @internal — read by `FunctionToolRouter.attach` so dispatch-time
|
|
203
|
+
* approvals see the consumer's current handler (or the default).
|
|
204
|
+
* Exposed under the `_` prefix to keep it off the public IDE
|
|
205
|
+
* autocomplete; same convention as
|
|
206
|
+
* {@link _attachFallbackHandler} / {@link _bindConnection}.
|
|
207
|
+
*/
|
|
208
|
+
_getApprovalHandler() {
|
|
209
|
+
return this.approvalHandler;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* @internal — used by `FunctionToolRouter` to pre-check the wire
|
|
213
|
+
* after an approval resolves, BEFORE invoking the function-tool
|
|
214
|
+
* handler. Without the gate, a long-parked approval (Slack / web UI
|
|
215
|
+
* / pager round-trip) can resolve approve AFTER the auto-reconnect
|
|
216
|
+
* loop exhausted; the handler then runs (real-world side effect),
|
|
217
|
+
* `sendResponse` throws {@link QodoColdAddressError} because
|
|
218
|
+
* `clearTerminalState` already wiped the per-tool-call session map,
|
|
219
|
+
* and the swallow path in {@link attachDispatcher}'s send-fallback
|
|
220
|
+
* drops the response without a signal. Pinned in
|
|
221
|
+
* `tests/qar-mode/hitl-state-machine-matrix.test.ts`.
|
|
222
|
+
*
|
|
223
|
+
* Returns `false` when the connection is not currently addressable
|
|
224
|
+
* for outbound sends — either pre-`connect()`, mid-reconnect, or
|
|
225
|
+
* post-`failed`. The router synthesizes
|
|
226
|
+
* `outcome: 'error', error: 'connection lost before approval resolved'`
|
|
227
|
+
* for this case so the handler never executes; the result still
|
|
228
|
+
* routes through the normal dispatcher send path, which (correctly)
|
|
229
|
+
* swallows it once more because the wire is dead — but the
|
|
230
|
+
* load-bearing behavior is the **handler skip**, not the response.
|
|
231
|
+
*/
|
|
232
|
+
_isConnectionLive() {
|
|
233
|
+
return this.isConnectionLive();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* @internal — wired by `QodoClient.connect()` once the WS is open. Attaches
|
|
237
|
+
* a fan-out dispatcher to the connection so an inbound `tool.request` always
|
|
238
|
+
* round-trips a response, even when no `onRequest` handler is registered.
|
|
239
|
+
* Without this seam QAR's agent loop would hang on the call set.
|
|
240
|
+
*/
|
|
241
|
+
_bindConnection(connection) {
|
|
242
|
+
this.attachDispatcher(connection);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* @internal — used by `QodoClient` to attach SDK-side auto-bridges
|
|
246
|
+
* (`qodo-skills` MCP, generic remote-MCP). Fallback handlers are
|
|
247
|
+
* walked AFTER primary handlers (consumer-registered via {@link
|
|
248
|
+
* onRequest}). They don't count toward the dispatcher's
|
|
249
|
+
* "zero-handlers → deterministic deny" check, so a consumer who
|
|
250
|
+
* registers a remote MCP but no `onRequest` handler still sees the
|
|
251
|
+
* deny fallback fire on unknown tools rather than hanging.
|
|
252
|
+
*
|
|
253
|
+
* Same registration / unsubscribe ergonomics as {@link onRequest};
|
|
254
|
+
* the API split is purely internal so consumers don't accidentally
|
|
255
|
+
* register against the fallback stack.
|
|
256
|
+
*/
|
|
257
|
+
_attachFallbackHandler(handler) {
|
|
258
|
+
this.fallbackHandlers.push(handler);
|
|
259
|
+
try {
|
|
260
|
+
this.attachDispatcher();
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
const idx = this.fallbackHandlers.lastIndexOf(handler);
|
|
264
|
+
if (idx >= 0)
|
|
265
|
+
this.fallbackHandlers.splice(idx, 1);
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
unsubscribe: () => {
|
|
270
|
+
const idx = this.fallbackHandlers.indexOf(handler);
|
|
271
|
+
if (idx >= 0)
|
|
272
|
+
this.fallbackHandlers.splice(idx, 1);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Register a handler for inbound wire-validation failures — `tool.request`
|
|
278
|
+
* envelopes the SDK dropped because they violated the wire shape
|
|
279
|
+
* (missing `node_name`, empty `calls`). The handler receives a typed
|
|
280
|
+
* `QodoWireValidationError` that carries the offending `message_id` and
|
|
281
|
+
* the field that failed validation, so consumer telemetry can branch on
|
|
282
|
+
* the exact protocol drift without parsing the `[@qodo/sdk]` log line.
|
|
283
|
+
*
|
|
284
|
+
* Observer-style: every registered handler fires on every drop (no
|
|
285
|
+
* winner-takes-all dispatch). Handlers run inside a defensive try/catch
|
|
286
|
+
* — a throwing handler logs to `console.error` and is otherwise swallowed
|
|
287
|
+
* so the dispatch hot path stays alive.
|
|
288
|
+
*
|
|
289
|
+
* The SDK has no `tool_call_id` to respond against for these envelopes,
|
|
290
|
+
* so registering a handler does NOT make the runtime see a response —
|
|
291
|
+
* QAR's per-task tool-response timeout remains the wire-side safety net.
|
|
292
|
+
* This surface exists purely so consumer code can observe the failure.
|
|
293
|
+
*
|
|
294
|
+
* Returning `Subscription` mirrors `onRequest` — `unsubscribe()` removes
|
|
295
|
+
* the handler; the dispatcher itself stays attached (it's the path that
|
|
296
|
+
* fires the breadcrumb log even when no handler is registered).
|
|
297
|
+
*/
|
|
298
|
+
onWireValidationError(handler) {
|
|
299
|
+
this.wireValidationHandlers.push(handler);
|
|
300
|
+
try {
|
|
301
|
+
this.attachDispatcher();
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
// Roll back the registration so the failed call leaves no trace.
|
|
305
|
+
const idx = this.wireValidationHandlers.lastIndexOf(handler);
|
|
306
|
+
if (idx >= 0)
|
|
307
|
+
this.wireValidationHandlers.splice(idx, 1);
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
unsubscribe: () => {
|
|
312
|
+
const idx = this.wireValidationHandlers.indexOf(handler);
|
|
313
|
+
if (idx >= 0)
|
|
314
|
+
this.wireValidationHandlers.splice(idx, 1);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* @internal — invoked by the dispatcher when it drops a malformed inbound
|
|
320
|
+
* `tool.request`. Fires every registered `onWireValidationError` handler
|
|
321
|
+
* with a typed error; per-handler exceptions are logged + swallowed so
|
|
322
|
+
* one buggy observer can't crash dispatch.
|
|
323
|
+
*/
|
|
324
|
+
emitWireValidationError(err) {
|
|
325
|
+
// Snapshot the handler list — a handler may unsubscribe itself while
|
|
326
|
+
// we iterate. Mirrors `dispatch()`'s pattern with `onRequest` handlers.
|
|
327
|
+
const snapshot = [...this.wireValidationHandlers];
|
|
328
|
+
for (const handler of snapshot) {
|
|
329
|
+
try {
|
|
330
|
+
// Sync throw → caught here. Async rejection → caught by the
|
|
331
|
+
// Promise.resolve() chain below. Without the async guard a
|
|
332
|
+
// consumer registering `async (err) => { await persist(err); }`
|
|
333
|
+
// would surface their handler's rejections as a process-level
|
|
334
|
+
// `unhandledRejection`.
|
|
335
|
+
const result = handler(err);
|
|
336
|
+
if (isThenable(result)) {
|
|
337
|
+
result.then(undefined, (handlerErr) => {
|
|
338
|
+
logHandlerObserverError('onWireValidationError', handlerErr);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (handlerErr) {
|
|
343
|
+
logHandlerObserverError('onWireValidationError', handlerErr);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async respond(nodeName, responses, opts) {
|
|
348
|
+
validateNodeName(nodeName, 'ToolClient.respond');
|
|
349
|
+
const batch = Array.isArray(responses)
|
|
350
|
+
? responses
|
|
351
|
+
: [responses];
|
|
352
|
+
if (batch.length === 0) {
|
|
353
|
+
throw new QodoWireValidationError('ToolClient.respond: responses must be a non-empty list — tool.response always carries ≥ 1 item.');
|
|
354
|
+
}
|
|
355
|
+
for (const r of batch) {
|
|
356
|
+
if (!isValidToolResponseResult(r.result)) {
|
|
357
|
+
throw new TypeError('ToolClient.respond: every response.result must be a ToolResponseResult ({ outcome: "success" | "error" | "denied", ... })');
|
|
358
|
+
}
|
|
359
|
+
if (typeof r.tool_call_id !== 'string' || r.tool_call_id.length === 0) {
|
|
360
|
+
throw new TypeError('ToolClient.respond: every response must carry a non-empty tool_call_id');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const connection = this.resolveConnection();
|
|
364
|
+
// Per-call span family — one `qar.client.tool.respond` span per response
|
|
365
|
+
// item so per-call latency + outcome land independently in traces. Spans
|
|
366
|
+
// share the same parent (whatever was active when the consumer called
|
|
367
|
+
// respond()).
|
|
368
|
+
//
|
|
369
|
+
// The per-call `session_id` is looked up by tool_call_id — every
|
|
370
|
+
// response item echoes back to the session of its originating
|
|
371
|
+
// `tool.request`. When the consumer passed `opts.sessionId`
|
|
372
|
+
// (cold-respond after process restart), use it for the span
|
|
373
|
+
// attribute — the in-memory map will be empty in that scenario, but
|
|
374
|
+
// the consumer knows the right value. `Connection.sendEnvelope`
|
|
375
|
+
// performs its own lookup and throws `QodoColdAddressError` on miss
|
|
376
|
+
// if no override is provided.
|
|
377
|
+
const spans = batch.map((r) => this.spanRecorder.startToolRespondSpan({
|
|
378
|
+
sessionId: opts?.sessionId ?? connection.getSessionForToolCall(r.tool_call_id),
|
|
379
|
+
toolCallId: r.tool_call_id,
|
|
380
|
+
outcome: toolOutcomeFor(r.result.outcome),
|
|
381
|
+
nodeName,
|
|
382
|
+
}));
|
|
383
|
+
try {
|
|
384
|
+
// Use the FIRST span's context for the actual WS write so the outbound
|
|
385
|
+
// envelope's `traceparent` references something deterministic. The other
|
|
386
|
+
// per-call spans still record latency individually; their context isn't
|
|
387
|
+
// attached to the wire frame (a single envelope can carry only one
|
|
388
|
+
// traceparent). Cross-process trace continuity per call is preserved by
|
|
389
|
+
// the matched `qar.tool_call_id` attribute on both sides.
|
|
390
|
+
spans[0].withContext(() => this.sendResponse(nodeName, batch, opts?.sessionId));
|
|
391
|
+
for (const s of spans)
|
|
392
|
+
s.succeed();
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
for (const s of spans)
|
|
396
|
+
s.fail(err);
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
attachDispatcher(explicitConnection) {
|
|
401
|
+
const connection = explicitConnection ?? this.resolveConnection();
|
|
402
|
+
if (this.dispatcher !== null && this.dispatcherConnection === connection)
|
|
403
|
+
return;
|
|
404
|
+
// Connection swap (rare — disconnect + reconnect between onRequest calls):
|
|
405
|
+
// detach the stale dispatcher from the old connection before re-attaching.
|
|
406
|
+
if (this.dispatcher !== null && this.dispatcherConnection !== null) {
|
|
407
|
+
try {
|
|
408
|
+
this.dispatcherConnection.unsubscribe(this.dispatcher);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// The old connection may already be torn down — best-effort cleanup.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.dispatcher = new ToolRequestDispatcher(this.handlers, this.fallbackHandlers, (nodeName, responses) => {
|
|
415
|
+
// Two failure modes for the send: connection closed mid-flight (no
|
|
416
|
+
// useful retry), or `JSON.stringify` blew up on a payload that slipped
|
|
417
|
+
// past the validator (e.g., a Date / Map masquerading as a JsonObject —
|
|
418
|
+
// the recursive validator catches cycles + non-JSON primitives but
|
|
419
|
+
// can't catch every prototype-shaped impostor). For the second case
|
|
420
|
+
// we attempt a deterministic per-call `status: 'error'` envelope so
|
|
421
|
+
// QAR's agent loop unblocks instead of timing out.
|
|
422
|
+
try {
|
|
423
|
+
this.sendResponse(nodeName, responses);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
if (!this.isConnectionLive())
|
|
428
|
+
return;
|
|
429
|
+
if (this.isInternalSerializationError(err, responses)) {
|
|
430
|
+
try {
|
|
431
|
+
const fallback = responses.map((r) => ({
|
|
432
|
+
tool_call_id: r.tool_call_id,
|
|
433
|
+
result: {
|
|
434
|
+
outcome: 'error',
|
|
435
|
+
error: 'internal: failed to serialize tool response',
|
|
436
|
+
},
|
|
437
|
+
}));
|
|
438
|
+
this.sendResponse(nodeName, fallback);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Fallback also failed — best-effort; QAR's per-task
|
|
442
|
+
// tool-response timeout is the safety net.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}, () => {
|
|
447
|
+
// Connection closed (clean or via transport error). Drop our refs so
|
|
448
|
+
// a later `onRequest` (after a fresh `connect()`) re-attaches against
|
|
449
|
+
// the new connection instead of pointing at a dead one.
|
|
450
|
+
this.dispatcher = null;
|
|
451
|
+
this.dispatcherConnection = null;
|
|
452
|
+
},
|
|
453
|
+
// Per-call session lookup via the connection's `toolCallSessions`
|
|
454
|
+
// map — populated when the inbound `tool.request` landed, so it's
|
|
455
|
+
// guaranteed to be present here (the dispatcher only builds spans
|
|
456
|
+
// for envelopes it just absorbed).
|
|
457
|
+
(call, nodeName) => this.spanRecorder.startToolHandlerSpan({
|
|
458
|
+
sessionId: connection.getSessionForToolCall(call.tool_call_id),
|
|
459
|
+
toolCallId: call.tool_call_id,
|
|
460
|
+
toolName: call.name,
|
|
461
|
+
nodeName,
|
|
462
|
+
}), (err) => this.emitWireValidationError(err));
|
|
463
|
+
connection.subscribe(this.dispatcher);
|
|
464
|
+
this.dispatcherConnection = connection;
|
|
465
|
+
}
|
|
466
|
+
sendResponse(nodeName, responses, sessionOverride) {
|
|
467
|
+
const connection = this.resolveConnection();
|
|
468
|
+
const payload = {
|
|
469
|
+
node_name: nodeName,
|
|
470
|
+
responses: responses.map((r) => toResponseItem(r.tool_call_id, r.result)),
|
|
471
|
+
};
|
|
472
|
+
connection.sendEnvelope({ kind: 'tool.response', payload }, sessionOverride !== undefined ? { sessionId: sessionOverride } : undefined);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Snapshot whether the underlying connection is still open. Used by the
|
|
476
|
+
* dispatcher's send-fallback to distinguish "connection died" (give up) from
|
|
477
|
+
* "the payload itself was poisoned" (try a safe `status: 'error'`).
|
|
478
|
+
*/
|
|
479
|
+
isConnectionLive() {
|
|
480
|
+
try {
|
|
481
|
+
return this.resolveConnection().isOpen;
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Heuristic: a send failure is an internal serialization error (rather than
|
|
489
|
+
* a connection-level one) when the connection is live AND the batch we just
|
|
490
|
+
* tried to send wasn't already the safe internal-error fallback. Filtering
|
|
491
|
+
* on the fallback's own message stops a serialization-failed loop from
|
|
492
|
+
* looping.
|
|
493
|
+
*/
|
|
494
|
+
isInternalSerializationError(_err, lastBatch) {
|
|
495
|
+
const alreadyFallback = lastBatch.every((r) => r.result.outcome === 'error' && r.result.error.startsWith('internal: '));
|
|
496
|
+
return !alreadyFallback;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The fan-out subscription: walks the handler stack for every `tool.request`,
|
|
501
|
+
* routes the first non-`undefined` handler-return (single result or array of
|
|
502
|
+
* results) to the connection.
|
|
503
|
+
*
|
|
504
|
+
* Handlers receive the full `ToolRequestEnvelope` (so they can read
|
|
505
|
+
* `node_name` and the entire `calls` list). The dispatcher itself doesn't
|
|
506
|
+
* claim the envelope (`consider` returns `false`) so task subscriptions still
|
|
507
|
+
* see `tool.request` on their `for await` stream if the consumer wants to
|
|
508
|
+
* observe both surfaces.
|
|
509
|
+
*/
|
|
510
|
+
class ToolRequestDispatcher {
|
|
511
|
+
handlers;
|
|
512
|
+
fallbackHandlers;
|
|
513
|
+
onResult;
|
|
514
|
+
onConnectionClosed;
|
|
515
|
+
buildHandlerSpan;
|
|
516
|
+
onWireValidationError;
|
|
517
|
+
constructor(handlers,
|
|
518
|
+
/**
|
|
519
|
+
* SDK-side fallback handler stack (auto-bridges). Walked AFTER the
|
|
520
|
+
* primary {@link handlers} stack so consumer-registered `onRequest`
|
|
521
|
+
* handlers get first claim; the dispatcher's
|
|
522
|
+
* "zero-primary-handlers → deterministic deny" check is keyed off
|
|
523
|
+
* `handlers.length` only (independent of fallback presence) so
|
|
524
|
+
* consumer-side deny semantics aren't shadowed by SDK auto-bridges.
|
|
525
|
+
*/
|
|
526
|
+
fallbackHandlers, onResult, onConnectionClosed,
|
|
527
|
+
/**
|
|
528
|
+
* Span builder for the per-call inbound `tool.request` → handler invocation.
|
|
529
|
+
* Each dispatch opens one span per `ToolCall`; the dispatcher closes each
|
|
530
|
+
* with the matching per-call outcome before batching the responses.
|
|
531
|
+
*/
|
|
532
|
+
buildHandlerSpan,
|
|
533
|
+
/**
|
|
534
|
+
* Fired when an inbound `tool.request` is dropped because it violates
|
|
535
|
+
* the wire shape (empty `node_name` / empty `calls`). Lets the parent
|
|
536
|
+
* `ToolClient` route the typed error to its registered
|
|
537
|
+
* `onWireValidationError` observers AFTER the `[@qodo/sdk]` breadcrumb
|
|
538
|
+
* lands on `console.error`.
|
|
539
|
+
*/
|
|
540
|
+
onWireValidationError) {
|
|
541
|
+
this.handlers = handlers;
|
|
542
|
+
this.fallbackHandlers = fallbackHandlers;
|
|
543
|
+
this.onResult = onResult;
|
|
544
|
+
this.onConnectionClosed = onConnectionClosed;
|
|
545
|
+
this.buildHandlerSpan = buildHandlerSpan;
|
|
546
|
+
this.onWireValidationError = onWireValidationError;
|
|
547
|
+
}
|
|
548
|
+
consider(env) {
|
|
549
|
+
if (env.kind !== 'tool.request')
|
|
550
|
+
return false;
|
|
551
|
+
// Fire-and-forget guard against unhandled promise rejections.
|
|
552
|
+
// `dispatch()` already wraps its body in a top-level try/catch and
|
|
553
|
+
// best-efforts a `denied` envelope per call via
|
|
554
|
+
// `emitContainedFailure`. The .catch() here only fires if THAT path
|
|
555
|
+
// also throws — e.g. a sync exception escapes before the inner try runs
|
|
556
|
+
// (buggy interceptor wrapping the async function), or
|
|
557
|
+
// `emitContainedFailure` itself throws past its own swallowing layer.
|
|
558
|
+
// Either way: log a single breadcrumb so the failure is observable in
|
|
559
|
+
// operator logs, then swallow. NEVER let this become a process-level
|
|
560
|
+
// `unhandledRejection`.
|
|
561
|
+
void this.dispatch(env).catch((err) => {
|
|
562
|
+
logUnhandledDispatchFailure(err, env.message_id);
|
|
563
|
+
});
|
|
564
|
+
// Don't claim — task subscriptions also surface tool.request on the
|
|
565
|
+
// iterator for consumers that want to observe the round-trip.
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
considerClient(_ev) {
|
|
569
|
+
// Tool dispatcher is wire-only — it doesn't yield client lifecycle
|
|
570
|
+
// events to anyone. The `qar.client.*` events surface on `TaskEvent`
|
|
571
|
+
// iterators and on `client.receive()` instead. Parameter kept (unused)
|
|
572
|
+
// to satisfy the `InboundSubscription` interface signature exactly.
|
|
573
|
+
}
|
|
574
|
+
fail(_err) {
|
|
575
|
+
// The connection layer reports the failure to every subscription. We
|
|
576
|
+
// hand it back to ToolClient via the close hook so future onRequest()
|
|
577
|
+
// calls re-attach against a fresh connection rather than the dead one.
|
|
578
|
+
this.onConnectionClosed();
|
|
579
|
+
}
|
|
580
|
+
close() {
|
|
581
|
+
this.onConnectionClosed();
|
|
582
|
+
}
|
|
583
|
+
async dispatch(env) {
|
|
584
|
+
// Top-level guard — anything from `buildHandlerSpan` throwing (buggy OTel
|
|
585
|
+
// tracer impl) to a handler throwing synchronously past the inner mapper
|
|
586
|
+
// lands here. The fire-and-forget caller in `consider()` would otherwise
|
|
587
|
+
// surface this as an unhandled promise rejection.
|
|
588
|
+
// Containment policy: best-effort emit a `denied`/`error` envelope per
|
|
589
|
+
// call so QAR's agent loop unblocks; if even that fails, swallow — the
|
|
590
|
+
// runtime's per-task tool-response timeout is the safety net.
|
|
591
|
+
try {
|
|
592
|
+
await this.dispatchInner(env);
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
this.emitContainedFailure(env, err);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async dispatchInner(env) {
|
|
599
|
+
const { calls, node_name: nodeName } = env.payload;
|
|
600
|
+
// Inbound wire validation. An inbound envelope with empty
|
|
601
|
+
// `node_name` or empty `calls` can't be routed (no `tool_call_id` to
|
|
602
|
+
// respond against, no node to address). Throwing here would fail the
|
|
603
|
+
// connection across every subscription — far worse for a single
|
|
604
|
+
// malformed envelope. Log + drop, and let the runtime time out the call
|
|
605
|
+
// set on its end.
|
|
606
|
+
// Task subscriptions still observe the malformed envelope on their
|
|
607
|
+
// iterator, so consumers can also branch on `event.kind === 'tool.request'`
|
|
608
|
+
// and inspect `payload` if they need their own surface.
|
|
609
|
+
if (typeof nodeName !== 'string' || nodeName.length === 0) {
|
|
610
|
+
const violation = 'node_name is required and must be a non-empty string';
|
|
611
|
+
logWireValidationDrop({ violation, messageId: env.message_id });
|
|
612
|
+
this.fireWireValidationError(violation, env.message_id, undefined);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (!Array.isArray(calls) || calls.length === 0) {
|
|
616
|
+
const violation = 'calls must be a non-empty list';
|
|
617
|
+
logWireValidationDrop({ violation, messageId: env.message_id, nodeName });
|
|
618
|
+
this.fireWireValidationError(violation, env.message_id, nodeName);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
// Per-call spans (one per `ToolCall`). All share the same OTel parent —
|
|
622
|
+
// whatever context was active when the inbound envelope arrived. Spans
|
|
623
|
+
// close per-call with the matching outcome.
|
|
624
|
+
const spans = calls.map((c) => this.buildHandlerSpan(c, nodeName));
|
|
625
|
+
// `primaries.length === 0` is the consumer-side signal "no handler
|
|
626
|
+
// registered" (the dispatcher tracks consumer handlers separately
|
|
627
|
+
// from SDK auto-bridges). When no consumer registered a handler AND
|
|
628
|
+
// no SDK fallback claims, the dispatcher emits the deterministic
|
|
629
|
+
// deny envelope so QAR's agent loop unblocks.
|
|
630
|
+
const noPrimaryHandlers = this.handlers.length === 0;
|
|
631
|
+
if (noPrimaryHandlers && this.fallbackHandlers.length === 0) {
|
|
632
|
+
// Pre-fix behavior preserved verbatim for the "absolutely no
|
|
633
|
+
// handlers anywhere" case.
|
|
634
|
+
for (const s of spans)
|
|
635
|
+
this.finishHandlerSpan(s, 'denied');
|
|
636
|
+
const responses = calls.map((c) => ({
|
|
637
|
+
tool_call_id: c.tool_call_id,
|
|
638
|
+
result: { outcome: 'denied', error: 'no handler registered' },
|
|
639
|
+
}));
|
|
640
|
+
this.onResult(nodeName, responses);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// Stage 1 — walk consumer-registered handlers. If any claims (returns
|
|
644
|
+
// non-undefined), we emit + return. Snapshot the stack so a handler
|
|
645
|
+
// unsubscribing itself mid-iterate doesn't break the walk.
|
|
646
|
+
const primaryResult = await this.walkHandlerStack([...this.handlers], env, calls, spans, nodeName);
|
|
647
|
+
if (primaryResult === 'claimed')
|
|
648
|
+
return;
|
|
649
|
+
// Stage 2 — walk SDK fallback handlers (auto-bridges).
|
|
650
|
+
const fallbackResult = await this.walkHandlerStack([...this.fallbackHandlers], env, calls, spans, nodeName);
|
|
651
|
+
if (fallbackResult === 'claimed')
|
|
652
|
+
return;
|
|
653
|
+
// Both stages exhausted with no claim. Two terminal cases:
|
|
654
|
+
// (a) No consumer handlers existed → emit the deterministic deny
|
|
655
|
+
// so QAR's agent loop unblocks. This is the post-fix semantic:
|
|
656
|
+
// the SDK auto-bridges are advisory — they don't suppress the
|
|
657
|
+
// deny that a zero-consumer-handlers config would otherwise
|
|
658
|
+
// get.
|
|
659
|
+
// (b) Consumer handlers existed but all returned undefined → defer
|
|
660
|
+
// to manual `respond()`. The consumer's contract is "I'll
|
|
661
|
+
// respond later"; QAR's per-task tool-response timeout is the
|
|
662
|
+
// wire-side safety net.
|
|
663
|
+
if (noPrimaryHandlers) {
|
|
664
|
+
for (const s of spans)
|
|
665
|
+
this.finishHandlerSpan(s, 'denied');
|
|
666
|
+
const responses = calls.map((c) => ({
|
|
667
|
+
tool_call_id: c.tool_call_id,
|
|
668
|
+
result: { outcome: 'denied', error: 'no handler registered' },
|
|
669
|
+
}));
|
|
670
|
+
this.onResult(nodeName, responses);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// Every handler returned undefined — defer to manual `respond()`. The
|
|
674
|
+
// handler spans end here with no outcome attribute; the manual respond
|
|
675
|
+
// path opens its own `qar.client.tool.respond` spans carrying the
|
|
676
|
+
// outcome. QAR enforces a per-tool-response timeout server-side; the
|
|
677
|
+
// SDK doesn't double-time-out here.
|
|
678
|
+
for (const s of spans)
|
|
679
|
+
s.succeed();
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Walk an ordered handler stack once, returning `'claimed'` when any
|
|
683
|
+
* handler produces a non-`undefined` result (the dispatcher emits the
|
|
684
|
+
* response + returns to the caller), or `'all-undefined'` when every
|
|
685
|
+
* handler in the stack returned `undefined` (caller decides the next
|
|
686
|
+
* step — try fallbacks, deny, or defer).
|
|
687
|
+
*
|
|
688
|
+
* Centralizes the per-handler invocation + per-call response
|
|
689
|
+
* normalization + span outcome attribution so primary + fallback
|
|
690
|
+
* stacks share identical semantics.
|
|
691
|
+
*/
|
|
692
|
+
async walkHandlerStack(snapshot, env, calls, spans, nodeName) {
|
|
693
|
+
for (const handler of snapshot) {
|
|
694
|
+
let raw;
|
|
695
|
+
try {
|
|
696
|
+
// Run the handler inside the FIRST span's context so any
|
|
697
|
+
// child spans the handler opens (e.g. wrapping its own work in
|
|
698
|
+
// `tracer.startActiveSpan`) parent under our
|
|
699
|
+
// `qar.client.tool.handler` — without this, the handler's spans
|
|
700
|
+
// would leaf back to whatever was active when the inbound
|
|
701
|
+
// `tool.request` arrived. For multi-call batches, the parent is
|
|
702
|
+
// calls[0]'s span; per-call attribution still lands via
|
|
703
|
+
// `qar.tool_call_id` on each sibling span.
|
|
704
|
+
raw = await spans[0].withContext(() => handler(env));
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
// A handler-thrown exception applies to the whole batch — there's no
|
|
708
|
+
// per-call attribution available from the throw. Map it once and
|
|
709
|
+
// replicate across every call. `denied` (ToolDeniedError) is intent,
|
|
710
|
+
// not failure → succeed-with-attr; anything else is an error path.
|
|
711
|
+
const mapped = mapHandlerError(err);
|
|
712
|
+
const outcome = mapped.outcome;
|
|
713
|
+
for (const s of spans) {
|
|
714
|
+
if (outcome === 'denied') {
|
|
715
|
+
this.finishHandlerSpan(s, 'denied');
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
s.setAttribute(QAR_TOOL_OUTCOME, outcome);
|
|
719
|
+
s.fail(err);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const responses = calls.map((c) => ({
|
|
723
|
+
tool_call_id: c.tool_call_id,
|
|
724
|
+
result: mapped,
|
|
725
|
+
}));
|
|
726
|
+
this.onResult(nodeName, responses);
|
|
727
|
+
return 'claimed';
|
|
728
|
+
}
|
|
729
|
+
if (raw === undefined)
|
|
730
|
+
continue;
|
|
731
|
+
const normalized = this.normalizeHandlerReturn(raw, calls);
|
|
732
|
+
if (normalized === null) {
|
|
733
|
+
// Length mismatch / malformed — fail the whole batch, surface generic.
|
|
734
|
+
for (const s of spans) {
|
|
735
|
+
s.setAttribute(QAR_TOOL_OUTCOME, 'error');
|
|
736
|
+
s.fail(new Error('handler returned malformed ToolResponseResult batch'));
|
|
737
|
+
}
|
|
738
|
+
const fallback = calls.map((c) => ({
|
|
739
|
+
tool_call_id: c.tool_call_id,
|
|
740
|
+
result: {
|
|
741
|
+
outcome: 'error',
|
|
742
|
+
error: 'handler returned malformed ToolResponseResult batch',
|
|
743
|
+
},
|
|
744
|
+
}));
|
|
745
|
+
this.onResult(nodeName, fallback);
|
|
746
|
+
return 'claimed';
|
|
747
|
+
}
|
|
748
|
+
const validated = [];
|
|
749
|
+
let anyMalformed = false;
|
|
750
|
+
normalized.forEach((result, idx) => {
|
|
751
|
+
if (!isValidToolResponseResult(result)) {
|
|
752
|
+
anyMalformed = true;
|
|
753
|
+
spans[idx].setAttribute(QAR_TOOL_OUTCOME, 'error');
|
|
754
|
+
spans[idx].fail(new Error('handler returned malformed ToolResponseResult'));
|
|
755
|
+
validated.push({
|
|
756
|
+
tool_call_id: calls[idx].tool_call_id,
|
|
757
|
+
result: {
|
|
758
|
+
outcome: 'error',
|
|
759
|
+
error: 'handler returned malformed ToolResponseResult',
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
this.finishHandlerSpan(spans[idx], toolOutcomeFor(result.outcome));
|
|
765
|
+
validated.push({ tool_call_id: calls[idx].tool_call_id, result });
|
|
766
|
+
});
|
|
767
|
+
void anyMalformed;
|
|
768
|
+
this.onResult(nodeName, validated);
|
|
769
|
+
return 'claimed';
|
|
770
|
+
}
|
|
771
|
+
return 'all-undefined';
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Coerce the handler's return into a positional `ToolResponseResult[]`
|
|
775
|
+
* matching `calls`. Returns `null` on length mismatch / non-array, non-result
|
|
776
|
+
* shape — the caller emits a generic "malformed" batch.
|
|
777
|
+
*/
|
|
778
|
+
normalizeHandlerReturn(raw, calls) {
|
|
779
|
+
if (Array.isArray(raw)) {
|
|
780
|
+
const arr = raw;
|
|
781
|
+
if (arr.length !== calls.length)
|
|
782
|
+
return null;
|
|
783
|
+
return arr;
|
|
784
|
+
}
|
|
785
|
+
// Single result — only valid when the request has exactly one call.
|
|
786
|
+
if (calls.length !== 1)
|
|
787
|
+
return null;
|
|
788
|
+
return [raw];
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Close a handler span with the documented outcome attribute. `error`
|
|
792
|
+
* outcomes (handler returned `outcome: "error"`) are *not* span errors —
|
|
793
|
+
* the wire path succeeded; the tool itself reported failure. Span ERROR
|
|
794
|
+
* status is reserved for unhandled exceptions in the dispatch path.
|
|
795
|
+
*/
|
|
796
|
+
finishHandlerSpan(span, outcome) {
|
|
797
|
+
span.setAttribute(QAR_TOOL_OUTCOME, outcome);
|
|
798
|
+
span.succeed();
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Best-effort containment for an unexpected failure in `dispatchInner` —
|
|
802
|
+
* triggered when something outside the handler-error mapping path throws
|
|
803
|
+
* (most realistically: an OTel tracer impl throwing from `buildHandlerSpan`
|
|
804
|
+
* before the per-call try/catch can run). Emits a `denied` envelope per
|
|
805
|
+
* call so QAR's agent loop unblocks. Any further failure (e.g., the
|
|
806
|
+
* tracer is wedged AND the send fails too) is swallowed — the runtime's
|
|
807
|
+
* per-task tool-response timeout is the final safety net.
|
|
808
|
+
*/
|
|
809
|
+
emitContainedFailure(env, _err) {
|
|
810
|
+
try {
|
|
811
|
+
const nodeName = env.payload?.node_name;
|
|
812
|
+
const calls = env.payload?.calls;
|
|
813
|
+
if (typeof nodeName !== 'string' ||
|
|
814
|
+
nodeName.length === 0 ||
|
|
815
|
+
!Array.isArray(calls) ||
|
|
816
|
+
calls.length === 0) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const responses = calls.map((c) => ({
|
|
820
|
+
tool_call_id: c.tool_call_id,
|
|
821
|
+
result: { outcome: 'denied', error: 'internal: dispatch failed' },
|
|
822
|
+
}));
|
|
823
|
+
this.onResult(nodeName, responses);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// Containment of the containment — swallow.
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Build a `QodoWireValidationError` carrying structured drop metadata and
|
|
831
|
+
* hand it to the parent `ToolClient` for fan-out to registered
|
|
832
|
+
* `onWireValidationError` observers. Defensive: any failure inside the
|
|
833
|
+
* handler-fan-out path is swallowed by the parent, but a synchronous
|
|
834
|
+
* exception during error CONSTRUCTION (extremely unlikely) is also
|
|
835
|
+
* caught here so it can't escape the dispatch hot path.
|
|
836
|
+
*/
|
|
837
|
+
fireWireValidationError(violation, messageId, nodeName) {
|
|
838
|
+
try {
|
|
839
|
+
const err = new QodoWireValidationError(`inbound tool.request envelope dropped: ${violation}`, { messageId, nodeName, scope: 'inbound' });
|
|
840
|
+
this.onWireValidationError(err);
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// Even constructing the error / dispatching the observers wedged.
|
|
844
|
+
// The breadcrumb log already landed; that's the contract.
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Map an exception thrown by a `ToolRequestHandler` to a `ToolResponseResult`.
|
|
850
|
+
* `ToolDeniedError` is the typed escape path (operator approval flow rejected,
|
|
851
|
+
* capability lookup denied) → `outcome: 'denied'`. Everything else maps to
|
|
852
|
+
* `outcome: 'error'` so the agent loop sees a generic failure rather than a
|
|
853
|
+
* silent denial. Truncation lives at the wire boundary (`toResponseItem`) so
|
|
854
|
+
* the cap applies uniformly to handler-thrown, handler-returned, and
|
|
855
|
+
* manual-respond paths.
|
|
856
|
+
*/
|
|
857
|
+
function mapHandlerError(err) {
|
|
858
|
+
if (err instanceof ToolDeniedError) {
|
|
859
|
+
return { outcome: 'denied', error: err.message };
|
|
860
|
+
}
|
|
861
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
862
|
+
return { outcome: 'error', error: message };
|
|
863
|
+
}
|
|
864
|
+
function truncate(s) {
|
|
865
|
+
if (s.length <= MAX_ERROR_LEN)
|
|
866
|
+
return s;
|
|
867
|
+
return `${s.slice(0, MAX_ERROR_LEN - 1)}…`;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Validate a `node_name` argument supplied at the outbound boundary. The wire
|
|
871
|
+
* requires a non-empty string; empty / non-string values produce a typed
|
|
872
|
+
* `QodoWireValidationError` so consumer bugs surface synchronously instead
|
|
873
|
+
* of as a malformed envelope.
|
|
874
|
+
*/
|
|
875
|
+
function validateNodeName(value, where) {
|
|
876
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
877
|
+
throw new QodoWireValidationError(`${where}: node_name is required and must be a non-empty string.`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/** Cap on individual field values rendered into the wire-validation breadcrumb. */
|
|
881
|
+
const LOG_FIELD_MAX_LEN = 200;
|
|
882
|
+
/**
|
|
883
|
+
* Emit a single diagnostic line when the dispatcher drops a malformed inbound
|
|
884
|
+
* `tool.request` envelope (empty `node_name` or empty `calls`). Without a
|
|
885
|
+
* signal, consumers have nothing to grep for when QAR's tool-loop times
|
|
886
|
+
* out and they need to diagnose protocol drift.
|
|
887
|
+
*
|
|
888
|
+
* Wire-provided fields (`nodeName`, `messageId`) are sanitized via
|
|
889
|
+
* `safeForLog` before interpolation — control characters in a malformed
|
|
890
|
+
* envelope would otherwise corrupt the log format and let an upstream
|
|
891
|
+
* bug (or hostile peer) forge fake breadcrumb lines. Each field is also
|
|
892
|
+
* clamped to `LOG_FIELD_MAX_LEN` so an attacker can't blow up the log
|
|
893
|
+
* line size.
|
|
894
|
+
*
|
|
895
|
+
* Logged at `console.error` because `console.warn` isn't part of the SDK's
|
|
896
|
+
* existing in-repo logging vocabulary (only `console.error` is used; see
|
|
897
|
+
* `src/client/observers.ts`). Wrapped defensively — a hostile or wedged
|
|
898
|
+
* `console` must never crash the dispatch hot path.
|
|
899
|
+
*/
|
|
900
|
+
function logWireValidationDrop(fields) {
|
|
901
|
+
try {
|
|
902
|
+
// `violation` is SDK-authored static prose, never wire-derived — no need
|
|
903
|
+
// to sanitize. `messageId` and `nodeName` come from the inbound envelope
|
|
904
|
+
// and MUST be sanitized.
|
|
905
|
+
const parts = [
|
|
906
|
+
`[@qodo/sdk] tool.request envelope dropped: ${fields.violation}`,
|
|
907
|
+
`message_id=${safeForLog(fields.messageId)}`,
|
|
908
|
+
];
|
|
909
|
+
if (fields.nodeName !== undefined) {
|
|
910
|
+
parts.push(`node_name=${safeForLog(fields.nodeName)}`);
|
|
911
|
+
}
|
|
912
|
+
// eslint-disable-next-line no-console
|
|
913
|
+
console.error(parts.join(' '));
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
// Console itself is wedged — give up; the wire path is the contract.
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Logged when an observer registered via `onWireValidationError(handler)`
|
|
921
|
+
* itself throws. Mirrors the swallow-and-surface pattern in
|
|
922
|
+
* `KindObserverPort.logHandlerError` (`src/client/observers.ts`) so the same
|
|
923
|
+
* "[@qodo/sdk]" breadcrumb vocabulary covers every consumer-handler failure.
|
|
924
|
+
*/
|
|
925
|
+
// `isThenable` lives in `./internal/thenable.ts` so this module and
|
|
926
|
+
// `observers.ts` share one canonical implementation — a local copy here
|
|
927
|
+
// missing the `typeof === 'function'` arm would let callable thenables
|
|
928
|
+
// escape the async-containment path.
|
|
929
|
+
function logHandlerObserverError(surface, err) {
|
|
930
|
+
try {
|
|
931
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
932
|
+
// eslint-disable-next-line no-console
|
|
933
|
+
console.error(`[@qodo/sdk] ${surface} handler threw — swallowed: ${safeForLog(msg)}`);
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
try {
|
|
937
|
+
// eslint-disable-next-line no-console
|
|
938
|
+
console.error(`[@qodo/sdk] ${surface} handler threw — swallowed (logger error)`);
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
// Console fully wedged — drop.
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function logUnhandledDispatchFailure(err, messageId) {
|
|
946
|
+
try {
|
|
947
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
948
|
+
// eslint-disable-next-line no-console
|
|
949
|
+
console.error(`[@qodo/sdk] tool.request dispatch failed past containment: ${safeForLog(message)} ` +
|
|
950
|
+
`message_id=${safeForLog(messageId)}`);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// Even the breadcrumb is wedged. Drop silently — the priority is keeping
|
|
954
|
+
// the dispatch hot path alive.
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Render a wire-provided value into a single log-safe token. Replaces
|
|
959
|
+
* control characters (newlines, tabs, carriage returns, NULs, every C0/C1
|
|
960
|
+
* range) with their `\xNN` / `\uNNNN` escapes, wraps the result in quotes,
|
|
961
|
+
* and clamps to `LOG_FIELD_MAX_LEN` so a single malformed field can't
|
|
962
|
+
* dominate the log line. Non-string values fall back to `JSON.stringify`
|
|
963
|
+
* (defensive — TS callers always pass strings, but inbound envelopes are
|
|
964
|
+
* runtime-untrusted).
|
|
965
|
+
*/
|
|
966
|
+
function safeForLog(value) {
|
|
967
|
+
let s;
|
|
968
|
+
if (typeof value === 'string') {
|
|
969
|
+
s = value;
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
try {
|
|
973
|
+
s = JSON.stringify(value);
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
s = String(value);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
// Escape:
|
|
980
|
+
// - ASCII C0 control chars (0x00-0x1F, 0x7F) and the C1 range (0x80-0x9F)
|
|
981
|
+
// — any of these can break grep, terminal output, or log aggregators.
|
|
982
|
+
// - U+2028 / U+2029 — Unicode line/paragraph separators; some log parsers
|
|
983
|
+
// (and some V8 versions' `JSON.stringify`) treat them as newlines.
|
|
984
|
+
// - U+202A–U+202E + U+2066–U+2069 — Unicode bidi overrides (LRE/RLE/PDF/
|
|
985
|
+
// LRO/RLO + FSI/PDI family). Without escaping these, a hostile node_name
|
|
986
|
+
// can right-to-left-flip part of the log line so tampering visually
|
|
987
|
+
// looks like a legitimate field.
|
|
988
|
+
// - U+2060 (Word Joiner) + U+FEFF (BOM / ZWNBSP) — invisible format chars
|
|
989
|
+
// that defeat grep-based debugging by inserting zero-width separators.
|
|
990
|
+
// Ordinary printable Unicode is left intact so non-ASCII node names still
|
|
991
|
+
// render readably.
|
|
992
|
+
s = s.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029\u202a-\u202e\u2066-\u2069\u2060\ufeff]/g, (c) => {
|
|
993
|
+
const code = c.charCodeAt(0);
|
|
994
|
+
return code <= 0xff
|
|
995
|
+
? `\\x${code.toString(16).padStart(2, '0')}`
|
|
996
|
+
: `\\u${code.toString(16).padStart(4, '0')}`;
|
|
997
|
+
});
|
|
998
|
+
if (s.length > LOG_FIELD_MAX_LEN) {
|
|
999
|
+
s = `${s.slice(0, LOG_FIELD_MAX_LEN - 1)}…`;
|
|
1000
|
+
}
|
|
1001
|
+
return JSON.stringify(s);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Runtime guard for the SDK-side `ToolResponseResult` discriminated union.
|
|
1005
|
+
*
|
|
1006
|
+
* The wire shape is the flat `ToolResponseItem` (`status` + optional `result`
|
|
1007
|
+
* / `error`); the SDK convenience union narrows that. Validating here means a
|
|
1008
|
+
* buggy handler can't ship a malformed envelope onto the wire.
|
|
1009
|
+
*/
|
|
1010
|
+
function isValidToolResponseResult(x) {
|
|
1011
|
+
if (typeof x !== 'object' || x === null)
|
|
1012
|
+
return false;
|
|
1013
|
+
const r = x;
|
|
1014
|
+
switch (r.outcome) {
|
|
1015
|
+
case 'success':
|
|
1016
|
+
// Recursive JSON-shape check (catches cycles + non-JSON primitives like
|
|
1017
|
+
// BigInt, Symbol, function, NaN/Infinity, undefined). Without this the
|
|
1018
|
+
// wire-side `JSON.stringify` would either throw (cycle) or silently
|
|
1019
|
+
// drop fields (non-JSON primitives).
|
|
1020
|
+
return isJsonObject(r.result);
|
|
1021
|
+
case 'error':
|
|
1022
|
+
return typeof r.error === 'string';
|
|
1023
|
+
case 'denied':
|
|
1024
|
+
return r.error === undefined || typeof r.error === 'string';
|
|
1025
|
+
default:
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Recursive JSON-object validator with cycle detection. Mirrors the
|
|
1031
|
+
* `JsonObject` / `JsonValue` shapes in `src/qar/json.ts`.
|
|
1032
|
+
*
|
|
1033
|
+
* - rejects arrays (a top-level `result` must be a JSON object, not array);
|
|
1034
|
+
* - rejects `BigInt`, `Symbol`, `function`, `undefined` values;
|
|
1035
|
+
* - rejects non-finite numbers (`NaN`, `Infinity`) — these become `null`
|
|
1036
|
+
* under `JSON.stringify` which silently corrupts the result on the wire;
|
|
1037
|
+
* - tracks visited objects in a `WeakSet` so a cyclic payload returns
|
|
1038
|
+
* `false` instead of triggering the recursion bomb at serialize time.
|
|
1039
|
+
*/
|
|
1040
|
+
function isJsonObject(x, seen = new WeakSet()) {
|
|
1041
|
+
if (typeof x !== 'object' || x === null || Array.isArray(x))
|
|
1042
|
+
return false;
|
|
1043
|
+
if (seen.has(x))
|
|
1044
|
+
return false;
|
|
1045
|
+
seen.add(x);
|
|
1046
|
+
for (const value of Object.values(x)) {
|
|
1047
|
+
if (!isJsonValue(value, seen))
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
function isJsonValue(x, seen) {
|
|
1053
|
+
if (x === null)
|
|
1054
|
+
return true;
|
|
1055
|
+
switch (typeof x) {
|
|
1056
|
+
case 'string':
|
|
1057
|
+
case 'boolean':
|
|
1058
|
+
return true;
|
|
1059
|
+
case 'number':
|
|
1060
|
+
return Number.isFinite(x);
|
|
1061
|
+
case 'object': {
|
|
1062
|
+
if (Array.isArray(x)) {
|
|
1063
|
+
if (seen.has(x))
|
|
1064
|
+
return false;
|
|
1065
|
+
seen.add(x);
|
|
1066
|
+
return x.every((item) => isJsonValue(item, seen));
|
|
1067
|
+
}
|
|
1068
|
+
return isJsonObject(x, seen);
|
|
1069
|
+
}
|
|
1070
|
+
default:
|
|
1071
|
+
// 'bigint', 'symbol', 'function', 'undefined' → not JSON-representable.
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Translate one SDK-side `ToolResponseResult` into the flat wire item.
|
|
1077
|
+
*
|
|
1078
|
+
* Wire-faithful mapping:
|
|
1079
|
+
*
|
|
1080
|
+
* - `success` → `status: 'ok'`, `result` carried as-is.
|
|
1081
|
+
* - `error` → `status: 'error'`, `error: { code: 'error', message }`.
|
|
1082
|
+
* - `denied` → `status: 'error'`, `error: { code: 'denied', message? }`.
|
|
1083
|
+
*
|
|
1084
|
+
* `denied` collapses to wire `status: 'error'` with a distinct `error.code` so
|
|
1085
|
+
* QAR's tool-loop telemetry can still distinguish policy rejection from
|
|
1086
|
+
* generic failure. Truncation happens HERE (not in callers) so every outbound
|
|
1087
|
+
* error/denied envelope is bounded — handler-thrown, handler-returned, and
|
|
1088
|
+
* `respond()` paths all go through this single seam.
|
|
1089
|
+
*/
|
|
1090
|
+
function toResponseItem(toolCallId, result) {
|
|
1091
|
+
switch (result.outcome) {
|
|
1092
|
+
case 'success':
|
|
1093
|
+
return {
|
|
1094
|
+
tool_call_id: toolCallId,
|
|
1095
|
+
status: 'ok',
|
|
1096
|
+
result: result.result,
|
|
1097
|
+
};
|
|
1098
|
+
case 'error':
|
|
1099
|
+
return {
|
|
1100
|
+
tool_call_id: toolCallId,
|
|
1101
|
+
status: 'error',
|
|
1102
|
+
error: { code: 'error', message: truncate(result.error) },
|
|
1103
|
+
};
|
|
1104
|
+
case 'denied':
|
|
1105
|
+
return {
|
|
1106
|
+
tool_call_id: toolCallId,
|
|
1107
|
+
status: 'error',
|
|
1108
|
+
error: {
|
|
1109
|
+
code: 'denied',
|
|
1110
|
+
message: result.error !== undefined ? truncate(result.error) : 'denied',
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
//# sourceMappingURL=ToolClient.js.map
|