@nodetool-ai/runtime 0.7.0-rc.23 → 0.8.2
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/README.md +29 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/agent-memory.d.ts +118 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +186 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/async-local-storage.d.ts +20 -0
- package/dist/async-local-storage.d.ts.map +1 -0
- package/dist/async-local-storage.js +28 -0
- package/dist/async-local-storage.js.map +1 -0
- package/dist/blender-executor.d.ts +93 -0
- package/dist/blender-executor.d.ts.map +1 -0
- package/dist/blender-executor.js +294 -0
- package/dist/blender-executor.js.map +1 -0
- package/dist/comfy-executor.d.ts +23 -1
- package/dist/comfy-executor.d.ts.map +1 -1
- package/dist/comfy-executor.js +234 -55
- package/dist/comfy-executor.js.map +1 -1
- package/dist/context-packer.d.ts +1 -1
- package/dist/context-packer.d.ts.map +1 -1
- package/dist/context-packer.js +23 -25
- package/dist/context-packer.js.map +1 -1
- package/dist/context.d.ts +982 -127
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2649 -530
- package/dist/context.js.map +1 -1
- package/dist/cost-reconciler.d.ts +32 -0
- package/dist/cost-reconciler.d.ts.map +1 -0
- package/dist/cost-reconciler.js +21 -0
- package/dist/cost-reconciler.js.map +1 -0
- package/dist/entities.d.ts +33 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +43 -0
- package/dist/entities.js.map +1 -0
- package/dist/external-media-fetch.d.ts +29 -0
- package/dist/external-media-fetch.d.ts.map +1 -0
- package/dist/external-media-fetch.js +38 -0
- package/dist/external-media-fetch.js.map +1 -0
- package/dist/generation-receipt.d.ts +86 -0
- package/dist/generation-receipt.d.ts.map +1 -0
- package/dist/generation-receipt.js +137 -0
- package/dist/generation-receipt.js.map +1 -0
- package/dist/generation-registry.d.ts +75 -0
- package/dist/generation-registry.d.ts.map +1 -0
- package/dist/generation-registry.js +144 -0
- package/dist/generation-registry.js.map +1 -0
- package/dist/google/client.d.ts +196 -0
- package/dist/google/client.d.ts.map +1 -0
- package/dist/google/client.js +396 -0
- package/dist/google/client.js.map +1 -0
- package/dist/google/index.d.ts +3 -0
- package/dist/google/index.d.ts.map +1 -0
- package/dist/google/index.js +3 -0
- package/dist/google/index.js.map +1 -0
- package/dist/google/token.d.ts +15 -0
- package/dist/google/token.d.ts.map +1 -0
- package/dist/google/token.js +24 -0
- package/dist/google/token.js.map +1 -0
- package/dist/host-binaries.d.ts +106 -0
- package/dist/host-binaries.d.ts.map +1 -0
- package/dist/host-binaries.js +352 -0
- package/dist/host-binaries.js.map +1 -0
- package/dist/image-codec.d.ts +76 -0
- package/dist/image-codec.d.ts.map +1 -0
- package/dist/image-codec.js +293 -0
- package/dist/image-codec.js.map +1 -0
- package/dist/image-model-tasks.d.ts +15 -0
- package/dist/image-model-tasks.d.ts.map +1 -0
- package/dist/image-model-tasks.js +24 -0
- package/dist/image-model-tasks.js.map +1 -0
- package/dist/index.d.ts +67 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -4
- package/dist/index.js.map +1 -1
- package/dist/invocation-account.d.ts +43 -0
- package/dist/invocation-account.d.ts.map +1 -0
- package/dist/invocation-account.js +60 -0
- package/dist/invocation-account.js.map +1 -0
- package/dist/media-ref-bytes.d.ts +35 -0
- package/dist/media-ref-bytes.d.ts.map +1 -0
- package/dist/media-ref-bytes.js +191 -0
- package/dist/media-ref-bytes.js.map +1 -0
- package/dist/node-executor.d.ts +119 -2
- package/dist/node-executor.d.ts.map +1 -1
- package/dist/permission-gate.d.ts +152 -0
- package/dist/permission-gate.d.ts.map +1 -0
- package/dist/permission-gate.js +87 -0
- package/dist/permission-gate.js.map +1 -0
- package/dist/prefixed-storage-adapter.d.ts +33 -0
- package/dist/prefixed-storage-adapter.d.ts.map +1 -0
- package/dist/prefixed-storage-adapter.js +70 -0
- package/dist/prefixed-storage-adapter.js.map +1 -0
- package/dist/prompt-asset-refs.d.ts +222 -0
- package/dist/prompt-asset-refs.d.ts.map +1 -0
- package/dist/prompt-asset-refs.js +751 -0
- package/dist/prompt-asset-refs.js.map +1 -0
- package/dist/provider-cache.d.ts +3 -0
- package/dist/provider-cache.d.ts.map +1 -0
- package/dist/provider-cache.js +8 -0
- package/dist/provider-cache.js.map +1 -0
- package/dist/providers/aki-provider.d.ts +35 -10
- package/dist/providers/aki-provider.d.ts.map +1 -1
- package/dist/providers/aki-provider.js +90 -77
- package/dist/providers/aki-provider.js.map +1 -1
- package/dist/providers/alibaba-provider.d.ts +39 -0
- package/dist/providers/alibaba-provider.d.ts.map +1 -0
- package/dist/providers/alibaba-provider.js +86 -0
- package/dist/providers/alibaba-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +52 -3
- package/dist/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/providers/anthropic-provider.js +1020 -338
- package/dist/providers/anthropic-provider.js.map +1 -1
- package/dist/providers/atlascloud-provider.d.ts +103 -0
- package/dist/providers/atlascloud-provider.d.ts.map +1 -0
- package/dist/providers/atlascloud-provider.js +1031 -0
- package/dist/providers/atlascloud-provider.js.map +1 -0
- package/dist/providers/atlascloud-transport.d.ts +100 -0
- package/dist/providers/atlascloud-transport.d.ts.map +1 -0
- package/dist/providers/atlascloud-transport.js +364 -0
- package/dist/providers/atlascloud-transport.js.map +1 -0
- package/dist/providers/atlascloud-webhook-registry.d.ts +35 -0
- package/dist/providers/atlascloud-webhook-registry.d.ts.map +1 -0
- package/dist/providers/atlascloud-webhook-registry.js +104 -0
- package/dist/providers/atlascloud-webhook-registry.js.map +1 -0
- package/dist/providers/audio-mime.d.ts +11 -0
- package/dist/providers/audio-mime.d.ts.map +1 -0
- package/dist/providers/audio-mime.js +46 -0
- package/dist/providers/audio-mime.js.map +1 -0
- package/dist/providers/base-provider.d.ts +501 -32
- package/dist/providers/base-provider.d.ts.map +1 -1
- package/dist/providers/base-provider.js +1280 -87
- package/dist/providers/base-provider.js.map +1 -1
- package/dist/providers/cassette-provider.d.ts +217 -0
- package/dist/providers/cassette-provider.d.ts.map +1 -0
- package/dist/providers/cassette-provider.js +502 -0
- package/dist/providers/cassette-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +6 -12
- package/dist/providers/cerebras-provider.d.ts.map +1 -1
- package/dist/providers/cerebras-provider.js +8 -33
- package/dist/providers/cerebras-provider.js.map +1 -1
- package/dist/providers/claude-agent-provider.d.ts +107 -74
- package/dist/providers/claude-agent-provider.d.ts.map +1 -1
- package/dist/providers/claude-agent-provider.js +1279 -517
- package/dist/providers/claude-agent-provider.js.map +1 -1
- package/dist/providers/codex-provider.d.ts +84 -0
- package/dist/providers/codex-provider.d.ts.map +1 -0
- package/dist/providers/codex-provider.js +505 -0
- package/dist/providers/codex-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +7 -3
- package/dist/providers/cohere-provider.d.ts.map +1 -1
- package/dist/providers/cohere-provider.js +14 -2
- package/dist/providers/cohere-provider.js.map +1 -1
- package/dist/providers/comfy-cloud-provider.d.ts +30 -0
- package/dist/providers/comfy-cloud-provider.d.ts.map +1 -0
- package/dist/providers/comfy-cloud-provider.js +42 -0
- package/dist/providers/comfy-cloud-provider.js.map +1 -0
- package/dist/providers/content-filter.d.ts +46 -0
- package/dist/providers/content-filter.d.ts.map +1 -0
- package/dist/providers/content-filter.js +92 -0
- package/dist/providers/content-filter.js.map +1 -0
- package/dist/providers/context-exceeded.d.ts +37 -0
- package/dist/providers/context-exceeded.d.ts.map +1 -0
- package/dist/providers/context-exceeded.js +114 -0
- package/dist/providers/context-exceeded.js.map +1 -0
- package/dist/providers/context-window.d.ts +44 -0
- package/dist/providers/context-window.d.ts.map +1 -0
- package/dist/providers/context-window.js +145 -0
- package/dist/providers/context-window.js.map +1 -0
- package/dist/providers/contract/index.d.ts +4 -0
- package/dist/providers/contract/index.d.ts.map +1 -0
- package/dist/providers/contract/index.js +4 -0
- package/dist/providers/contract/index.js.map +1 -0
- package/dist/providers/contract/probe-manifest.d.ts +75 -0
- package/dist/providers/contract/probe-manifest.d.ts.map +1 -0
- package/dist/providers/contract/probe-manifest.js +379 -0
- package/dist/providers/contract/probe-manifest.js.map +1 -0
- package/dist/providers/contract/probe-runner.d.ts +57 -0
- package/dist/providers/contract/probe-runner.d.ts.map +1 -0
- package/dist/providers/contract/probe-runner.js +180 -0
- package/dist/providers/contract/probe-runner.js.map +1 -0
- package/dist/providers/contract/redact.d.ts +29 -0
- package/dist/providers/contract/redact.d.ts.map +1 -0
- package/dist/providers/contract/redact.js +113 -0
- package/dist/providers/contract/redact.js.map +1 -0
- package/dist/providers/core-tools.d.ts +75 -0
- package/dist/providers/core-tools.d.ts.map +1 -0
- package/dist/providers/core-tools.js +128 -0
- package/dist/providers/core-tools.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +69 -50
- package/dist/providers/cost-calculator.d.ts.map +1 -1
- package/dist/providers/cost-calculator.js +207 -397
- package/dist/providers/cost-calculator.js.map +1 -1
- package/dist/providers/credential-check.d.ts +45 -0
- package/dist/providers/credential-check.d.ts.map +1 -0
- package/dist/providers/credential-check.js +164 -0
- package/dist/providers/credential-check.js.map +1 -0
- package/dist/providers/custom-model-kinds.d.ts +33 -0
- package/dist/providers/custom-model-kinds.d.ts.map +1 -0
- package/dist/providers/custom-model-kinds.js +96 -0
- package/dist/providers/custom-model-kinds.js.map +1 -0
- package/dist/providers/custom-openai-provider.d.ts +83 -0
- package/dist/providers/custom-openai-provider.d.ts.map +1 -0
- package/dist/providers/custom-openai-provider.js +190 -0
- package/dist/providers/custom-openai-provider.js.map +1 -0
- package/dist/providers/custom-provider-registry.d.ts +19 -0
- package/dist/providers/custom-provider-registry.d.ts.map +1 -0
- package/dist/providers/custom-provider-registry.js +52 -0
- package/dist/providers/custom-provider-registry.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +8 -14
- package/dist/providers/deepseek-provider.d.ts.map +1 -1
- package/dist/providers/deepseek-provider.js +6 -35
- package/dist/providers/deepseek-provider.js.map +1 -1
- package/dist/providers/defaults.d.ts +12 -0
- package/dist/providers/defaults.d.ts.map +1 -0
- package/dist/providers/defaults.js +12 -0
- package/dist/providers/defaults.js.map +1 -0
- package/dist/providers/dreamina-models.d.ts +5 -0
- package/dist/providers/dreamina-models.d.ts.map +1 -0
- package/dist/providers/dreamina-models.js +57 -0
- package/dist/providers/dreamina-models.js.map +1 -0
- package/dist/providers/dreamina-music.d.ts +52 -0
- package/dist/providers/dreamina-music.d.ts.map +1 -0
- package/dist/providers/dreamina-music.js +115 -0
- package/dist/providers/dreamina-music.js.map +1 -0
- package/dist/providers/dreamina-provider.d.ts +62 -0
- package/dist/providers/dreamina-provider.d.ts.map +1 -0
- package/dist/providers/dreamina-provider.js +790 -0
- package/dist/providers/dreamina-provider.js.map +1 -0
- package/dist/providers/elevenlabs-provider.d.ts +50 -0
- package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
- package/dist/providers/elevenlabs-provider.js +147 -0
- package/dist/providers/elevenlabs-provider.js.map +1 -0
- package/dist/providers/entity-references.d.ts +31 -0
- package/dist/providers/entity-references.d.ts.map +1 -0
- package/dist/providers/entity-references.js +113 -0
- package/dist/providers/entity-references.js.map +1 -0
- package/dist/providers/evolink-provider.d.ts +40 -0
- package/dist/providers/evolink-provider.d.ts.map +1 -0
- package/dist/providers/evolink-provider.js +312 -0
- package/dist/providers/evolink-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +74 -2
- package/dist/providers/fake-provider.d.ts.map +1 -1
- package/dist/providers/fake-provider.js +213 -1
- package/dist/providers/fake-provider.js.map +1 -1
- package/dist/providers/fal-generations.d.ts +106 -0
- package/dist/providers/fal-generations.d.ts.map +1 -0
- package/dist/providers/fal-generations.js +286 -0
- package/dist/providers/fal-generations.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +173 -8
- package/dist/providers/fal-provider.d.ts.map +1 -1
- package/dist/providers/fal-provider.js +1396 -64
- package/dist/providers/fal-provider.js.map +1 -1
- package/dist/providers/fal-queue.d.ts +47 -0
- package/dist/providers/fal-queue.d.ts.map +1 -0
- package/dist/providers/fal-queue.js +231 -0
- package/dist/providers/fal-queue.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +150 -9
- package/dist/providers/gemini-provider.d.ts.map +1 -1
- package/dist/providers/gemini-provider.js +1515 -316
- package/dist/providers/gemini-provider.js.map +1 -1
- package/dist/providers/gmi-provider.d.ts +21 -0
- package/dist/providers/gmi-provider.d.ts.map +1 -0
- package/dist/providers/gmi-provider.js +34 -0
- package/dist/providers/gmi-provider.js.map +1 -0
- package/dist/providers/gpt-image-size.d.ts +3 -0
- package/dist/providers/gpt-image-size.d.ts.map +1 -0
- package/dist/providers/gpt-image-size.js +37 -0
- package/dist/providers/gpt-image-size.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +9 -12
- package/dist/providers/groq-provider.d.ts.map +1 -1
- package/dist/providers/groq-provider.js +19 -33
- package/dist/providers/groq-provider.js.map +1 -1
- package/dist/providers/groq-request.d.ts +38 -0
- package/dist/providers/groq-request.d.ts.map +1 -0
- package/dist/providers/groq-request.js +138 -0
- package/dist/providers/groq-request.js.map +1 -0
- package/dist/providers/higgsfield-provider.d.ts +31 -0
- package/dist/providers/higgsfield-provider.d.ts.map +1 -0
- package/dist/providers/higgsfield-provider.js +143 -0
- package/dist/providers/higgsfield-provider.js.map +1 -0
- package/dist/providers/higgsfield-transport.d.ts +106 -0
- package/dist/providers/higgsfield-transport.d.ts.map +1 -0
- package/dist/providers/higgsfield-transport.js +182 -0
- package/dist/providers/higgsfield-transport.js.map +1 -0
- package/dist/providers/http-transport.d.ts +74 -0
- package/dist/providers/http-transport.d.ts.map +1 -0
- package/dist/providers/http-transport.js +152 -0
- package/dist/providers/http-transport.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +73 -4
- package/dist/providers/huggingface-provider.d.ts.map +1 -1
- package/dist/providers/huggingface-provider.js +240 -34
- package/dist/providers/huggingface-provider.js.map +1 -1
- package/dist/providers/image-mime.d.ts +39 -0
- package/dist/providers/image-mime.d.ts.map +1 -0
- package/dist/providers/image-mime.js +111 -0
- package/dist/providers/image-mime.js.map +1 -0
- package/dist/providers/image-ref.d.ts +21 -0
- package/dist/providers/image-ref.d.ts.map +1 -0
- package/dist/providers/image-ref.js +44 -0
- package/dist/providers/image-ref.js.map +1 -0
- package/dist/providers/index.d.ts +87 -8
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +250 -56
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/jina-provider.d.ts +12 -3
- package/dist/providers/jina-provider.d.ts.map +1 -1
- package/dist/providers/jina-provider.js +21 -7
- package/dist/providers/jina-provider.js.map +1 -1
- package/dist/providers/kie-provider.d.ts +172 -10
- package/dist/providers/kie-provider.d.ts.map +1 -1
- package/dist/providers/kie-provider.js +1190 -228
- package/dist/providers/kie-provider.js.map +1 -1
- package/dist/providers/kie-webhook-registry.d.ts +28 -0
- package/dist/providers/kie-webhook-registry.d.ts.map +1 -0
- package/dist/providers/kie-webhook-registry.js +91 -0
- package/dist/providers/kie-webhook-registry.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +23 -36
- package/dist/providers/llama-provider.d.ts.map +1 -1
- package/dist/providers/llama-provider.js +50 -355
- package/dist/providers/llama-provider.js.map +1 -1
- package/dist/providers/lmstudio-provider.d.ts +4 -7
- package/dist/providers/lmstudio-provider.d.ts.map +1 -1
- package/dist/providers/lmstudio-provider.js +24 -15
- package/dist/providers/lmstudio-provider.js.map +1 -1
- package/dist/providers/loop-tracing.d.ts +17 -0
- package/dist/providers/loop-tracing.d.ts.map +1 -0
- package/dist/providers/loop-tracing.js +132 -0
- package/dist/providers/loop-tracing.js.map +1 -0
- package/dist/providers/manifest-models.d.ts +341 -1
- package/dist/providers/manifest-models.d.ts.map +1 -1
- package/dist/providers/manifest-models.js +1106 -22
- package/dist/providers/manifest-models.js.map +1 -1
- package/dist/providers/media-mime.d.ts +14 -0
- package/dist/providers/media-mime.d.ts.map +1 -0
- package/dist/providers/media-mime.js +35 -0
- package/dist/providers/media-mime.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +16 -2
- package/dist/providers/meshy-provider.d.ts.map +1 -1
- package/dist/providers/meshy-provider.js +82 -46
- package/dist/providers/meshy-provider.js.map +1 -1
- package/dist/providers/meta-provider.d.ts +22 -0
- package/dist/providers/meta-provider.d.ts.map +1 -0
- package/dist/providers/meta-provider.js +54 -0
- package/dist/providers/meta-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +27 -18
- package/dist/providers/minimax-provider.d.ts.map +1 -1
- package/dist/providers/minimax-provider.js +222 -72
- package/dist/providers/minimax-provider.js.map +1 -1
- package/dist/providers/mistral-provider.d.ts +8 -12
- package/dist/providers/mistral-provider.d.ts.map +1 -1
- package/dist/providers/mistral-provider.js +15 -34
- package/dist/providers/mistral-provider.js.map +1 -1
- package/dist/providers/moonshot-provider.d.ts +11 -15
- package/dist/providers/moonshot-provider.d.ts.map +1 -1
- package/dist/providers/moonshot-provider.js +33 -23
- package/dist/providers/moonshot-provider.js.map +1 -1
- package/dist/providers/node-llama-cpp-provider.d.ts +99 -0
- package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
- package/dist/providers/node-llama-cpp-provider.js +484 -0
- package/dist/providers/node-llama-cpp-provider.js.map +1 -0
- package/dist/providers/nodetool-provider.d.ts +49 -0
- package/dist/providers/nodetool-provider.d.ts.map +1 -0
- package/dist/providers/nodetool-provider.js +222 -0
- package/dist/providers/nodetool-provider.js.map +1 -0
- package/dist/providers/oauth/browser-launcher.d.ts +23 -0
- package/dist/providers/oauth/browser-launcher.d.ts.map +1 -0
- package/dist/providers/oauth/browser-launcher.js +45 -0
- package/dist/providers/oauth/browser-launcher.js.map +1 -0
- package/dist/providers/oauth/claude-code-credentials.d.ts +74 -0
- package/dist/providers/oauth/claude-code-credentials.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-credentials.js +133 -0
- package/dist/providers/oauth/claude-code-credentials.js.map +1 -0
- package/dist/providers/oauth/claude-code-login.d.ts +124 -0
- package/dist/providers/oauth/claude-code-login.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-login.js +230 -0
- package/dist/providers/oauth/claude-code-login.js.map +1 -0
- package/dist/providers/oauth/claude-code-oauth-client.d.ts +111 -0
- package/dist/providers/oauth/claude-code-oauth-client.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-oauth-client.js +207 -0
- package/dist/providers/oauth/claude-code-oauth-client.js.map +1 -0
- package/dist/providers/oauth/errors.d.ts +54 -0
- package/dist/providers/oauth/errors.d.ts.map +1 -0
- package/dist/providers/oauth/errors.js +79 -0
- package/dist/providers/oauth/errors.js.map +1 -0
- package/dist/providers/oauth/index.d.ts +105 -0
- package/dist/providers/oauth/index.d.ts.map +1 -0
- package/dist/providers/oauth/index.js +167 -0
- package/dist/providers/oauth/index.js.map +1 -0
- package/dist/providers/oauth/jwt-claims.d.ts +23 -0
- package/dist/providers/oauth/jwt-claims.d.ts.map +1 -0
- package/dist/providers/oauth/jwt-claims.js +67 -0
- package/dist/providers/oauth/jwt-claims.js.map +1 -0
- package/dist/providers/oauth/local-callback-server.d.ts +60 -0
- package/dist/providers/oauth/local-callback-server.d.ts.map +1 -0
- package/dist/providers/oauth/local-callback-server.js +148 -0
- package/dist/providers/oauth/local-callback-server.js.map +1 -0
- package/dist/providers/oauth/oauth-client.d.ts +55 -0
- package/dist/providers/oauth/oauth-client.d.ts.map +1 -0
- package/dist/providers/oauth/oauth-client.js +161 -0
- package/dist/providers/oauth/oauth-client.js.map +1 -0
- package/dist/providers/oauth/openai-oauth-provider.d.ts +129 -0
- package/dist/providers/oauth/openai-oauth-provider.d.ts.map +1 -0
- package/dist/providers/oauth/openai-oauth-provider.js +265 -0
- package/dist/providers/oauth/openai-oauth-provider.js.map +1 -0
- package/dist/providers/oauth/pkce-helper.d.ts +43 -0
- package/dist/providers/oauth/pkce-helper.d.ts.map +1 -0
- package/dist/providers/oauth/pkce-helper.js +57 -0
- package/dist/providers/oauth/pkce-helper.js.map +1 -0
- package/dist/providers/oauth/redaction.d.ts +22 -0
- package/dist/providers/oauth/redaction.d.ts.map +1 -0
- package/dist/providers/oauth/redaction.js +59 -0
- package/dist/providers/oauth/redaction.js.map +1 -0
- package/dist/providers/oauth/secure-credential-store.d.ts +67 -0
- package/dist/providers/oauth/secure-credential-store.d.ts.map +1 -0
- package/dist/providers/oauth/secure-credential-store.js +73 -0
- package/dist/providers/oauth/secure-credential-store.js.map +1 -0
- package/dist/providers/oauth/token-store.d.ts +53 -0
- package/dist/providers/oauth/token-store.d.ts.map +1 -0
- package/dist/providers/oauth/token-store.js +78 -0
- package/dist/providers/oauth/token-store.js.map +1 -0
- package/dist/providers/oauth/types.d.ts +64 -0
- package/dist/providers/oauth/types.d.ts.map +1 -0
- package/dist/providers/oauth/types.js +12 -0
- package/dist/providers/oauth/types.js.map +1 -0
- package/dist/providers/object-tracking.d.ts +47 -0
- package/dist/providers/object-tracking.d.ts.map +1 -0
- package/dist/providers/object-tracking.js +46 -0
- package/dist/providers/object-tracking.js.map +1 -0
- package/dist/providers/offline-model-index.d.ts +20 -0
- package/dist/providers/offline-model-index.d.ts.map +1 -0
- package/dist/providers/offline-model-index.js +112 -0
- package/dist/providers/offline-model-index.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +30 -2
- package/dist/providers/ollama-provider.d.ts.map +1 -1
- package/dist/providers/ollama-provider.js +198 -43
- package/dist/providers/ollama-provider.js.map +1 -1
- package/dist/providers/openai-compat/client.d.ts +57 -0
- package/dist/providers/openai-compat/client.d.ts.map +1 -0
- package/dist/providers/openai-compat/client.js +157 -0
- package/dist/providers/openai-compat/client.js.map +1 -0
- package/dist/providers/openai-compat/decode.d.ts +36 -0
- package/dist/providers/openai-compat/decode.d.ts.map +1 -0
- package/dist/providers/openai-compat/decode.js +55 -0
- package/dist/providers/openai-compat/decode.js.map +1 -0
- package/dist/providers/openai-compat/errors.d.ts +54 -0
- package/dist/providers/openai-compat/errors.d.ts.map +1 -0
- package/dist/providers/openai-compat/errors.js +144 -0
- package/dist/providers/openai-compat/errors.js.map +1 -0
- package/dist/providers/openai-compat/index.d.ts +8 -0
- package/dist/providers/openai-compat/index.d.ts.map +1 -0
- package/dist/providers/openai-compat/index.js +5 -0
- package/dist/providers/openai-compat/index.js.map +1 -0
- package/dist/providers/openai-compat/sse.d.ts +14 -0
- package/dist/providers/openai-compat/sse.d.ts.map +1 -0
- package/dist/providers/openai-compat/sse.js +69 -0
- package/dist/providers/openai-compat/sse.js.map +1 -0
- package/dist/providers/openai-compat/types.d.ts +75 -0
- package/dist/providers/openai-compat/types.d.ts.map +1 -0
- package/dist/providers/openai-compat/types.js +9 -0
- package/dist/providers/openai-compat/types.js.map +1 -0
- package/dist/providers/openai-compat-provider.d.ts +90 -0
- package/dist/providers/openai-compat-provider.d.ts.map +1 -0
- package/dist/providers/openai-compat-provider.js +282 -0
- package/dist/providers/openai-compat-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +98 -27
- package/dist/providers/openai-provider.d.ts.map +1 -1
- package/dist/providers/openai-provider.js +1037 -105
- package/dist/providers/openai-provider.js.map +1 -1
- package/dist/providers/openrouter-provider.d.ts +58 -13
- package/dist/providers/openrouter-provider.d.ts.map +1 -1
- package/dist/providers/openrouter-provider.js +383 -51
- package/dist/providers/openrouter-provider.js.map +1 -1
- package/dist/providers/provider-call-failure.d.ts +39 -0
- package/dist/providers/provider-call-failure.d.ts.map +1 -0
- package/dist/providers/provider-call-failure.js +132 -0
- package/dist/providers/provider-call-failure.js.map +1 -0
- package/dist/providers/provider-error.d.ts +59 -0
- package/dist/providers/provider-error.d.ts.map +1 -0
- package/dist/providers/provider-error.js +171 -0
- package/dist/providers/provider-error.js.map +1 -0
- package/dist/providers/provider-generations.d.ts +99 -0
- package/dist/providers/provider-generations.d.ts.map +1 -0
- package/dist/providers/provider-generations.js +71 -0
- package/dist/providers/provider-generations.js.map +1 -0
- package/dist/providers/provider-queue.d.ts +62 -0
- package/dist/providers/provider-queue.d.ts.map +1 -0
- package/dist/providers/provider-queue.js +28 -0
- package/dist/providers/provider-queue.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +33 -14
- package/dist/providers/provider-registry.d.ts.map +1 -1
- package/dist/providers/provider-registry.js +85 -68
- package/dist/providers/provider-registry.js.map +1 -1
- package/dist/providers/provider-request-log.d.ts +53 -0
- package/dist/providers/provider-request-log.d.ts.map +1 -0
- package/dist/providers/provider-request-log.js +168 -0
- package/dist/providers/provider-request-log.js.map +1 -0
- package/dist/providers/provider-session.d.ts +3 -0
- package/dist/providers/provider-session.d.ts.map +1 -0
- package/dist/providers/provider-session.js +10 -0
- package/dist/providers/provider-session.js.map +1 -0
- package/dist/providers/provider-transport.d.ts +11 -0
- package/dist/providers/provider-transport.d.ts.map +1 -0
- package/dist/providers/provider-transport.js +11 -0
- package/dist/providers/provider-transport.js.map +1 -0
- package/dist/providers/python-provider.d.ts +26 -13
- package/dist/providers/python-provider.d.ts.map +1 -1
- package/dist/providers/python-provider.js +204 -25
- package/dist/providers/python-provider.js.map +1 -1
- package/dist/providers/replicate-provider.d.ts +91 -8
- package/dist/providers/replicate-provider.d.ts.map +1 -1
- package/dist/providers/replicate-provider.js +823 -65
- package/dist/providers/replicate-provider.js.map +1 -1
- package/dist/providers/replicate-retry.d.ts +30 -0
- package/dist/providers/replicate-retry.d.ts.map +1 -0
- package/dist/providers/replicate-retry.js +97 -0
- package/dist/providers/replicate-retry.js.map +1 -0
- package/dist/providers/requesty-provider.d.ts +30 -0
- package/dist/providers/requesty-provider.d.ts.map +1 -0
- package/dist/providers/requesty-provider.js +87 -0
- package/dist/providers/requesty-provider.js.map +1 -0
- package/dist/providers/responses-api.d.ts +27 -0
- package/dist/providers/responses-api.d.ts.map +1 -0
- package/dist/providers/responses-api.js +297 -0
- package/dist/providers/responses-api.js.map +1 -0
- package/dist/providers/reve-provider.d.ts +46 -0
- package/dist/providers/reve-provider.d.ts.map +1 -0
- package/dist/providers/reve-provider.js +165 -0
- package/dist/providers/reve-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +25 -2
- package/dist/providers/rodin-provider.d.ts.map +1 -1
- package/dist/providers/rodin-provider.js +95 -51
- package/dist/providers/rodin-provider.js.map +1 -1
- package/dist/providers/safe-url.d.ts +70 -0
- package/dist/providers/safe-url.d.ts.map +1 -0
- package/dist/providers/safe-url.js +277 -0
- package/dist/providers/safe-url.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +9 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -1
- package/dist/providers/scripted-provider.js +86 -10
- package/dist/providers/scripted-provider.js.map +1 -1
- package/dist/providers/structured-output.d.ts +42 -0
- package/dist/providers/structured-output.d.ts.map +1 -0
- package/dist/providers/structured-output.js +83 -0
- package/dist/providers/structured-output.js.map +1 -0
- package/dist/providers/together-operations.d.ts +27 -0
- package/dist/providers/together-operations.d.ts.map +1 -0
- package/dist/providers/together-operations.js +95 -0
- package/dist/providers/together-operations.js.map +1 -0
- package/dist/providers/together-provider.d.ts +18 -18
- package/dist/providers/together-provider.d.ts.map +1 -1
- package/dist/providers/together-provider.js +140 -367
- package/dist/providers/together-provider.js.map +1 -1
- package/dist/providers/topaz-provider.d.ts +45 -0
- package/dist/providers/topaz-provider.d.ts.map +1 -0
- package/dist/providers/topaz-provider.js +244 -0
- package/dist/providers/topaz-provider.js.map +1 -0
- package/dist/providers/types.d.ts +641 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +81 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/useapi-provider.d.ts +28 -0
- package/dist/providers/useapi-provider.d.ts.map +1 -0
- package/dist/providers/useapi-provider.js +274 -0
- package/dist/providers/useapi-provider.js.map +1 -0
- package/dist/providers/video-duration.d.ts +6 -0
- package/dist/providers/video-duration.d.ts.map +1 -0
- package/dist/providers/video-duration.js +63 -0
- package/dist/providers/video-duration.js.map +1 -0
- package/dist/providers/video-frame-fallback.d.ts +39 -0
- package/dist/providers/video-frame-fallback.d.ts.map +1 -0
- package/dist/providers/video-frame-fallback.js +155 -0
- package/dist/providers/video-frame-fallback.js.map +1 -0
- package/dist/providers/video-frames.d.ts +61 -0
- package/dist/providers/video-frames.d.ts.map +1 -0
- package/dist/providers/video-frames.js +208 -0
- package/dist/providers/video-frames.js.map +1 -0
- package/dist/providers/video-mime.d.ts +14 -0
- package/dist/providers/video-mime.d.ts.map +1 -0
- package/dist/providers/video-mime.js +44 -0
- package/dist/providers/video-mime.js.map +1 -0
- package/dist/providers/video-to-audio.d.ts +34 -0
- package/dist/providers/video-to-audio.d.ts.map +1 -0
- package/dist/providers/video-to-audio.js +62 -0
- package/dist/providers/video-to-audio.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +20 -8
- package/dist/providers/vllm-provider.d.ts.map +1 -1
- package/dist/providers/vllm-provider.js +44 -23
- package/dist/providers/vllm-provider.js.map +1 -1
- package/dist/providers/voyage-provider.d.ts +12 -3
- package/dist/providers/voyage-provider.d.ts.map +1 -1
- package/dist/providers/voyage-provider.js +21 -7
- package/dist/providers/voyage-provider.js.map +1 -1
- package/dist/providers/xai-provider.d.ts +56 -13
- package/dist/providers/xai-provider.d.ts.map +1 -1
- package/dist/providers/xai-provider.js +307 -22
- package/dist/providers/xai-provider.js.map +1 -1
- package/dist/python-bridge-base.d.ts +358 -0
- package/dist/python-bridge-base.d.ts.map +1 -0
- package/dist/python-bridge-base.js +1557 -0
- package/dist/python-bridge-base.js.map +1 -0
- package/dist/python-bridge-codec.d.ts +28 -0
- package/dist/python-bridge-codec.d.ts.map +1 -0
- package/dist/python-bridge-codec.js +39 -0
- package/dist/python-bridge-codec.js.map +1 -0
- package/dist/python-bridge-factory.d.ts +52 -0
- package/dist/python-bridge-factory.d.ts.map +1 -0
- package/dist/python-bridge-factory.js +69 -0
- package/dist/python-bridge-factory.js.map +1 -0
- package/dist/python-bridge-framing.d.ts +68 -0
- package/dist/python-bridge-framing.d.ts.map +1 -0
- package/dist/python-bridge-framing.js +91 -0
- package/dist/python-bridge-framing.js.map +1 -0
- package/dist/python-bridge-types.d.ts +545 -2
- package/dist/python-bridge-types.d.ts.map +1 -1
- package/dist/python-bridge-types.js +49 -1
- package/dist/python-bridge-types.js.map +1 -1
- package/dist/python-graph-resolver.d.ts +57 -0
- package/dist/python-graph-resolver.d.ts.map +1 -0
- package/dist/python-graph-resolver.js +79 -0
- package/dist/python-graph-resolver.js.map +1 -0
- package/dist/python-node-executor.d.ts +36 -4
- package/dist/python-node-executor.d.ts.map +1 -1
- package/dist/python-node-executor.js +177 -86
- package/dist/python-node-executor.js.map +1 -1
- package/dist/python-stdio-bridge.d.ts +18 -37
- package/dist/python-stdio-bridge.d.ts.map +1 -1
- package/dist/python-stdio-bridge.js +148 -475
- package/dist/python-stdio-bridge.js.map +1 -1
- package/dist/python-websocket-bridge.d.ts +140 -0
- package/dist/python-websocket-bridge.d.ts.map +1 -0
- package/dist/python-websocket-bridge.js +561 -0
- package/dist/python-websocket-bridge.js.map +1 -0
- package/dist/python-worker-stderr.d.ts +4 -0
- package/dist/python-worker-stderr.d.ts.map +1 -0
- package/dist/python-worker-stderr.js +24 -0
- package/dist/python-worker-stderr.js.map +1 -0
- package/dist/recommended-models.d.ts +2 -2
- package/dist/recommended-models.d.ts.map +1 -1
- package/dist/recommended-models.js +44 -4
- package/dist/recommended-models.js.map +1 -1
- package/dist/recoverable-node-error.d.ts +31 -0
- package/dist/recoverable-node-error.d.ts.map +1 -0
- package/dist/recoverable-node-error.js +37 -0
- package/dist/recoverable-node-error.js.map +1 -0
- package/dist/redact-params.d.ts +13 -0
- package/dist/redact-params.d.ts.map +1 -0
- package/dist/redact-params.js +57 -0
- package/dist/redact-params.js.map +1 -0
- package/dist/run-context.d.ts +20 -0
- package/dist/run-context.d.ts.map +1 -0
- package/dist/run-context.js +18 -0
- package/dist/run-context.js.map +1 -0
- package/dist/run-trace-context.d.ts +30 -0
- package/dist/run-trace-context.d.ts.map +1 -0
- package/dist/run-trace-context.js +80 -0
- package/dist/run-trace-context.js.map +1 -0
- package/dist/run-trace-processor.d.ts +65 -0
- package/dist/run-trace-processor.d.ts.map +1 -0
- package/dist/run-trace-processor.js +417 -0
- package/dist/run-trace-processor.js.map +1 -0
- package/dist/run-trace-serialization.d.ts +5 -0
- package/dist/run-trace-serialization.d.ts.map +1 -0
- package/dist/run-trace-serialization.js +113 -0
- package/dist/run-trace-serialization.js.map +1 -0
- package/dist/sandbox-module-catalog.d.ts +43 -0
- package/dist/sandbox-module-catalog.d.ts.map +1 -0
- package/dist/sandbox-module-catalog.js +25 -0
- package/dist/sandbox-module-catalog.js.map +1 -0
- package/dist/storage-workspace.d.ts +62 -0
- package/dist/storage-workspace.d.ts.map +1 -0
- package/dist/storage-workspace.js +441 -0
- package/dist/storage-workspace.js.map +1 -0
- package/dist/swappable-python-bridge.d.ts +83 -0
- package/dist/swappable-python-bridge.d.ts.map +1 -0
- package/dist/swappable-python-bridge.js +246 -0
- package/dist/swappable-python-bridge.js.map +1 -0
- package/dist/telemetry.d.ts +16 -3
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +76 -20
- package/dist/telemetry.js.map +1 -1
- package/dist/testing.d.ts +81 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +130 -0
- package/dist/testing.js.map +1 -0
- package/dist/token-counter.d.ts +30 -0
- package/dist/token-counter.d.ts.map +1 -0
- package/dist/token-counter.js +156 -0
- package/dist/token-counter.js.map +1 -0
- package/dist/trace-exporters.d.ts +11 -28
- package/dist/trace-exporters.d.ts.map +1 -1
- package/dist/trace-exporters.js +69 -22
- package/dist/trace-exporters.js.map +1 -1
- package/dist/tracing-helpers.d.ts +60 -26
- package/dist/tracing-helpers.d.ts.map +1 -1
- package/dist/tracing-helpers.js +100 -25
- package/dist/tracing-helpers.js.map +1 -1
- package/dist/trim-video-window.d.ts +8 -0
- package/dist/trim-video-window.d.ts.map +1 -0
- package/dist/trim-video-window.js +50 -0
- package/dist/trim-video-window.js.map +1 -0
- package/dist/turn-budget.d.ts +278 -0
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/turn-budget.js +365 -0
- package/dist/turn-budget.js.map +1 -0
- package/dist/variable-channel.d.ts +48 -0
- package/dist/variable-channel.d.ts.map +1 -0
- package/dist/variable-channel.js +110 -0
- package/dist/variable-channel.js.map +1 -0
- package/dist/workspace.d.ts +119 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +112 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +121 -19
|
@@ -1,601 +1,1363 @@
|
|
|
1
|
+
import { getTracer } from "../telemetry.js";
|
|
2
|
+
import { tracedProviderLoop, tracedToolCall, beginProviderRound, providerRoundContext, traceContentAllowed, messageTraceContent, traceTextContent } from "./loop-tracing.js";
|
|
1
3
|
/**
|
|
2
|
-
* Claude
|
|
4
|
+
* ClaudeAgentProvider — Claude reached through the official
|
|
5
|
+
* `@anthropic-ai/claude-agent-sdk` instead of an API key.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
7
|
+
* Unlike {@link AnthropicProvider}, this provider sends no `ANTHROPIC_API_KEY`:
|
|
8
|
+
* the SDK drives the bundled `claude` binary (shipped as an optional platform
|
|
9
|
+
* dependency, so there is nothing to install on PATH) and lets it authenticate
|
|
10
|
+
* with the machine's logged-in Claude subscription (the credentials stored
|
|
11
|
+
* under `~/.claude`). The SDK streams `SDKMessage`s, which we translate into the
|
|
12
|
+
* cross-provider {@link ProviderStreamItem} stream.
|
|
6
13
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
14
|
+
* `@anthropic-ai/claude-agent-sdk` is a *soft* (optional peer) dependency: it is
|
|
15
|
+
* not installed by default and must be added with the package manager before
|
|
16
|
+
* this provider can run. It is loaded lazily via {@link loadSdk}, so a missing
|
|
17
|
+
* package surfaces as a clear install hint only when the provider is actually
|
|
18
|
+
* used — the rest of the runtime, and the browser worker bundle, never need it.
|
|
11
19
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* It is a *pure LLM* provider: the agentic tool loop is collapsed to a single
|
|
21
|
+
* turn with every built-in tool disabled (`allowedTools: []`), filesystem
|
|
22
|
+
* settings/skills disabled (`settingSources: []`), permission prompts neutered
|
|
23
|
+
* (`permissionMode: "dontAsk"`), and the default agent system prompt fully
|
|
24
|
+
* replaced (`systemPrompt`), so the model behaves like a plain chat completion —
|
|
25
|
+
* text in, text (and thinking) out.
|
|
14
26
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
27
|
+
* Session continuity: when a `threadId` is supplied the provider routes the
|
|
28
|
+
* thread through a single upstream session and, on subsequent turns, resumes it
|
|
29
|
+
* (`options.resume`) and sends only the new user delta. The continuation token
|
|
30
|
+
* ({@link ProviderSession}) is surfaced as a {@link ProviderSessionUpdate} and
|
|
31
|
+
* persisted by the chat layer onto the assistant message; the in-memory map
|
|
32
|
+
* below is only a within-process cache (the DB column is authoritative).
|
|
19
33
|
*/
|
|
20
|
-
import
|
|
21
|
-
import
|
|
34
|
+
import * as fs from "node:fs/promises";
|
|
35
|
+
import * as os from "node:os";
|
|
36
|
+
import * as path from "node:path";
|
|
37
|
+
import { createLogger, importOptionalModule } from "@nodetool-ai/config";
|
|
22
38
|
import { z } from "zod";
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
import { BaseProvider, budgetStopItem, resolveTurnBudget } from "./base-provider.js";
|
|
40
|
+
import { SDK_NATIVE_TOOL_REPLACEMENTS, sdkNativeReplacements } from "./core-tools.js";
|
|
41
|
+
import { isProviderMessageEvent, isProviderSessionUpdate, isProviderStop } from "./types.js";
|
|
42
|
+
import { hashSystemPrompt } from "./provider-session.js";
|
|
43
|
+
import { PROVIDER_IDS, isFiniteNumber, isNonEmptyString, isObjectLike, isString } from "@nodetool-ai/protocol";
|
|
44
|
+
const log = createLogger("nodetool.runtime.providers.claude-agent");
|
|
45
|
+
/** Replacement system prompt used when the caller supplies none. */
|
|
46
|
+
const DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant.";
|
|
25
47
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
48
|
+
* The Claude Agent SDK's built-in tools. Passed as `disallowedTools` on the
|
|
49
|
+
* tool-free path so a "pure LLM" completion cannot execute host tools (Bash,
|
|
50
|
+
* file, web) under bypassPermissions.
|
|
29
51
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
const SDK_BUILTIN_TOOLS = [
|
|
53
|
+
"Bash",
|
|
54
|
+
"BashOutput",
|
|
55
|
+
"KillShell",
|
|
56
|
+
"Read",
|
|
57
|
+
"Write",
|
|
58
|
+
"Edit",
|
|
59
|
+
"MultiEdit",
|
|
60
|
+
"NotebookEdit",
|
|
61
|
+
"Glob",
|
|
62
|
+
"Grep",
|
|
63
|
+
"WebFetch",
|
|
64
|
+
"WebSearch",
|
|
65
|
+
"Task",
|
|
66
|
+
"TodoWrite"
|
|
67
|
+
];
|
|
68
|
+
/** The built-ins a tool-using turn keeps even when it offered no counterpart. */
|
|
69
|
+
const SDK_WEB_TOOLS = ["WebSearch", "WebFetch"];
|
|
38
70
|
/**
|
|
39
|
-
*
|
|
40
|
-
* a
|
|
71
|
+
* Env vars a nested Claude subscription session leaks into its children. The
|
|
72
|
+
* SDK spawns the bundled CLI as a subprocess; left in place these would make it
|
|
73
|
+
* believe it is itself nested. We strip them via `options.env` rather than any
|
|
74
|
+
* raw `child_process` plumbing.
|
|
41
75
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const original = error instanceof Error ? error.message : String(error);
|
|
46
|
-
const lower = original.toLowerCase();
|
|
47
|
-
// SDK package itself not installed (optional dependency missing).
|
|
48
|
-
if (lower.includes("cannot find module") &&
|
|
49
|
-
lower.includes("claude-agent-sdk")) {
|
|
50
|
-
return new ClaudeAgentError("sdk_not_installed", "The @anthropic-ai/claude-agent-sdk package is not installed. " +
|
|
51
|
-
"The Claude Agent provider requires this optional dependency — install it with " +
|
|
52
|
-
"`npm install @anthropic-ai/claude-agent-sdk`, then restart NodeTool. " +
|
|
53
|
-
`Original error: ${original}`, error);
|
|
54
|
-
}
|
|
55
|
-
// Claude Code CLI binary not on PATH.
|
|
56
|
-
if (lower.includes("enoent") ||
|
|
57
|
-
lower.includes("claude: not found") ||
|
|
58
|
-
lower.includes("command not found: claude") ||
|
|
59
|
-
(lower.includes("spawn") && lower.includes("claude"))) {
|
|
60
|
-
return new ClaudeAgentError("cli_not_found", "Claude Code CLI not found on PATH. The Claude Agent provider spawns the " +
|
|
61
|
-
"`claude` command — install it from https://docs.anthropic.com/claude-code/install " +
|
|
62
|
-
"(or via `npm install -g @anthropic-ai/claude-code`), then run `claude login` to " +
|
|
63
|
-
`authenticate. Original error: ${original}`, error);
|
|
64
|
-
}
|
|
65
|
-
// Running as root — SDK refuses --dangerously-skip-permissions.
|
|
66
|
-
if (lower.includes("dangerously-skip-permissions") ||
|
|
67
|
-
(lower.includes("root") && lower.includes("refus")) ||
|
|
68
|
-
(lower.includes("uid") && lower.includes("0"))) {
|
|
69
|
-
return new ClaudeAgentError("running_as_root", "The Claude Agent SDK refuses to run as root (uid=0) because it passes " +
|
|
70
|
-
"--dangerously-skip-permissions to the Claude CLI. Run NodeTool as a non-root " +
|
|
71
|
-
"user, or switch to the `anthropic` provider which uses ANTHROPIC_API_KEY " +
|
|
72
|
-
`directly. Original error: ${original}`, error);
|
|
73
|
-
}
|
|
74
|
-
// Abort.
|
|
75
|
-
if (lower.includes("abort") ||
|
|
76
|
-
error instanceof Error && error.name === "AbortError") {
|
|
77
|
-
return new ClaudeAgentError("aborted", "Claude Agent request was aborted.", error);
|
|
78
|
-
}
|
|
79
|
-
// Authentication failure.
|
|
80
|
-
if (lower.includes("unauthorized") ||
|
|
81
|
-
lower.includes("401") ||
|
|
82
|
-
lower.includes("403") ||
|
|
83
|
-
lower.includes("forbidden") ||
|
|
84
|
-
lower.includes("not logged in") ||
|
|
85
|
-
lower.includes("not authenticated") ||
|
|
86
|
-
lower.includes("invalid api key") ||
|
|
87
|
-
lower.includes("authentication") ||
|
|
88
|
-
lower.includes("oauth")) {
|
|
89
|
-
return new ClaudeAgentError("auth", "Claude Agent authentication failed. Run `claude login` in a terminal to " +
|
|
90
|
-
"sign in with your Claude subscription, or set ANTHROPIC_API_KEY. " +
|
|
91
|
-
`Original error: ${original}`, error);
|
|
92
|
-
}
|
|
93
|
-
// Rate limit.
|
|
94
|
-
if (lower.includes("429") ||
|
|
95
|
-
lower.includes("rate limit") ||
|
|
96
|
-
lower.includes("too many requests") ||
|
|
97
|
-
lower.includes("quota")) {
|
|
98
|
-
return new ClaudeAgentError("rate_limit", "Claude Agent rate limit reached. Wait a moment and retry, or check your " +
|
|
99
|
-
`Claude subscription usage limits. Original error: ${original}`, error);
|
|
100
|
-
}
|
|
101
|
-
// Context length.
|
|
102
|
-
if (lower.includes("context length") ||
|
|
103
|
-
lower.includes("context window") ||
|
|
104
|
-
lower.includes("token limit") ||
|
|
105
|
-
lower.includes("maximum context") ||
|
|
106
|
-
lower.includes("too long") ||
|
|
107
|
-
lower.includes("prompt is too long")) {
|
|
108
|
-
return new ClaudeAgentError("context_length", "Claude Agent request exceeded the model's context window. Shorten the " +
|
|
109
|
-
"conversation (clear older messages) or switch to a model with a larger " +
|
|
110
|
-
`context window. Original error: ${original}`, error);
|
|
111
|
-
}
|
|
112
|
-
// Invalid / unknown model.
|
|
113
|
-
if (lower.includes("model") &&
|
|
114
|
-
(lower.includes("invalid") ||
|
|
115
|
-
lower.includes("not found") ||
|
|
116
|
-
lower.includes("unknown") ||
|
|
117
|
-
lower.includes("does not exist"))) {
|
|
118
|
-
return new ClaudeAgentError("invalid_model", "Claude Agent could not use the requested model. Check that the model ID " +
|
|
119
|
-
"is valid and available on your Claude subscription. " +
|
|
120
|
-
`Original error: ${original}`, error);
|
|
121
|
-
}
|
|
122
|
-
return new ClaudeAgentError("unknown", `Claude Agent provider error: ${original}`, error);
|
|
123
|
-
}
|
|
124
|
-
const CLAUDE_AGENT_MODELS = [
|
|
125
|
-
{
|
|
126
|
-
id: "claude-opus-4-7",
|
|
127
|
-
name: "Claude Opus 4.7",
|
|
128
|
-
provider: "claude_agent"
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: "claude-sonnet-4-6",
|
|
132
|
-
name: "Claude Sonnet 4.6",
|
|
133
|
-
provider: "claude_agent"
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: "claude-haiku-4-5",
|
|
137
|
-
name: "Claude Haiku 4.5",
|
|
138
|
-
provider: "claude_agent"
|
|
139
|
-
}
|
|
140
|
-
];
|
|
141
|
-
const DISALLOWED_TOOLS = [];
|
|
142
|
-
function extractText(content) {
|
|
143
|
-
if (typeof content === "string")
|
|
144
|
-
return content;
|
|
145
|
-
if (Array.isArray(content)) {
|
|
146
|
-
return content
|
|
147
|
-
.filter((c) => c.type === "text")
|
|
148
|
-
.map((c) => c.text)
|
|
149
|
-
.join("\n");
|
|
150
|
-
}
|
|
151
|
-
return String(content ?? "");
|
|
152
|
-
}
|
|
76
|
+
const NESTED_SESSION_ENV = /^(CLAUDECODE|CLAUDE_(CODE|SESSION|ENABLE|AFTER|AUTO)_[A-Za-z0-9_]*)$/;
|
|
77
|
+
/** MCP server name under which NodeTool's tools are exposed to the SDK. */
|
|
78
|
+
const TOOL_SERVER_NAME = "nodetool_tools";
|
|
153
79
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
80
|
+
* Turns a structured-output call may take. The SDK validates the answer
|
|
81
|
+
* against the schema and, when it does not match, hands the model the errors
|
|
82
|
+
* and asks again — each retry is a turn. With one turn, the first answer that
|
|
83
|
+
* misses the schema fails the call as `error_max_turns`.
|
|
156
84
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const enumValues = prop.enum;
|
|
160
|
-
if (enumValues && Array.isArray(enumValues)) {
|
|
161
|
-
// z.enum requires string literals
|
|
162
|
-
if (enumValues.every((v) => typeof v === "string")) {
|
|
163
|
-
return z.enum(enumValues);
|
|
164
|
-
}
|
|
165
|
-
return z.any();
|
|
166
|
-
}
|
|
167
|
-
switch (type) {
|
|
168
|
-
case "string":
|
|
169
|
-
return z.string();
|
|
170
|
-
case "number":
|
|
171
|
-
case "integer":
|
|
172
|
-
return z.number();
|
|
173
|
-
case "boolean":
|
|
174
|
-
return z.boolean();
|
|
175
|
-
case "array": {
|
|
176
|
-
const items = prop.items;
|
|
177
|
-
if (items) {
|
|
178
|
-
return z.array(jsonSchemaPropertyToZod(items, z));
|
|
179
|
-
}
|
|
180
|
-
return z.array(z.any());
|
|
181
|
-
}
|
|
182
|
-
case "object": {
|
|
183
|
-
const properties = prop.properties;
|
|
184
|
-
if (properties) {
|
|
185
|
-
const shape = {};
|
|
186
|
-
const required = prop.required ?? [];
|
|
187
|
-
for (const [key, val] of Object.entries(properties)) {
|
|
188
|
-
let zodType = jsonSchemaPropertyToZod(val, z);
|
|
189
|
-
if (!required.includes(key)) {
|
|
190
|
-
zodType = zodType.optional();
|
|
191
|
-
}
|
|
192
|
-
shape[key] = zodType;
|
|
193
|
-
}
|
|
194
|
-
return z.object(shape);
|
|
195
|
-
}
|
|
196
|
-
return z.record(z.string(), z.any());
|
|
197
|
-
}
|
|
198
|
-
default:
|
|
199
|
-
return z.any();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
85
|
+
const STRUCTURED_OUTPUT_MAX_TURNS = 3;
|
|
86
|
+
const TOOL_PREFIX = `mcp__${TOOL_SERVER_NAME}__`;
|
|
202
87
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
88
|
+
* The member expression the retired guest toolbelt was called through:
|
|
89
|
+
* `await tools.<name>({…})`. Models trained on it still emit it verbatim as a
|
|
90
|
+
* tool name, so it is stripped on the way in. Nothing produces it any more.
|
|
205
91
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
92
|
+
const GUEST_TOOL_PREFIX = "tools.";
|
|
93
|
+
/** Default cap on internal agent turns when tools are in play. */
|
|
94
|
+
const DEFAULT_TOOL_TURNS = 16;
|
|
95
|
+
/** Name of the local plugin the user's DB skills are materialized into. */
|
|
96
|
+
const SKILLS_PLUGIN_NAME = "nodetool-user-skills";
|
|
97
|
+
/**
|
|
98
|
+
* Lazily import the optional `@anthropic-ai/claude-agent-sdk`. The SDK is a soft
|
|
99
|
+
* (peer) dependency — it is not bundled with the app and must be installed at
|
|
100
|
+
* runtime from the Package Manager (Software → Claude Agent SDK), which drops it
|
|
101
|
+
* into the user-managed optional-node `node_modules`. {@link importOptionalModule}
|
|
102
|
+
* hides the specifier from bundlers (so the browser/worker bundle never pulls in
|
|
103
|
+
* the Node-only SDK) and falls back to resolving it from that optional root via
|
|
104
|
+
* `NODETOOL_OPTIONAL_NODE_MODULES`, so a plain ESM `import` (which ignores
|
|
105
|
+
* NODE_PATH) still finds a Package-Manager install. A missing package surfaces
|
|
106
|
+
* as a clear, actionable error at call time.
|
|
107
|
+
*/
|
|
108
|
+
async function loadSdk() {
|
|
109
|
+
try {
|
|
110
|
+
return await importOptionalModule("@anthropic-ai/claude-agent-sdk");
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
throw new Error("The Claude Agent provider requires the optional " +
|
|
114
|
+
"'@anthropic-ai/claude-agent-sdk' package, which is not installed. " +
|
|
115
|
+
"Install it from the Package Manager (Software → Claude Agent SDK).", { cause: err });
|
|
223
116
|
}
|
|
224
|
-
return shape;
|
|
225
117
|
}
|
|
226
118
|
export class ClaudeAgentProvider extends BaseProvider {
|
|
119
|
+
/** The SDK runs with bypassPermissions, so its built-in `WebSearch` is live. */
|
|
120
|
+
get supportsNativeWebSearch() {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
227
123
|
static requiredSecrets() {
|
|
124
|
+
// Auth lives in the SDK's own credential store (~/.claude), not in an env
|
|
125
|
+
// secret — there is nothing for the registry to resolve.
|
|
228
126
|
return [];
|
|
229
127
|
}
|
|
128
|
+
injectedQueryFn;
|
|
129
|
+
injectedCreateMcpServerFn;
|
|
230
130
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
131
|
+
* Within-process cache of the active session per thread. The persisted
|
|
132
|
+
* `provider_session` column is the source of truth; this only spares a DB
|
|
133
|
+
* round-trip for back-to-back turns in the same process.
|
|
233
134
|
*/
|
|
234
|
-
|
|
235
|
-
constructor(_secrets = {}) {
|
|
236
|
-
super(
|
|
135
|
+
sessions = new Map();
|
|
136
|
+
constructor(_secrets = {}, options = {}) {
|
|
137
|
+
super(PROVIDER_IDS.CLAUDE_AGENT_SDK);
|
|
138
|
+
this.injectedQueryFn = options.queryFn ?? null;
|
|
139
|
+
this.injectedCreateMcpServerFn = options.createMcpServerFn ?? null;
|
|
140
|
+
}
|
|
141
|
+
/** The subscription token is the SDK's business — never hand it to a sandbox. */
|
|
142
|
+
getContainerEnv() {
|
|
143
|
+
return {};
|
|
237
144
|
}
|
|
238
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Tools run inside the SDK's own agent loop ({@link generateLoop}): NodeTool's
|
|
147
|
+
* tools are exposed as an in-process MCP server whose handlers call back into
|
|
148
|
+
* the harness's tool executor. The single-turn {@link generateMessages} stays
|
|
149
|
+
* tool-free.
|
|
150
|
+
*/
|
|
151
|
+
async hasToolSupport() {
|
|
239
152
|
return true;
|
|
240
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Stable model aliases the SDK resolves to the latest dated model. Aliases
|
|
156
|
+
* avoid pinning a version that ages out; the concrete model id the SDK
|
|
157
|
+
* selects is captured from the stream for accurate cost attribution.
|
|
158
|
+
*/
|
|
241
159
|
async getAvailableLanguageModels() {
|
|
242
|
-
|
|
160
|
+
const provider = PROVIDER_IDS.CLAUDE_AGENT_SDK;
|
|
161
|
+
return [
|
|
162
|
+
{ id: "fable", name: "Claude Fable (subscription)", provider },
|
|
163
|
+
{ id: "opus", name: "Claude Opus (subscription)", provider },
|
|
164
|
+
{ id: "sonnet", name: "Claude Sonnet (subscription)", provider },
|
|
165
|
+
{ id: "haiku", name: "Claude Haiku (subscription)", provider }
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
/** Resolve the SDK `query`, lazily importing it off the Node-only package. */
|
|
169
|
+
async loadQuery() {
|
|
170
|
+
if (this.injectedQueryFn)
|
|
171
|
+
return this.injectedQueryFn;
|
|
172
|
+
const mod = await loadSdk();
|
|
173
|
+
return mod.query;
|
|
174
|
+
}
|
|
175
|
+
/** Resolve the SDK `createSdkMcpServer`, lazily importing the Node-only pkg. */
|
|
176
|
+
async loadCreateMcpServer() {
|
|
177
|
+
if (this.injectedCreateMcpServerFn)
|
|
178
|
+
return this.injectedCreateMcpServerFn;
|
|
179
|
+
const mod = await loadSdk();
|
|
180
|
+
// SAFETY: the SDK's factory is generic over each tool's Zod shape; this
|
|
181
|
+
// provider always hands it the default `AnyZodRawShape` definitions.
|
|
182
|
+
return mod.createSdkMcpServer;
|
|
243
183
|
}
|
|
244
184
|
/**
|
|
245
|
-
*
|
|
185
|
+
* Run the SDK's own agent loop. Tool-free turns behave exactly like
|
|
186
|
+
* {@link generateMessages}; with `tools` we expose them as an in-process MCP
|
|
187
|
+
* server whose handlers call `executeTool`, let the SDK loop (`maxTurns > 1`),
|
|
188
|
+
* and translate its `tool_use`/`tool_result` stream into ToolCall items and
|
|
189
|
+
* persistable {@link ProviderMessageEvent}s.
|
|
246
190
|
*/
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
191
|
+
async *generateLoop(args) {
|
|
192
|
+
return yield* tracedProviderLoop(this.provider, args.model, () => this.generateSdkLoop(args));
|
|
193
|
+
}
|
|
194
|
+
async *generateSdkLoop(args) {
|
|
195
|
+
// The SDK owns the loop, so honoring the budget is this override's job:
|
|
196
|
+
// reserve before the first turn, and again after every assistant turn that
|
|
197
|
+
// requested tools — that is the point where the SDK will make another call.
|
|
198
|
+
// A refusal aborts the session instead of letting it spend past the cap.
|
|
199
|
+
const { runBudget, turnBudget } = resolveTurnBudget(args.turnBudget);
|
|
200
|
+
if (runBudget?.deadline.expired()) {
|
|
201
|
+
yield budgetStopItem(runBudget);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Every turn this session makes holds its own reservation, and the whole
|
|
205
|
+
// set is settled in the `finally` below — the SDK reports one number for
|
|
206
|
+
// the session, not one per turn.
|
|
207
|
+
const reservations = [];
|
|
208
|
+
if (turnBudget) {
|
|
209
|
+
const first = this._admitTurn(turnBudget, args.model, args.messages);
|
|
210
|
+
if (first === null) {
|
|
211
|
+
yield budgetStopItem(runBudget);
|
|
212
|
+
return;
|
|
253
213
|
}
|
|
214
|
+
reservations.push(first);
|
|
254
215
|
}
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
216
|
+
// Normally prefer native equivalents to duplicate MCP surfaces. A caller
|
|
217
|
+
// restricting the loop keeps its own dispatch instead.
|
|
218
|
+
const offered = args.tools ?? [];
|
|
219
|
+
const replaced = args.providedToolsOnly
|
|
220
|
+
? new Set()
|
|
221
|
+
: sdkNativeReplacements(offered.map((t) => t.name), args.workspaceDir);
|
|
222
|
+
const tools = offered.filter((t) => !replaced.has(t.name));
|
|
223
|
+
if (replaced.size > 0) {
|
|
224
|
+
log.debug("Using SDK built-ins in place of NodeTool tools", {
|
|
225
|
+
replaced: [...replaced]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
beginProviderRound(this.provider, args.model);
|
|
229
|
+
const executeTool = args.executeTool;
|
|
230
|
+
// A tool dispatches either through its own `execute` or the harness
|
|
231
|
+
// `executeTool`; build the MCP server when at least one route exists.
|
|
232
|
+
const hasToolExecute = tools.some((t) => t.execute);
|
|
233
|
+
// The SDK loop is stopped by aborting this controller — a terminal tool
|
|
234
|
+
// fires it after running. Bridge the caller's signal into it too, and pass
|
|
235
|
+
// its signal (overriding args.signal) down so runTurn cancels on either.
|
|
236
|
+
const abortController = new AbortController();
|
|
237
|
+
const onExternalAbort = () => abortController.abort();
|
|
238
|
+
if (args.signal) {
|
|
239
|
+
if (args.signal.aborted)
|
|
240
|
+
abortController.abort();
|
|
241
|
+
else
|
|
242
|
+
args.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
243
|
+
}
|
|
244
|
+
let mcp = null;
|
|
245
|
+
if (tools.length > 0 && (executeTool || hasToolExecute)) {
|
|
246
|
+
const createServer = await this.loadCreateMcpServer();
|
|
247
|
+
// MCP tool results carry typed content blocks, so image-bearing results
|
|
248
|
+
// (view_image) are returned as real image blocks the SDK hands to Claude —
|
|
249
|
+
// no flattening to text on the agent-SDK path.
|
|
250
|
+
const defs = tools.map((t) => toolDefinition(t, async (name, toolArgs) => {
|
|
251
|
+
// The SDK runs parallel calls of one tool in the same millisecond,
|
|
252
|
+
// so a clock alone would hand them one id.
|
|
253
|
+
const toolCallId = `call_${name}_${crypto.randomUUID()}`;
|
|
254
|
+
const result = t.execute
|
|
255
|
+
? await tracedToolCall({ id: toolCallId, name, args: toolArgs }, () => { const execute = t.execute; if (!execute) {
|
|
256
|
+
throw new Error("Tool dispatch was removed");
|
|
257
|
+
} return execute(toolArgs, toolCallId); })
|
|
258
|
+
: executeTool
|
|
259
|
+
? await tracedToolCall({ id: toolCallId, name, args: toolArgs }, () => executeTool({ id: toolCallId, name, args: toolArgs }))
|
|
260
|
+
: `Tool "${name}" is not available`;
|
|
261
|
+
// A terminal tool ends the SDK loop after its result is delivered.
|
|
262
|
+
if (t.terminal)
|
|
263
|
+
abortController.abort();
|
|
264
|
+
return result;
|
|
265
|
+
}, args.resolveMedia));
|
|
266
|
+
const server = createServer({
|
|
267
|
+
name: TOOL_SERVER_NAME,
|
|
268
|
+
version: "1.0.0",
|
|
269
|
+
tools: defs
|
|
270
|
+
});
|
|
271
|
+
mcp = {
|
|
272
|
+
mcpServers: { [TOOL_SERVER_NAME]: server },
|
|
273
|
+
allowedTools: tools.map((t) => `${TOOL_PREFIX}${t.name}`)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Materialize the user's skills into a throwaway local plugin the SDK's
|
|
277
|
+
// native skill loader discovers. Best-effort: a disk failure drops skills
|
|
278
|
+
// for the turn, it does not sink it. Cleaned up in the `finally` below.
|
|
279
|
+
let skillsPlugin = null;
|
|
280
|
+
if (!args.providedToolsOnly && (args.skills?.length ?? 0) > 0) {
|
|
281
|
+
try {
|
|
282
|
+
skillsPlugin = await materializeSkillsPlugin(args.skills ?? []);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
log.warn("Failed to materialize user skills for the SDK", {
|
|
286
|
+
error: err instanceof Error ? err.message : String(err)
|
|
287
|
+
});
|
|
260
288
|
}
|
|
261
289
|
}
|
|
262
|
-
|
|
263
|
-
|
|
290
|
+
const costBefore = turnBudget ? this.getTotalCost() : 0;
|
|
291
|
+
// Set when the budget refuses a further turn: the SDK session is aborted
|
|
292
|
+
// and the stop is yielded once the stream has drained, so it stays the
|
|
293
|
+
// loop's final item.
|
|
294
|
+
let budgetStop = null;
|
|
295
|
+
try {
|
|
296
|
+
const stream = this.runWithSession({ ...args, signal: abortController.signal }, {
|
|
297
|
+
emitMessages: true,
|
|
298
|
+
// Turns are for tool rounds, and a replaced tool still costs one —
|
|
299
|
+
// key this on what the caller offered, not on what survived into MCP.
|
|
300
|
+
maxTurns: offered.length > 0 ? (args.maxIterations ?? DEFAULT_TOOL_TURNS) : 1,
|
|
301
|
+
mcp,
|
|
302
|
+
toolsOffered: offered.length > 0,
|
|
303
|
+
providedToolsOnly: args.providedToolsOnly,
|
|
304
|
+
builtinTools: args.providedToolsOnly
|
|
305
|
+
? []
|
|
306
|
+
: turnBuiltins(replaced, offered.length > 0, skillsPlugin !== null),
|
|
307
|
+
cwd: args.workspaceDir,
|
|
308
|
+
skillsPlugin
|
|
309
|
+
});
|
|
310
|
+
// The SDK grows the conversation internally, so reserving against the
|
|
311
|
+
// opening prompt every time would under-count every later turn — badly,
|
|
312
|
+
// on a tool-heavy loop. The messages it emits are the same ones it is
|
|
313
|
+
// accumulating, so the transcript is rebuilt here to reserve against.
|
|
314
|
+
const transcript = [...args.messages];
|
|
315
|
+
for await (const item of stream) {
|
|
316
|
+
yield item;
|
|
317
|
+
if (!turnBudget)
|
|
318
|
+
continue;
|
|
319
|
+
if (!isProviderMessageEvent(item))
|
|
320
|
+
continue;
|
|
321
|
+
transcript.push(item.message);
|
|
322
|
+
const isToolCallingTurn = item.message.role === "assistant" &&
|
|
323
|
+
(item.message.toolCalls?.length ?? 0) > 0;
|
|
324
|
+
if (!isToolCallingTurn)
|
|
325
|
+
continue;
|
|
326
|
+
const next = this._admitTurn(turnBudget, args.model, transcript);
|
|
327
|
+
if (next === null) {
|
|
328
|
+
budgetStop = budgetStopItem(runBudget);
|
|
329
|
+
abortController.abort();
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
reservations.push(next);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
// The SDK reports usage once, on the terminal `result` message, so a
|
|
337
|
+
// per-turn actual doesn't exist here. Reservations accumulate untouched
|
|
338
|
+
// for the session's duration — conservative by construction — and the
|
|
339
|
+
// real number lands when the session ends.
|
|
340
|
+
//
|
|
341
|
+
// Except when it doesn't: an aborted session never emits `result`, so
|
|
342
|
+
// nothing is recorded even though its turns ran and were billed. That is
|
|
343
|
+
// the *normal* ending here — a terminal tool (`finish_step`) aborts the
|
|
344
|
+
// query — so booking zero would mean the cap never counted a single
|
|
345
|
+
// successful decision on this provider. A zero delta is therefore read
|
|
346
|
+
// as "unknown", and the reserved worst case is charged instead.
|
|
347
|
+
if (turnBudget) {
|
|
348
|
+
const observed = this.getTotalCost() - costBefore;
|
|
349
|
+
// One number covers the whole session, so it is booked against the
|
|
350
|
+
// first reservation and the rest settle at zero; with no number at
|
|
351
|
+
// all, each reservation is charged its own worst case.
|
|
352
|
+
let remaining = observed > 0 ? observed : null;
|
|
353
|
+
for (const reservation of reservations) {
|
|
354
|
+
turnBudget.commit(reservation, remaining);
|
|
355
|
+
if (remaining !== null)
|
|
356
|
+
remaining = 0;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (args.signal)
|
|
360
|
+
args.signal.removeEventListener("abort", onExternalAbort);
|
|
361
|
+
// Remove the throwaway skills plugin. The SDK's subprocess has finished
|
|
362
|
+
// reading it by now (the stream is drained), so best-effort cleanup is
|
|
363
|
+
// safe; a leftover temp dir is not worth failing the turn over.
|
|
364
|
+
if (skillsPlugin) {
|
|
365
|
+
await fs
|
|
366
|
+
.rm(skillsPlugin.dir, { recursive: true, force: true })
|
|
367
|
+
.catch(() => { });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (budgetStop) {
|
|
371
|
+
yield budgetStop;
|
|
264
372
|
}
|
|
265
|
-
return { systemPrompt, prompt };
|
|
266
|
-
}
|
|
267
|
-
/** Look up an existing Claude session for the given threadId. */
|
|
268
|
-
getSessionId(threadId) {
|
|
269
|
-
if (!threadId)
|
|
270
|
-
return undefined;
|
|
271
|
-
return this._sessions.get(threadId);
|
|
272
|
-
}
|
|
273
|
-
/** Store the Claude session ID for a threadId. */
|
|
274
|
-
setSessionId(threadId, sessionId) {
|
|
275
|
-
this._sessions.set(threadId, sessionId);
|
|
276
373
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
374
|
+
async *generateMessages(args) {
|
|
375
|
+
// Single-turn, tool-free primitive. The agent loop lives in generateLoop.
|
|
376
|
+
const resultTool = args.tools?.length === 1 &&
|
|
377
|
+
args.toolChoice === args.tools[0].name
|
|
378
|
+
? args.tools[0]
|
|
379
|
+
: undefined;
|
|
380
|
+
const resultSchema = resultTool?.inputSchema;
|
|
381
|
+
const config = {
|
|
382
|
+
emitMessages: false,
|
|
383
|
+
maxTurns: args.maxTurns ?? (resultTool ? STRUCTURED_OUTPUT_MAX_TURNS : 1),
|
|
384
|
+
mcp: null,
|
|
385
|
+
toolsOffered: false,
|
|
386
|
+
builtinTools: []
|
|
387
|
+
};
|
|
388
|
+
if (resultTool && resultSchema) {
|
|
389
|
+
config.outputFormat = { type: "json_schema", schema: resultSchema };
|
|
390
|
+
config.resultToolName = resultTool.name;
|
|
391
|
+
}
|
|
392
|
+
yield* this.runWithSession(args, config);
|
|
280
393
|
}
|
|
281
394
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
395
|
+
* Shared session machinery for {@link generateMessages} and
|
|
396
|
+
* {@link generateLoop}: decide resume vs fresh (best-effort, with a
|
|
397
|
+
* full-history priming fallback), then drive one SDK turn via {@link runTurn}.
|
|
398
|
+
* `config` selects single-turn streaming vs the tool/message-emitting loop.
|
|
284
399
|
*/
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
return sdk.tool(t.name, t.description ?? "", zodShape, async (args) => {
|
|
289
|
-
// Track the tool call for yielding to the caller
|
|
290
|
-
const toolCall = {
|
|
291
|
-
id: `call_${Date.now()}_${toolCallTracker.length}`,
|
|
292
|
-
name: t.name,
|
|
293
|
-
args
|
|
294
|
-
};
|
|
295
|
-
toolCallTracker.push(toolCall);
|
|
296
|
-
log.debug("MCP tool called", { name: t.name, args });
|
|
297
|
-
try {
|
|
298
|
-
const result = await onToolCall(t.name, args);
|
|
299
|
-
return { content: [{ type: "text", text: result }] };
|
|
300
|
-
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
const errorMsg = `Tool execution error: ${e}`;
|
|
303
|
-
log.error("MCP tool error", { name: t.name, error: errorMsg });
|
|
304
|
-
return {
|
|
305
|
-
content: [{ type: "text", text: errorMsg }],
|
|
306
|
-
isError: true
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
return sdk.createSdkMcpServer({
|
|
312
|
-
name: MCP_SERVER_NAME,
|
|
313
|
-
version: "1.0.0",
|
|
314
|
-
tools: mcpTools
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
async *generateMessages(args) {
|
|
318
|
-
const toolCount = args.tools?.length ?? 0;
|
|
319
|
-
const hasOnToolCall = !!args.onToolCall;
|
|
320
|
-
const hasTools = toolCount > 0 && hasOnToolCall;
|
|
321
|
-
const { systemPrompt, prompt } = this.extractPrompt(args.messages);
|
|
400
|
+
async *runWithSession(args, config) {
|
|
401
|
+
const systemPrompt = extractSystemPrompt(args.messages);
|
|
402
|
+
const systemHash = hashSystemPrompt(systemPrompt);
|
|
322
403
|
const threadId = args.threadId ?? null;
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
404
|
+
// Source of truth is the token threaded in from the persisted assistant
|
|
405
|
+
// message; the in-memory cache is a fallback for same-process turns.
|
|
406
|
+
const prior = args.providerSession ??
|
|
407
|
+
(threadId ? (this.sessions.get(threadId) ?? null) : null);
|
|
408
|
+
const canResume = prior != null &&
|
|
409
|
+
prior.providerId === this.provider &&
|
|
410
|
+
prior.model === args.model &&
|
|
411
|
+
(prior.systemHash == null || prior.systemHash === systemHash) &&
|
|
412
|
+
args.messages.length > prior.checkpoint;
|
|
413
|
+
// Cache the session in-process only when the caller doesn't thread a token
|
|
414
|
+
// in (the CLI path). When it does (the websocket path), the DB is
|
|
415
|
+
// authoritative and the emitted checkpoint may be relative to a trimmed
|
|
416
|
+
// view, so caching it would corrupt a later cache-based resume.
|
|
417
|
+
const cacheSession = args.providerSession == null;
|
|
418
|
+
// RESUME is best-effort: a session file may have been pruned/expired. If the
|
|
419
|
+
// resume query fails *before* any content reached the consumer, fall back to
|
|
420
|
+
// a fresh session; if it fails mid-stream we surface the error instead.
|
|
421
|
+
const emitted = { content: false };
|
|
422
|
+
if (canResume && prior) {
|
|
423
|
+
const delta = buildResumeDelta(args.messages, prior.checkpoint);
|
|
424
|
+
try {
|
|
425
|
+
yield* this.runTurn(args, {
|
|
426
|
+
prompt: delta,
|
|
427
|
+
resume: prior.token,
|
|
428
|
+
systemPrompt,
|
|
429
|
+
systemHash,
|
|
430
|
+
threadId,
|
|
431
|
+
emitted,
|
|
432
|
+
cacheSession,
|
|
433
|
+
config
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
if (emitted.content)
|
|
439
|
+
throw err;
|
|
440
|
+
log.warn("Claude session resume failed; starting fresh", {
|
|
441
|
+
error: err instanceof Error ? err.message : String(err)
|
|
442
|
+
});
|
|
443
|
+
}
|
|
360
444
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
445
|
+
// Fresh / fallback. When the caller handed us only the delta (a trimmed
|
|
446
|
+
// `messages` view) plus a loader, prime from the full conversation so prior
|
|
447
|
+
// context isn't lost — the session may have expired or the system prompt
|
|
448
|
+
// changed. Otherwise `messages` already holds the full history.
|
|
449
|
+
const freshMessages = args.loadFullHistory
|
|
450
|
+
? await args.loadFullHistory()
|
|
451
|
+
: args.messages;
|
|
452
|
+
yield* this.runTurn(args, {
|
|
453
|
+
prompt: buildFreshPrompt(freshMessages),
|
|
454
|
+
resume: undefined,
|
|
455
|
+
systemPrompt,
|
|
456
|
+
systemHash,
|
|
364
457
|
threadId,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
458
|
+
emitted,
|
|
459
|
+
cacheSession,
|
|
460
|
+
config
|
|
368
461
|
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
key.startsWith("CLAUDE_CODE_") ||
|
|
378
|
-
key.startsWith("CLAUDE_SESSION_") ||
|
|
379
|
-
key.startsWith("CLAUDE_ENABLE_") ||
|
|
380
|
-
key.startsWith("CLAUDE_AFTER_") ||
|
|
381
|
-
key.startsWith("CLAUDE_AUTO_")) {
|
|
382
|
-
delete cleanEnv[key];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// Bridge AbortSignal → AbortController for the SDK
|
|
386
|
-
let abortController;
|
|
462
|
+
}
|
|
463
|
+
/** Drive one SDK `query` turn and translate its messages into stream items. */
|
|
464
|
+
async *runTurn(args, plan) {
|
|
465
|
+
const queryFn = await this.loadQuery();
|
|
466
|
+
// Cancellation: the SDK stops the underlying query when its AbortController
|
|
467
|
+
// fires. Bridge the caller's signal onto a fresh controller per turn.
|
|
468
|
+
const abortController = new AbortController();
|
|
469
|
+
const onAbort = () => abortController.abort();
|
|
387
470
|
if (args.signal) {
|
|
388
|
-
|
|
389
|
-
if (args.signal.aborted) {
|
|
471
|
+
if (args.signal.aborted)
|
|
390
472
|
abortController.abort();
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
args.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
394
|
-
}
|
|
473
|
+
else
|
|
474
|
+
args.signal.addEventListener("abort", onAbort, { once: true });
|
|
395
475
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
476
|
+
const options = {
|
|
477
|
+
systemPrompt: plan.systemPrompt,
|
|
478
|
+
// Pass aliases ("sonnet"/"opus"/"haiku") straight through to the SDK.
|
|
479
|
+
model: args.model || undefined,
|
|
480
|
+
// 1 for a single turn; higher when tools may drive multiple rounds.
|
|
481
|
+
maxTurns: plan.config.maxTurns,
|
|
482
|
+
// Auto-approve NodeTool's MCP tools (this is the no-prompt allowlist, not
|
|
483
|
+
// an availability restriction). Empty (pure LLM) when tool-free.
|
|
484
|
+
allowedTools: plan.config.mcp?.allowedTools ?? [],
|
|
485
|
+
// Do NOT load repo .claude / CLAUDE.md / skills.
|
|
486
|
+
settingSources: [],
|
|
487
|
+
// Run without asking for permissions: never prompt AND never deny. This
|
|
488
|
+
// lets the SDK agent use the built-ins `tools` keeps alongside
|
|
489
|
+
// NodeTool's MCP tools. bypassPermissions requires the explicit
|
|
490
|
+
// safety flag below.
|
|
491
|
+
permissionMode: "bypassPermissions",
|
|
492
|
+
allowDangerouslySkipPermissions: true,
|
|
493
|
+
// On the tool-free path (the documented "pure LLM" primitive used by
|
|
494
|
+
// generateMessage/generateMessages), explicitly disable the SDK's
|
|
495
|
+
// built-in tools. Under bypassPermissions they are otherwise live and
|
|
496
|
+
// auto-approved, so a prompt-injected "call Bash …" in untrusted text
|
|
497
|
+
// being summarized/classified would execute on the host. allowedTools is
|
|
498
|
+
// only a no-prompt approval list, not an availability restriction.
|
|
499
|
+
//
|
|
500
|
+
// The test is `toolsOffered`, not `mcp`: a caller whose every tool was
|
|
501
|
+
// replaced by a built-in offered tools and has no MCP server, and
|
|
502
|
+
// disabling the built-ins there would leave it with nothing.
|
|
503
|
+
//
|
|
504
|
+
// `ToolSearch` is disabled on BOTH paths. It searches tools the SDK has
|
|
505
|
+
// deferred, and NodeTool defers none — it registers its handful of tools
|
|
506
|
+
// in-process — so the search always comes back empty. A model that fell
|
|
507
|
+
// back to it after a mis-named tool call got nothing and stalled the turn.
|
|
508
|
+
// Supplied-only loops also disable Skill and keep all supplied dispatch
|
|
509
|
+
// inside the caller's MCP boundary.
|
|
510
|
+
disallowedTools: plan.config.providedToolsOnly
|
|
511
|
+
? [...SDK_BUILTIN_TOOLS, "Skill", "ToolSearch"]
|
|
512
|
+
: plan.config.toolsOffered
|
|
513
|
+
? ["ToolSearch"]
|
|
514
|
+
: [...SDK_BUILTIN_TOOLS, "ToolSearch"],
|
|
515
|
+
tools: plan.config.builtinTools,
|
|
516
|
+
// Only the in-process NodeTool server. Without this the child also loads
|
|
517
|
+
// the account's claude.ai connectors and the user's MCP servers, and
|
|
518
|
+
// their tool schemas grow every request of the loop.
|
|
519
|
+
strictMcpConfig: true,
|
|
520
|
+
// Hide the skills bundled with the SDK. The plugin below names its own.
|
|
521
|
+
skills: [],
|
|
522
|
+
includePartialMessages: true,
|
|
523
|
+
// Setting env REPLACES the child env, so spread process.env minus the
|
|
524
|
+
// nested-session leakage. Preserves PATH/HOME/ANTHROPIC_BASE_URL/proxies.
|
|
525
|
+
env: buildChildEnv(),
|
|
526
|
+
abortController
|
|
527
|
+
};
|
|
528
|
+
if (args.effort !== undefined) {
|
|
529
|
+
options.effort = args.effort;
|
|
530
|
+
}
|
|
531
|
+
if (args.thinking) {
|
|
532
|
+
options.thinking =
|
|
533
|
+
args.thinking.type === "manual"
|
|
534
|
+
? { type: "enabled", budgetTokens: args.thinking.budgetTokens }
|
|
535
|
+
: { type: args.thinking.type };
|
|
536
|
+
}
|
|
537
|
+
if (plan.config.outputFormat) {
|
|
538
|
+
options.outputFormat = plan.config.outputFormat;
|
|
539
|
+
}
|
|
540
|
+
// Anchor the path-scoped built-ins to the run's workspace, which is where
|
|
541
|
+
// the NodeTool tools they replace were contained.
|
|
542
|
+
if (plan.config.cwd) {
|
|
543
|
+
options.cwd = plan.config.cwd;
|
|
544
|
+
}
|
|
545
|
+
if (plan.config.mcp) {
|
|
546
|
+
options.mcpServers = plan.config.mcp.mcpServers;
|
|
547
|
+
}
|
|
548
|
+
// Enable the SDK's native skill loop over the user's DB skills, which were
|
|
549
|
+
// materialized into an isolated local plugin. This rides `plugins`, NOT
|
|
550
|
+
// `settingSources` (kept `[]`), so no `~/.claude` / `.claude` settings or
|
|
551
|
+
// `CLAUDE.md` are read; `skipMcpDiscovery` blocks any `.mcp.json` the dir
|
|
552
|
+
// might carry. `skills` names the exact user skills to enable, so any
|
|
553
|
+
// bundled skill is hidden from the listing and the `Skill` tool.
|
|
554
|
+
if (plan.config.skillsPlugin) {
|
|
555
|
+
options.plugins = [
|
|
556
|
+
{
|
|
557
|
+
type: "local",
|
|
558
|
+
path: plan.config.skillsPlugin.dir,
|
|
559
|
+
skipMcpDiscovery: true
|
|
420
560
|
}
|
|
421
|
-
|
|
561
|
+
];
|
|
562
|
+
options.skills = plan.config.skillsPlugin.names;
|
|
422
563
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
564
|
+
if (plan.resume) {
|
|
565
|
+
options.resume = plan.resume;
|
|
566
|
+
}
|
|
567
|
+
// Log the exact wire body (sans the non-serializable AbortController, MCP
|
|
568
|
+
// server instances, and the full env, which can carry secrets) so the
|
|
569
|
+
// request-log UI shows what was sent without leaking the environment.
|
|
570
|
+
this.recordRequestPayload({
|
|
571
|
+
prompt: plan.prompt,
|
|
572
|
+
resume: plan.resume ?? null,
|
|
573
|
+
options: {
|
|
574
|
+
...options,
|
|
575
|
+
abortController: undefined,
|
|
576
|
+
env: undefined,
|
|
577
|
+
mcpServers: plan.config.mcp
|
|
578
|
+
? Object.keys(plan.config.mcp.mcpServers ?? {})
|
|
579
|
+
: undefined
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
log.debug("Claude Agent SDK request", {
|
|
583
|
+
model: args.model,
|
|
584
|
+
resume: Boolean(plan.resume)
|
|
585
|
+
});
|
|
586
|
+
const tracer = getTracer();
|
|
587
|
+
const startLlm = () => {
|
|
588
|
+
const span = tracer?.startSpan(`llm.stream ${this.provider}/${args.model}`, {}, providerRoundContext());
|
|
589
|
+
span?.setAttributes({ "llm.provider": this.provider, "llm.model": args.model, "llm.request.stream": true });
|
|
590
|
+
if (traceContentAllowed()) {
|
|
591
|
+
span?.setAttribute("llm.request.messages", messageTraceContent(args.messages));
|
|
592
|
+
}
|
|
593
|
+
return span;
|
|
594
|
+
};
|
|
595
|
+
let llmSpan = startLlm();
|
|
596
|
+
let llmResponse = "";
|
|
597
|
+
let observedTurn = false;
|
|
598
|
+
const closeLlm = () => {
|
|
599
|
+
if (traceContentAllowed()) {
|
|
600
|
+
llmSpan?.setAttribute("llm.response.content", llmResponse);
|
|
601
|
+
}
|
|
602
|
+
llmSpan?.end();
|
|
603
|
+
llmSpan = undefined;
|
|
604
|
+
llmResponse = "";
|
|
605
|
+
};
|
|
606
|
+
let resolvedModel = args.model;
|
|
607
|
+
// When partial deltas stream, we render from them and skip the final
|
|
608
|
+
// assistant message to avoid duplication; if a build omits partials we fall
|
|
609
|
+
// back to the final message's content blocks.
|
|
610
|
+
let streamedFromPartials = false;
|
|
611
|
+
// The CLI answers a refused login with a synthetic assistant message, then
|
|
612
|
+
// a "success" result flagged `is_error`. Hold its text back from the stream
|
|
613
|
+
// and fail the call with it instead.
|
|
614
|
+
let authFailure = null;
|
|
615
|
+
// The SDK splits one API assistant turn into one frame per content block —
|
|
616
|
+
// thinking, then text, then each tool_use. Emitting a message per frame
|
|
617
|
+
// would hand the consumer a text-only assistant message in the middle of a
|
|
618
|
+
// tool round, which every chat surface reads as "the turn ended" (the chat
|
|
619
|
+
// UI flips the composer back to Send and hides Stop), and would write an
|
|
620
|
+
// empty row for a thinking-only frame. Buffer the blocks and emit one
|
|
621
|
+
// message per assistant turn, text and tool calls together, the way every
|
|
622
|
+
// other provider does.
|
|
623
|
+
let pending = null;
|
|
624
|
+
const flushPending = () => {
|
|
625
|
+
const buffered = pending;
|
|
626
|
+
pending = null;
|
|
627
|
+
if (!buffered)
|
|
628
|
+
return null;
|
|
629
|
+
if (!buffered.text && buffered.toolCalls.length === 0)
|
|
630
|
+
return null;
|
|
631
|
+
return {
|
|
632
|
+
type: "message",
|
|
633
|
+
message: {
|
|
634
|
+
role: "assistant",
|
|
635
|
+
content: buffered.text || null,
|
|
636
|
+
toolCalls: buffered.toolCalls.length ? buffered.toolCalls : null
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
};
|
|
439
640
|
try {
|
|
440
|
-
for await (const msg of
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
641
|
+
for await (const msg of queryFn({ prompt: sdkPrompt(plan.prompt), options })) {
|
|
642
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
643
|
+
// Capture the session and surface it immediately so a streaming
|
|
644
|
+
// consumer can persist it onto the assistant message it creates.
|
|
645
|
+
if (isNonEmptyString(msg.model))
|
|
646
|
+
resolvedModel = msg.model;
|
|
647
|
+
if (plan.threadId) {
|
|
648
|
+
const session = {
|
|
649
|
+
providerId: this.provider,
|
|
650
|
+
model: args.model,
|
|
651
|
+
token: msg.session_id,
|
|
652
|
+
checkpoint: args.messages.length,
|
|
653
|
+
systemHash: plan.systemHash
|
|
654
|
+
};
|
|
655
|
+
if (plan.cacheSession)
|
|
656
|
+
this.sessions.set(plan.threadId, session);
|
|
657
|
+
yield { type: "session", session };
|
|
658
|
+
}
|
|
453
659
|
continue;
|
|
454
660
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
661
|
+
if (msg.type === "stream_event") {
|
|
662
|
+
if (msg.event.type === "message_start") {
|
|
663
|
+
if (observedTurn) {
|
|
664
|
+
closeLlm();
|
|
665
|
+
beginProviderRound(this.provider, args.model);
|
|
666
|
+
llmSpan = startLlm();
|
|
667
|
+
}
|
|
668
|
+
observedTurn = true;
|
|
460
669
|
}
|
|
461
|
-
|
|
462
|
-
|
|
670
|
+
if (msg.event.type === "message_stop") {
|
|
671
|
+
closeLlm();
|
|
672
|
+
}
|
|
673
|
+
const captured = capturedModelFromPartial(msg);
|
|
674
|
+
if (captured)
|
|
675
|
+
resolvedModel = captured;
|
|
676
|
+
const delta = partialDelta(msg);
|
|
677
|
+
if (delta?.text != null) {
|
|
678
|
+
streamedFromPartials = true;
|
|
679
|
+
plan.emitted.content = true;
|
|
680
|
+
if (traceContentAllowed()) {
|
|
681
|
+
llmResponse = traceTextContent(llmResponse + delta.text);
|
|
682
|
+
}
|
|
683
|
+
yield { type: "chunk", content: delta.text, done: false };
|
|
684
|
+
}
|
|
685
|
+
else if (delta?.thinking != null) {
|
|
686
|
+
streamedFromPartials = true;
|
|
687
|
+
plan.emitted.content = true;
|
|
688
|
+
yield {
|
|
689
|
+
type: "chunk",
|
|
690
|
+
content: delta.thinking,
|
|
691
|
+
done: false,
|
|
692
|
+
thinking: true
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
463
696
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const text = content
|
|
470
|
-
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
471
|
-
.map((b) => b.text)
|
|
697
|
+
if (msg.type === "assistant") {
|
|
698
|
+
if (msg.error === "authentication_failed") {
|
|
699
|
+
authFailure = finalBlocks(msg)
|
|
700
|
+
.filter((block) => !block.thinking)
|
|
701
|
+
.map((block) => block.content)
|
|
472
702
|
.join("");
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const m = msg.message;
|
|
706
|
+
if (m && isNonEmptyString(m.model))
|
|
707
|
+
resolvedModel = m.model;
|
|
708
|
+
// Fallback only: no partials arrived, so render text/thinking from the
|
|
709
|
+
// final content blocks — kept strictly separate, never merged.
|
|
710
|
+
if (!streamedFromPartials) {
|
|
711
|
+
for (const block of finalBlocks(msg)) {
|
|
712
|
+
if (!block.thinking && traceContentAllowed()) {
|
|
713
|
+
llmResponse = traceTextContent(llmResponse + block.content);
|
|
714
|
+
}
|
|
715
|
+
plan.emitted.content = true;
|
|
716
|
+
yield block.thinking
|
|
717
|
+
? {
|
|
718
|
+
type: "chunk",
|
|
719
|
+
content: block.content,
|
|
720
|
+
done: false,
|
|
721
|
+
thinking: true
|
|
722
|
+
}
|
|
723
|
+
: { type: "chunk", content: block.content, done: false };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// The loop needs each assistant turn (text + any tool calls) as a
|
|
727
|
+
// persistable message, and a ToolCall item per call for live display.
|
|
728
|
+
// The ToolCall items go out immediately; the message waits for the
|
|
729
|
+
// rest of the turn's blocks (see `pending`).
|
|
730
|
+
if (plan.config.emitMessages) {
|
|
731
|
+
const { text, toolCalls } = assistantParts(msg);
|
|
732
|
+
for (const tc of toolCalls)
|
|
733
|
+
yield tc;
|
|
734
|
+
const parentToolUseId = msg.parent_tool_use_id ?? null;
|
|
735
|
+
if (pending && pending.parentToolUseId !== parentToolUseId) {
|
|
736
|
+
// A subagent's turn and the main agent's are different turns.
|
|
737
|
+
const flushed = flushPending();
|
|
738
|
+
if (flushed)
|
|
739
|
+
yield flushed;
|
|
478
740
|
}
|
|
741
|
+
pending ??= { parentToolUseId, text: "", toolCalls: [] };
|
|
742
|
+
pending.text += text;
|
|
743
|
+
pending.toolCalls.push(...toolCalls);
|
|
479
744
|
}
|
|
480
745
|
continue;
|
|
481
746
|
}
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
747
|
+
// Tool results come back as a user message; surface them as tool
|
|
748
|
+
// messages so the harness can persist them. The assistant turn that
|
|
749
|
+
// asked for them is complete by now, so it goes out first.
|
|
750
|
+
if (msg.type === "user") {
|
|
751
|
+
if (plan.config.emitMessages) {
|
|
752
|
+
const flushed = flushPending();
|
|
753
|
+
if (flushed)
|
|
754
|
+
yield flushed;
|
|
755
|
+
for (const tr of toolResultsFromUser(msg)) {
|
|
756
|
+
yield {
|
|
757
|
+
type: "message",
|
|
758
|
+
message: {
|
|
759
|
+
role: "tool",
|
|
760
|
+
toolCallId: tr.toolCallId,
|
|
761
|
+
content: tr.content
|
|
762
|
+
}
|
|
763
|
+
};
|
|
496
764
|
}
|
|
497
765
|
}
|
|
498
|
-
// Reset for next turn (if multi-turn agentic query)
|
|
499
|
-
streamedTextLength = 0;
|
|
500
766
|
continue;
|
|
501
767
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (isError || (subtype && subtype.startsWith("error"))) {
|
|
509
|
-
const detail = resultText ?? this.describeErrorSubtype(subtype);
|
|
510
|
-
const kind = subtype === "error_max_turns"
|
|
511
|
-
? "max_turns"
|
|
512
|
-
: subtype === "error_during_execution"
|
|
513
|
-
? "execution"
|
|
514
|
-
: "unknown";
|
|
515
|
-
const message = kind === "max_turns"
|
|
516
|
-
? `Claude Agent stopped after reaching the max turn limit without ` +
|
|
517
|
-
`finishing. Increase maxTurns, simplify the task, or break it into ` +
|
|
518
|
-
`smaller steps. Detail: ${detail}`
|
|
519
|
-
: kind === "execution"
|
|
520
|
-
? `Claude Agent execution error: ${detail}`
|
|
521
|
-
: `Claude Agent returned an error result (${subtype ?? "unknown"}): ${detail}`;
|
|
522
|
-
log.error("Claude Agent result error", {
|
|
523
|
-
subtype,
|
|
524
|
-
is_error: isError,
|
|
525
|
-
detail
|
|
526
|
-
});
|
|
527
|
-
throw new ClaudeAgentError(kind, message);
|
|
768
|
+
if (msg.type === "result") {
|
|
769
|
+
// Bill every terminal result, not just the successful ones: an
|
|
770
|
+
// errored/max-turns run still consumed (and was charged for) tokens.
|
|
771
|
+
this.trackResultUsage(msg, resolvedModel);
|
|
772
|
+
if (authFailure !== null && msg.is_error) {
|
|
773
|
+
throw claudeAuthError(authFailure);
|
|
528
774
|
}
|
|
529
|
-
if (
|
|
530
|
-
|
|
775
|
+
if (msg.subtype === "success") {
|
|
776
|
+
const flushed = flushPending();
|
|
777
|
+
if (flushed)
|
|
778
|
+
yield flushed;
|
|
779
|
+
const structured = msg.structured_output;
|
|
780
|
+
if (plan.config.resultToolName &&
|
|
781
|
+
isObjectLike(structured) &&
|
|
782
|
+
!Array.isArray(structured)) {
|
|
783
|
+
yield {
|
|
784
|
+
id: `result_${msg.uuid}`,
|
|
785
|
+
name: plan.config.resultToolName,
|
|
786
|
+
args: structured
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
yield { type: "chunk", content: "", done: true };
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
throw resultError(msg);
|
|
531
793
|
}
|
|
794
|
+
continue;
|
|
532
795
|
}
|
|
533
796
|
}
|
|
797
|
+
// A stream that ended without a terminal `result` still owes its last
|
|
798
|
+
// assistant turn.
|
|
799
|
+
const trailing = flushPending();
|
|
800
|
+
if (trailing)
|
|
801
|
+
yield trailing;
|
|
534
802
|
}
|
|
535
803
|
catch (err) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
});
|
|
542
|
-
throw classified;
|
|
804
|
+
// The query can reject (auth failure, missing session, spawn error). Never
|
|
805
|
+
// collapse to a generic string — surface the real message.
|
|
806
|
+
if (args.signal?.aborted)
|
|
807
|
+
return;
|
|
808
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
543
809
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
810
|
+
finally {
|
|
811
|
+
closeLlm();
|
|
812
|
+
// Terminate the SDK query on every exit path. On normal completion the
|
|
813
|
+
// query has already finished and this is a no-op; on an early `break`
|
|
814
|
+
// (consumer cancelled) or a throw it is the only thing that stops the
|
|
815
|
+
// subprocess, which otherwise runs its whole agentic loop to completion.
|
|
816
|
+
abortController.abort();
|
|
817
|
+
if (args.signal)
|
|
818
|
+
args.signal.removeEventListener("abort", onAbort);
|
|
547
819
|
}
|
|
548
|
-
|
|
549
|
-
|
|
820
|
+
}
|
|
821
|
+
/** Record token usage from a terminal `result` message (success or error). */
|
|
822
|
+
trackResultUsage(msg, model) {
|
|
823
|
+
const usage = msg.usage;
|
|
824
|
+
if (!usage)
|
|
825
|
+
return;
|
|
826
|
+
const input = num(usage.input_tokens);
|
|
827
|
+
const cacheRead = num(usage.cache_read_input_tokens);
|
|
828
|
+
const cacheWrite = num(usage.cache_creation_input_tokens);
|
|
829
|
+
// genai-prices expects the full prompt total (uncached + cache read/write),
|
|
830
|
+
// matching AnthropicProvider's accounting.
|
|
831
|
+
this.trackUsage(model, {
|
|
832
|
+
inputTokens: input + cacheRead + cacheWrite,
|
|
833
|
+
outputTokens: num(usage.output_tokens),
|
|
834
|
+
cachedTokens: cacheRead,
|
|
835
|
+
cacheWriteTokens: cacheWrite
|
|
836
|
+
});
|
|
550
837
|
}
|
|
551
838
|
async generateMessage(args) {
|
|
552
|
-
|
|
839
|
+
let content = "";
|
|
553
840
|
const toolCalls = [];
|
|
554
841
|
for await (const item of this.generateMessages(args)) {
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if ("id" in item && "name" in item && "args" in item) {
|
|
842
|
+
if (isProviderSessionUpdate(item) ||
|
|
843
|
+
isProviderMessageEvent(item) ||
|
|
844
|
+
isProviderStop(item))
|
|
845
|
+
continue;
|
|
846
|
+
if ("args" in item) {
|
|
561
847
|
toolCalls.push(item);
|
|
562
848
|
}
|
|
849
|
+
else if (!item.thinking && isString(item.content)) {
|
|
850
|
+
content += item.content;
|
|
851
|
+
}
|
|
563
852
|
}
|
|
564
853
|
return {
|
|
565
854
|
role: "assistant",
|
|
566
|
-
content:
|
|
567
|
-
|
|
855
|
+
content: content || null,
|
|
856
|
+
toolCalls: toolCalls.length ? toolCalls : null
|
|
568
857
|
};
|
|
569
858
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
859
|
+
}
|
|
860
|
+
function num(value) {
|
|
861
|
+
return isFiniteNumber(value) ? value : 0;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` matches {@link NESTED_SESSION_ENV} but is the CLI's
|
|
865
|
+
* credential, not session leakage: on a headless host (CI) with no interactive
|
|
866
|
+
* `~/.claude` login it is the *only* way the child authenticates. Keep it.
|
|
867
|
+
*/
|
|
868
|
+
const CHILD_ENV_ALLOWLIST = /^CLAUDE_CODE_OAUTH_TOKEN$/;
|
|
869
|
+
/**
|
|
870
|
+
* A copy of the current environment with nested-session leakage stripped, for
|
|
871
|
+
* the SDK's `options.env`. `ANTHROPIC_BASE_URL` and the `HTTP(S)_PROXY` vars are
|
|
872
|
+
* preserved (they are not matched by {@link NESTED_SESSION_ENV}) so API routing
|
|
873
|
+
* keeps working; `CLAUDE_CODE_OAUTH_TOKEN` is explicitly allowlisted so
|
|
874
|
+
* token-based auth survives on headless hosts.
|
|
875
|
+
*/
|
|
876
|
+
function buildChildEnv() {
|
|
877
|
+
const env = {};
|
|
878
|
+
const source = typeof process !== "undefined" ? process.env : {};
|
|
879
|
+
for (const [key, value] of Object.entries(source)) {
|
|
880
|
+
if (value === undefined)
|
|
881
|
+
continue;
|
|
882
|
+
if (NESTED_SESSION_ENV.test(key) && !CHILD_ENV_ALLOWLIST.test(key))
|
|
883
|
+
continue;
|
|
884
|
+
env[key] = value;
|
|
885
|
+
}
|
|
886
|
+
// The claude.ai connectors come from the account login, not from settings.
|
|
887
|
+
// This turns them off in the child.
|
|
888
|
+
env.ENABLE_CLAUDEAI_MCP_SERVERS = "false";
|
|
889
|
+
return env;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* The built-ins one loop turn keeps: those that replace an offered NodeTool
|
|
893
|
+
* tool, the web pair when the caller offered any tool, and `Skill` when a
|
|
894
|
+
* skills plugin is loaded.
|
|
895
|
+
*/
|
|
896
|
+
function turnBuiltins(replaced, toolsOffered, hasSkillsPlugin) {
|
|
897
|
+
const builtins = new Set();
|
|
898
|
+
for (const name of replaced) {
|
|
899
|
+
const builtin = SDK_NATIVE_TOOL_REPLACEMENTS.get(name);
|
|
900
|
+
if (builtin)
|
|
901
|
+
builtins.add(builtin);
|
|
902
|
+
}
|
|
903
|
+
if (toolsOffered)
|
|
904
|
+
for (const name of SDK_WEB_TOOLS)
|
|
905
|
+
builtins.add(name);
|
|
906
|
+
if (hasSkillsPlugin)
|
|
907
|
+
builtins.add("Skill");
|
|
908
|
+
return [...builtins];
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* A DB skill name that is safe to use as a filesystem directory. The DB
|
|
912
|
+
* validates names (lowercase `a-z0-9-`), but path safety is enforced here too:
|
|
913
|
+
* a name with a separator or `.`-segment could otherwise escape the plugin
|
|
914
|
+
* dir. Defense in depth — an unsafe name is skipped, not written.
|
|
915
|
+
*/
|
|
916
|
+
function isFilesystemSafeSkillName(name) {
|
|
917
|
+
return /^[a-z0-9][a-z0-9-]{0,63}$/.test(name);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Write the user's DB skills to a throwaway local plugin the SDK's native skill
|
|
921
|
+
* loader can discover, and return `{ dir, names }` (or null when nothing was
|
|
922
|
+
* written). The layout is the Claude Code plugin format:
|
|
923
|
+
*
|
|
924
|
+
* <dir>/.claude-plugin/plugin.json
|
|
925
|
+
* <dir>/skills/<name>/SKILL.md (name/description frontmatter + body)
|
|
926
|
+
*
|
|
927
|
+
* This is deliberately separate from `settingSources`: a `plugins` entry loads
|
|
928
|
+
* skills without enabling `~/.claude` / `.claude` settings or `CLAUDE.md`, and
|
|
929
|
+
* `skipMcpDiscovery` blocks any `.mcp.json` the dir might contain. The caller
|
|
930
|
+
* owns cleanup (see {@link ClaudeAgentProvider.generateLoop}'s `finally`).
|
|
931
|
+
*/
|
|
932
|
+
async function materializeSkillsPlugin(skills) {
|
|
933
|
+
const usable = skills.filter((s) => isFilesystemSafeSkillName(s.name) && s.content.trim().length > 0);
|
|
934
|
+
if (usable.length === 0)
|
|
935
|
+
return null;
|
|
936
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-skills-"));
|
|
937
|
+
await fs.mkdir(path.join(dir, ".claude-plugin"), { recursive: true });
|
|
938
|
+
await fs.writeFile(path.join(dir, ".claude-plugin", "plugin.json"), JSON.stringify({
|
|
939
|
+
name: SKILLS_PLUGIN_NAME,
|
|
940
|
+
version: "1.0.0",
|
|
941
|
+
description: "User-defined skills from NodeTool."
|
|
942
|
+
}), "utf8");
|
|
943
|
+
const names = [];
|
|
944
|
+
for (const skill of usable) {
|
|
945
|
+
const skillDir = path.join(dir, "skills", skill.name);
|
|
946
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
947
|
+
// JSON-stringify the description: a JSON string is a valid YAML double-
|
|
948
|
+
// quoted scalar, so colons / quotes / newlines can't break the frontmatter.
|
|
949
|
+
const md = [
|
|
950
|
+
"---",
|
|
951
|
+
`name: ${skill.name}`,
|
|
952
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
953
|
+
"---",
|
|
954
|
+
"",
|
|
955
|
+
skill.content.trim(),
|
|
956
|
+
""
|
|
957
|
+
].join("\n");
|
|
958
|
+
await fs.writeFile(path.join(skillDir, "SKILL.md"), md, "utf8");
|
|
959
|
+
names.push(skill.name);
|
|
599
960
|
}
|
|
961
|
+
return { dir, names };
|
|
962
|
+
}
|
|
963
|
+
/** Pull a text/thinking delta out of a partial `stream_event`, if any. */
|
|
964
|
+
function partialDelta(msg) {
|
|
965
|
+
const event = msg.event;
|
|
966
|
+
if (event.type !== "content_block_delta")
|
|
967
|
+
return null;
|
|
968
|
+
const delta = event.delta;
|
|
969
|
+
if (delta.type === "text_delta")
|
|
970
|
+
return { text: delta.text };
|
|
971
|
+
if (delta.type === "thinking_delta")
|
|
972
|
+
return { thinking: delta.thinking };
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
/** The concrete dated model id, captured from a partial `message_start`. */
|
|
976
|
+
function capturedModelFromPartial(msg) {
|
|
977
|
+
const event = msg.event;
|
|
978
|
+
if (event.type !== "message_start")
|
|
979
|
+
return null;
|
|
980
|
+
const model = event.message?.model;
|
|
981
|
+
return isNonEmptyString(model) && model !== "<synthetic>" ? model : null;
|
|
982
|
+
}
|
|
983
|
+
/** Text/thinking blocks of a final assistant message, kept separate. */
|
|
984
|
+
function finalBlocks(msg) {
|
|
985
|
+
const content = msg.message?.content;
|
|
986
|
+
if (!Array.isArray(content))
|
|
987
|
+
return [];
|
|
988
|
+
const out = [];
|
|
989
|
+
for (const block of content) {
|
|
990
|
+
if (block.type === "text" && isString(block.text)) {
|
|
991
|
+
out.push({ content: block.text, thinking: false });
|
|
992
|
+
}
|
|
993
|
+
else if (block.type === "thinking" && isString(block.thinking)) {
|
|
994
|
+
out.push({ content: block.thinking, thinking: true });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Recover the plain tool name from what the model emitted.
|
|
1001
|
+
*
|
|
1002
|
+
* Two prefixes get stripped, in order:
|
|
1003
|
+
* - `mcp__<server>__`, which the SDK adds to in-process MCP tool names.
|
|
1004
|
+
* - `tools.`, the retired guest toolbelt's call form, which models still
|
|
1005
|
+
* turn into a top-level tool name. Without this the call reaches no tool
|
|
1006
|
+
* at all.
|
|
1007
|
+
*/
|
|
1008
|
+
function stripToolPrefix(name) {
|
|
1009
|
+
let n = isString(name) ? name : "";
|
|
1010
|
+
if (n.startsWith(TOOL_PREFIX))
|
|
1011
|
+
n = n.slice(TOOL_PREFIX.length);
|
|
1012
|
+
if (n.startsWith(GUEST_TOOL_PREFIX))
|
|
1013
|
+
n = n.slice(GUEST_TOOL_PREFIX.length);
|
|
1014
|
+
return n;
|
|
1015
|
+
}
|
|
1016
|
+
/** Split a final assistant message into its text and its tool calls. */
|
|
1017
|
+
function assistantParts(msg) {
|
|
1018
|
+
const content = msg.message?.content;
|
|
1019
|
+
let text = "";
|
|
1020
|
+
const toolCalls = [];
|
|
1021
|
+
if (Array.isArray(content)) {
|
|
1022
|
+
for (const raw of content) {
|
|
1023
|
+
const b = raw;
|
|
1024
|
+
if (b.type === "text" && isString(b.text)) {
|
|
1025
|
+
text += b.text;
|
|
1026
|
+
}
|
|
1027
|
+
else if (b.type === "tool_use") {
|
|
1028
|
+
toolCalls.push({
|
|
1029
|
+
id: isString(b.id) ? b.id : `call_${toolCalls.length}`,
|
|
1030
|
+
name: stripToolPrefix(b.name),
|
|
1031
|
+
args: isObjectLike(b.input)
|
|
1032
|
+
? b.input
|
|
1033
|
+
: {}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return { text, toolCalls };
|
|
1039
|
+
}
|
|
1040
|
+
/** Extract `tool_result` blocks from a user message the SDK emitted. */
|
|
1041
|
+
function toolResultsFromUser(msg) {
|
|
1042
|
+
const content = msg.message?.content;
|
|
1043
|
+
const out = [];
|
|
1044
|
+
if (Array.isArray(content)) {
|
|
1045
|
+
for (const raw of content) {
|
|
1046
|
+
const b = raw;
|
|
1047
|
+
if (b.type === "tool_result" && isString(b.tool_use_id)) {
|
|
1048
|
+
out.push({
|
|
1049
|
+
toolCallId: b.tool_use_id,
|
|
1050
|
+
content: toolResultText(b.content)
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return out;
|
|
1056
|
+
}
|
|
1057
|
+
/** Flatten an MCP tool-result content payload to text. */
|
|
1058
|
+
function toolResultText(content) {
|
|
1059
|
+
if (isString(content))
|
|
1060
|
+
return content;
|
|
1061
|
+
if (Array.isArray(content)) {
|
|
1062
|
+
return content
|
|
1063
|
+
.map((c) => {
|
|
1064
|
+
const cc = c;
|
|
1065
|
+
return cc.type === "text" && isString(cc.text) ? cc.text : "";
|
|
1066
|
+
})
|
|
1067
|
+
.join("");
|
|
1068
|
+
}
|
|
1069
|
+
return "";
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Build an in-process MCP tool definition that bridges a NodeTool
|
|
1073
|
+
* {@link ProviderTool} to the harness's tool executor. The JSON-Schema input is
|
|
1074
|
+
* shimmed to the Zod raw shape the SDK expects.
|
|
1075
|
+
*/
|
|
1076
|
+
function toolDefinition(tool, run, resolveMedia) {
|
|
1077
|
+
return {
|
|
1078
|
+
name: tool.name,
|
|
1079
|
+
description: tool.description ?? "",
|
|
1080
|
+
inputSchema: jsonSchemaToZodShape(tool.inputSchema),
|
|
1081
|
+
handler: async (toolArgs) => {
|
|
1082
|
+
const result = await run(tool.name, toolArgs ?? {});
|
|
1083
|
+
const resolved = Array.isArray(result) &&
|
|
1084
|
+
result.some((part) => part.type === "image_url") &&
|
|
1085
|
+
resolveMedia
|
|
1086
|
+
? await resolveMedia([{ role: "user", content: result }])
|
|
1087
|
+
: null;
|
|
1088
|
+
const content = resolved?.[0]?.content;
|
|
1089
|
+
return {
|
|
1090
|
+
content: toolResultToMcpContent(Array.isArray(content) ? content : result)
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Decode a NodeTool image content part into an MCP image block (raw base64 +
|
|
1097
|
+
* mimeType). Returns null for a remote URL we cannot inline — MCP image content
|
|
1098
|
+
* is base64-only, so the caller reports that the image could not be shown.
|
|
1099
|
+
*/
|
|
1100
|
+
function toMcpImageBlock(image) {
|
|
1101
|
+
let base64;
|
|
1102
|
+
let mimeType = image.mimeType;
|
|
1103
|
+
const fromDataUri = (uri) => {
|
|
1104
|
+
const match = /^data:([^;,]+)?(;base64)?,([\s\S]*)$/.exec(uri);
|
|
1105
|
+
if (!match)
|
|
1106
|
+
return;
|
|
1107
|
+
mimeType = mimeType ?? match[1] ?? "image/png";
|
|
1108
|
+
base64 = match[2]
|
|
1109
|
+
? (match[3] ?? "")
|
|
1110
|
+
: Buffer.from(decodeURIComponent(match[3] ?? "")).toString("base64");
|
|
1111
|
+
};
|
|
1112
|
+
if (isString(image.data)) {
|
|
1113
|
+
if (image.data.startsWith("data:"))
|
|
1114
|
+
fromDataUri(image.data);
|
|
1115
|
+
else
|
|
1116
|
+
base64 = image.data;
|
|
1117
|
+
}
|
|
1118
|
+
else if (image.data instanceof Uint8Array) {
|
|
1119
|
+
base64 = Buffer.from(image.data).toString("base64");
|
|
1120
|
+
}
|
|
1121
|
+
if (!base64 && isString(image.uri) && image.uri.startsWith("data:")) {
|
|
1122
|
+
fromDataUri(image.uri);
|
|
1123
|
+
}
|
|
1124
|
+
if (!base64)
|
|
1125
|
+
return null;
|
|
1126
|
+
return { type: "image", data: base64, mimeType: mimeType ?? "image/png" };
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Convert a tool result into MCP content blocks. Text passes through; image
|
|
1130
|
+
* parts become MCP image blocks (the SDK forwards them to Claude as real
|
|
1131
|
+
* images). An image that can't be inlined becomes a failure note.
|
|
1132
|
+
*/
|
|
1133
|
+
export function toolResultToMcpContent(result) {
|
|
1134
|
+
if (isString(result)) {
|
|
1135
|
+
return [{ type: "text", text: result }];
|
|
1136
|
+
}
|
|
1137
|
+
const blocks = [];
|
|
1138
|
+
for (const part of result) {
|
|
1139
|
+
if (part.type === "text") {
|
|
1140
|
+
blocks.push({ type: "text", text: part.text });
|
|
1141
|
+
}
|
|
1142
|
+
else if (part.type === "image_url") {
|
|
1143
|
+
const img = toMcpImageBlock(part.image);
|
|
1144
|
+
if (img)
|
|
1145
|
+
blocks.push(img);
|
|
1146
|
+
else {
|
|
1147
|
+
blocks.push({
|
|
1148
|
+
type: "text",
|
|
1149
|
+
text: isString(part.image.uri)
|
|
1150
|
+
? `[image could not be shown: ${part.image.uri}]`
|
|
1151
|
+
: "[image could not be shown]"
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (blocks.length === 0) {
|
|
1157
|
+
blocks.push({ type: "text", text: "[no content]" });
|
|
1158
|
+
}
|
|
1159
|
+
return blocks;
|
|
1160
|
+
}
|
|
1161
|
+
/** Convert a JSON-Schema object's properties to a Zod raw shape. */
|
|
1162
|
+
function jsonSchemaToZodShape(schema) {
|
|
1163
|
+
const props = schema?.properties ?? {};
|
|
1164
|
+
const required = new Set(schema?.required ?? []);
|
|
1165
|
+
const shape = {};
|
|
1166
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
1167
|
+
let zt = jsonPropToZod(raw);
|
|
1168
|
+
if (!required.has(key))
|
|
1169
|
+
zt = zt.optional();
|
|
1170
|
+
shape[key] = zt;
|
|
1171
|
+
}
|
|
1172
|
+
return shape;
|
|
1173
|
+
}
|
|
1174
|
+
/** Convert one JSON-Schema property to a Zod type (the subset NodeTool uses). */
|
|
1175
|
+
function jsonPropToZod(prop) {
|
|
1176
|
+
const desc = isString(prop?.description) ? prop.description : undefined;
|
|
1177
|
+
let zt;
|
|
1178
|
+
switch (prop?.type) {
|
|
1179
|
+
case "string":
|
|
1180
|
+
zt = z.string();
|
|
1181
|
+
break;
|
|
1182
|
+
case "number":
|
|
1183
|
+
case "integer":
|
|
1184
|
+
zt = z.number();
|
|
1185
|
+
break;
|
|
1186
|
+
case "boolean":
|
|
1187
|
+
zt = z.boolean();
|
|
1188
|
+
break;
|
|
1189
|
+
case "array":
|
|
1190
|
+
zt = z.array(prop.items
|
|
1191
|
+
? jsonPropToZod(prop.items)
|
|
1192
|
+
: z.unknown());
|
|
1193
|
+
break;
|
|
1194
|
+
case "object": {
|
|
1195
|
+
const shape = jsonSchemaToZodShape(prop);
|
|
1196
|
+
// Free-form objects (no declared sub-properties — e.g. add_node's
|
|
1197
|
+
// `node_properties`) must keep arbitrary keys. `z.object({})` strips
|
|
1198
|
+
// every key, silently dropping all node configuration on the SDK tool
|
|
1199
|
+
// bridge; passthrough preserves the nested values.
|
|
1200
|
+
zt =
|
|
1201
|
+
Object.keys(shape).length > 0
|
|
1202
|
+
? z.object(shape)
|
|
1203
|
+
: z.object({}).passthrough();
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
default:
|
|
1207
|
+
zt = z.unknown();
|
|
1208
|
+
}
|
|
1209
|
+
return desc ? zt.describe(desc) : zt;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* A refused Claude login, carrying status 401 so the provider layer classifies
|
|
1213
|
+
* it as a credential failure and the UI offers to sign in again.
|
|
1214
|
+
*/
|
|
1215
|
+
function claudeAuthError(detail) {
|
|
1216
|
+
const message = detail.trim() || "The Claude login was refused";
|
|
1217
|
+
return Object.assign(new Error(`Claude sign-in failed (401): ${message}`), {
|
|
1218
|
+
status: 401
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
/** Build a descriptive Error from a non-success `result` message. */
|
|
1222
|
+
function resultError(msg) {
|
|
1223
|
+
const parts = [`Claude Agent SDK query failed (${msg.subtype})`];
|
|
1224
|
+
if ("errors" in msg && Array.isArray(msg.errors) && msg.errors.length) {
|
|
1225
|
+
parts.push(msg.errors.join("; "));
|
|
1226
|
+
}
|
|
1227
|
+
else if ("result" in msg && isNonEmptyString(msg.result)) {
|
|
1228
|
+
parts.push(msg.result);
|
|
1229
|
+
}
|
|
1230
|
+
const denials = (msg).permission_denials;
|
|
1231
|
+
if (Array.isArray(denials) && denials.length) {
|
|
1232
|
+
parts.push(`permission denied: ${denials.map((d) => d.tool_name ?? "?").join(", ")}`);
|
|
1233
|
+
}
|
|
1234
|
+
return new Error(parts.join(": "));
|
|
1235
|
+
}
|
|
1236
|
+
/** Flatten a message's content to plain text (text blocks only). */
|
|
1237
|
+
function textOf(content) {
|
|
1238
|
+
if (content == null)
|
|
1239
|
+
return "";
|
|
1240
|
+
if (isString(content))
|
|
1241
|
+
return content;
|
|
1242
|
+
return content
|
|
1243
|
+
.filter((c) => c.type === "text")
|
|
1244
|
+
.map((c) => c.text)
|
|
1245
|
+
.join("");
|
|
1246
|
+
}
|
|
1247
|
+
/** Join all system messages into the replacement system prompt. */
|
|
1248
|
+
function extractSystemPrompt(messages) {
|
|
1249
|
+
const system = messages
|
|
1250
|
+
.filter((m) => m.role === "system")
|
|
1251
|
+
.map((m) => textOf(m.content))
|
|
1252
|
+
.filter(Boolean)
|
|
1253
|
+
.join("\n\n");
|
|
1254
|
+
return system || DEFAULT_SYSTEM_PROMPT;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The new turn(s) to send when resuming: only the user messages added since the
|
|
1258
|
+
* session's checkpoint. Assistant messages in the slice were produced by the
|
|
1259
|
+
* resumed session itself, so they are already known to it and must not be
|
|
1260
|
+
* replayed (re-feeding them would present the model its own prior answer as
|
|
1261
|
+
* user input).
|
|
1262
|
+
*/
|
|
1263
|
+
function buildResumeDelta(messages, checkpoint) {
|
|
1264
|
+
const delta = messages.slice(checkpoint).filter((m) => m.role === "user");
|
|
1265
|
+
if (hasImages(delta)) {
|
|
1266
|
+
return delta.flatMap((message, index) => [
|
|
1267
|
+
...(index ? [{ type: "text", text: "\n\n" }] : []),
|
|
1268
|
+
...promptBlocks(message.content)
|
|
1269
|
+
]);
|
|
1270
|
+
}
|
|
1271
|
+
return delta
|
|
1272
|
+
.map((m) => textOf(m.content))
|
|
1273
|
+
.filter(Boolean)
|
|
1274
|
+
.join("\n\n");
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* The prompt for a fresh session. A lone user turn is sent verbatim. When there
|
|
1278
|
+
* is pre-existing FOREIGN history (a cold thread, a model switch, or an
|
|
1279
|
+
* edited/branched conversation) we prime context ONCE with a single delimited
|
|
1280
|
+
* user message instead of rebuilding a `Human:/Assistant:` transcript — the SDK
|
|
1281
|
+
* cannot import external assistant turns, so this is deliberate context priming,
|
|
1282
|
+
* not a faithful reconstruction. Text and inline images are retained, while
|
|
1283
|
+
* thinking is stripped.
|
|
1284
|
+
*/
|
|
1285
|
+
function buildFreshPrompt(messages) {
|
|
1286
|
+
const convo = messages.filter((m) => m.role !== "system");
|
|
1287
|
+
if (convo.length === 0)
|
|
1288
|
+
return "";
|
|
1289
|
+
if (convo.length === 1 && convo[0].role === "user") {
|
|
1290
|
+
return hasImages(convo) ? promptBlocks(convo[0].content) : textOf(convo[0].content);
|
|
1291
|
+
}
|
|
1292
|
+
const last = convo[convo.length - 1];
|
|
1293
|
+
const newTurn = last.role === "user" ? textOf(last.content) : "";
|
|
1294
|
+
const prior = last.role === "user" ? convo.slice(0, -1) : convo;
|
|
1295
|
+
if (hasImages(convo)) {
|
|
1296
|
+
const blocks = [];
|
|
1297
|
+
if (prior.length) {
|
|
1298
|
+
blocks.push({ type: "text", text: "<conversation_so_far>\n" });
|
|
1299
|
+
for (const message of prior) {
|
|
1300
|
+
blocks.push({ type: "text", text: `${message.role === "assistant" ? "Assistant" : "User"}: ` }, ...promptBlocks(message.content), { type: "text", text: "\n\n" });
|
|
1301
|
+
}
|
|
1302
|
+
blocks.push({ type: "text", text: "</conversation_so_far>\n\n" });
|
|
1303
|
+
}
|
|
1304
|
+
if (last.role === "user") {
|
|
1305
|
+
blocks.push(...promptBlocks(last.content));
|
|
1306
|
+
}
|
|
1307
|
+
return blocks;
|
|
1308
|
+
}
|
|
1309
|
+
const transcript = prior
|
|
1310
|
+
.map((m) => {
|
|
1311
|
+
const text = textOf(m.content);
|
|
1312
|
+
if (!text)
|
|
1313
|
+
return "";
|
|
1314
|
+
return `${m.role === "assistant" ? "Assistant" : "User"}: ${text}`;
|
|
1315
|
+
})
|
|
1316
|
+
.filter(Boolean)
|
|
1317
|
+
.join("\n\n");
|
|
1318
|
+
const primed = transcript
|
|
1319
|
+
? `<conversation_so_far>\n${transcript}\n</conversation_so_far>`
|
|
1320
|
+
: "";
|
|
1321
|
+
return [primed, newTurn].filter(Boolean).join("\n\n");
|
|
1322
|
+
}
|
|
1323
|
+
function hasImages(messages) {
|
|
1324
|
+
return messages.some((message) => Array.isArray(message.content) &&
|
|
1325
|
+
message.content.some((part) => part.type === "image_url"));
|
|
1326
|
+
}
|
|
1327
|
+
/** Images must already be inline. This boundary never fetches caller URLs. */
|
|
1328
|
+
function promptBlocks(content) {
|
|
1329
|
+
if (isString(content)) {
|
|
1330
|
+
return [{ type: "text", text: content }];
|
|
1331
|
+
}
|
|
1332
|
+
const blocks = [];
|
|
1333
|
+
for (const part of content ?? []) {
|
|
1334
|
+
if (part.type === "text") {
|
|
1335
|
+
blocks.push({ type: "text", text: part.text });
|
|
1336
|
+
}
|
|
1337
|
+
else if (part.type === "image_url") {
|
|
1338
|
+
const image = toMcpImageBlock(part.image);
|
|
1339
|
+
if (!image) {
|
|
1340
|
+
throw new Error("Claude Agent image messages require inline image data. Resolve the media reference before dispatch.");
|
|
1341
|
+
}
|
|
1342
|
+
const mime = image.mimeType;
|
|
1343
|
+
if (mime !== "image/png" && mime !== "image/jpeg" && mime !== "image/gif" && mime !== "image/webp") {
|
|
1344
|
+
throw new Error(`Claude Agent image messages do not support ${mime}. Convert the image before dispatch.`);
|
|
1345
|
+
}
|
|
1346
|
+
blocks.push({ type: "image", source: { type: "base64", media_type: mime, data: image.data } });
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return blocks;
|
|
1350
|
+
}
|
|
1351
|
+
function sdkPrompt(content) {
|
|
1352
|
+
if (isString(content)) {
|
|
1353
|
+
return content;
|
|
1354
|
+
}
|
|
1355
|
+
return (async function* () {
|
|
1356
|
+
yield {
|
|
1357
|
+
type: "user",
|
|
1358
|
+
parent_tool_use_id: null,
|
|
1359
|
+
message: { role: "user", content }
|
|
1360
|
+
};
|
|
1361
|
+
})();
|
|
600
1362
|
}
|
|
601
1363
|
//# sourceMappingURL=claude-agent-provider.js.map
|