@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,2522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TaskClient` — task lifecycle surface, mirroring QAR's `task.*` envelope kinds.
|
|
3
|
+
*/
|
|
4
|
+
import { collectInlineAgentSpecPreWireIssues, normalizeInlineAgentSpec } from '../qar/agentSpec.js';
|
|
5
|
+
import { TaskSubscription } from './connection.js';
|
|
6
|
+
import { AsyncQueue } from './iterator.js';
|
|
7
|
+
import { longestPathFrom, validateInlineGraphSpec, } from './inlineGraph.js';
|
|
8
|
+
import { classForServerErrorCode, QodoAdmissionStalledError, QodoAdmissionTimeoutError, QodoAgentSpecRejectedError, QodoCancelAbortedError, QodoIdempotencyKeyValidationError, QodoInlineAgentValidationError, QodoInlineGraphValidationError, QodoMcpUnavailableError, QodoStreamAbortedError, QodoUnknownMcpError, QodoUnknownMcpToolError, QodoUnknownServerError, RequiredGraphSpecError, } from './errors.js';
|
|
9
|
+
import { McpClientPool } from './mcp/McpClientPool.js';
|
|
10
|
+
import { projectMcpTools } from './mcp/projection.js';
|
|
11
|
+
import { qodoSkillsFunctionToolDefs } from '../skills/mcp/server.js';
|
|
12
|
+
import { asMessageId, asTaskId, uuidv7 } from './uuid.js';
|
|
13
|
+
import { QodoSkillError, SkillAmbiguousPinError, SkillNotFoundError, SkillsBudgetExceededError, } from '../skills/manager.js';
|
|
14
|
+
import { injectIntoAgentSpec, injectIntoGraphSpec, } from '../skills/inject.js';
|
|
15
|
+
import { activationFailureMessage, lookupFromSnapshot, renderActiveSkillsBlock, resolveAllActivations, } from '../skills/activation.js';
|
|
16
|
+
export class TaskClient {
|
|
17
|
+
resolveConnection;
|
|
18
|
+
spanRecorder;
|
|
19
|
+
registry;
|
|
20
|
+
resolveSpecs;
|
|
21
|
+
metrics;
|
|
22
|
+
skills;
|
|
23
|
+
bindFunctionToolDefs;
|
|
24
|
+
/**
|
|
25
|
+
* @param resolveConnection Returns the live `Connection` or throws if the
|
|
26
|
+
* client isn't connected yet.
|
|
27
|
+
* @param spanRecorder Recorder for `qar.client.task.*` spans. No-op
|
|
28
|
+
* when OTel isn't configured.
|
|
29
|
+
* @param registry Local agent + MCP registries. `tasks.start` reads
|
|
30
|
+
* this to resolve `RegisteredAgentRef` → wire
|
|
31
|
+
* `agent_id`.
|
|
32
|
+
* @param resolveSpecs Lazily-resolved `SpecsClient` used by the
|
|
33
|
+
* opt-in `preflight: true` path on
|
|
34
|
+
* `startWithGraph()`. Lazy because `QodoClient`
|
|
35
|
+
* constructs `tasks` before `specs`; defer
|
|
36
|
+
* dereference until the caller actually asks
|
|
37
|
+
* for pre-flight validation.
|
|
38
|
+
*/
|
|
39
|
+
constructor(resolveConnection, spanRecorder, registry, resolveSpecs,
|
|
40
|
+
/**
|
|
41
|
+
* Transport-metric store passed through to each constructed
|
|
42
|
+
* `TaskSubscription` so the connection's reconnect/replay path can
|
|
43
|
+
* increment `replay_envelopes_received_total` /
|
|
44
|
+
* `replay_anchor_missing_total` against absorbed envelopes. Optional
|
|
45
|
+
* for back-compat with consumers wiring `TaskClient` directly in
|
|
46
|
+
* tests; production wires from `QodoClient`.
|
|
47
|
+
*/
|
|
48
|
+
metrics,
|
|
49
|
+
/**
|
|
50
|
+
* Skills foundation manager. When provided, the `startWithAgent` and
|
|
51
|
+
* `startWithGraph` paths await the manager's `discover()` (idempotent)
|
|
52
|
+
* and append the rendered slim index to the spec's `instructions`
|
|
53
|
+
* before the wire write. Undefined when the consumer didn't pass
|
|
54
|
+
* `ClientOptions.skills` — those paths stay verbatim.
|
|
55
|
+
*/
|
|
56
|
+
skills,
|
|
57
|
+
/**
|
|
58
|
+
* `defineFunctionTool` auto-bind hook. Called at every
|
|
59
|
+
* `startWithAgent` / `startWithGraph` entry point so handlers
|
|
60
|
+
* attached to `FunctionToolDef` entries (via the helper's
|
|
61
|
+
* symbol-keyed handler bag) get installed on the `ToolClient`
|
|
62
|
+
* before the wire `task.start` lands. Optional — when undefined
|
|
63
|
+
* (e.g. `TaskClient` constructed in isolation in tests), the bind
|
|
64
|
+
* step is skipped and consumers fall back to manual
|
|
65
|
+
* `client.tools.onRequest(...)` registration.
|
|
66
|
+
*/
|
|
67
|
+
bindFunctionToolDefs) {
|
|
68
|
+
this.resolveConnection = resolveConnection;
|
|
69
|
+
this.spanRecorder = spanRecorder;
|
|
70
|
+
this.registry = registry;
|
|
71
|
+
this.resolveSpecs = resolveSpecs;
|
|
72
|
+
this.metrics = metrics;
|
|
73
|
+
this.skills = skills;
|
|
74
|
+
this.bindFunctionToolDefs = bindFunctionToolDefs;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Send `task.start` and yield the resulting `TaskEvent` stream until `task.done`.
|
|
78
|
+
*
|
|
79
|
+
* The iterator is the canonical streaming primitive — consumers `for await` and
|
|
80
|
+
* `switch (event.kind)` over the QAR discriminator. Breaking the iterator
|
|
81
|
+
* before `task.done` arrives sends a best-effort `task.cancel` so the runtime
|
|
82
|
+
* can free its resources promptly.
|
|
83
|
+
*
|
|
84
|
+
* `payload.agent_id` accepts either a wire-shape `string` or a
|
|
85
|
+
* `RegisteredAgentRef` from `client.qar.registerAgent`. Refs are resolved
|
|
86
|
+
* to `ref.name` for the outgoing envelope; the local agent body never
|
|
87
|
+
* crosses the wire.
|
|
88
|
+
*/
|
|
89
|
+
start(payload, opts) {
|
|
90
|
+
// Validate `idempotencyKey` synchronously before the wire write.
|
|
91
|
+
// Throws `QodoIdempotencyKeyValidationError` on bad inputs — fail-fast
|
|
92
|
+
// DX rather than waiting for QAR to round-trip an error envelope.
|
|
93
|
+
if (opts?.idempotencyKey !== undefined) {
|
|
94
|
+
validateIdempotencyKey(opts.idempotencyKey);
|
|
95
|
+
}
|
|
96
|
+
const resolvedAgentId = this.registry.resolveAgentId(payload.agent_id);
|
|
97
|
+
const wirePayload = {
|
|
98
|
+
...payload,
|
|
99
|
+
agent_id: resolvedAgentId,
|
|
100
|
+
// Forward the caller-supplied key under the wire snake_case name.
|
|
101
|
+
// Strip the field entirely when absent — server treats
|
|
102
|
+
// `idempotency_key: null` and field-absent identically per the
|
|
103
|
+
// Pydantic `default=None` semantic, but the SDK keeps the wire
|
|
104
|
+
// shape minimal so a fresh-eyes diff of the encoded JSON shows
|
|
105
|
+
// exactly the consumer's intent.
|
|
106
|
+
...(opts?.idempotencyKey !== undefined ? { idempotency_key: opts.idempotencyKey } : {}),
|
|
107
|
+
};
|
|
108
|
+
// Pre-allocate the root message id at the public-API boundary so the
|
|
109
|
+
// derived `task_id` (= `task.start.message_id`) is available
|
|
110
|
+
// synchronously on the returned iterable. The iterable's `taskId`
|
|
111
|
+
// property lets consumers call `client.tasks.cancel(stream.taskId)`
|
|
112
|
+
// without waiting for the first inbound envelope.
|
|
113
|
+
const rootMessageId = asMessageId(uuidv7());
|
|
114
|
+
const taskId = asTaskId(rootMessageId);
|
|
115
|
+
// Deferred Promises for the `task.started` admission ack —
|
|
116
|
+
// `sessionId` for the server-derived session UUID and
|
|
117
|
+
// `admittedTaskId` for the canonical task_id. Both settle
|
|
118
|
+
// atomically on the same admission outcome.
|
|
119
|
+
const sessionIdDeferred = createDeferred();
|
|
120
|
+
const admittedTaskIdDeferred = createDeferred();
|
|
121
|
+
const admissionResultDeferred = createDeferred();
|
|
122
|
+
const ackDeferreds = {
|
|
123
|
+
sessionIdDeferred,
|
|
124
|
+
admittedTaskIdDeferred,
|
|
125
|
+
admissionResultDeferred,
|
|
126
|
+
};
|
|
127
|
+
// Closure used by both the single-shot path and the admission
|
|
128
|
+
// retry wrapper. Each call builds a fresh subscription against the
|
|
129
|
+
// supplied rootMessageId — on retry the wrapper passes a fresh id so
|
|
130
|
+
// the duplicate-message-id guard in `Connection.sendEnvelope` stays
|
|
131
|
+
// happy.
|
|
132
|
+
const dispatch = (rmid) => this.subscribeAndSend(() => ({ kind: 'task.start', payload: wirePayload, rootMessageId: rmid }), asTaskId(rmid), opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
133
|
+
sessionId,
|
|
134
|
+
messageId,
|
|
135
|
+
agentId: resolvedAgentId,
|
|
136
|
+
...(wirePayload.skill !== undefined && wirePayload.skill !== null
|
|
137
|
+
? { skillName: wirePayload.skill }
|
|
138
|
+
: {}),
|
|
139
|
+
}).lifecycle, rmid, ackDeferreds);
|
|
140
|
+
// Only the deterministic-key path ever sees `admission_in_progress`
|
|
141
|
+
// (the omitted-key path mints a fresh uuidv7 server-side per call —
|
|
142
|
+
// no collisions possible). Wrap with the retry layer when the key is
|
|
143
|
+
// present; otherwise stay on the existing fast path so the DX
|
|
144
|
+
// default doesn't pay the extra iterator-wrapping cost. Either way
|
|
145
|
+
// the first attempt is dispatched eagerly so the wire `task.start`
|
|
146
|
+
// is in flight before the public method returns (preserves the
|
|
147
|
+
// `cancel(stream.taskId)` race-free contract).
|
|
148
|
+
const firstSub = dispatch(rootMessageId);
|
|
149
|
+
const iter = opts?.idempotencyKey !== undefined
|
|
150
|
+
? TaskClient.wrapWithAdmissionRetry(firstSub, dispatch, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred)
|
|
151
|
+
: // Wrap server errors on the by-id path too. It doesn't have its
|
|
152
|
+
// own spec rejection but the cancel/resume/HITL families apply
|
|
153
|
+
// to any running task.
|
|
154
|
+
TaskClient.wrapServerErrorsIterator(firstSub);
|
|
155
|
+
return attachTaskId(iter, taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Send `task.start { agent, input }` and yield the resulting `TaskEvent`
|
|
159
|
+
* stream until `task.done` — inline-AgentSpec dispatch.
|
|
160
|
+
*
|
|
161
|
+
* The spec is validated client-side (the SDK-enforceable rule subset,
|
|
162
|
+
* see `collectInlineAgentSpecPreWireIssues`) BEFORE the envelope hits
|
|
163
|
+
* the wire. A `QodoInlineAgentValidationError` is thrown when any
|
|
164
|
+
* forbidden-key or missing-required-field rule fails — the error's
|
|
165
|
+
* `issues` array carries every violation with a `rule_id` +
|
|
166
|
+
* JSON-Pointer `path` so consumers can route both client-side and
|
|
167
|
+
* server-side rejections through the same branch logic.
|
|
168
|
+
*
|
|
169
|
+
* Opt into `{ preflight: true }` to additionally call
|
|
170
|
+
* `client.specs.validate(agent)` (HTTP) before opening the task — that
|
|
171
|
+
* catches the rules the SDK can't enforce locally (catalog tool
|
|
172
|
+
* registration, skill version pinning, spawnable `agent_id`
|
|
173
|
+
* resolution). Default `false`.
|
|
174
|
+
*
|
|
175
|
+
* Iterator + early-termination semantics match `start()` — breaking the
|
|
176
|
+
* iterator before `task.done` arrives sends best-effort `task.cancel`.
|
|
177
|
+
* Inline-agent spans carry `qar.agent.kind = "inline"` and
|
|
178
|
+
* `qar.agent_id = "inline:<message_id>"`.
|
|
179
|
+
*
|
|
180
|
+
* Server-side rejection: when QAR's `_handle_task_start` emits an
|
|
181
|
+
* `error { code: 'agent_spec_rejected' }` envelope, the iterator
|
|
182
|
+
* rejects with `QodoAgentSpecRejectedError` instead of yielding the
|
|
183
|
+
* envelope as a `kind: 'error'` event — consumers don't need to
|
|
184
|
+
* defensively narrow the error case out of `TaskEvent.kind`.
|
|
185
|
+
*/
|
|
186
|
+
startWithAgent(payload, opts) {
|
|
187
|
+
// Cheap fail-fast `idempotencyKey` check runs BEFORE the agent-spec
|
|
188
|
+
// validator so a typo'd key surfaces ahead of the
|
|
189
|
+
// potentially-multi-issue spec validation pass.
|
|
190
|
+
if (opts?.idempotencyKey !== undefined) {
|
|
191
|
+
validateIdempotencyKey(opts.idempotencyKey);
|
|
192
|
+
}
|
|
193
|
+
// Pre-wire validation — synchronous, no IO, fail fast before either
|
|
194
|
+
// the optional pre-flight HTTP call or the WS write. Collect-then-throw
|
|
195
|
+
// so the consumer sees every rule in one go (better for IDE / Studio
|
|
196
|
+
// surfaces showing multiple diagnostics simultaneously).
|
|
197
|
+
const preIssues = collectInlineAgentSpecPreWireIssues(payload.agent);
|
|
198
|
+
if (preIssues.length > 0) {
|
|
199
|
+
throw new QodoInlineAgentValidationError(preIssues);
|
|
200
|
+
}
|
|
201
|
+
// `defineFunctionTool` auto-bind. Any `FunctionToolDef` in the
|
|
202
|
+
// spec's `tools[]` that carries the SDK-local handler symbol gets
|
|
203
|
+
// registered with the per-client `FunctionToolRouter` so the
|
|
204
|
+
// inbound `tool.request` dispatch knows where to route calls. The
|
|
205
|
+
// wire envelope itself stays unchanged — the handler lives behind a
|
|
206
|
+
// Symbol that `JSON.stringify` skips.
|
|
207
|
+
if (this.bindFunctionToolDefs !== undefined && payload.agent.tools != null) {
|
|
208
|
+
this.bindFunctionToolDefs(payload.agent.tools);
|
|
209
|
+
}
|
|
210
|
+
// Pre-allocate root message id at the public-API boundary so `taskId`
|
|
211
|
+
// is synchronously available on the returned iterable.
|
|
212
|
+
const rootMessageId = asMessageId(uuidv7());
|
|
213
|
+
const taskId = asTaskId(rootMessageId);
|
|
214
|
+
const sessionIdDeferred = createDeferred();
|
|
215
|
+
const admittedTaskIdDeferred = createDeferred();
|
|
216
|
+
const admissionResultDeferred = createDeferred();
|
|
217
|
+
const ackDeferreds = {
|
|
218
|
+
sessionIdDeferred,
|
|
219
|
+
admittedTaskIdDeferred,
|
|
220
|
+
admissionResultDeferred,
|
|
221
|
+
};
|
|
222
|
+
// Slim-index injection. When the manager has finished discovery,
|
|
223
|
+
// inject synchronously — the common case once `client.connect()` has
|
|
224
|
+
// resolved (connect awaits manager.discover()). When the snapshot
|
|
225
|
+
// isn't ready yet (consumer called startWithAgent before connect
|
|
226
|
+
// resolved), fall through to the eager-promise dispatch path so we
|
|
227
|
+
// await discover() before the wire write.
|
|
228
|
+
const skillsPinned = opts?.skills;
|
|
229
|
+
const needsSkillsAwait = this.skills !== undefined && this.skills.currentSnapshot === null;
|
|
230
|
+
// MCP projection. When the consumer set `mcpTools` (or the sibling
|
|
231
|
+
// `mcpToolOverrides`), the wire-boundary projection must await the
|
|
232
|
+
// pool's catalog-settle window — so route through the
|
|
233
|
+
// eager-promise dispatch path. The sync path stays for the
|
|
234
|
+
// no-discovery / no-overrides DX-default case.
|
|
235
|
+
const needsMcpProjection = payload.agent.mcpTools !== undefined ||
|
|
236
|
+
payload.agent.mcpToolOverrides !== undefined;
|
|
237
|
+
let agentForWire = normalizeInlineAgentSpec(payload.agent);
|
|
238
|
+
// Skills inject runs sync ONLY when neither async-path trigger is
|
|
239
|
+
// armed. `needsMcpProjection` forces the eager-async dispatch
|
|
240
|
+
// route, and that route re-runs `applyAgentSkillsInjection` inside
|
|
241
|
+
// `runAgentPreflightAndDispatch`. The slim-index / active-skills
|
|
242
|
+
// injector is not idempotent (it appends content), so double-
|
|
243
|
+
// applying duplicates the instruction text. Gate the sync branch
|
|
244
|
+
// on BOTH `!needsSkillsAwait` AND `!needsMcpProjection`.
|
|
245
|
+
if (this.skills !== undefined && !needsSkillsAwait && !needsMcpProjection) {
|
|
246
|
+
agentForWire = this.applyAgentSkillsInjection(agentForWire, skillsPinned, opts?.skillsCurrentFile);
|
|
247
|
+
}
|
|
248
|
+
// Auto-include the qodo-skills.* tool surface in the sync-dispatch
|
|
249
|
+
// path so the `<available_skills>` preamble's instruction ("Use the
|
|
250
|
+
// qodo-skills.get_skill tool to ...") is actionable. The async
|
|
251
|
+
// projection path runs the same merge inside
|
|
252
|
+
// `projectMcpToolsForWire` so callers that DO hit that branch get
|
|
253
|
+
// the same wire surface. Sync auto-include doesn't need a settle
|
|
254
|
+
// wait — the 3 tool defs are constants.
|
|
255
|
+
if (this.skills !== undefined && !needsMcpProjection) {
|
|
256
|
+
const original = agentForWire.tools ?? [];
|
|
257
|
+
const merged = mergeQodoSkillsTools(original);
|
|
258
|
+
if (merged !== original) {
|
|
259
|
+
agentForWire = { ...agentForWire, tools: merged };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const wirePayload = {
|
|
263
|
+
agent: agentForWire,
|
|
264
|
+
input: payload.input ?? {},
|
|
265
|
+
...(payload.skill !== undefined && payload.skill !== null
|
|
266
|
+
? { skill: payload.skill }
|
|
267
|
+
: {}),
|
|
268
|
+
// Forward the caller-supplied key under the wire snake_case name.
|
|
269
|
+
// Strip when absent so the encoded JSON matches the DX-default
|
|
270
|
+
// path one-to-one (server treats absent and `null` identically per
|
|
271
|
+
// Pydantic, but the SDK keeps the wire minimal for readability).
|
|
272
|
+
...(opts?.idempotencyKey !== undefined
|
|
273
|
+
? { idempotency_key: opts.idempotencyKey }
|
|
274
|
+
: {}),
|
|
275
|
+
};
|
|
276
|
+
// Pre-flight branch: validate FIRST via `POST /v1/specs/validate`,
|
|
277
|
+
// and only then call `subscribeAndSend()` to write `task.start` on
|
|
278
|
+
// the wire. Without this ordering the wire write fires before the
|
|
279
|
+
// HTTP validate resolves, and a rule-rejected spec would still
|
|
280
|
+
// start a server task. The non-preflight path stays synchronous so
|
|
281
|
+
// the common case avoids the async-generator microtask boundary.
|
|
282
|
+
if (opts?.preflight === true || needsSkillsAwait || needsMcpProjection) {
|
|
283
|
+
// Kick off preflight + (on success) `subscribeAndSend` eagerly so
|
|
284
|
+
// the wire `task.start` is in flight by the time the public method
|
|
285
|
+
// returns. Without this, a consumer that reads `stream.taskId` and
|
|
286
|
+
// calls `tasks.cancel(stream.taskId)` before iterating would race
|
|
287
|
+
// the cancel envelope ahead of the (still-deferred) `task.start`.
|
|
288
|
+
// The iterable then awaits the eager promise and yields from the
|
|
289
|
+
// resulting subscription.
|
|
290
|
+
const subPromise = this.runAgentPreflightAndDispatch(wirePayload, opts ?? {}, rootMessageId, taskId, ackDeferreds);
|
|
291
|
+
// Swallow the unhandled-rejection until the consumer iterates and
|
|
292
|
+
// we re-throw via `yieldFromPromisedSub`. Without this attach,
|
|
293
|
+
// Node treats the rejection as "unhandled" during the microtask
|
|
294
|
+
// window between dispatch failure and consumer iteration.
|
|
295
|
+
subPromise.catch((err) => {
|
|
296
|
+
// Surface the preflight failure to anyone awaiting
|
|
297
|
+
// `stream.sessionId` / `stream.admittedTaskId` so the promises
|
|
298
|
+
// don't hang.
|
|
299
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
300
|
+
if (!sessionIdDeferred.settled())
|
|
301
|
+
sessionIdDeferred.reject(wrapped);
|
|
302
|
+
if (!admittedTaskIdDeferred.settled())
|
|
303
|
+
admittedTaskIdDeferred.reject(wrapped);
|
|
304
|
+
if (!admissionResultDeferred.settled())
|
|
305
|
+
admissionResultDeferred.reject(wrapped);
|
|
306
|
+
});
|
|
307
|
+
return attachTaskId(yieldFromPromisedSub(subPromise, true), taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
|
|
308
|
+
}
|
|
309
|
+
// Closure used by both single-shot dispatch and the admission retry
|
|
310
|
+
// wrapper (when `idempotencyKey` is set). Each call builds a fresh
|
|
311
|
+
// subscription against the supplied rootMessageId.
|
|
312
|
+
const dispatch = (rmid) => this.subscribeAndSend(() => ({
|
|
313
|
+
kind: 'task.start',
|
|
314
|
+
payload: wirePayload,
|
|
315
|
+
rootMessageId: rmid,
|
|
316
|
+
}), asTaskId(rmid), opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
317
|
+
sessionId,
|
|
318
|
+
messageId,
|
|
319
|
+
agentId: `inline:${messageId}`,
|
|
320
|
+
agentKind: 'inline',
|
|
321
|
+
...(wirePayload.skill !== undefined && wirePayload.skill !== null
|
|
322
|
+
? { skillName: wirePayload.skill }
|
|
323
|
+
: {}),
|
|
324
|
+
}).lifecycle, rmid, ackDeferreds);
|
|
325
|
+
const firstSub = dispatch(rootMessageId);
|
|
326
|
+
const iter = opts?.idempotencyKey !== undefined
|
|
327
|
+
? TaskClient.wrapWithAdmissionRetry(firstSub, dispatch, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred)
|
|
328
|
+
: TaskClient.wrapServerErrorsIterator(firstSub);
|
|
329
|
+
return attachTaskId(iter, taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Eagerly run `client.specs.validate(agent)` and, on success, dispatch
|
|
333
|
+
* the `task.start` envelope via `subscribeAndSend`. Returns a
|
|
334
|
+
* `Promise<TaskSubscription>` that resolves once the wire write has
|
|
335
|
+
* happened — kicked off in the public method body so the dispatch is
|
|
336
|
+
* in flight by the time the consumer can read `stream.taskId`.
|
|
337
|
+
*
|
|
338
|
+
* `preflight: true` still means "validate before the wire write" per
|
|
339
|
+
* the documented contract; we just no longer wait for iteration to
|
|
340
|
+
* start the validate.
|
|
341
|
+
*
|
|
342
|
+
* Server-side rule failures throw `QodoAgentSpecRejectedError` so
|
|
343
|
+
* consumers route SDK + server rejections through one branch.
|
|
344
|
+
*/
|
|
345
|
+
async runAgentPreflightAndDispatch(wirePayload, opts, rootMessageId, taskId,
|
|
346
|
+
/**
|
|
347
|
+
* Deferred pair for the `task.started` admission ack (sessionId +
|
|
348
|
+
* admittedTaskId). Forwarded into the eventual `subscribeAndSend` so
|
|
349
|
+
* the inbound ack resolves both caller-facing Promises on the
|
|
350
|
+
* `TaskStartIterable`.
|
|
351
|
+
*/
|
|
352
|
+
ackDeferreds) {
|
|
353
|
+
// Await skills discovery (idempotent) and inject before (a) the
|
|
354
|
+
// optional /v1/specs/validate call, so the validator sees the full
|
|
355
|
+
// agent spec the consumer will dispatch, and (b) the wire write.
|
|
356
|
+
let finalPayload = wirePayload;
|
|
357
|
+
if (this.skills !== undefined) {
|
|
358
|
+
await this.skills.discover();
|
|
359
|
+
const injectedAgent = this.applyAgentSkillsInjection(wirePayload.agent, opts.skills, opts.skillsCurrentFile);
|
|
360
|
+
finalPayload = { ...wirePayload, agent: injectedAgent };
|
|
361
|
+
}
|
|
362
|
+
// MCP projection. Runs AFTER skills inject (skills mutate
|
|
363
|
+
// instructions, never tools) and BEFORE the optional /v1/specs/validate
|
|
364
|
+
// call so the validator sees the post-projection `tools[]` the wire
|
|
365
|
+
// envelope will carry. Throws synchronously on unknown / unreachable
|
|
366
|
+
// MCPs — the caller already wrapped this call in a `.catch` that
|
|
367
|
+
// settles `sessionId` / `admittedTaskId` deferreds so the thrown
|
|
368
|
+
// error reaches every consumer await.
|
|
369
|
+
const projectedAgent = await this.projectMcpToolsForWire(finalPayload.agent);
|
|
370
|
+
if (projectedAgent !== finalPayload.agent) {
|
|
371
|
+
finalPayload = { ...finalPayload, agent: projectedAgent };
|
|
372
|
+
}
|
|
373
|
+
if (opts.preflight !== true) {
|
|
374
|
+
// Skills-only async path — no preflight HTTP call. Dispatch now.
|
|
375
|
+
return this.subscribeAndSend(() => ({
|
|
376
|
+
kind: 'task.start',
|
|
377
|
+
payload: finalPayload,
|
|
378
|
+
rootMessageId,
|
|
379
|
+
}), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
380
|
+
sessionId,
|
|
381
|
+
messageId,
|
|
382
|
+
agentId: `inline:${messageId}`,
|
|
383
|
+
agentKind: 'inline',
|
|
384
|
+
...(finalPayload.skill !== undefined && finalPayload.skill !== null
|
|
385
|
+
? { skillName: finalPayload.skill }
|
|
386
|
+
: {}),
|
|
387
|
+
}).lifecycle, rootMessageId, ackDeferreds);
|
|
388
|
+
}
|
|
389
|
+
const resolver = this.resolveSpecs;
|
|
390
|
+
if (resolver === undefined) {
|
|
391
|
+
throw new Error("tasks.startWithAgent({ preflight: true }) requires the client's SpecsClient — " +
|
|
392
|
+
'this TaskClient was constructed without a specs resolver');
|
|
393
|
+
}
|
|
394
|
+
const result = await resolver().validate(finalPayload.agent);
|
|
395
|
+
if (!result.valid) {
|
|
396
|
+
// `SpecValidateResult` guarantees `errors.length > 0` on the
|
|
397
|
+
// `valid: false` arm, but a misbehaving server could violate it.
|
|
398
|
+
// Synthesize a single `preflight_rejected` issue so
|
|
399
|
+
// `QodoAgentSpecRejectedError`'s constructor invariant always
|
|
400
|
+
// holds and we surface a readable message rather than crashing
|
|
401
|
+
// the iterator with a TypeError.
|
|
402
|
+
const issues = result.errors.length > 0
|
|
403
|
+
? result.errors.map((e) => ({
|
|
404
|
+
rule_id: e.rule_id,
|
|
405
|
+
path: e.path,
|
|
406
|
+
message: e.message,
|
|
407
|
+
}))
|
|
408
|
+
: [
|
|
409
|
+
{
|
|
410
|
+
rule_id: 'preflight_rejected',
|
|
411
|
+
path: '',
|
|
412
|
+
message: 'Pre-flight validation rejected the spec without reporting rule_ids',
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
const head = issues[0];
|
|
416
|
+
throw new QodoAgentSpecRejectedError(issues, `Inline AgentSpec rejected by pre-flight (POST /v1/specs/validate): ` +
|
|
417
|
+
`${head.rule_id} @ ${head.path || '<root>'}: ${head.message}`);
|
|
418
|
+
}
|
|
419
|
+
// Pre-flight passed — write `task.start` on the wire now.
|
|
420
|
+
return this.subscribeAndSend(() => ({
|
|
421
|
+
kind: 'task.start',
|
|
422
|
+
payload: finalPayload,
|
|
423
|
+
rootMessageId,
|
|
424
|
+
}), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
425
|
+
sessionId,
|
|
426
|
+
messageId,
|
|
427
|
+
agentId: `inline:${messageId}`,
|
|
428
|
+
agentKind: 'inline',
|
|
429
|
+
...(finalPayload.skill !== undefined && finalPayload.skill !== null
|
|
430
|
+
? { skillName: finalPayload.skill }
|
|
431
|
+
: {}),
|
|
432
|
+
}).lifecycle, rootMessageId, ackDeferreds);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Wrap the underlying subscription so a server-side `error { code: ... }`
|
|
436
|
+
* envelope arriving on the iterator surfaces as a rejected typed error
|
|
437
|
+
* instead of a yielded `kind: 'error'` event. Covers the documented
|
|
438
|
+
* typed-error catalog:
|
|
439
|
+
*
|
|
440
|
+
* - `agent_spec_rejected` → `QodoAgentSpecRejectedError`
|
|
441
|
+
* - the cancel / resume / HITL / graph code family → the matching
|
|
442
|
+
* `QodoServerError` subclass
|
|
443
|
+
*
|
|
444
|
+
* Other error envelopes (`replay_anchor_missing`,
|
|
445
|
+
* `envelope_parse_error`, future codes the SDK hasn't catalogued)
|
|
446
|
+
* pass through unchanged as `kind: 'error'` iterator events so
|
|
447
|
+
* existing consumer code reading them keeps working. Callers that
|
|
448
|
+
* want to convert an arbitrary error envelope into a typed throw
|
|
449
|
+
* can wrap it themselves with `QodoUnknownServerError` (exposed via
|
|
450
|
+
* the public surface) or `errorFromServerErrorEnvelope` (internal,
|
|
451
|
+
* stable across the typed catalog).
|
|
452
|
+
*
|
|
453
|
+
* Non-error envelopes pass through unchanged.
|
|
454
|
+
*
|
|
455
|
+
* **Why this isn't an `async function*`.** Native async generators don't
|
|
456
|
+
* propagate `.return()` to the inner iterator until the generator's
|
|
457
|
+
* body has entered (one `.next()` call has fired). The SDK's
|
|
458
|
+
* iterator-early-termination contract is that breaking the iterator
|
|
459
|
+
* BEFORE the first envelope arrives still fires a best-effort
|
|
460
|
+
* `task.cancel` — that relies on `.return()` reaching `sub.return()`.
|
|
461
|
+
* The explicit iterator object below forwards every protocol call
|
|
462
|
+
* straight to the underlying subscription's iterator so the
|
|
463
|
+
* cancel-on-break path stays intact.
|
|
464
|
+
*/
|
|
465
|
+
static wrapServerErrorsIterator(inner) {
|
|
466
|
+
return {
|
|
467
|
+
[Symbol.asyncIterator]() {
|
|
468
|
+
const innerIter = inner[Symbol.asyncIterator]();
|
|
469
|
+
return {
|
|
470
|
+
async next() {
|
|
471
|
+
const result = await innerIter.next();
|
|
472
|
+
if (!result.done && result.value.kind === 'error') {
|
|
473
|
+
const code = result.value.payload.code;
|
|
474
|
+
if (typeof code === 'string' && isTypedErrorCode(code)) {
|
|
475
|
+
// Drain the underlying subscription before throwing — the
|
|
476
|
+
// `error` envelope already closed the inner queue on the
|
|
477
|
+
// SDK side, but defensive in case a downstream wraps a
|
|
478
|
+
// longer-lived iterator.
|
|
479
|
+
innerIter.return?.().catch(() => undefined);
|
|
480
|
+
throw errorFromServerErrorEnvelope(result.value);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
},
|
|
485
|
+
async return(value) {
|
|
486
|
+
if (innerIter.return !== undefined) {
|
|
487
|
+
return innerIter.return(value);
|
|
488
|
+
}
|
|
489
|
+
return { value: value, done: true };
|
|
490
|
+
},
|
|
491
|
+
async throw(err) {
|
|
492
|
+
if (innerIter.throw !== undefined) {
|
|
493
|
+
return innerIter.throw(err);
|
|
494
|
+
}
|
|
495
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Send `task.start { graph, input }` and yield the resulting `TaskEvent`
|
|
503
|
+
* stream until `task.done` — inline-graph-spec dispatch.
|
|
504
|
+
*
|
|
505
|
+
* The graph is validated client-side (the SDK-enforceable rule subset,
|
|
506
|
+
* see `validateInlineGraphSpec`) BEFORE the envelope hits the wire. A
|
|
507
|
+
* `QodoInlineGraphValidationError` is thrown when any rule fails — the
|
|
508
|
+
* error's `failures` array carries every violation with a `rule_id` +
|
|
509
|
+
* JSON-Pointer `path` so consumers can route both client-side and
|
|
510
|
+
* server-side rejections through the same branch logic.
|
|
511
|
+
*
|
|
512
|
+
* Opt into `{ preflight: true }` to additionally call
|
|
513
|
+
* `client.specs.validate(graph)` (HTTP) before opening the task — that
|
|
514
|
+
* catches the rules the SDK can't enforce locally (model registry probes,
|
|
515
|
+
* AgentSpec composition against the live agent table). Default `false`.
|
|
516
|
+
*
|
|
517
|
+
* Iterator + early-termination semantics match `start()` — breaking the
|
|
518
|
+
* iterator before `task.done` arrives sends best-effort `task.cancel`.
|
|
519
|
+
* Inline-graph spans carry `qar.graph.entry`, `qar.graph.depth`, and
|
|
520
|
+
* `qar.agent.kind = "inline"` attrs in addition to the standard
|
|
521
|
+
* `qar.client.task.start` set.
|
|
522
|
+
*/
|
|
523
|
+
startWithGraph(payload, opts) {
|
|
524
|
+
// Cheap fail-fast `idempotencyKey` check runs BEFORE the graph
|
|
525
|
+
// validator so a typo'd key surfaces immediately, ahead of the
|
|
526
|
+
// potentially-multi-issue graph validation pass.
|
|
527
|
+
if (opts?.idempotencyKey !== undefined) {
|
|
528
|
+
validateIdempotencyKey(opts.idempotencyKey);
|
|
529
|
+
}
|
|
530
|
+
// Local validation — synchronous, no IO, fail fast before either the
|
|
531
|
+
// optional pre-flight HTTP call or the WS write.
|
|
532
|
+
const validateOpts = opts?.maxDepth !== undefined ? { maxDepth: opts.maxDepth } : undefined;
|
|
533
|
+
validateInlineGraphSpec(payload.graph, validateOpts);
|
|
534
|
+
// `defineFunctionTool` auto-bind for graph dispatch — walk every
|
|
535
|
+
// inline-agent node and bind any handlers attached to their
|
|
536
|
+
// `tools[]` entries. Static refs (`agent_id`) and nested subgraphs
|
|
537
|
+
// are handled recursively. Mirrors the matching bind in
|
|
538
|
+
// `startWithAgent`.
|
|
539
|
+
if (this.bindFunctionToolDefs !== undefined) {
|
|
540
|
+
collectGraphFunctionTools(payload.graph, this.bindFunctionToolDefs);
|
|
541
|
+
}
|
|
542
|
+
// Compute the graph depth once for the OTel span attr.
|
|
543
|
+
const depth = TaskClient.computeGraphDepth(payload.graph);
|
|
544
|
+
// Pre-allocate root message id so `taskId` is synchronously available
|
|
545
|
+
// on the returned iterable.
|
|
546
|
+
const rootMessageId = asMessageId(uuidv7());
|
|
547
|
+
const taskId = asTaskId(rootMessageId);
|
|
548
|
+
const sessionIdDeferred = createDeferred();
|
|
549
|
+
const admittedTaskIdDeferred = createDeferred();
|
|
550
|
+
const admissionResultDeferred = createDeferred();
|
|
551
|
+
const ackDeferreds = {
|
|
552
|
+
sessionIdDeferred,
|
|
553
|
+
admittedTaskIdDeferred,
|
|
554
|
+
admissionResultDeferred,
|
|
555
|
+
};
|
|
556
|
+
// Slim-index injection. Synchronous when the manager's catalog has
|
|
557
|
+
// already been discovered (the common case post-connect()); falls
|
|
558
|
+
// through to the eager-promise dispatch path when discovery is still
|
|
559
|
+
// pending.
|
|
560
|
+
const skillsPinned = opts?.skills;
|
|
561
|
+
const needsSkillsAwait = this.skills !== undefined && this.skills.currentSnapshot === null;
|
|
562
|
+
// MCP projection: any inline-agent node carrying `mcpTools` /
|
|
563
|
+
// `mcpToolOverrides` triggers the eager-async dispatch path so the
|
|
564
|
+
// pool catalogs settle before projection.
|
|
565
|
+
const needsMcpProjection = graphHasMcpProjection(payload.graph);
|
|
566
|
+
let graphForWire = payload.graph;
|
|
567
|
+
// Sync inject only when no async-path trigger is armed (see
|
|
568
|
+
// `startWithAgent`'s matching comment — same idempotency
|
|
569
|
+
// concern; `applyGraphSkillsInjection` walks every inline-agent
|
|
570
|
+
// node and would otherwise duplicate per-node instruction text).
|
|
571
|
+
if (this.skills !== undefined && !needsSkillsAwait && !needsMcpProjection) {
|
|
572
|
+
graphForWire = this.applyGraphSkillsInjection(graphForWire, skillsPinned, opts?.skillsCurrentFile);
|
|
573
|
+
}
|
|
574
|
+
// Auto-include the qodo-skills.* tool surface in every inline-agent
|
|
575
|
+
// node of the graph when skills are configured. Mirror of the
|
|
576
|
+
// matching block in `startWithAgent` — the async projection path
|
|
577
|
+
// runs the same merge per-node inside `projectMcpToolsForGraphWire`
|
|
578
|
+
// for the eager-dispatch branch.
|
|
579
|
+
if (this.skills !== undefined && !needsMcpProjection) {
|
|
580
|
+
graphForWire = mergeQodoSkillsToolsIntoGraph(graphForWire);
|
|
581
|
+
}
|
|
582
|
+
const wirePayload = {
|
|
583
|
+
graph: graphForWire,
|
|
584
|
+
input: payload.input ?? {},
|
|
585
|
+
...(payload.skill !== undefined && payload.skill !== null
|
|
586
|
+
? { skill: payload.skill }
|
|
587
|
+
: {}),
|
|
588
|
+
// Forward the caller-supplied key when present.
|
|
589
|
+
...(opts?.idempotencyKey !== undefined
|
|
590
|
+
? { idempotency_key: opts.idempotencyKey }
|
|
591
|
+
: {}),
|
|
592
|
+
};
|
|
593
|
+
// Opt-in pre-flight: the iterator we return is async, so the HTTP call
|
|
594
|
+
// gets wrapped in an async generator that runs `validate` first, then
|
|
595
|
+
// delegates to the regular subscription. A `valid: false` result raises
|
|
596
|
+
// `QodoInlineGraphValidationError` so consumers see the same error shape
|
|
597
|
+
// whether the SDK or QAR rejected the spec.
|
|
598
|
+
if (opts?.preflight === true || needsSkillsAwait || needsMcpProjection) {
|
|
599
|
+
// Eager dispatch — see the matching comment on `startWithAgent`:
|
|
600
|
+
// the preflight + wire write must run before the consumer can race
|
|
601
|
+
// a `tasks.cancel(stream.taskId)` against the deferred
|
|
602
|
+
// `task.start`.
|
|
603
|
+
const subPromise = this.runGraphPreflightAndDispatch(payload, wirePayload, depth, opts ?? {}, rootMessageId, taskId, ackDeferreds);
|
|
604
|
+
subPromise.catch((err) => {
|
|
605
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
606
|
+
if (!sessionIdDeferred.settled())
|
|
607
|
+
sessionIdDeferred.reject(wrapped);
|
|
608
|
+
if (!admittedTaskIdDeferred.settled())
|
|
609
|
+
admittedTaskIdDeferred.reject(wrapped);
|
|
610
|
+
if (!admissionResultDeferred.settled())
|
|
611
|
+
admissionResultDeferred.reject(wrapped);
|
|
612
|
+
});
|
|
613
|
+
// Wrap server errors on the preflighted graph path so consumers get
|
|
614
|
+
// typed errors regardless of which dispatch path they invoke.
|
|
615
|
+
return attachTaskId(yieldFromPromisedSub(subPromise, true), taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
|
|
616
|
+
}
|
|
617
|
+
// Closure used by both single-shot dispatch and the admission retry
|
|
618
|
+
// wrapper (when `idempotencyKey` is set). Each call builds a fresh
|
|
619
|
+
// subscription against the supplied rootMessageId.
|
|
620
|
+
const dispatch = (rmid) => this.subscribeAndSend(() => ({
|
|
621
|
+
kind: 'task.start',
|
|
622
|
+
payload: wirePayload,
|
|
623
|
+
rootMessageId: rmid,
|
|
624
|
+
}), asTaskId(rmid), opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
625
|
+
sessionId,
|
|
626
|
+
messageId,
|
|
627
|
+
...(wirePayload.skill !== undefined && wirePayload.skill !== null
|
|
628
|
+
? { skillName: wirePayload.skill }
|
|
629
|
+
: {}),
|
|
630
|
+
graphEntry: payload.graph.entry,
|
|
631
|
+
graphDepth: depth,
|
|
632
|
+
agentKind: TaskClient.computeAgentKind(payload.graph),
|
|
633
|
+
}).lifecycle, rmid, ackDeferreds);
|
|
634
|
+
// Route `error` envelopes through the typed-error wrapper so
|
|
635
|
+
// `graph_spec_rejected` / `agent_reconstruction_failed` / etc.
|
|
636
|
+
// surface as typed throws instead of yielded `kind: 'error'` events
|
|
637
|
+
// on the iterator.
|
|
638
|
+
const firstSub = dispatch(rootMessageId);
|
|
639
|
+
const iter = opts?.idempotencyKey !== undefined
|
|
640
|
+
? TaskClient.wrapWithAdmissionRetry(firstSub, dispatch, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred)
|
|
641
|
+
: TaskClient.wrapServerErrorsIterator(firstSub);
|
|
642
|
+
return attachTaskId(iter, taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Eagerly run `client.specs.validate(graph)` and, on success, dispatch
|
|
646
|
+
* the `task.start` envelope via `subscribeAndSend`. Returns a
|
|
647
|
+
* `Promise<TaskSubscription>` that resolves once the wire write has
|
|
648
|
+
* happened. Validation rejection surfaces as
|
|
649
|
+
* `QodoInlineGraphValidationError` so consumers don't need a second
|
|
650
|
+
* `instanceof` branch for `SpecValidateResult` failures.
|
|
651
|
+
*/
|
|
652
|
+
async runGraphPreflightAndDispatch(inboundPayload, wirePayload, depth, opts, rootMessageId, taskId,
|
|
653
|
+
/**
|
|
654
|
+
* Deferred pair for the `task.started` admission ack (sessionId +
|
|
655
|
+
* admittedTaskId). Forwarded into the eventual `subscribeAndSend` so
|
|
656
|
+
* the caller's `TaskStartIterable.sessionId` + `.admittedTaskId`
|
|
657
|
+
* Promises resolve on the ack.
|
|
658
|
+
*/
|
|
659
|
+
ackDeferreds) {
|
|
660
|
+
// Await skills discovery + inject into every inline-agent node before
|
|
661
|
+
// the validator round-trip and the wire write.
|
|
662
|
+
let finalPayload = wirePayload;
|
|
663
|
+
let inboundForSpan = inboundPayload;
|
|
664
|
+
if (this.skills !== undefined) {
|
|
665
|
+
await this.skills.discover();
|
|
666
|
+
const injectedGraph = this.applyGraphSkillsInjection(wirePayload.graph, opts.skills, opts.skillsCurrentFile);
|
|
667
|
+
finalPayload = { ...wirePayload, graph: injectedGraph };
|
|
668
|
+
inboundForSpan = { ...inboundPayload, graph: injectedGraph };
|
|
669
|
+
}
|
|
670
|
+
// MCP projection across every inline-agent node. Runs AFTER
|
|
671
|
+
// skills inject (skills mutate instructions; projection mutates
|
|
672
|
+
// `tools[]`) and BEFORE the optional /v1/specs/validate so the
|
|
673
|
+
// validator sees the post-projection wire surface.
|
|
674
|
+
const projectedGraph = await this.projectMcpToolsForGraphWire(finalPayload.graph);
|
|
675
|
+
if (projectedGraph !== finalPayload.graph) {
|
|
676
|
+
finalPayload = { ...finalPayload, graph: projectedGraph };
|
|
677
|
+
inboundForSpan = { ...inboundForSpan, graph: projectedGraph };
|
|
678
|
+
}
|
|
679
|
+
if (opts.preflight !== true) {
|
|
680
|
+
// Skills-only async path — no preflight HTTP call. Dispatch now.
|
|
681
|
+
return this.subscribeAndSend(() => ({
|
|
682
|
+
kind: 'task.start',
|
|
683
|
+
payload: finalPayload,
|
|
684
|
+
rootMessageId,
|
|
685
|
+
}), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
686
|
+
sessionId,
|
|
687
|
+
messageId,
|
|
688
|
+
...(finalPayload.skill !== undefined && finalPayload.skill !== null
|
|
689
|
+
? { skillName: finalPayload.skill }
|
|
690
|
+
: {}),
|
|
691
|
+
graphEntry: inboundForSpan.graph.entry,
|
|
692
|
+
graphDepth: depth,
|
|
693
|
+
agentKind: TaskClient.computeAgentKind(inboundForSpan.graph),
|
|
694
|
+
}).lifecycle, rootMessageId, ackDeferreds);
|
|
695
|
+
}
|
|
696
|
+
const specsResolver = this.resolveSpecs;
|
|
697
|
+
if (specsResolver === undefined) {
|
|
698
|
+
throw new Error("tasks.startWithGraph({ preflight: true }) requires the client's SpecsClient — " +
|
|
699
|
+
"this TaskClient was constructed without a specs resolver");
|
|
700
|
+
}
|
|
701
|
+
const specs = specsResolver();
|
|
702
|
+
const result = await specs.validate(inboundForSpan.graph);
|
|
703
|
+
if (!result.valid) {
|
|
704
|
+
throw new QodoInlineGraphValidationError(result.errors.map((e) => ({
|
|
705
|
+
rule_id: e.rule_id,
|
|
706
|
+
path: e.path,
|
|
707
|
+
message: e.message,
|
|
708
|
+
})));
|
|
709
|
+
}
|
|
710
|
+
return this.subscribeAndSend(() => ({
|
|
711
|
+
kind: 'task.start',
|
|
712
|
+
payload: finalPayload,
|
|
713
|
+
rootMessageId,
|
|
714
|
+
}), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
|
|
715
|
+
sessionId,
|
|
716
|
+
messageId,
|
|
717
|
+
...(finalPayload.skill !== undefined && finalPayload.skill !== null
|
|
718
|
+
? { skillName: finalPayload.skill }
|
|
719
|
+
: {}),
|
|
720
|
+
graphEntry: inboundForSpan.graph.entry,
|
|
721
|
+
graphDepth: depth,
|
|
722
|
+
agentKind: TaskClient.computeAgentKind(inboundForSpan.graph),
|
|
723
|
+
}).lifecycle, rootMessageId, ackDeferreds);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Compute the longest-path depth (edge count) from `entry` for the
|
|
727
|
+
* `qar.graph.depth` span attr. Returns 0 when the graph has no edges
|
|
728
|
+
* (single-node graphs).
|
|
729
|
+
*
|
|
730
|
+
* Delegates to the validator's shared `longestPathFrom` so the two call
|
|
731
|
+
* sites can't drift on cycle handling.
|
|
732
|
+
*/
|
|
733
|
+
static computeGraphDepth(graph) {
|
|
734
|
+
return longestPathFrom(graph.entry, graph.edges ?? []);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Return the documented `qar.agent.kind` value for a graph: `"inline"`
|
|
738
|
+
* when any node carries an inline `AgentSpec` (or a nested `GraphSpec`),
|
|
739
|
+
* `"agent_id"` when every node is a server-static reference. The
|
|
740
|
+
* `qar.agent.kind` attribute is documented with the closed value set
|
|
741
|
+
* `"inline" | "agent_id"` — keep this in sync with
|
|
742
|
+
* `src/observability/attributes.ts`.
|
|
743
|
+
*/
|
|
744
|
+
static computeAgentKind(graph) {
|
|
745
|
+
for (const agent of Object.values(graph.agents ?? {})) {
|
|
746
|
+
const inner = agent.spec;
|
|
747
|
+
if (typeof inner === 'object' && inner !== null && 'kind' in inner) {
|
|
748
|
+
return 'inline';
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return 'agent_id';
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Send `task.continue` for an existing `task_id` and yield the resulting `TaskEvent`
|
|
755
|
+
* stream until `task.done`. Same iterator semantics as `start`.
|
|
756
|
+
*
|
|
757
|
+
* Payload shape mirrors QAR's `TaskContinuePayload` minus `task_id` — the id is
|
|
758
|
+
* already supplied as the first argument. `task.continue` carries no
|
|
759
|
+
* `agent_id`/`skill` because those are bound at `task.start` time.
|
|
760
|
+
*/
|
|
761
|
+
continue(taskId, payload, opts) {
|
|
762
|
+
const fullPayload = { task_id: taskId, ...payload };
|
|
763
|
+
const sub = this.subscribeAndSend((rootMessageId) => ({ kind: 'task.continue', payload: fullPayload, rootMessageId }), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskContinueSpan({ sessionId, messageId, taskId }).lifecycle);
|
|
764
|
+
// Typed-error wrap on continue — `task_not_found`,
|
|
765
|
+
// `cross_pod_resume_aborted`, `agent_reconstruction_failed`,
|
|
766
|
+
// `message_history_load_failed`, the HITL family, etc. all surface
|
|
767
|
+
// here when the server rejects the continue.
|
|
768
|
+
return TaskClient.wrapServerErrorsIterator(sub);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Cold-start helper. Unifies `task.start` and `task.continue` behind
|
|
772
|
+
* a single call so consumers with deterministic `idempotencyKey`
|
|
773
|
+
* derivation (Slack bots, CLI tools, IDE plugins, web apps without
|
|
774
|
+
* server affinity) don't have to track in-memory
|
|
775
|
+
* `(idempotencyKey → taskId)` mappings across process restarts.
|
|
776
|
+
*
|
|
777
|
+
* Behavior, gated on QAR's `task.started` admission response:
|
|
778
|
+
*
|
|
779
|
+
* - **No session yet** (`isNew === true`, no `previousTaskId`) —
|
|
780
|
+
* fresh dispatch. The returned handle iterates the newly-started
|
|
781
|
+
* task's event stream.
|
|
782
|
+
* - **Existing session, `state === 'auto-paused'`** (`isNew === false`) —
|
|
783
|
+
* QAR returned the existing `task_id`. The helper auto-emits a
|
|
784
|
+
* `tasks.continue(taskId, { input })` to deliver the caller's
|
|
785
|
+
* input; the returned handle iterates the continue subscription's
|
|
786
|
+
* events.
|
|
787
|
+
* - **Existing session, `state === 'running'`** (`isNew === false`) —
|
|
788
|
+
* QAR returned the existing `task_id`. Same auto-continue path;
|
|
789
|
+
* QAR queues the input until the next auto-pause boundary.
|
|
790
|
+
* - **Terminal re-dispatch** (`isNew === true` + `previousTaskId` +
|
|
791
|
+
* `previousState`) — QAR created a NEW task on the same
|
|
792
|
+
* `session_id`, inheriting the full `session_messages` history
|
|
793
|
+
* (B-raw). The handle iterates the new task's stream; consumer
|
|
794
|
+
* can read `previousTaskId` + `previousState` to surface
|
|
795
|
+
* "this is a new task after a prior failure" UX.
|
|
796
|
+
*
|
|
797
|
+
* `graphSpec` is required for the fresh-dispatch branch. On
|
|
798
|
+
* already-dispatched sessions QAR ignores it (the existing task's
|
|
799
|
+
* spec is the source of truth). The helper throws
|
|
800
|
+
* {@link RequiredGraphSpecError} synchronously if `graphSpec` is
|
|
801
|
+
* `undefined` — consumers who don't have a graphSpec on hand should
|
|
802
|
+
* call {@link continue} or {@link forceResume} directly.
|
|
803
|
+
*
|
|
804
|
+
* Paired with QAR-side idempotent admission. Older QAR builds without
|
|
805
|
+
* idempotent admission either reject `session_already_dispatched` (the
|
|
806
|
+
* case this helper fixes) or — for fresh sessions — behave identically
|
|
807
|
+
* (the helper defaults `isNew` to `true` when the field is absent in
|
|
808
|
+
* the ack).
|
|
809
|
+
*/
|
|
810
|
+
async continueOrStart(idempotencyKey, graphSpec, options = {}) {
|
|
811
|
+
if (graphSpec === undefined) {
|
|
812
|
+
throw new RequiredGraphSpecError(idempotencyKey);
|
|
813
|
+
}
|
|
814
|
+
const stream = this.startWithGraph({
|
|
815
|
+
graph: graphSpec,
|
|
816
|
+
...(options.input !== undefined ? { input: options.input } : {}),
|
|
817
|
+
}, { idempotencyKey });
|
|
818
|
+
const admission = await stream.admissionResult;
|
|
819
|
+
let iter;
|
|
820
|
+
if (admission.isNew) {
|
|
821
|
+
iter = stream;
|
|
822
|
+
}
|
|
823
|
+
else if (options.input !== undefined) {
|
|
824
|
+
// Idempotent return — pivot to `task.continue` to deliver the
|
|
825
|
+
// caller's input. QAR routes the input through the existing
|
|
826
|
+
// task's queue; the continue subscription owns the event stream
|
|
827
|
+
// from here.
|
|
828
|
+
//
|
|
829
|
+
// The cast widens `QodoTaskStartInput` (the typed `task.start.input`
|
|
830
|
+
// shape) to `TaskContinuePayload['input']` (codegen's
|
|
831
|
+
// `{ [k: string]: unknown }`). The runtime values are identical —
|
|
832
|
+
// QAR consumes the same `user_query` / `deps_overrides` / `metadata`
|
|
833
|
+
// fields on continue. Without the cast, TypeScript would reject
|
|
834
|
+
// the assignment because the typed shape lacks the open-shape
|
|
835
|
+
// index signature codegen emits.
|
|
836
|
+
iter = this.continue(admission.taskId, {
|
|
837
|
+
input: options.input,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
// is_new=false and no input — the caller just wanted a lookup.
|
|
842
|
+
// The start stream auto-closed in `TaskSubscription.consider` on
|
|
843
|
+
// `is_new === false`; expose an immediately-empty iterator.
|
|
844
|
+
iter = stream;
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
[Symbol.asyncIterator]: () => iter[Symbol.asyncIterator](),
|
|
848
|
+
taskId: admission.taskId,
|
|
849
|
+
sessionId: admission.sessionId,
|
|
850
|
+
isNew: admission.isNew,
|
|
851
|
+
...(admission.previousTaskId !== undefined
|
|
852
|
+
? { previousTaskId: admission.previousTaskId }
|
|
853
|
+
: {}),
|
|
854
|
+
...(admission.previousState !== undefined
|
|
855
|
+
? { previousState: admission.previousState }
|
|
856
|
+
: {}),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Explicit stuck-session recovery. Sends `task.forceResume` and
|
|
861
|
+
* awaits the matching `task.force_resumed` ack.
|
|
862
|
+
*
|
|
863
|
+
* Use case: a prior consumer process died mid-tool-call or mid-HITL;
|
|
864
|
+
* the session is stuck `running` with outstanding `tool_call_id` /
|
|
865
|
+
* `hitl_id` that will never resolve. `forceResume` triggers QAR to:
|
|
866
|
+
*
|
|
867
|
+
* 1. Cancel the in-flight tool call (mirrors the system-cancel
|
|
868
|
+
* primitives used by `task.cancel` and the A2A bridge).
|
|
869
|
+
* 2. Synth-resolve pending HITLs.
|
|
870
|
+
* 3. Append a `{role: 'system', content: 'process restart — outstanding
|
|
871
|
+
* tool call cancelled'}` marker to `session_messages` (audit trail).
|
|
872
|
+
* 4. Transition the session to `'auto-paused'`.
|
|
873
|
+
*
|
|
874
|
+
* If the session is already `auto-paused` or terminal, QAR
|
|
875
|
+
* passthroughs and reports the actual state.
|
|
876
|
+
*
|
|
877
|
+
* Returns `{taskId, state}` — the consumer can then call
|
|
878
|
+
* `tasks.continue(taskId, { input })` with their next turn's input.
|
|
879
|
+
*/
|
|
880
|
+
forceResume(idempotencyKey) {
|
|
881
|
+
// Same wire validation as `task.start.idempotencyKey` — surfaces
|
|
882
|
+
// bad-input synchronously rather than waiting for a server
|
|
883
|
+
// round-trip.
|
|
884
|
+
validateIdempotencyKey(idempotencyKey);
|
|
885
|
+
const connection = this.resolveConnection();
|
|
886
|
+
const rootMessageId = asMessageId(uuidv7());
|
|
887
|
+
const deferred = createDeferred();
|
|
888
|
+
const sub = new TaskSubscription(rootMessageId, undefined,
|
|
889
|
+
// onEarlyReturn — no-op. forceResume is a one-shot async call;
|
|
890
|
+
// there's no consumer-iterator to break, and a stray `task.cancel`
|
|
891
|
+
// here would target the wrong task (the recovered session's
|
|
892
|
+
// taskId isn't known yet).
|
|
893
|
+
() => undefined, (s) => connection.unsubscribe(s), undefined, this.metrics, false, undefined, {
|
|
894
|
+
resolve: (args) => deferred.resolve(args),
|
|
895
|
+
reject: (err) => deferred.reject(err),
|
|
896
|
+
});
|
|
897
|
+
connection.subscribe(sub);
|
|
898
|
+
try {
|
|
899
|
+
connection.sendEnvelope({
|
|
900
|
+
kind: 'task.forceResume',
|
|
901
|
+
payload: { idempotency_key: idempotencyKey },
|
|
902
|
+
}, { messageId: rootMessageId });
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
sub.fail(err instanceof Error ? err : new Error(String(err)));
|
|
906
|
+
throw err;
|
|
907
|
+
}
|
|
908
|
+
return deferred.promise.then((args) => ({
|
|
909
|
+
taskId: args.taskId,
|
|
910
|
+
state: args.state,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Send `task.cancel` and await the matching `task.done { status: "canceled" }`.
|
|
915
|
+
*
|
|
916
|
+
* The returned promise resolves once the server confirms the cancel via
|
|
917
|
+
* `task.done`. If the server reports failure (`task.done { status: "failed" }`)
|
|
918
|
+
* during cancellation, `status: 'failed'` surfaces here so callers can
|
|
919
|
+
* distinguish — they didn't ask for it, but it's still a terminal state.
|
|
920
|
+
*
|
|
921
|
+
* Cross-process recovery: pass `opts.sessionId` to cancel a task whose
|
|
922
|
+
* `task_id` was hydrated from durable storage (i.e. the SDK never
|
|
923
|
+
* observed the originating `task.started` in this process). Without the
|
|
924
|
+
* override the SDK throws {@link QodoColdAddressError} before the wire
|
|
925
|
+
* write.
|
|
926
|
+
*
|
|
927
|
+
* `opts.signal` semantic: **"signal aborts wait-for-task.done"**. If the
|
|
928
|
+
* signal fires before `task.done` arrives, `cancel` throws
|
|
929
|
+
* {@link QodoCancelAbortedError} with the task_id and the abort reason.
|
|
930
|
+
* The wire `task.cancel` was already sent; the underlying task's
|
|
931
|
+
* terminal ack still routes through its `tasks.start` /
|
|
932
|
+
* `tasks.continue` subscription. The signal does NOT cause a second
|
|
933
|
+
* `task.cancel` (that would be redundant — the consumer's intent is
|
|
934
|
+
* "stop waiting", not "cancel twice"). Pre-aborted signals throw
|
|
935
|
+
* synchronously before any wire write.
|
|
936
|
+
*/
|
|
937
|
+
async cancel(taskId, payload, opts) {
|
|
938
|
+
const fullPayload = { task_id: taskId, ...(payload ?? {}) };
|
|
939
|
+
// Pre-aborted signals short-circuit BEFORE any wire write so the
|
|
940
|
+
// consumer's intent ("don't even bother") is honored without a
|
|
941
|
+
// server round-trip. `tasks.cancel` is `async`, so the throw
|
|
942
|
+
// surfaces as a rejected Promise; consumers' `try/catch` around
|
|
943
|
+
// `await tasks.cancel(...)` catches it identically to the
|
|
944
|
+
// post-send-abort case below. `kind: 'pre_aborted'` lets consumers
|
|
945
|
+
// branch on "did any wire traffic happen?" without parsing the
|
|
946
|
+
// error message.
|
|
947
|
+
const signal = opts?.signal;
|
|
948
|
+
if (signal?.aborted === true) {
|
|
949
|
+
throw new QodoCancelAbortedError('pre_aborted', taskId, signal.reason);
|
|
950
|
+
}
|
|
951
|
+
// Cancel is short-lived but distinct from the task's own span: it covers
|
|
952
|
+
// the WS write + the wait for `task.done(canceled)`. We open it with
|
|
953
|
+
// `undefined` for the subscription so the wrapped `task.done` doesn't
|
|
954
|
+
// double-close the task span (the underlying task's span is owned by the
|
|
955
|
+
// `tasks.start` / `tasks.continue` subscription, not this one).
|
|
956
|
+
//
|
|
957
|
+
// The span is created by the buildSpan callback (closing-over the local
|
|
958
|
+
// `cancelSpan` so we can fail it from the catch arm if `subscribeAndSend`
|
|
959
|
+
// itself throws — without that, a `Connection.sendEnvelope` failure
|
|
960
|
+
// before the subscription was returned would leak the span).
|
|
961
|
+
let cancelSpan;
|
|
962
|
+
let sub;
|
|
963
|
+
// Don't forward `signal` to subscribeAndSend — its generic
|
|
964
|
+
// bindAbort would emit a SECOND `task.cancel` on abort, which is
|
|
965
|
+
// redundant (we're already cancelling) and would race the user's
|
|
966
|
+
// payload `reason`. Pass only the cold-address `sessionId` override.
|
|
967
|
+
const subOpts = opts?.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined;
|
|
968
|
+
try {
|
|
969
|
+
sub = this.subscribeAndSend((rootMessageId) => ({ kind: 'task.cancel', payload: fullPayload, rootMessageId }), taskId, subOpts, undefined, (sessionId, messageId) => {
|
|
970
|
+
cancelSpan = this.spanRecorder.startTaskCancelSpan({ sessionId, messageId, taskId });
|
|
971
|
+
return undefined;
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
cancelSpan?.fail(err);
|
|
976
|
+
throw err;
|
|
977
|
+
}
|
|
978
|
+
// Wire the abort signal to close the subscription locally. The
|
|
979
|
+
// for-await below exits and falls through to the typed throw.
|
|
980
|
+
// Cleanup removes the listener so a long-lived signal doesn't leak
|
|
981
|
+
// references through the subscription closure once the cancel
|
|
982
|
+
// resolves.
|
|
983
|
+
//
|
|
984
|
+
// `abortedDuringWait` is read by the for-await exit branch below;
|
|
985
|
+
// TS's control-flow analysis narrows `signal.aborted` against the
|
|
986
|
+
// earlier pre-aborted-check at function entry, so we capture the
|
|
987
|
+
// late-aborted state in a separate flag.
|
|
988
|
+
let abortListener;
|
|
989
|
+
let abortedDuringWait = false;
|
|
990
|
+
let abortReasonDuringWait;
|
|
991
|
+
if (signal !== undefined) {
|
|
992
|
+
abortListener = () => {
|
|
993
|
+
abortedDuringWait = true;
|
|
994
|
+
abortReasonDuringWait = signal.reason;
|
|
995
|
+
sub.close();
|
|
996
|
+
};
|
|
997
|
+
signal.addEventListener('abort', abortListener, { once: true });
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
for await (const event of sub) {
|
|
1001
|
+
if (event.kind === 'task.done' && event.payload.task_id === taskId) {
|
|
1002
|
+
const done = event;
|
|
1003
|
+
// `done.payload.task_id` arrives from codegen as plain `string` (the
|
|
1004
|
+
// overlay brands the public `TaskDonePayload` type but the generated
|
|
1005
|
+
// envelope's `payload` references the unbranded codegen variant).
|
|
1006
|
+
// We've already matched against `taskId` by equality above, so it is
|
|
1007
|
+
// the same UUID — re-using the input branded value keeps the cast
|
|
1008
|
+
// off the boundary.
|
|
1009
|
+
//
|
|
1010
|
+
// Surface the server's actual terminal status verbatim — including
|
|
1011
|
+
// `'completed'`, which means the task finished before our cancel was
|
|
1012
|
+
// processed (the race the docs promise we'll surface).
|
|
1013
|
+
if (done.payload.status === 'failed') {
|
|
1014
|
+
cancelSpan?.fail(new Error(done.payload.error ?? 'cancel returned failed'));
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
cancelSpan?.succeed();
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
task_id: taskId,
|
|
1021
|
+
status: done.payload.status,
|
|
1022
|
+
...(done.payload.error !== undefined && done.payload.error !== null
|
|
1023
|
+
? { error: done.payload.error }
|
|
1024
|
+
: {}),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
// A server `error` envelope arriving on the cancel iterator
|
|
1028
|
+
// with a documented typed code (e.g. `cancel_routing_failed`,
|
|
1029
|
+
// `cancel_failed`, `task_not_found`, `task_not_waiting`) is
|
|
1030
|
+
// surfaced as a typed throw so consumers can branch on
|
|
1031
|
+
// `instanceof QodoCancelRoutingFailedError` etc. instead of
|
|
1032
|
+
// racing the absent `task.done`. Unrecognized codes continue
|
|
1033
|
+
// to fall through into the "connection closed before
|
|
1034
|
+
// task.done" fallback so we don't change behavior for codes
|
|
1035
|
+
// the SDK hasn't catalogued yet.
|
|
1036
|
+
if (event.kind === 'error') {
|
|
1037
|
+
const code = event.payload.code;
|
|
1038
|
+
if (typeof code === 'string' && isTypedErrorCode(code)) {
|
|
1039
|
+
const err = errorFromServerErrorEnvelope(event);
|
|
1040
|
+
cancelSpan?.fail(err);
|
|
1041
|
+
throw err;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// for-await drained without a `task.done` — either the abort
|
|
1046
|
+
// fired (consumer chose not to wait) or the connection closed.
|
|
1047
|
+
if (abortedDuringWait) {
|
|
1048
|
+
const err = new QodoCancelAbortedError('aborted_after_send', taskId, abortReasonDuringWait);
|
|
1049
|
+
cancelSpan?.fail(err);
|
|
1050
|
+
throw err;
|
|
1051
|
+
}
|
|
1052
|
+
cancelSpan?.fail(new Error('connection closed before task.done arrived'));
|
|
1053
|
+
return {
|
|
1054
|
+
task_id: taskId,
|
|
1055
|
+
status: 'failed',
|
|
1056
|
+
error: 'connection closed before task.done arrived',
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
catch (err) {
|
|
1060
|
+
cancelSpan?.fail(err);
|
|
1061
|
+
throw err;
|
|
1062
|
+
}
|
|
1063
|
+
finally {
|
|
1064
|
+
if (abortListener !== undefined && signal !== undefined) {
|
|
1065
|
+
signal.removeEventListener('abort', abortListener);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Send `task.resubscribe { task_id, since_message_id? }` and yield replayed
|
|
1071
|
+
* `TaskEvent`s from the server-held ring buffer (per-session cap of 1000
|
|
1072
|
+
* envelopes). No client outbox — durability is server-side.
|
|
1073
|
+
*
|
|
1074
|
+
* Use this when you have a `task_id` from a prior connection (e.g. across a
|
|
1075
|
+
* process restart) and want to catch up from a known anchor. For automatic
|
|
1076
|
+
* reconnect-on-drop within a single `connect()` lifetime, the SDK does this
|
|
1077
|
+
* for you transparently — see `qar.client.reconnect*` events on the
|
|
1078
|
+
* canonical iterator.
|
|
1079
|
+
*
|
|
1080
|
+
* If the server's ring buffer has rotated past `sinceMessageId` (very long
|
|
1081
|
+
* disconnects, or a missing/wrong anchor), the server emits an `error`
|
|
1082
|
+
* envelope with a `replay_anchor_missing`-style code; that event surfaces
|
|
1083
|
+
* as the iterator's final value and ends the stream. The consumer can
|
|
1084
|
+
* decide to start the task fresh or surrender.
|
|
1085
|
+
*/
|
|
1086
|
+
resubscribe(taskId, opts) {
|
|
1087
|
+
const payload = {
|
|
1088
|
+
task_id: taskId,
|
|
1089
|
+
since_message_id: opts?.sinceMessageId ?? null,
|
|
1090
|
+
};
|
|
1091
|
+
return this.subscribeAndSend((rootMessageId) => ({ kind: 'task.resubscribe', payload, rootMessageId }), taskId, opts,
|
|
1092
|
+
// Resubscribe is observation, not initiation: breaking the iterator
|
|
1093
|
+
// must NOT cancel the underlying task. The user may still want it
|
|
1094
|
+
// running; they can call `tasks.cancel` separately if not.
|
|
1095
|
+
//
|
|
1096
|
+
// The outbound `task.resubscribe.message_id` is the replay anchor —
|
|
1097
|
+
// every descendant counts as a replay envelope so the SDK can stamp
|
|
1098
|
+
// `replay_envelopes_received_total`.
|
|
1099
|
+
{ suppressEarlyReturnCancel: true, rootIsReplayAnchor: true }, (sessionId, messageId) => this.spanRecorder.startTaskResubscribeSpan({
|
|
1100
|
+
sessionId,
|
|
1101
|
+
messageId,
|
|
1102
|
+
taskId,
|
|
1103
|
+
...(opts?.sinceMessageId !== undefined ? { sinceMessageId: opts.sinceMessageId } : {}),
|
|
1104
|
+
}).lifecycle);
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Pre-allocate a `MessageId`, register a `TaskSubscription` against it, then
|
|
1108
|
+
* send the outbound envelope using the same id. Subscribing before sending
|
|
1109
|
+
* eliminates the race where the server's first reply could land before the
|
|
1110
|
+
* subscription is in place.
|
|
1111
|
+
*
|
|
1112
|
+
* Pre-aborted signals short-circuit BEFORE the send so we never start
|
|
1113
|
+
* server-side work the consumer has already given up on.
|
|
1114
|
+
*
|
|
1115
|
+
* `preallocatedRootMessageId` lets the caller mint the root id at the
|
|
1116
|
+
* public-API boundary so it can derive `task_id` synchronously for
|
|
1117
|
+
* `task.start` envelopes (the wire protocol codifies
|
|
1118
|
+
* `task_id == task.start.message_id`). When omitted, we allocate fresh —
|
|
1119
|
+
* the `task.continue` / `task.cancel` / `task.resubscribe` call sites
|
|
1120
|
+
* already carry the task_id as an explicit arg.
|
|
1121
|
+
*/
|
|
1122
|
+
subscribeAndSend(buildOutbound, knownTaskId, opts, behavior,
|
|
1123
|
+
/**
|
|
1124
|
+
* Optional span builder. Returns a `SpanLifecycle` to attach to the
|
|
1125
|
+
* subscription (closed on terminal/fail/early-return), or `undefined`
|
|
1126
|
+
* when the caller manages span lifetime itself (e.g. `tasks.cancel`).
|
|
1127
|
+
* The first arg is the per-Task `session_id` if known (`undefined`
|
|
1128
|
+
* on the `task.start` path before admission completes); the span
|
|
1129
|
+
* recorder treats it as an optional attribute.
|
|
1130
|
+
*/
|
|
1131
|
+
buildSpan, preallocatedRootMessageId,
|
|
1132
|
+
/**
|
|
1133
|
+
* Optional `task.started` admission-ack deferreds wired in by the
|
|
1134
|
+
* `task.start` family (`tasks.start` / `tasks.startWithAgent` /
|
|
1135
|
+
* `tasks.startWithGraph`). The constructed `TaskSubscription`
|
|
1136
|
+
* resolves all three deferreds atomically when `task.started` arrives:
|
|
1137
|
+
* `sessionId` from the envelope's inherited `session_id` field,
|
|
1138
|
+
* `admittedTaskId` from the payload's `task_id` field, and
|
|
1139
|
+
* `admissionResultDeferred` with the full `TaskAdmissionResult`
|
|
1140
|
+
* (idempotent-admission fields `isNew` / `state` / `previousTaskId` /
|
|
1141
|
+
* `previousState`). `undefined` for non-start paths (`task.continue`
|
|
1142
|
+
* / `task.cancel` / `task.resubscribe`) which already know both ids
|
|
1143
|
+
* from a prior admission.
|
|
1144
|
+
*/
|
|
1145
|
+
taskStartedDeferred) {
|
|
1146
|
+
const connection = this.resolveConnection();
|
|
1147
|
+
const rootMessageId = preallocatedRootMessageId ?? asMessageId(uuidv7());
|
|
1148
|
+
let onAbortCleanup;
|
|
1149
|
+
const suppressEarlyReturnCancel = behavior?.suppressEarlyReturnCancel === true;
|
|
1150
|
+
// Build the outbound shape NOW (rather than at send time below) so
|
|
1151
|
+
// we can gate `opts.sessionId` span attribution on the kind. On
|
|
1152
|
+
// `task.start` the consumer's `sessionId` is documented as ignored
|
|
1153
|
+
// (admission CREATES the session) — and using it for span
|
|
1154
|
+
// attribution would pollute the span with a caller-supplied value
|
|
1155
|
+
// the wire never sees. Only the ongoing kinds (`task.continue` /
|
|
1156
|
+
// `task.cancel` / `task.resubscribe`) legitimately consume the
|
|
1157
|
+
// override.
|
|
1158
|
+
//
|
|
1159
|
+
// `buildOutbound` is a pure constructor over `rootMessageId` (no
|
|
1160
|
+
// side effects), so calling it twice is safe — but we cache the
|
|
1161
|
+
// result and reuse below to avoid the redundant work.
|
|
1162
|
+
const outbound = buildOutbound(rootMessageId);
|
|
1163
|
+
const isStart = outbound.kind === 'task.start';
|
|
1164
|
+
// No connection-level session_id. For `task.continue` /
|
|
1165
|
+
// `task.cancel` / `task.resubscribe` we already know `knownTaskId`
|
|
1166
|
+
// here and can look up the per-Task session captured from the prior
|
|
1167
|
+
// `task.started`. For `task.start` (knownTaskId === undefined at the
|
|
1168
|
+
// public entry point — derived from the outbound message_id but no
|
|
1169
|
+
// ack yet), the span opens without a session attribute; the
|
|
1170
|
+
// subscription's later `task.started` handler stamps the canonical
|
|
1171
|
+
// id on the lifecycle once admission completes.
|
|
1172
|
+
//
|
|
1173
|
+
// When the consumer passed `opts.sessionId` (the cold-address path),
|
|
1174
|
+
// use that for span attribution on ongoing kinds — the in-memory map
|
|
1175
|
+
// will be empty on cross-process recovery, so the lookup would
|
|
1176
|
+
// return `undefined` and the span would miss its `qar.session_id`
|
|
1177
|
+
// attribute despite the consumer knowing the right value. On
|
|
1178
|
+
// `task.start` we ignore `opts.sessionId` per the contract
|
|
1179
|
+
// documented in `TaskOptions.sessionId`.
|
|
1180
|
+
const knownSessionId = isStart
|
|
1181
|
+
? undefined
|
|
1182
|
+
: (opts?.sessionId ??
|
|
1183
|
+
(knownTaskId !== undefined ? connection.getSessionForTask(knownTaskId) : undefined));
|
|
1184
|
+
const span = buildSpan?.(knownSessionId, rootMessageId);
|
|
1185
|
+
const sub = new TaskSubscription(rootMessageId, knownTaskId, (taskIdAtCancel) => {
|
|
1186
|
+
// `tasks.resubscribe` is observation-only — breaking the iterator
|
|
1187
|
+
// there must NOT cancel the underlying task (the consumer may still
|
|
1188
|
+
// want it running; they'll call `tasks.cancel` separately if not).
|
|
1189
|
+
if (suppressEarlyReturnCancel)
|
|
1190
|
+
return;
|
|
1191
|
+
const cancelTaskId = taskIdAtCancel ?? knownTaskId;
|
|
1192
|
+
if (cancelTaskId !== undefined && connection.isOpen) {
|
|
1193
|
+
try {
|
|
1194
|
+
connection.sendEnvelope({
|
|
1195
|
+
kind: 'task.cancel',
|
|
1196
|
+
payload: { task_id: cancelTaskId, reason: 'iterator early-termination' },
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
// Connection may have closed mid-flight; cancel is best-effort.
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}, (s) => {
|
|
1204
|
+
connection.unsubscribe(s);
|
|
1205
|
+
// Retract any throttled frame still sitting in the connection's
|
|
1206
|
+
// backpressure queue. Without this, a queued `task.continue`
|
|
1207
|
+
// would still hit the wire on the next `flow.resume` even though
|
|
1208
|
+
// the consumer has already canceled / aborted / broken the
|
|
1209
|
+
// iterator — producing `cancel → resume → continue` reordering
|
|
1210
|
+
// on the server. No-op if the frame already flushed or never
|
|
1211
|
+
// entered the throttle path.
|
|
1212
|
+
connection.dropQueued(rootMessageId);
|
|
1213
|
+
// Drop the AbortSignal listener so a long-lived signal that's
|
|
1214
|
+
// reused across many tasks doesn't accumulate dead closures
|
|
1215
|
+
// pinning every completed subscription + connection in memory.
|
|
1216
|
+
onAbortCleanup?.();
|
|
1217
|
+
onAbortCleanup = undefined;
|
|
1218
|
+
}, span, this.metrics, behavior?.rootIsReplayAnchor === true, taskStartedDeferred !== undefined
|
|
1219
|
+
? {
|
|
1220
|
+
resolve: ({ sessionId, taskId: ackTaskId, isNew, state, previousTaskId, previousState, }) => {
|
|
1221
|
+
taskStartedDeferred.sessionIdDeferred.resolve(sessionId);
|
|
1222
|
+
taskStartedDeferred.admittedTaskIdDeferred.resolve(ackTaskId);
|
|
1223
|
+
taskStartedDeferred.admissionResultDeferred.resolve({
|
|
1224
|
+
taskId: ackTaskId,
|
|
1225
|
+
sessionId,
|
|
1226
|
+
isNew,
|
|
1227
|
+
...(state !== undefined ? { state } : {}),
|
|
1228
|
+
...(previousTaskId !== undefined ? { previousTaskId } : {}),
|
|
1229
|
+
...(previousState !== undefined ? { previousState } : {}),
|
|
1230
|
+
});
|
|
1231
|
+
},
|
|
1232
|
+
reject: (err) => {
|
|
1233
|
+
taskStartedDeferred.sessionIdDeferred.reject(err);
|
|
1234
|
+
taskStartedDeferred.admittedTaskIdDeferred.reject(err);
|
|
1235
|
+
taskStartedDeferred.admissionResultDeferred.reject(err);
|
|
1236
|
+
},
|
|
1237
|
+
}
|
|
1238
|
+
: undefined);
|
|
1239
|
+
if (opts?.signal?.aborted === true) {
|
|
1240
|
+
// Pre-aborted: never send the outbound envelope. For `task.continue`
|
|
1241
|
+
// / `task.resubscribe` / `task.cancel` (we know the id) and the
|
|
1242
|
+
// user wants to give up before we start, emit `task.cancel` so the
|
|
1243
|
+
// running task on the server tears down promptly UNLESS this is a
|
|
1244
|
+
// resubscribe-style observation that mustn't cancel the live task.
|
|
1245
|
+
// For `task.start` (no id yet) there's nothing for the server to
|
|
1246
|
+
// cancel — just close the iterator.
|
|
1247
|
+
if (knownTaskId !== undefined &&
|
|
1248
|
+
connection.isOpen &&
|
|
1249
|
+
!suppressEarlyReturnCancel) {
|
|
1250
|
+
try {
|
|
1251
|
+
// Forward the cold-address `sessionId` (if supplied) so the
|
|
1252
|
+
// pre-abort cancel resolves the same session the never-sent
|
|
1253
|
+
// ongoing envelope would have used. Without this, cold
|
|
1254
|
+
// continue/resubscribe with a pre-aborted signal would
|
|
1255
|
+
// silently drop the cancel via the catch below.
|
|
1256
|
+
connection.sendEnvelope({
|
|
1257
|
+
kind: 'task.cancel',
|
|
1258
|
+
payload: { task_id: knownTaskId, reason: 'pre-aborted signal' },
|
|
1259
|
+
}, opts?.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
|
|
1260
|
+
}
|
|
1261
|
+
catch {
|
|
1262
|
+
// best-effort — common case is cold-`tasks.start` pre-abort,
|
|
1263
|
+
// where the task was never admitted server-side and there's
|
|
1264
|
+
// no session to address. `QodoColdAddressError` lands here
|
|
1265
|
+
// and we silently drop (the server has nothing to cancel).
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
sub.close();
|
|
1269
|
+
return sub;
|
|
1270
|
+
}
|
|
1271
|
+
connection.subscribe(sub);
|
|
1272
|
+
// `outbound` was built earlier to gate span sessionId on the kind;
|
|
1273
|
+
// reuse here.
|
|
1274
|
+
try {
|
|
1275
|
+
// Run the outbound `sendEnvelope` inside the SDK span's context.
|
|
1276
|
+
// This is what makes `Connection`'s `TraceContextProvider.current()`
|
|
1277
|
+
// see our SDK span as the active span at envelope-encode time, so
|
|
1278
|
+
// the outbound `traceparent` references the SDK span's id —
|
|
1279
|
+
// letting QAR parent its server-side `qar.protocol.<kind>` span
|
|
1280
|
+
// under ours. Without `withContext`, the active span at send time
|
|
1281
|
+
// would still be the consumer's span (or none), and the SDK span
|
|
1282
|
+
// would be a leaf — breaking the cross-process trace tree.
|
|
1283
|
+
//
|
|
1284
|
+
// The `span` may be undefined when the call site doesn't open one
|
|
1285
|
+
// (e.g. `tasks.cancel` opens its own span out-of-band). Fall
|
|
1286
|
+
// through to the unwrapped path in that case.
|
|
1287
|
+
//
|
|
1288
|
+
// Forward the caller-supplied cold-address `sessionId` (if any)
|
|
1289
|
+
// on the three ongoing kinds. Omitted on `task.start` — that path
|
|
1290
|
+
// doesn't carry a wire `session_id` (admission CREATES the
|
|
1291
|
+
// session). `sendEnvelope`'s `resolveOutboundSessionId` honours
|
|
1292
|
+
// the override first, then falls back to the in-memory map, then
|
|
1293
|
+
// throws `QodoColdAddressError` — so consumers carrying their own
|
|
1294
|
+
// `{ taskId, sessionId }` pair survive the cold-address path
|
|
1295
|
+
// without a wire round-trip.
|
|
1296
|
+
const sessionOverride = opts?.sessionId;
|
|
1297
|
+
const send = () => {
|
|
1298
|
+
switch (outbound.kind) {
|
|
1299
|
+
case 'task.start':
|
|
1300
|
+
connection.sendEnvelope({ kind: 'task.start', payload: outbound.payload }, { messageId: rootMessageId });
|
|
1301
|
+
break;
|
|
1302
|
+
case 'task.continue':
|
|
1303
|
+
connection.sendEnvelope({ kind: 'task.continue', payload: outbound.payload }, sessionOverride !== undefined
|
|
1304
|
+
? { messageId: rootMessageId, sessionId: sessionOverride }
|
|
1305
|
+
: { messageId: rootMessageId });
|
|
1306
|
+
break;
|
|
1307
|
+
case 'task.cancel':
|
|
1308
|
+
connection.sendEnvelope({ kind: 'task.cancel', payload: outbound.payload }, sessionOverride !== undefined
|
|
1309
|
+
? { messageId: rootMessageId, sessionId: sessionOverride }
|
|
1310
|
+
: { messageId: rootMessageId });
|
|
1311
|
+
break;
|
|
1312
|
+
case 'task.resubscribe':
|
|
1313
|
+
connection.sendEnvelope({ kind: 'task.resubscribe', payload: outbound.payload }, sessionOverride !== undefined
|
|
1314
|
+
? { messageId: rootMessageId, sessionId: sessionOverride }
|
|
1315
|
+
: { messageId: rootMessageId });
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
if (span !== undefined) {
|
|
1320
|
+
span.withContext(send);
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
send();
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
sub.fail(err instanceof Error ? err : new Error(String(err)));
|
|
1328
|
+
throw err;
|
|
1329
|
+
}
|
|
1330
|
+
if (opts?.signal !== undefined) {
|
|
1331
|
+
onAbortCleanup = this.bindAbort(connection, sub, opts.signal, opts.sessionId);
|
|
1332
|
+
}
|
|
1333
|
+
return sub;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Retry wrapper for `admission_in_progress` errors.
|
|
1337
|
+
*
|
|
1338
|
+
* When a deterministic-key admission collides with an in-flight admission
|
|
1339
|
+
* on the same `(tenant_id, idempotency_key)`, QAR emits
|
|
1340
|
+
* `error { code: 'admission_in_progress', retry_after_ms? }` instead of
|
|
1341
|
+
* starting work. The SDK MUST retry with the same payload (server
|
|
1342
|
+
* derives the same `session_id`) after the hinted delay — or, if absent,
|
|
1343
|
+
* after exponential backoff with jitter — capped at
|
|
1344
|
+
* `PENDING_ADMISSION_TIMEOUT` (5 min). The wrapper only fires on the
|
|
1345
|
+
* deterministic-key path; the DX-default omitted-key path never sees
|
|
1346
|
+
* `admission_in_progress` (each call mints a fresh `uuidv7` server-side
|
|
1347
|
+
* — no collisions possible).
|
|
1348
|
+
*
|
|
1349
|
+
* Returns an `AsyncIterable<TaskEvent>` that wraps the underlying
|
|
1350
|
+
* subscription. The wrapper:
|
|
1351
|
+
*
|
|
1352
|
+
* 1. Calls `buildAttempt(rootMessageId)` to register a fresh
|
|
1353
|
+
* `TaskSubscription` and emit the wire `task.start`.
|
|
1354
|
+
* 2. Reads the first event.
|
|
1355
|
+
* 3. If `error { code: 'admission_in_progress' }`: capture the
|
|
1356
|
+
* scoped `session_id` and `retry_after_ms` hint, sleep, retry
|
|
1357
|
+
* with a fresh `rootMessageId`. Lifecycle bookkeeping
|
|
1358
|
+
* (subscription close, span end) flows through the underlying
|
|
1359
|
+
* subscription's terminal handling on the error envelope.
|
|
1360
|
+
* 4. If `admission_stalled` typed throw arrives via the
|
|
1361
|
+
* wrap-server-errors layer: propagate (terminal, non-retryable).
|
|
1362
|
+
* 5. Otherwise: yield the first event and pass through every
|
|
1363
|
+
* subsequent event.
|
|
1364
|
+
*
|
|
1365
|
+
* After {@link PENDING_ADMISSION_TIMEOUT_MS} elapsed wall-clock time
|
|
1366
|
+
* the wrapper throws {@link QodoAdmissionTimeoutError} carrying the
|
|
1367
|
+
* scoped `session_id` so the caller can `task.resubscribe` if a
|
|
1368
|
+
* recovery path exists.
|
|
1369
|
+
*/
|
|
1370
|
+
static wrapWithAdmissionRetry(
|
|
1371
|
+
/**
|
|
1372
|
+
* Pre-dispatched first attempt — already subscribed and the wire
|
|
1373
|
+
* `task.start` is in flight before the wrapper is constructed.
|
|
1374
|
+
* Eager dispatch preserves the SDK contract that
|
|
1375
|
+
* `client.tasks.cancel(stream.taskId)` can be called immediately
|
|
1376
|
+
* after `client.tasks.start(...)` returns without iterating the
|
|
1377
|
+
* stream first.
|
|
1378
|
+
*/
|
|
1379
|
+
firstSub, buildAttempt, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred) {
|
|
1380
|
+
// The retry loop runs in a **background driver** independent of
|
|
1381
|
+
// consumer iteration. Advancing retries inside `next()` would mean a
|
|
1382
|
+
// caller who awaited `stream.sessionId` BEFORE iterating could hang
|
|
1383
|
+
// forever (the retry would never sleep, never re-dispatch, never
|
|
1384
|
+
// observe `task.started`). Instead the driver:
|
|
1385
|
+
//
|
|
1386
|
+
// 1. Pulls from the current attempt's iterator eagerly.
|
|
1387
|
+
// 2. Detects `admission_in_progress`, sleeps, re-dispatches.
|
|
1388
|
+
// 3. Pushes every other event to a consumer-facing `AsyncQueue`.
|
|
1389
|
+
// 4. Settles `sessionIdDeferred` + `admittedTaskIdDeferred` from
|
|
1390
|
+
// the inbound `task.started` (resolved by the underlying
|
|
1391
|
+
// `TaskSubscription`'s resolver path) OR rejects both on
|
|
1392
|
+
// timeout / typed terminal so the caller's
|
|
1393
|
+
// `await stream.sessionId` always settles.
|
|
1394
|
+
//
|
|
1395
|
+
// The returned `AsyncIterable` is a thin adapter over the queue —
|
|
1396
|
+
// breaking it early closes the queue, which the driver respects
|
|
1397
|
+
// on its next loop iteration.
|
|
1398
|
+
const consumerQueue = new AsyncQueue();
|
|
1399
|
+
let driverAborted = false;
|
|
1400
|
+
// Hoist the current attempt's subscription reference to the wrapper
|
|
1401
|
+
// scope so the consumer-facing iterator's `return()` / `throw()` can
|
|
1402
|
+
// close the underlying subscription on abort. Without this, the
|
|
1403
|
+
// driver may be blocked inside `await iter.next()` on the AsyncQueue
|
|
1404
|
+
// and never observe `driverAborted` if no envelopes arrive — the
|
|
1405
|
+
// subscription leaks and best-effort cancel never fires.
|
|
1406
|
+
let currentSub = firstSub;
|
|
1407
|
+
const abortFromConsumer = () => {
|
|
1408
|
+
driverAborted = true;
|
|
1409
|
+
// Reject the admission deferreds: without this, `await
|
|
1410
|
+
// stream.sessionId` after an early-break by the consumer would
|
|
1411
|
+
// hang forever. Use a typed `QodoStreamAbortedError` so callers
|
|
1412
|
+
// can branch on voluntary-abort vs. genuine admission failure.
|
|
1413
|
+
const abortErr = new QodoStreamAbortedError();
|
|
1414
|
+
if (!sessionIdDeferred.settled())
|
|
1415
|
+
sessionIdDeferred.reject(abortErr);
|
|
1416
|
+
if (!admittedTaskIdDeferred.settled())
|
|
1417
|
+
admittedTaskIdDeferred.reject(abortErr);
|
|
1418
|
+
// Close the current attempt's subscription. `return()` (vs
|
|
1419
|
+
// `close()`) is the right call: it triggers
|
|
1420
|
+
// `onEarlyReturn` → best-effort `task.cancel` to QAR if we know
|
|
1421
|
+
// the task_id, then closes the queue and unregisters. Closing
|
|
1422
|
+
// the queue is what unblocks the driver's `await iter.next()`
|
|
1423
|
+
// → the driver sees `r.done`, checks `driverAborted`, exits.
|
|
1424
|
+
try {
|
|
1425
|
+
void currentSub.return();
|
|
1426
|
+
}
|
|
1427
|
+
catch {
|
|
1428
|
+
/* best-effort — subscription may already be closed via
|
|
1429
|
+
* terminal envelope or transport drop */
|
|
1430
|
+
}
|
|
1431
|
+
consumerQueue.close();
|
|
1432
|
+
};
|
|
1433
|
+
const runDriver = async () => {
|
|
1434
|
+
const startedAt = Date.now();
|
|
1435
|
+
let attempt = 0;
|
|
1436
|
+
let lastSessionId;
|
|
1437
|
+
while (true) {
|
|
1438
|
+
if (driverAborted) {
|
|
1439
|
+
consumerQueue.close();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
let admissionInProgress;
|
|
1443
|
+
try {
|
|
1444
|
+
const wrapped = TaskClient.wrapServerErrorsIterator(currentSub);
|
|
1445
|
+
const iter = wrapped[Symbol.asyncIterator]();
|
|
1446
|
+
while (true) {
|
|
1447
|
+
const r = await iter.next();
|
|
1448
|
+
if (driverAborted) {
|
|
1449
|
+
try {
|
|
1450
|
+
await iter.return?.(undefined);
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
/* best-effort */
|
|
1454
|
+
}
|
|
1455
|
+
consumerQueue.close();
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (r.done)
|
|
1459
|
+
break;
|
|
1460
|
+
const ev = r.value;
|
|
1461
|
+
if (ev.kind === 'error') {
|
|
1462
|
+
const code = ev.payload.code;
|
|
1463
|
+
if (typeof code === 'string' && code === 'admission_in_progress') {
|
|
1464
|
+
admissionInProgress = ev;
|
|
1465
|
+
// Drain the current attempt's iter — its queue was
|
|
1466
|
+
// closed by the error envelope already; return() is a
|
|
1467
|
+
// no-op safety net (idempotent per AsyncQueue contract).
|
|
1468
|
+
try {
|
|
1469
|
+
await iter.return?.(undefined);
|
|
1470
|
+
}
|
|
1471
|
+
catch {
|
|
1472
|
+
/* best-effort */
|
|
1473
|
+
}
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
// Forward non-admission events to the consumer queue.
|
|
1478
|
+
consumerQueue.push(ev);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
catch (err) {
|
|
1482
|
+
// The wrap layer threw a typed `QodoServerError` (e.g.
|
|
1483
|
+
// `QodoAdmissionStalledError`) — propagate to the consumer
|
|
1484
|
+
// queue and settle deferreds. Same path covers transport
|
|
1485
|
+
// failures that surfaced as iterator throws.
|
|
1486
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
1487
|
+
if (!sessionIdDeferred.settled())
|
|
1488
|
+
sessionIdDeferred.reject(wrapped);
|
|
1489
|
+
if (!admittedTaskIdDeferred.settled())
|
|
1490
|
+
admittedTaskIdDeferred.reject(wrapped);
|
|
1491
|
+
if (!admissionResultDeferred.settled())
|
|
1492
|
+
admissionResultDeferred.reject(wrapped);
|
|
1493
|
+
consumerQueue.fail(wrapped);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (admissionInProgress === undefined) {
|
|
1497
|
+
// Clean terminal (task.done) or non-admission error fell
|
|
1498
|
+
// through. Close the consumer queue.
|
|
1499
|
+
consumerQueue.close();
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
// Retry path
|
|
1503
|
+
const sid = admissionInProgress.session_id;
|
|
1504
|
+
if (typeof sid === 'string' && !isZeroUuid(sid))
|
|
1505
|
+
lastSessionId = sid;
|
|
1506
|
+
const hint = parseRetryAfterMsHint(admissionInProgress);
|
|
1507
|
+
const wait = hint ?? computeAdmissionRetryBackoffMs(attempt);
|
|
1508
|
+
const elapsed = Date.now() - startedAt;
|
|
1509
|
+
if (elapsed + wait > PENDING_ADMISSION_TIMEOUT_MS) {
|
|
1510
|
+
const timeoutErr = new QodoAdmissionTimeoutError(elapsed, attempt + 1, lastSessionId);
|
|
1511
|
+
if (!sessionIdDeferred.settled())
|
|
1512
|
+
sessionIdDeferred.reject(timeoutErr);
|
|
1513
|
+
if (!admittedTaskIdDeferred.settled())
|
|
1514
|
+
admittedTaskIdDeferred.reject(timeoutErr);
|
|
1515
|
+
if (!admissionResultDeferred.settled())
|
|
1516
|
+
admissionResultDeferred.reject(timeoutErr);
|
|
1517
|
+
consumerQueue.fail(timeoutErr);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
await sleepMs(wait);
|
|
1521
|
+
if (driverAborted) {
|
|
1522
|
+
consumerQueue.close();
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
attempt += 1;
|
|
1526
|
+
currentSub = buildAttempt(asMessageId(uuidv7()));
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
// Kick off the driver eagerly. Unhandled-rejection guard so a
|
|
1530
|
+
// driver-internal bug doesn't crash Node before the consumer
|
|
1531
|
+
// sees the queue's failure.
|
|
1532
|
+
void runDriver().catch((err) => {
|
|
1533
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
1534
|
+
if (!sessionIdDeferred.settled())
|
|
1535
|
+
sessionIdDeferred.reject(wrapped);
|
|
1536
|
+
if (!admittedTaskIdDeferred.settled())
|
|
1537
|
+
admittedTaskIdDeferred.reject(wrapped);
|
|
1538
|
+
if (!admissionResultDeferred.settled())
|
|
1539
|
+
admissionResultDeferred.reject(wrapped);
|
|
1540
|
+
if (!consumerQueue.isClosed)
|
|
1541
|
+
consumerQueue.fail(wrapped);
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
[Symbol.asyncIterator]() {
|
|
1545
|
+
return {
|
|
1546
|
+
next() {
|
|
1547
|
+
return consumerQueue.next();
|
|
1548
|
+
},
|
|
1549
|
+
async return(value) {
|
|
1550
|
+
abortFromConsumer();
|
|
1551
|
+
return { value: value, done: true };
|
|
1552
|
+
},
|
|
1553
|
+
async throw(err) {
|
|
1554
|
+
abortFromConsumer();
|
|
1555
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
1556
|
+
},
|
|
1557
|
+
};
|
|
1558
|
+
},
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Wire an `AbortSignal` to the subscription. On abort, send `task.cancel`
|
|
1563
|
+
* (if we know the task id) and end the iterator. Returns a cleanup
|
|
1564
|
+
* function the subscription's `onClose` invokes so the listener doesn't
|
|
1565
|
+
* outlive the task (matters when a single AbortController is reused
|
|
1566
|
+
* across many short-lived tasks).
|
|
1567
|
+
*/
|
|
1568
|
+
bindAbort(connection, sub, signal, sessionOverride) {
|
|
1569
|
+
const onAbort = () => {
|
|
1570
|
+
const taskId = sub.currentTaskId;
|
|
1571
|
+
if (taskId !== undefined && connection.isOpen) {
|
|
1572
|
+
try {
|
|
1573
|
+
// Forward the cold-address `sessionId` override. A cold
|
|
1574
|
+
// `task.continue` queued under `flow.pause` hasn't committed
|
|
1575
|
+
// its override to `taskSessions` yet when the abort fires;
|
|
1576
|
+
// the cancel needs the explicit value to resolve.
|
|
1577
|
+
connection.sendEnvelope({
|
|
1578
|
+
kind: 'task.cancel',
|
|
1579
|
+
payload: { task_id: taskId, reason: 'abort signal' },
|
|
1580
|
+
}, sessionOverride !== undefined ? { sessionId: sessionOverride } : undefined);
|
|
1581
|
+
}
|
|
1582
|
+
catch {
|
|
1583
|
+
// best-effort
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
// `sub.close()` (NOT `sub.return()`) — `return()` invokes
|
|
1587
|
+
// `onEarlyReturn` which would emit a second `task.cancel`. The
|
|
1588
|
+
// abort path already sent the cancel above; closing the queue
|
|
1589
|
+
// ends the iterator without re-firing the early-return handler.
|
|
1590
|
+
sub.close();
|
|
1591
|
+
};
|
|
1592
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1593
|
+
return () => signal.removeEventListener('abort', onAbort);
|
|
1594
|
+
}
|
|
1595
|
+
// -------------------------------------------------------------------
|
|
1596
|
+
// Skills foundation glue
|
|
1597
|
+
// -------------------------------------------------------------------
|
|
1598
|
+
/**
|
|
1599
|
+
* Inject the rendered active-skills block + slim index into an
|
|
1600
|
+
* `InlineAgentSpec` synchronously. Validates caller-pinned skills
|
|
1601
|
+
* against the cached snapshot first; unresolved names throw
|
|
1602
|
+
* `SkillNotFoundError`. Returns the original spec when the catalog
|
|
1603
|
+
* is empty or when no `SkillsManager` is configured.
|
|
1604
|
+
*
|
|
1605
|
+
* When the caller pinned any skills, the SDK resolves the `requires:`
|
|
1606
|
+
* chain (with the depth/breadth/token caps applied), renders the
|
|
1607
|
+
* bodies into an `<active_skills>` block, and prepends that block to
|
|
1608
|
+
* the spec's `instructions` *before* the slim-index block. Trust
|
|
1609
|
+
* violations, cap breaches, cycles, or unresolved deps all throw
|
|
1610
|
+
* `QodoSkillError` so no partial body lands on the wire.
|
|
1611
|
+
*/
|
|
1612
|
+
applyAgentSkillsInjection(spec, pinned, currentFile) {
|
|
1613
|
+
if (this.skills === undefined)
|
|
1614
|
+
return spec;
|
|
1615
|
+
const effectivePinned = this.resolveEffectivePinned(pinned);
|
|
1616
|
+
if (effectivePinned !== undefined && effectivePinned.length > 0) {
|
|
1617
|
+
this.validatePinnedSync(effectivePinned);
|
|
1618
|
+
}
|
|
1619
|
+
const activation = this.composeActiveSkills(effectivePinned);
|
|
1620
|
+
const renderOpts = this.buildRenderOptions(effectivePinned, currentFile);
|
|
1621
|
+
const injection = injectIntoAgentSpec(spec, this.skills, renderOpts);
|
|
1622
|
+
this.dispatchSkillsEvents(injection.events);
|
|
1623
|
+
this.throwIfPinsOmitted(injection.omittedPinned, renderOpts);
|
|
1624
|
+
this.dispatchSkillsEvents(activation.events);
|
|
1625
|
+
const finalSpec = activation.text.length > 0
|
|
1626
|
+
? { ...injection.spec, instructions: prependInstructions(injection.spec.instructions, activation.text) }
|
|
1627
|
+
: injection.spec;
|
|
1628
|
+
return finalSpec;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Inject the active-skills block + slim index into every inline-agent
|
|
1632
|
+
* node of an `InlineGraphSpec`. Mirrors `applyAgentSkillsInjection`
|
|
1633
|
+
* for the graph case.
|
|
1634
|
+
*/
|
|
1635
|
+
applyGraphSkillsInjection(graph, pinned, currentFile) {
|
|
1636
|
+
if (this.skills === undefined)
|
|
1637
|
+
return graph;
|
|
1638
|
+
const effectivePinned = this.resolveEffectivePinned(pinned);
|
|
1639
|
+
if (effectivePinned !== undefined && effectivePinned.length > 0) {
|
|
1640
|
+
this.validatePinnedSync(effectivePinned);
|
|
1641
|
+
}
|
|
1642
|
+
const activation = this.composeActiveSkills(effectivePinned);
|
|
1643
|
+
const renderOpts = this.buildRenderOptions(effectivePinned, currentFile);
|
|
1644
|
+
const injection = injectIntoGraphSpec(graph, this.skills, renderOpts);
|
|
1645
|
+
this.dispatchSkillsEvents(injection.events);
|
|
1646
|
+
this.throwIfPinsOmitted(injection.omittedPinned, renderOpts);
|
|
1647
|
+
this.dispatchSkillsEvents(activation.events);
|
|
1648
|
+
if (activation.text.length === 0)
|
|
1649
|
+
return injection.spec;
|
|
1650
|
+
return {
|
|
1651
|
+
...injection.spec,
|
|
1652
|
+
agents: prependActiveSkillsToGraphAgents(injection.spec.agents, activation.text),
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* MCP projection at the wire boundary. When the consumer set
|
|
1657
|
+
* `mcpTools` (or `mcpToolOverrides`), this:
|
|
1658
|
+
*
|
|
1659
|
+
* 1. Awaits the MCP pool's catalog-settle window so cold-start
|
|
1660
|
+
* `tools/list` round-trips can complete before projection.
|
|
1661
|
+
* 2. Runs `projectMcpTools` against the live pool snapshot.
|
|
1662
|
+
* 3. Partitions the diagnostics:
|
|
1663
|
+
* - `unknownMcps` and `unknownMcpTools` always reject (consumer
|
|
1664
|
+
* code drift; pre-deploy-detectable).
|
|
1665
|
+
* - `unreachableMcps` is policed per-MCP via the registry's
|
|
1666
|
+
* `unavailability` setting. `'fail'` raises
|
|
1667
|
+
* `QodoMcpUnavailableError`; `'warn'` omits the MCP's tools
|
|
1668
|
+
* and emits a console warning.
|
|
1669
|
+
* 4. Returns a fresh `InlineAgentSpec` with `tools[]` replaced by
|
|
1670
|
+
* the projected surface and `mcpTools` / `mcpToolOverrides`
|
|
1671
|
+
* stripped (SDK-only fields — QAR rejects them at the wire
|
|
1672
|
+
* validator via `D10-R5` if they slipped through).
|
|
1673
|
+
*
|
|
1674
|
+
* When the spec carries neither `mcpTools` nor `mcpToolOverrides`, the
|
|
1675
|
+
* helper returns the input verbatim — projection is opt-in, the v1
|
|
1676
|
+
* `tools: FunctionToolDef[]` ergonomic continues to work.
|
|
1677
|
+
*/
|
|
1678
|
+
async projectMcpToolsForWire(spec) {
|
|
1679
|
+
const hasSelector = spec.mcpTools !== undefined;
|
|
1680
|
+
const hasOverrides = spec.mcpToolOverrides !== undefined;
|
|
1681
|
+
// Auto-include the `qodo-skills.*` tool surface whenever the
|
|
1682
|
+
// consumer opted into skills. The slim-index preamble (rendered into
|
|
1683
|
+
// `instructions` by the skills injector) tells the LLM "Use the
|
|
1684
|
+
// qodo-skills.get_skill tool to ..."; that instruction is only
|
|
1685
|
+
// actionable when the three tools actually land on the wire's
|
|
1686
|
+
// `function_tools`. Projection is the SDK's single surface that
|
|
1687
|
+
// decides what tools the model can see, so it owns the include.
|
|
1688
|
+
//
|
|
1689
|
+
// No skills + no selector + no overrides → projection is a no-op;
|
|
1690
|
+
// return verbatim to preserve the v1 ergonomic where
|
|
1691
|
+
// `tools: FunctionToolDef[]` reaches the wire untouched.
|
|
1692
|
+
const skillsConfigured = this.skills !== undefined;
|
|
1693
|
+
if (!hasSelector && !hasOverrides && !skillsConfigured)
|
|
1694
|
+
return spec;
|
|
1695
|
+
// Build prefix policy + unavailability policy from the registry
|
|
1696
|
+
// snapshot. The registry's `getMcpPolicy` returns defaults
|
|
1697
|
+
// (`prefixToolNames: true`, `unavailability: 'fail'`) for any
|
|
1698
|
+
// unregistered name, so passing the full registered set here is
|
|
1699
|
+
// sufficient; the projection function ignores entries not in the
|
|
1700
|
+
// pool snapshot.
|
|
1701
|
+
const prefixPolicy = new Map();
|
|
1702
|
+
const unavailabilityByName = new Map();
|
|
1703
|
+
for (const { name } of this.registry.listMcps()) {
|
|
1704
|
+
const policy = this.registry.getMcpPolicy(name);
|
|
1705
|
+
prefixPolicy.set(name, policy.prefixToolNames);
|
|
1706
|
+
unavailabilityByName.set(name, policy.unavailability);
|
|
1707
|
+
}
|
|
1708
|
+
// Acquire the live pool — fall back to a fresh empty pool when no
|
|
1709
|
+
// remote/stdio MCPs are registered so the projection function
|
|
1710
|
+
// doesn't need a null-pool branch. An empty pool yields
|
|
1711
|
+
// empty registered-names + empty catalog snapshot; the selector's
|
|
1712
|
+
// unknown-MCP diagnostics then flag every referenced name.
|
|
1713
|
+
const livePool = this.registry._getMcpPool();
|
|
1714
|
+
const pool = livePool ?? new McpClientPool();
|
|
1715
|
+
if (livePool !== null) {
|
|
1716
|
+
// Bounded wait — `waitForCatalogsToSettle` defaults to 250ms.
|
|
1717
|
+
// Catalogs already populated resolve immediately. Cold-start
|
|
1718
|
+
// failures (Promise rejection) are swallowed by the settle
|
|
1719
|
+
// helper; they surface here as `unreachableMcps`.
|
|
1720
|
+
await livePool.waitForCatalogsToSettle();
|
|
1721
|
+
}
|
|
1722
|
+
const projectionInput = {
|
|
1723
|
+
mcpTools: spec.mcpTools,
|
|
1724
|
+
consumerTools: spec.tools ?? undefined,
|
|
1725
|
+
mcpToolOverrides: spec
|
|
1726
|
+
.mcpToolOverrides,
|
|
1727
|
+
};
|
|
1728
|
+
// SDK-transport ('sdk') MCPs aren't in the pool — their dispatch
|
|
1729
|
+
// goes through the in-process handler bridge. Pass their names as
|
|
1730
|
+
// "extra known" so referencing them in `mcpTools` doesn't trip
|
|
1731
|
+
// `QodoUnknownMcpError`. Projection still won't auto-project tools
|
|
1732
|
+
// for them (no tools/list contract); consumer ships those via
|
|
1733
|
+
// `tools[]`.
|
|
1734
|
+
const sdkTransportNames = new Set(this.registry._listSdkTransportMcpNames());
|
|
1735
|
+
const result = projectMcpTools(projectionInput, pool, prefixPolicy, sdkTransportNames);
|
|
1736
|
+
// Partition diagnostics. Unknown MCPs / tools are unconditional
|
|
1737
|
+
// throws — they indicate consumer code drift, not transient flake.
|
|
1738
|
+
if (result.diagnostics.unknownMcps.length > 0) {
|
|
1739
|
+
throw new QodoUnknownMcpError(result.diagnostics.unknownMcps);
|
|
1740
|
+
}
|
|
1741
|
+
if (result.diagnostics.unknownMcpTools.length > 0) {
|
|
1742
|
+
throw new QodoUnknownMcpToolError(result.diagnostics.unknownMcpTools);
|
|
1743
|
+
}
|
|
1744
|
+
// Per-MCP unavailability policy. `'fail'` names collect into the
|
|
1745
|
+
// typed error payload; `'warn'` names emit a structured console
|
|
1746
|
+
// warning so observability consumers see the degradation without
|
|
1747
|
+
// the task failing.
|
|
1748
|
+
const failNames = [];
|
|
1749
|
+
const warnNames = [];
|
|
1750
|
+
for (const name of result.diagnostics.unreachableMcps) {
|
|
1751
|
+
const policy = unavailabilityByName.get(name) ?? 'fail';
|
|
1752
|
+
if (policy === 'fail')
|
|
1753
|
+
failNames.push(name);
|
|
1754
|
+
else
|
|
1755
|
+
warnNames.push(name);
|
|
1756
|
+
}
|
|
1757
|
+
if (warnNames.length > 0) {
|
|
1758
|
+
// Structured-payload warning. Stable JSON shape so
|
|
1759
|
+
// `gtrace`-style log aggregators parse cleanly; one
|
|
1760
|
+
// line per unreachable MCP so each event is independently
|
|
1761
|
+
// grep-able. `event` is the stable discriminator;
|
|
1762
|
+
// `reason` distinguishes cold-start (only case in v1) from a
|
|
1763
|
+
// future post-cache-disconnect surface.
|
|
1764
|
+
//
|
|
1765
|
+
// The typed-event-handler surface
|
|
1766
|
+
// (`QodoClientOptions.onMcpUnavailable`) is a follow-up — so
|
|
1767
|
+
// consumers wanting first-class observability can grep this
|
|
1768
|
+
// payload until that lands.
|
|
1769
|
+
const ts = new Date().toISOString();
|
|
1770
|
+
for (const name of warnNames) {
|
|
1771
|
+
// eslint-disable-next-line no-console
|
|
1772
|
+
console.warn(JSON.stringify({
|
|
1773
|
+
event: 'qodo.sdk.mcp.unavailable',
|
|
1774
|
+
mcpName: name,
|
|
1775
|
+
ts,
|
|
1776
|
+
reason: 'cold_start_unreachable',
|
|
1777
|
+
hint: "Check MCP connectivity; tools omitted from this task's projection.",
|
|
1778
|
+
}));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
if (failNames.length > 0) {
|
|
1782
|
+
throw new QodoMcpUnavailableError(failNames);
|
|
1783
|
+
}
|
|
1784
|
+
// Auto-include the qodo-skills.* tool surface when skills are
|
|
1785
|
+
// configured. The 3 entries are appended AFTER projection so a
|
|
1786
|
+
// consumer-shipped tool with a colliding name (rare — consumers
|
|
1787
|
+
// shouldn't author tools named `qodo-skills.*`) still wins via the
|
|
1788
|
+
// dedup-by-name pass below, mirroring the projection's
|
|
1789
|
+
// "consumer-shipped wins on collision" invariant.
|
|
1790
|
+
//
|
|
1791
|
+
// Whether the consumer also referenced `qodo-skills` in their
|
|
1792
|
+
// `mcpTools.allowlist` doesn't matter — the projection step above
|
|
1793
|
+
// can't synthesize tools for an sdk-transport MCP (no `tools/list`
|
|
1794
|
+
// catalog), so we'd land here with the 3 entries missing regardless.
|
|
1795
|
+
// De-dup-by-name guards against the unlikely future where projection
|
|
1796
|
+
// grows an sdk-transport bridge that DOES emit the qodo-skills
|
|
1797
|
+
// catalog; we'd still ship exactly three entries.
|
|
1798
|
+
const projectedTools = skillsConfigured
|
|
1799
|
+
? mergeQodoSkillsTools(result.tools)
|
|
1800
|
+
: result.tools;
|
|
1801
|
+
// Strip the SDK-only fields and replace `tools[]` with the projected
|
|
1802
|
+
// surface. Object spread + delete keeps the original spec immutable
|
|
1803
|
+
// (consumers may hold references for retry / display).
|
|
1804
|
+
const stripped = stripMcpToolsFromSpec(spec);
|
|
1805
|
+
return { ...stripped, tools: projectedTools };
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* MCP projection over an `InlineGraphSpec`. Walks every node whose
|
|
1809
|
+
* `spec` is an inline `InlineAgentSpec` and applies
|
|
1810
|
+
* {@link projectMcpToolsForWire} to it; static refs and nested
|
|
1811
|
+
* subgraphs are recursed into. Returns a fresh `InlineGraphSpec` with
|
|
1812
|
+
* the same shape but every per-node `tools[]` replaced by the
|
|
1813
|
+
* projected surface. Diagnostic errors propagate from the first
|
|
1814
|
+
* node that fails — earlier nodes have already projected
|
|
1815
|
+
* successfully, but the wire write hasn't started so the early
|
|
1816
|
+
* partial work is invisible.
|
|
1817
|
+
*/
|
|
1818
|
+
async projectMcpToolsForGraphWire(graph) {
|
|
1819
|
+
const agentEntries = Object.entries(graph.agents);
|
|
1820
|
+
const newAgents = {};
|
|
1821
|
+
let touched = false;
|
|
1822
|
+
for (const [name, agentNode] of agentEntries) {
|
|
1823
|
+
const node = agentNode;
|
|
1824
|
+
const inner = node.spec;
|
|
1825
|
+
if (isInlineAgentNode(inner)) {
|
|
1826
|
+
const projected = await this.projectMcpToolsForWire(inner);
|
|
1827
|
+
if (projected !== inner) {
|
|
1828
|
+
newAgents[name] = { ...node, spec: projected };
|
|
1829
|
+
touched = true;
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
newAgents[name] = node;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
else if (isInlineGraphNode(inner)) {
|
|
1836
|
+
const projectedGraph = await this.projectMcpToolsForGraphWire(inner);
|
|
1837
|
+
if (projectedGraph !== inner) {
|
|
1838
|
+
newAgents[name] = { ...node, spec: projectedGraph };
|
|
1839
|
+
touched = true;
|
|
1840
|
+
}
|
|
1841
|
+
else {
|
|
1842
|
+
newAgents[name] = node;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
newAgents[name] = node;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (!touched)
|
|
1850
|
+
return graph;
|
|
1851
|
+
return { ...graph, agents: newAgents };
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Resolve the caller-pinned roots into a rendered active-skills block
|
|
1855
|
+
* + telemetry events. Pure — relies on the already-loaded snapshot
|
|
1856
|
+
* (callers await `manager.discover()` before getting here). Returns
|
|
1857
|
+
* an empty result when no pins are effective.
|
|
1858
|
+
*/
|
|
1859
|
+
composeActiveSkills(effectivePinned) {
|
|
1860
|
+
if (this.skills === undefined)
|
|
1861
|
+
return { text: '', skills: [], events: [] };
|
|
1862
|
+
if (effectivePinned === undefined || effectivePinned.length === 0) {
|
|
1863
|
+
return { text: '', skills: [], events: [] };
|
|
1864
|
+
}
|
|
1865
|
+
const snapshot = this.skills.currentSnapshot;
|
|
1866
|
+
if (snapshot === null) {
|
|
1867
|
+
// Defensive — the public entry points await discover() before this
|
|
1868
|
+
// is invoked. Falling through with an empty block keeps the wire
|
|
1869
|
+
// payload coherent if a future refactor changes the call order.
|
|
1870
|
+
return { text: '', skills: [], events: [] };
|
|
1871
|
+
}
|
|
1872
|
+
const lookup = lookupFromSnapshot(snapshot.skills, this.skills);
|
|
1873
|
+
const roots = [];
|
|
1874
|
+
const seen = new Set();
|
|
1875
|
+
for (const spec of effectivePinned) {
|
|
1876
|
+
const skill = lookup.get(spec);
|
|
1877
|
+
if (skill === null) {
|
|
1878
|
+
// validatePinnedSync would normally catch this; throw the same
|
|
1879
|
+
// typed error in case it slipped through.
|
|
1880
|
+
throw new SkillNotFoundError(spec);
|
|
1881
|
+
}
|
|
1882
|
+
if (seen.has(skill.fqn))
|
|
1883
|
+
continue;
|
|
1884
|
+
seen.add(skill.fqn);
|
|
1885
|
+
roots.push(skill);
|
|
1886
|
+
}
|
|
1887
|
+
const resolution = resolveAllActivations(roots, lookup);
|
|
1888
|
+
if (resolution.outcome === 'failed') {
|
|
1889
|
+
this.dispatchSkillsEvents(resolution.events);
|
|
1890
|
+
const failureReason = mapFailureReason(resolution.failure.result.outcome);
|
|
1891
|
+
throw new QodoSkillError(resolution.failure.root.fqn, failureReason, activationFailureMessage(resolution.failure.root, resolution.failure.result));
|
|
1892
|
+
}
|
|
1893
|
+
const rendered = renderActiveSkillsBlock(resolution.ordered, this.skills);
|
|
1894
|
+
// Deliberately do NOT call `manager.markActive(skill.fqn)` here.
|
|
1895
|
+
//
|
|
1896
|
+
// `composeActiveSkills` runs **before** the wire write is guaranteed
|
|
1897
|
+
// to succeed. The preflight HTTP call can still reject
|
|
1898
|
+
// (`agent_spec_rejected`), and the WebSocket `task.start` write can
|
|
1899
|
+
// still fail. Mutating the shared `activeFqns` set here would leak
|
|
1900
|
+
// past those failure modes — the next LLM-driven `get_skill` for
|
|
1901
|
+
// the same FQN would see the stale entry and suppress the
|
|
1902
|
+
// `sdk.skill.activated { source: 'llm' }` event (the MCP server's
|
|
1903
|
+
// `newlyActive` branch). Telemetry would then under-count fresh
|
|
1904
|
+
// activations, and the active set would claim skills the model has
|
|
1905
|
+
// not actually loaded.
|
|
1906
|
+
//
|
|
1907
|
+
// The caller-pin activation signal is the
|
|
1908
|
+
// `sdk.skill.activated { source: 'caller' }` event the renderer
|
|
1909
|
+
// emits — that's what consumers wire for "this skill is now part of
|
|
1910
|
+
// the conversation". `getActiveSkills()` and
|
|
1911
|
+
// `skillsMcpServer.loadedSkillFqns` reflect the strictly per-client
|
|
1912
|
+
// LLM-driven `get_skill` history, which is what the body cache
|
|
1913
|
+
// actually maps to.
|
|
1914
|
+
return rendered;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Surface a caller-pin overflow as a loud failure. The renderer drops
|
|
1918
|
+
* pinned skills that would push the block past its hard cap
|
|
1919
|
+
* (`2 × charBudget`, floored at `charBudget + 4096`). Silently
|
|
1920
|
+
* truncating the wire payload would make the model behave as if the
|
|
1921
|
+
* caller never pinned those skills — throw instead so the caller
|
|
1922
|
+
* fixes the pin list or raises the budget.
|
|
1923
|
+
*/
|
|
1924
|
+
throwIfPinsOmitted(omitted, renderOpts) {
|
|
1925
|
+
if (omitted.length === 0)
|
|
1926
|
+
return;
|
|
1927
|
+
const budget = renderOpts.charBudget ?? 8000;
|
|
1928
|
+
const hardCap = Math.max(budget * 2, budget + 4096);
|
|
1929
|
+
throw new SkillsBudgetExceededError(omitted.map((s) => s.fqn), hardCap);
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Resolve per-call pinned skills against the constructor-level
|
|
1933
|
+
* `SkillsConfig.activate` default:
|
|
1934
|
+
*
|
|
1935
|
+
* - `opts.skills === undefined` → fall back to the constructor's
|
|
1936
|
+
* `activate` (or `undefined` if none).
|
|
1937
|
+
* - `opts.skills === []` → explicit empty, no fallback.
|
|
1938
|
+
* - `opts.skills === [...]` → per-call wins outright.
|
|
1939
|
+
*/
|
|
1940
|
+
resolveEffectivePinned(perCall) {
|
|
1941
|
+
if (perCall !== undefined)
|
|
1942
|
+
return perCall;
|
|
1943
|
+
if (this.skills === undefined)
|
|
1944
|
+
return undefined;
|
|
1945
|
+
return this.skills.defaultActivate;
|
|
1946
|
+
}
|
|
1947
|
+
buildRenderOptions(pinned, currentFile) {
|
|
1948
|
+
const opts = {};
|
|
1949
|
+
if (pinned !== undefined) {
|
|
1950
|
+
opts.pinned = pinned;
|
|
1951
|
+
}
|
|
1952
|
+
if (currentFile !== undefined) {
|
|
1953
|
+
opts.currentFilePath = currentFile;
|
|
1954
|
+
}
|
|
1955
|
+
// Forward the constructor-level `indexCharBudget` so consumers who
|
|
1956
|
+
// configured a tighter budget actually see it applied.
|
|
1957
|
+
//
|
|
1958
|
+
// Validate before forwarding: `NaN`, `Infinity`, or non-positive
|
|
1959
|
+
// values would break `Math.max()` and `>` comparisons inside the
|
|
1960
|
+
// renderer's hard-cap logic (`x > NaN` is always false, so the cap
|
|
1961
|
+
// never fires). On invalid input, ignore and let the renderer fall
|
|
1962
|
+
// back to `DEFAULT_INDEX_CHAR_BUDGET`.
|
|
1963
|
+
if (this.skills !== undefined) {
|
|
1964
|
+
const budget = this.skills.indexCharBudget;
|
|
1965
|
+
if (budget !== undefined && Number.isFinite(budget) && budget > 0) {
|
|
1966
|
+
opts.charBudget = Math.floor(budget);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return opts;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Validate caller-pinned specifiers against the manager's already-loaded
|
|
1973
|
+
* snapshot. Throws `SkillNotFoundError` on a miss and
|
|
1974
|
+
* `SkillAmbiguousPinError` on a bare-name pin that matches multiple
|
|
1975
|
+
* vendors. Assumes the snapshot is present (the public entry-points
|
|
1976
|
+
* await `discover()` before calling this helper).
|
|
1977
|
+
*/
|
|
1978
|
+
validatePinnedSync(pinned) {
|
|
1979
|
+
if (this.skills === undefined)
|
|
1980
|
+
return;
|
|
1981
|
+
const snapshot = this.skills.currentSnapshot;
|
|
1982
|
+
if (snapshot === null)
|
|
1983
|
+
return;
|
|
1984
|
+
for (const spec of pinned) {
|
|
1985
|
+
const result = resolveSpecifierInList(snapshot.skills, spec);
|
|
1986
|
+
if (result.outcome === 'not_found') {
|
|
1987
|
+
throw new SkillNotFoundError(spec);
|
|
1988
|
+
}
|
|
1989
|
+
if (result.outcome === 'ambiguous') {
|
|
1990
|
+
throw new SkillAmbiguousPinError(spec, result.candidates);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
/** Forward renderer telemetry events to the manager's configured sink. */
|
|
1995
|
+
dispatchSkillsEvents(events) {
|
|
1996
|
+
if (this.skills === undefined || events.length === 0)
|
|
1997
|
+
return;
|
|
1998
|
+
this.skills.forwardEvents(events);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Prepend the rendered active-skills block to an `Instructions` field
|
|
2003
|
+
* (string or preset). For preset: write to `append` so the QAR-side prompt
|
|
2004
|
+
* builder concatenates after the preset body. The active block lands BEFORE
|
|
2005
|
+
* the existing append content + slim-index block, since the bodies are
|
|
2006
|
+
* authoritative.
|
|
2007
|
+
*/
|
|
2008
|
+
function prependInstructions(current, blockText) {
|
|
2009
|
+
if (typeof current === 'string') {
|
|
2010
|
+
if (current.length === 0)
|
|
2011
|
+
return blockText;
|
|
2012
|
+
return `${blockText}\n\n${current}`;
|
|
2013
|
+
}
|
|
2014
|
+
const existingAppend = current.append ?? '';
|
|
2015
|
+
const newAppend = existingAppend.length === 0
|
|
2016
|
+
? blockText
|
|
2017
|
+
: `${blockText}\n\n${existingAppend}`;
|
|
2018
|
+
return { preset: current.preset, append: newAppend };
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Walk a graph's agents map and prepend the active-skills block to every
|
|
2022
|
+
* inline `AgentSpec`. Static-ref nodes are returned untouched (no
|
|
2023
|
+
* instructions surface on the wire). Sub-graphs recurse.
|
|
2024
|
+
*/
|
|
2025
|
+
function prependActiveSkillsToGraphAgents(agents, blockText) {
|
|
2026
|
+
const out = {};
|
|
2027
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
2028
|
+
out[name] = prependToGraphAgent(agent, blockText);
|
|
2029
|
+
}
|
|
2030
|
+
return out;
|
|
2031
|
+
}
|
|
2032
|
+
function prependToGraphAgent(agent, blockText) {
|
|
2033
|
+
const inner = agent.spec;
|
|
2034
|
+
if (typeof inner !== 'object' || inner === null || !('kind' in inner)) {
|
|
2035
|
+
return agent;
|
|
2036
|
+
}
|
|
2037
|
+
if (inner.kind === 'AgentSpec') {
|
|
2038
|
+
return {
|
|
2039
|
+
...agent,
|
|
2040
|
+
spec: { ...inner, instructions: prependInstructions(inner.instructions, blockText) },
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
if (inner.kind === 'GraphSpec') {
|
|
2044
|
+
return {
|
|
2045
|
+
...agent,
|
|
2046
|
+
spec: { ...inner, agents: prependActiveSkillsToGraphAgents(inner.agents, blockText) },
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
return agent;
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Map the resolver's failure outcome onto the typed `QodoSkillError.reason`
|
|
2053
|
+
* field. The two enums diverge only in the `'ok'` variant (filtered out
|
|
2054
|
+
* before this is called).
|
|
2055
|
+
*/
|
|
2056
|
+
function mapFailureReason(outcome) {
|
|
2057
|
+
return outcome;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Synchronous specifier resolver used by `TaskClient` for caller-pinned
|
|
2061
|
+
* validation. Mirrors `SkillsManager.get` for the cached-snapshot case,
|
|
2062
|
+
* plus an ambiguity check: a bare name that matches multiple
|
|
2063
|
+
* `fqnNoVersion` values returns `ambiguous` so the caller surfaces a
|
|
2064
|
+
* loud `SkillAmbiguousPinError` rather than silently picking one.
|
|
2065
|
+
*/
|
|
2066
|
+
function resolveSpecifierInList(skills, spec) {
|
|
2067
|
+
const versionMatch = /^(.+?)@([^@]+)$/.exec(spec);
|
|
2068
|
+
if (versionMatch !== null) {
|
|
2069
|
+
const fqnNoVersion = versionMatch[1];
|
|
2070
|
+
const version = versionMatch[2];
|
|
2071
|
+
return skills.some((s) => s.fqnNoVersion === fqnNoVersion && s.version === version)
|
|
2072
|
+
? { outcome: 'found' }
|
|
2073
|
+
: { outcome: 'not_found' };
|
|
2074
|
+
}
|
|
2075
|
+
if (spec.includes('/')) {
|
|
2076
|
+
return skills.some((s) => s.fqnNoVersion === spec)
|
|
2077
|
+
? { outcome: 'found' }
|
|
2078
|
+
: { outcome: 'not_found' };
|
|
2079
|
+
}
|
|
2080
|
+
const distinctFqnNoVersion = new Set();
|
|
2081
|
+
for (const s of skills) {
|
|
2082
|
+
if (s.name === spec)
|
|
2083
|
+
distinctFqnNoVersion.add(s.fqnNoVersion);
|
|
2084
|
+
}
|
|
2085
|
+
if (distinctFqnNoVersion.size === 0)
|
|
2086
|
+
return { outcome: 'not_found' };
|
|
2087
|
+
if (distinctFqnNoVersion.size === 1)
|
|
2088
|
+
return { outcome: 'found' };
|
|
2089
|
+
return {
|
|
2090
|
+
outcome: 'ambiguous',
|
|
2091
|
+
candidates: [...distinctFqnNoVersion].sort(),
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Parse a server `error` envelope into the appropriate typed error class:
|
|
2096
|
+
*
|
|
2097
|
+
* - `agent_spec_rejected` → `QodoAgentSpecRejectedError` (carries
|
|
2098
|
+
* `issues: InlineAgentSpecIssue[]`).
|
|
2099
|
+
* - Any code recognized by `classForServerErrorCode` →
|
|
2100
|
+
* the matching `QodoServerError` subclass.
|
|
2101
|
+
* - Anything else → `QodoUnknownServerError` so consumers' `catch
|
|
2102
|
+
* (err: QodoServerError)` blocks still see structured fields.
|
|
2103
|
+
*
|
|
2104
|
+
* The wire `payload.errors` array is duck-typed (codegen still narrows
|
|
2105
|
+
* to the base `ErrorPayload` shape without the `errors` extension), so
|
|
2106
|
+
* absent/malformed entries default to an empty array and the typed
|
|
2107
|
+
* error class carries just `code` + `message`.
|
|
2108
|
+
*/
|
|
2109
|
+
function errorFromServerErrorEnvelope(envelope) {
|
|
2110
|
+
const payload = envelope.payload;
|
|
2111
|
+
if (payload.code === 'agent_spec_rejected') {
|
|
2112
|
+
return errorFromAgentSpecRejection(envelope);
|
|
2113
|
+
}
|
|
2114
|
+
const entries = [];
|
|
2115
|
+
if (Array.isArray(payload.errors)) {
|
|
2116
|
+
for (const entry of payload.errors) {
|
|
2117
|
+
if (entry === null || typeof entry !== 'object')
|
|
2118
|
+
continue;
|
|
2119
|
+
const rule_id = typeof entry.rule_id === 'string' ? entry.rule_id : payload.code;
|
|
2120
|
+
const path = typeof entry.path === 'string' ? entry.path : '';
|
|
2121
|
+
const message = typeof entry.message === 'string' ? entry.message : (payload.message ?? '');
|
|
2122
|
+
entries.push({ rule_id, path, message });
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
const offendingMessageId = typeof payload.offending_message_id === 'string' ? payload.offending_message_id : undefined;
|
|
2126
|
+
const message = payload.message ?? `server error: ${payload.code}`;
|
|
2127
|
+
const Cls = classForServerErrorCode(payload.code);
|
|
2128
|
+
if (Cls !== undefined) {
|
|
2129
|
+
const err = new Cls(message, entries, offendingMessageId);
|
|
2130
|
+
// Attach the envelope's inherited `session_id` to
|
|
2131
|
+
// `QodoAdmissionStalledError` so consumers reading the typed throw
|
|
2132
|
+
// can pull the derived session UUID without dipping into the
|
|
2133
|
+
// envelope. The ErrorEnv carries the derived `session_id` for
|
|
2134
|
+
// post-derivation failures (admission_stalled is one) — anything
|
|
2135
|
+
// but the all-zero pre-bind UUID is a valid scope.
|
|
2136
|
+
if (err instanceof QodoAdmissionStalledError) {
|
|
2137
|
+
const sid = envelope.session_id;
|
|
2138
|
+
if (typeof sid === 'string' && sid.length > 0 && !isZeroUuid(sid)) {
|
|
2139
|
+
err.attachSessionId(sid);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return err;
|
|
2143
|
+
}
|
|
2144
|
+
return new QodoUnknownServerError(payload.code, message, entries, offendingMessageId);
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Tell the pre-bind zero-UUID (`00000000-0000-0000-0000-000000000000`)
|
|
2148
|
+
* apart from a real derived session_id. The wire emits the zero-UUID on
|
|
2149
|
+
* pre-derivation errors (envelope parse, auth rejection, invalid
|
|
2150
|
+
* `idempotency_key`); post-derivation errors (`admission_stalled`,
|
|
2151
|
+
* `admission_in_progress`, etc.) carry the real derived UUID. Don't
|
|
2152
|
+
* surface the zero-UUID through typed errors — it's not a routable
|
|
2153
|
+
* address.
|
|
2154
|
+
*/
|
|
2155
|
+
function isZeroUuid(value) {
|
|
2156
|
+
return value === '00000000-0000-0000-0000-000000000000';
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Parse QAR's `agent_spec_rejected` envelope into a typed
|
|
2160
|
+
* `QodoAgentSpecRejectedError`. The envelope's `payload` carries the
|
|
2161
|
+
* structured `errors: [{ rule_id, path, message }]` array; codegen
|
|
2162
|
+
* types the payload as the base `ErrorPayload` (`{ code, message,
|
|
2163
|
+
* offending_message_id? }`) without the `errors` extension, so the
|
|
2164
|
+
* parse here is duck-typed: we read `errors` if present, otherwise
|
|
2165
|
+
* fall back to a single-entry list with the envelope's `code` as
|
|
2166
|
+
* `rule_id`.
|
|
2167
|
+
*/
|
|
2168
|
+
function errorFromAgentSpecRejection(envelope) {
|
|
2169
|
+
const payload = envelope.payload;
|
|
2170
|
+
const issues = [];
|
|
2171
|
+
if (Array.isArray(payload.errors)) {
|
|
2172
|
+
for (const entry of payload.errors) {
|
|
2173
|
+
if (entry === null || typeof entry !== 'object')
|
|
2174
|
+
continue;
|
|
2175
|
+
const rule_id = typeof entry.rule_id === 'string' ? entry.rule_id : payload.code;
|
|
2176
|
+
const path = typeof entry.path === 'string' ? entry.path : '';
|
|
2177
|
+
const message = typeof entry.message === 'string' ? entry.message : (payload.message ?? '');
|
|
2178
|
+
issues.push({ rule_id, path, message });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
if (issues.length === 0) {
|
|
2182
|
+
// Envelope didn't carry the structured `errors` array — synthesize a
|
|
2183
|
+
// single entry from the top-level `code` + `message` so the typed error
|
|
2184
|
+
// always carries at least one issue (the constructor's invariant).
|
|
2185
|
+
issues.push({
|
|
2186
|
+
rule_id: payload.code,
|
|
2187
|
+
path: '',
|
|
2188
|
+
message: payload.message ?? `Inline AgentSpec rejected with code ${payload.code}`,
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
return new QodoAgentSpecRejectedError(issues);
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Yield events from a `Promise<TaskSubscription>`. Used by the preflighted
|
|
2195
|
+
* `tasks.startWith*` paths so the preflight HTTP call + the wire
|
|
2196
|
+
* `task.start` write can be kicked off eagerly in the public method
|
|
2197
|
+
* (closing the cancel-race-during-preflight foot-gun), while the
|
|
2198
|
+
* returned iterable still lazily yields events the consumer iterates
|
|
2199
|
+
* through.
|
|
2200
|
+
*
|
|
2201
|
+
* `wrapServerErrors` flips on so a server-side `error` envelope arriving
|
|
2202
|
+
* on the subscription surfaces as a typed `QodoServerError` (or
|
|
2203
|
+
* `QodoAgentSpecRejectedError` for the spec-rejection carve-out)
|
|
2204
|
+
* instead of a yielded `kind: 'error'` event.
|
|
2205
|
+
*/
|
|
2206
|
+
async function* yieldFromPromisedSub(subPromise, wrapServerErrors) {
|
|
2207
|
+
const sub = await subPromise;
|
|
2208
|
+
for await (const event of sub) {
|
|
2209
|
+
if (wrapServerErrors && event.kind === 'error') {
|
|
2210
|
+
const code = event.payload.code;
|
|
2211
|
+
if (typeof code === 'string' && isTypedErrorCode(code)) {
|
|
2212
|
+
throw errorFromServerErrorEnvelope(event);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
yield event;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Whether the SDK has a typed `QodoServerError` subclass (or
|
|
2220
|
+
* `QodoAgentSpecRejectedError`) for this wire code. Used by the
|
|
2221
|
+
* iterator wrappers to decide whether to throw vs yield the
|
|
2222
|
+
* `kind: 'error'` event — codes without a typed class flow through
|
|
2223
|
+
* unchanged so existing consumer narrowing keeps working.
|
|
2224
|
+
*/
|
|
2225
|
+
function isTypedErrorCode(code) {
|
|
2226
|
+
if (code === 'agent_spec_rejected')
|
|
2227
|
+
return true;
|
|
2228
|
+
return classForServerErrorCode(code) !== undefined;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Decorate an `AsyncIterable<TaskEvent>` with the synchronous `taskId` and
|
|
2232
|
+
* the `sessionId` Promise so consumers can read both before iterating.
|
|
2233
|
+
*
|
|
2234
|
+
* `taskId` is the SDK-derived value — `task_id == task.start.message_id`
|
|
2235
|
+
* — available at envelope mint time.
|
|
2236
|
+
*
|
|
2237
|
+
* `sessionId` is the server-derived UUID arriving on the `task.started`
|
|
2238
|
+
* admission ack; the Promise resolves when the ack lands and rejects if
|
|
2239
|
+
* the subscription terminates without admission.
|
|
2240
|
+
*
|
|
2241
|
+
* Returns a thin wrapper object that delegates `[Symbol.asyncIterator]`
|
|
2242
|
+
* to the underlying iterable — we deliberately don't mutate `iter`.
|
|
2243
|
+
*/
|
|
2244
|
+
function attachTaskId(iter, taskId, admittedTaskId, sessionId, admissionResult) {
|
|
2245
|
+
return {
|
|
2246
|
+
[Symbol.asyncIterator]: () => iter[Symbol.asyncIterator](),
|
|
2247
|
+
taskId,
|
|
2248
|
+
admittedTaskId,
|
|
2249
|
+
sessionId,
|
|
2250
|
+
admissionResult,
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Cap on the SDK's `admission_in_progress` retry budget. Mirrors
|
|
2255
|
+
* `PENDING_ADMISSION_TIMEOUT` on the server side. After this wall-clock
|
|
2256
|
+
* window the SDK gives up retrying and throws
|
|
2257
|
+
* {@link QodoAdmissionTimeoutError}. Operator-facing default; consumers
|
|
2258
|
+
* who need a different cap can wrap the SDK call themselves.
|
|
2259
|
+
*/
|
|
2260
|
+
const PENDING_ADMISSION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
2261
|
+
/** Initial backoff for `admission_in_progress` retries when the server omits `retry_after_ms`. */
|
|
2262
|
+
const ADMISSION_RETRY_INITIAL_MS = 100;
|
|
2263
|
+
function createDeferred() {
|
|
2264
|
+
let resolve;
|
|
2265
|
+
let reject;
|
|
2266
|
+
let done = false;
|
|
2267
|
+
const promise = new Promise((res, rej) => {
|
|
2268
|
+
resolve = (v) => {
|
|
2269
|
+
if (done)
|
|
2270
|
+
return;
|
|
2271
|
+
done = true;
|
|
2272
|
+
res(v);
|
|
2273
|
+
};
|
|
2274
|
+
reject = (e) => {
|
|
2275
|
+
if (done)
|
|
2276
|
+
return;
|
|
2277
|
+
done = true;
|
|
2278
|
+
rej(e);
|
|
2279
|
+
};
|
|
2280
|
+
});
|
|
2281
|
+
// Default unhandled-rejection guard — the deferred may reject before any
|
|
2282
|
+
// consumer awaits `promise`. Without this attach Node logs an
|
|
2283
|
+
// `unhandledRejection` warning between rejection time and the consumer's
|
|
2284
|
+
// `await stream.sessionId` (which still re-surfaces the error).
|
|
2285
|
+
promise.catch(() => undefined);
|
|
2286
|
+
return { promise, resolve, reject, settled: () => done };
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Backoff schedule for `admission_in_progress` retries when the server
|
|
2290
|
+
* omits `retry_after_ms`. Exponential doubling from
|
|
2291
|
+
* `ADMISSION_RETRY_INITIAL_MS`, capped at the operator timeout. ±10%
|
|
2292
|
+
* multiplicative jitter so a pod stampede doesn't synchronize retry
|
|
2293
|
+
* waves.
|
|
2294
|
+
*
|
|
2295
|
+
* Pure function — `Math.random()` is the only impurity and is the source
|
|
2296
|
+
* of the jitter; tests can disable jitter by hand-rolling a wait (the
|
|
2297
|
+
* SDK doesn't expose a seam to override the RNG, matching the rest of
|
|
2298
|
+
* the codebase's transport timers).
|
|
2299
|
+
*/
|
|
2300
|
+
function computeAdmissionRetryBackoffMs(attempt) {
|
|
2301
|
+
// Apply the cap AFTER jitter so the return value can NEVER exceed
|
|
2302
|
+
// `PENDING_ADMISSION_TIMEOUT_MS`. Capping `base` first and then adding
|
|
2303
|
+
// jitter on top would let the result drift up to +10% above the
|
|
2304
|
+
// timeout.
|
|
2305
|
+
const base = ADMISSION_RETRY_INITIAL_MS * Math.pow(2, attempt);
|
|
2306
|
+
const jitter = base * 0.1 * Math.random();
|
|
2307
|
+
return Math.floor(Math.min(base + jitter, PENDING_ADMISSION_TIMEOUT_MS));
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Read the server's `retry_after_ms` hint off an `admission_in_progress`
|
|
2311
|
+
* envelope. The field is optional + non-negative integer milliseconds;
|
|
2312
|
+
* ill-typed values fall back to `undefined` so the caller uses the SDK's
|
|
2313
|
+
* exponential backoff.
|
|
2314
|
+
*/
|
|
2315
|
+
function parseRetryAfterMsHint(env) {
|
|
2316
|
+
const v = env.payload.retry_after_ms;
|
|
2317
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
2318
|
+
return Math.floor(v);
|
|
2319
|
+
return undefined;
|
|
2320
|
+
}
|
|
2321
|
+
/** Promise-returning setTimeout — distinct fn name keeps the call sites greppable. */
|
|
2322
|
+
function sleepMs(ms) {
|
|
2323
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Client-side `idempotencyKey` validation — mirrors QAR's wire validator
|
|
2327
|
+
* (`Field(min_length=1, max_length=512, pattern=r"^[^\x00]+$")` on a
|
|
2328
|
+
* Pydantic `str`) so a bad value fails fast on the SDK side rather than
|
|
2329
|
+
* waiting for the server to round-trip an error envelope. Length is
|
|
2330
|
+
* measured in **Unicode code points** (`[...key].length`), matching
|
|
2331
|
+
* Pydantic's `Field` semantics on `str` — NOT UTF-16 code units (which
|
|
2332
|
+
* `key.length` would give) and NOT UTF-8 bytes (relevant for the NUL
|
|
2333
|
+
* exclusion, where the wire validator's `[^\x00]+` pattern matches code
|
|
2334
|
+
* points and would not be fooled by a stray `U+0000`).
|
|
2335
|
+
*
|
|
2336
|
+
* Throws {@link QodoIdempotencyKeyValidationError} with a discriminated
|
|
2337
|
+
* `reason` so callers can branch without parsing the message.
|
|
2338
|
+
*
|
|
2339
|
+
* **Observability hygiene**: NEVER log the raw key from this function or
|
|
2340
|
+
* its call sites. The thrown error carries the code-point count, not the
|
|
2341
|
+
* value; consumers handling the error are responsible for keeping the
|
|
2342
|
+
* original input out of operator-facing surfaces.
|
|
2343
|
+
*/
|
|
2344
|
+
function validateIdempotencyKey(key) {
|
|
2345
|
+
// Spread iterates code points (handles surrogate pairs correctly);
|
|
2346
|
+
// `String.prototype.length` would count UTF-16 code units and miscount
|
|
2347
|
+
// anything outside the BMP. The wire limit is 512 **code points**.
|
|
2348
|
+
const codePoints = [...key].length;
|
|
2349
|
+
if (codePoints === 0) {
|
|
2350
|
+
throw new QodoIdempotencyKeyValidationError('too_short', 0);
|
|
2351
|
+
}
|
|
2352
|
+
if (codePoints > 512) {
|
|
2353
|
+
throw new QodoIdempotencyKeyValidationError('too_long', codePoints);
|
|
2354
|
+
}
|
|
2355
|
+
// NUL exclusion — mirrors QAR's `pattern=r"^[^\x00]+$"` validator.
|
|
2356
|
+
// We iterate code points (not UTF-16 code units) for consistency with
|
|
2357
|
+
// the length check above; `codePointAt(0) === 0` tests the U+0000 case
|
|
2358
|
+
// without embedding a NUL literal in source.
|
|
2359
|
+
for (const cp of key) {
|
|
2360
|
+
if (cp.codePointAt(0) === 0) {
|
|
2361
|
+
throw new QodoIdempotencyKeyValidationError('contains_nul', codePoints);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
// ---------------------------------------------------------------------------
|
|
2366
|
+
// MCP projection helpers
|
|
2367
|
+
// ---------------------------------------------------------------------------
|
|
2368
|
+
/**
|
|
2369
|
+
* Discriminate an `InlineGraphAgent.spec` union arm — `true` for the
|
|
2370
|
+
* inline `InlineAgentSpec` branch (matched by `kind` literal or absence
|
|
2371
|
+
* of the `agent_id` static-ref field). Mirrors the runtime discriminator
|
|
2372
|
+
* in `inlineGraph.ts` so the projection graph walker doesn't have to
|
|
2373
|
+
* import the private helper.
|
|
2374
|
+
*/
|
|
2375
|
+
function isInlineAgentNode(spec) {
|
|
2376
|
+
if (typeof spec !== 'object' || spec === null)
|
|
2377
|
+
return false;
|
|
2378
|
+
if (spec.agent_id !== undefined)
|
|
2379
|
+
return false;
|
|
2380
|
+
const kind = spec.kind;
|
|
2381
|
+
return kind === 'AgentSpec' || kind === undefined;
|
|
2382
|
+
}
|
|
2383
|
+
/** Discriminate the nested-subgraph arm of `InlineGraphAgent.spec`. */
|
|
2384
|
+
function isInlineGraphNode(spec) {
|
|
2385
|
+
if (typeof spec !== 'object' || spec === null)
|
|
2386
|
+
return false;
|
|
2387
|
+
return spec.kind === 'GraphSpec';
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Walk an `InlineGraphSpec` looking for any inline-agent node that
|
|
2391
|
+
* declares `mcpTools` / `mcpToolOverrides`. Returns `true` on the first
|
|
2392
|
+
* hit — the caller uses this to decide whether to route the dispatch
|
|
2393
|
+
* through the eager-async path that awaits MCP catalog settle. Recurses
|
|
2394
|
+
* into nested-subgraph nodes so a deeply-nested consumer still
|
|
2395
|
+
* triggers the async path at the root.
|
|
2396
|
+
*/
|
|
2397
|
+
/**
|
|
2398
|
+
* Walk an `InlineGraphSpec`'s agent nodes and feed every inline
|
|
2399
|
+
* agent's `tools[]` into the supplied bind callback. Static-ref nodes
|
|
2400
|
+
* (`{ agent_id }`) have no tools surface; sub-graphs recurse.
|
|
2401
|
+
*
|
|
2402
|
+
* Used at `startWithGraph` time so `defineFunctionTool` handlers
|
|
2403
|
+
* attached to per-node `tools[]` get installed on the client's
|
|
2404
|
+
* `ToolClient` before the wire `task.start` write.
|
|
2405
|
+
*/
|
|
2406
|
+
function collectGraphFunctionTools(graph, bind) {
|
|
2407
|
+
for (const node of Object.values(graph.agents)) {
|
|
2408
|
+
const inner = node.spec;
|
|
2409
|
+
if (isInlineAgentNode(inner)) {
|
|
2410
|
+
if (inner.tools != null && inner.tools.length > 0) {
|
|
2411
|
+
bind(inner.tools);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
else if (isInlineGraphNode(inner)) {
|
|
2415
|
+
collectGraphFunctionTools(inner, bind);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
function graphHasMcpProjection(graph) {
|
|
2420
|
+
for (const node of Object.values(graph.agents)) {
|
|
2421
|
+
const inner = node.spec;
|
|
2422
|
+
if (isInlineAgentNode(inner)) {
|
|
2423
|
+
if (inner.mcpTools !== undefined ||
|
|
2424
|
+
inner.mcpToolOverrides !== undefined) {
|
|
2425
|
+
return true;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
else if (isInlineGraphNode(inner)) {
|
|
2429
|
+
if (graphHasMcpProjection(inner))
|
|
2430
|
+
return true;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Append the `qodo-skills.*` projected tool surface to the
|
|
2437
|
+
* post-projection wire `tools[]`, deduping by name so a consumer-shipped
|
|
2438
|
+
* tool with a colliding name (rare; consumers shouldn't author tools
|
|
2439
|
+
* under the SDK-owned `qodo-skills.` prefix) still wins.
|
|
2440
|
+
*
|
|
2441
|
+
* Returns the original array reference when nothing would change so
|
|
2442
|
+
* downstream `result !== input` comparisons stay meaningful.
|
|
2443
|
+
*/
|
|
2444
|
+
function mergeQodoSkillsTools(projected) {
|
|
2445
|
+
const skillsDefs = qodoSkillsFunctionToolDefs();
|
|
2446
|
+
if (skillsDefs.length === 0)
|
|
2447
|
+
return projected;
|
|
2448
|
+
const existingNames = new Set();
|
|
2449
|
+
for (const tool of projected) {
|
|
2450
|
+
existingNames.add(tool.function.name);
|
|
2451
|
+
}
|
|
2452
|
+
const toAppend = [];
|
|
2453
|
+
for (const def of skillsDefs) {
|
|
2454
|
+
if (existingNames.has(def.function.name))
|
|
2455
|
+
continue;
|
|
2456
|
+
toAppend.push(def);
|
|
2457
|
+
}
|
|
2458
|
+
if (toAppend.length === 0)
|
|
2459
|
+
return projected;
|
|
2460
|
+
return [...projected, ...toAppend];
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Recursively apply {@link mergeQodoSkillsTools} to every inline-agent
|
|
2464
|
+
* node in a graph spec. Static refs and nested subgraph nodes are
|
|
2465
|
+
* walked through. Returns the original `graph` reference when nothing
|
|
2466
|
+
* needed to change.
|
|
2467
|
+
*/
|
|
2468
|
+
function mergeQodoSkillsToolsIntoGraph(graph) {
|
|
2469
|
+
const agentEntries = Object.entries(graph.agents);
|
|
2470
|
+
const newAgents = {};
|
|
2471
|
+
let touched = false;
|
|
2472
|
+
for (const [name, agentNode] of agentEntries) {
|
|
2473
|
+
const node = agentNode;
|
|
2474
|
+
const inner = node.spec;
|
|
2475
|
+
if (isInlineAgentNode(inner)) {
|
|
2476
|
+
const original = inner.tools ?? [];
|
|
2477
|
+
const merged = mergeQodoSkillsTools(original);
|
|
2478
|
+
if (merged !== original) {
|
|
2479
|
+
newAgents[name] = { ...node, spec: { ...inner, tools: merged } };
|
|
2480
|
+
touched = true;
|
|
2481
|
+
}
|
|
2482
|
+
else {
|
|
2483
|
+
newAgents[name] = node;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
else if (isInlineGraphNode(inner)) {
|
|
2487
|
+
const projected = mergeQodoSkillsToolsIntoGraph(inner);
|
|
2488
|
+
if (projected !== inner) {
|
|
2489
|
+
newAgents[name] = { ...node, spec: projected };
|
|
2490
|
+
touched = true;
|
|
2491
|
+
}
|
|
2492
|
+
else {
|
|
2493
|
+
newAgents[name] = node;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
else {
|
|
2497
|
+
newAgents[name] = node;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if (!touched)
|
|
2501
|
+
return graph;
|
|
2502
|
+
return { ...graph, agents: newAgents };
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Strip SDK-only `mcpTools` / `mcpToolOverrides` fields from an inline
|
|
2506
|
+
* `InlineAgentSpec`. The projection layer replaces `tools[]` separately —
|
|
2507
|
+
* this helper only handles the field removal so the wire envelope
|
|
2508
|
+
* doesn't carry surfaces QAR's wire validator (`D10-R5` on
|
|
2509
|
+
* `extra='forbid'`) would reject.
|
|
2510
|
+
*/
|
|
2511
|
+
function stripMcpToolsFromSpec(spec) {
|
|
2512
|
+
const hasField = spec.mcpTools !== undefined ||
|
|
2513
|
+
spec.mcpToolOverrides !== undefined;
|
|
2514
|
+
if (!hasField)
|
|
2515
|
+
return spec;
|
|
2516
|
+
// Destructure removes the two fields; rest carries every wire field.
|
|
2517
|
+
const { mcpTools: _mcpTools, mcpToolOverrides: _mcpToolOverrides, ...rest } = spec;
|
|
2518
|
+
void _mcpTools;
|
|
2519
|
+
void _mcpToolOverrides;
|
|
2520
|
+
return rest;
|
|
2521
|
+
}
|
|
2522
|
+
//# sourceMappingURL=TaskClient.js.map
|