@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
package/dist/context.js
CHANGED
|
@@ -1,18 +1,86 @@
|
|
|
1
|
+
import { getRunTraceScope } from "./run-trace-context.js";
|
|
2
|
+
import { withSpan } from "./tracing-helpers.js";
|
|
3
|
+
import { assetFileNameCandidates, buildAssetGenerationMetadata, isShortResourceId, packageAssetHttpPath, parsePackageAssetUri } from "@nodetool-ai/protocol";
|
|
4
|
+
import { AgentMemory } from "./agent-memory.js";
|
|
5
|
+
import { recordInvocationAsset, recordInvocationCost } from "./invocation-account.js";
|
|
6
|
+
import { VariableChannel } from "./variable-channel.js";
|
|
7
|
+
import { loadMediaRefBytes } from "./media-ref-bytes.js";
|
|
8
|
+
import { objectTrackingRequestSchema, parseObjectTrackingResult } from "./providers/object-tracking.js";
|
|
9
|
+
import { requestVideoToAudio, VIDEO_TO_AUDIO_TASK, VideoToAudioRequest } from "./providers/video-to-audio.js";
|
|
10
|
+
import { encodeRawImageRef, validateImageEvidence } from "./image-codec.js";
|
|
11
|
+
import { extForImageMime, sniffImageMime } from "./providers/image-mime.js";
|
|
12
|
+
import { GenerationScopeError, isAuthoritativeProviderTerminalError, runWithGenerationReceipt } from "./generation-receipt.js";
|
|
13
|
+
import { generationRegistry } from "./generation-registry.js";
|
|
14
|
+
import { redactGenerationParams } from "./redact-params.js";
|
|
15
|
+
import { isCallable, isBoolean, isNonEmptyString, isObjectLike, isNumber, isRecord, isString } from "@nodetool-ai/protocol";
|
|
16
|
+
import { expandAssetReferences, expandEntityRefs, inlineTextAssetRefs } from "./prompt-asset-refs.js";
|
|
17
|
+
import { getNodeBuiltinSync } from "@nodetool-ai/config";
|
|
18
|
+
// `node:fs/promises`, `node:path`, `node:url`, `node:crypto` are loaded
|
|
19
|
+
// lazily so this module loads in browser / Edge runtimes. `resolveWorkspacePath`
|
|
20
|
+
// and the `randomUUID` fallback both degrade gracefully when these are
|
|
21
|
+
// unavailable.
|
|
1
22
|
/**
|
|
2
|
-
* ProcessingContext
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
* ProcessingContext variable key carrying the provider/model the currently
|
|
24
|
+
* running agent loop is itself talking to. Every loop that executes tools
|
|
25
|
+
* (chat turns, `StepExecutor` sub-agents) stamps it, so a tool that launches
|
|
26
|
+
* another harness inherits the caller's model by default instead of demanding
|
|
27
|
+
* one or falling back to a server env default.
|
|
28
|
+
*/
|
|
29
|
+
export const ACTIVE_MODEL_CONTEXT_KEY = "active_agent_model";
|
|
30
|
+
const nodeCrypto = getNodeBuiltinSync("node:crypto");
|
|
31
|
+
const nodeFsP = getNodeBuiltinSync("node:fs/promises");
|
|
32
|
+
const nodePath = getNodeBuiltinSync("node:path");
|
|
33
|
+
const nodeUrl = getNodeBuiltinSync("node:url");
|
|
34
|
+
const randomUUID = nodeCrypto?.randomUUID
|
|
35
|
+
? nodeCrypto.randomUUID
|
|
36
|
+
: () => {
|
|
37
|
+
const g = globalThis;
|
|
38
|
+
if (g.crypto?.randomUUID)
|
|
39
|
+
return g.crypto.randomUUID();
|
|
40
|
+
return `id_${Date.now().toString(36)}_${Math.random()
|
|
41
|
+
.toString(36)
|
|
42
|
+
.slice(2, 10)}`;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* `process` is undefined in browser/edge runtimes. Reading `process.env`
|
|
46
|
+
* directly there throws `ReferenceError`, so this context (constructed by the
|
|
47
|
+
* in-browser workflow runner too) reads env through this guard instead.
|
|
11
48
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
49
|
+
const safeProcessEnv = () => typeof process !== "undefined" && process.env ? process.env : {};
|
|
50
|
+
function waitForRetry(delayMs, signal) {
|
|
51
|
+
return new Promise((resolveWait, rejectWait) => {
|
|
52
|
+
if (signal?.aborted) {
|
|
53
|
+
rejectWait(signal.reason ?? new Error("HTTP request aborted"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
rejectWait(signal?.reason ?? new Error("HTTP request aborted"));
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
signal?.removeEventListener("abort", onAbort);
|
|
62
|
+
resolveWait();
|
|
63
|
+
}, delayMs);
|
|
64
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Eager local bindings; unavailable off-Node, throw at call time.
|
|
68
|
+
function notOnNode(api) {
|
|
69
|
+
throw new Error(`${api} is unavailable in this runtime — configure a StorageAdapter instead`);
|
|
70
|
+
}
|
|
71
|
+
const mkdir = (...a) => nodeFsP ? nodeFsP.mkdir(...a) : notOnNode("node:fs/promises.mkdir");
|
|
72
|
+
const readFile = (...a) => nodeFsP ? nodeFsP.readFile(...a) : notOnNode("node:fs/promises.readFile");
|
|
73
|
+
const writeFile = (...a) => nodeFsP ? nodeFsP.writeFile(...a) : notOnNode("node:fs/promises.writeFile");
|
|
74
|
+
const basename = (p, ext) => nodePath ? nodePath.basename(p, ext) : notOnNode("node:path.basename");
|
|
75
|
+
const dirname = (p) => nodePath ? nodePath.dirname(p) : notOnNode("node:path.dirname");
|
|
76
|
+
const extname = (p) => nodePath ? nodePath.extname(p) : notOnNode("node:path.extname");
|
|
77
|
+
const isAbsolute = (p) => nodePath ? nodePath.isAbsolute(p) : notOnNode("node:path.isAbsolute");
|
|
78
|
+
const join = (...parts) => nodePath ? nodePath.join(...parts) : notOnNode("node:path.join");
|
|
79
|
+
const relative = (from, to) => nodePath ? nodePath.relative(from, to) : notOnNode("node:path.relative");
|
|
80
|
+
const resolve = (...parts) => nodePath ? nodePath.resolve(...parts) : notOnNode("node:path.resolve");
|
|
81
|
+
const pathToFileURL = (p) => nodeUrl ? nodeUrl.pathToFileURL(p) : notOnNode("node:url.pathToFileURL");
|
|
82
|
+
import { CONTENT_FILTER_MAX_RETRIES, contentFilterRetryDelayMs, isContentFilterRefusal } from "./providers/content-filter.js";
|
|
83
|
+
import { getProcessSandboxModuleCatalog } from "./sandbox-module-catalog.js";
|
|
16
84
|
/**
|
|
17
85
|
* In-memory cache adapter (default for tests and single-process execution).
|
|
18
86
|
*/
|
|
@@ -26,6 +94,8 @@ export class MemoryCache {
|
|
|
26
94
|
this._store.delete(key);
|
|
27
95
|
return undefined;
|
|
28
96
|
}
|
|
97
|
+
// SAFETY: `TValue` is what the caller stored under this key; a cache
|
|
98
|
+
// hands back exactly the value `set` was given for it.
|
|
29
99
|
return entry.value;
|
|
30
100
|
}
|
|
31
101
|
async set(key, value, ttlSeconds) {
|
|
@@ -39,195 +109,173 @@ export class MemoryCache {
|
|
|
39
109
|
this._store.delete(key);
|
|
40
110
|
}
|
|
41
111
|
}
|
|
112
|
+
/** Raised by a durable host when an idempotent request is already owned. */
|
|
113
|
+
export class GenerationAlreadyAcceptedError extends Error {
|
|
114
|
+
generationId;
|
|
115
|
+
constructor(generationId) {
|
|
116
|
+
super(`Generation ${generationId} was already accepted`);
|
|
117
|
+
this.generationId = generationId;
|
|
118
|
+
this.name = "GenerationAlreadyAcceptedError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let defaultModelInterfaces = null;
|
|
122
|
+
/**
|
|
123
|
+
* Install the model interfaces every context in this process falls back to.
|
|
124
|
+
*
|
|
125
|
+
* A host installs its persistence once at startup instead of each entrance
|
|
126
|
+
* remembering to wire it onto the context it builds. That forgetting is a
|
|
127
|
+
* defect generator: `nodetool debug` wired none while `workflows run` wired
|
|
128
|
+
* four, so a workflow that saves an image ran under one command and threw
|
|
129
|
+
* under the other. A context that sets its own interfaces still wins — this
|
|
130
|
+
* is the floor, not an override.
|
|
131
|
+
*
|
|
132
|
+
* Pass `null` to uninstall (a test restoring global state).
|
|
133
|
+
*/
|
|
134
|
+
export function setDefaultModelInterfaces(interfaces) {
|
|
135
|
+
defaultModelInterfaces = interfaces;
|
|
136
|
+
}
|
|
137
|
+
/** The process-wide default, or null when no host installed one. */
|
|
138
|
+
export function getDefaultModelInterfaces() {
|
|
139
|
+
return defaultModelInterfaces;
|
|
140
|
+
}
|
|
42
141
|
function isWithinRoot(root, target) {
|
|
43
142
|
const rel = relative(root, target);
|
|
44
143
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
45
144
|
}
|
|
46
|
-
function normalizeStorageKey(key) {
|
|
47
|
-
const cleaned = normalize(key.replaceAll("\\", "/")).replace(/^\/+/, "");
|
|
48
|
-
if (!cleaned ||
|
|
49
|
-
cleaned === "." ||
|
|
50
|
-
cleaned.startsWith("..") ||
|
|
51
|
-
cleaned.includes(`..${sep}`)) {
|
|
52
|
-
throw new Error(`Invalid storage key: ${key}`);
|
|
53
|
-
}
|
|
54
|
-
return cleaned;
|
|
55
|
-
}
|
|
56
|
-
function joinStorageKey(prefix, key) {
|
|
57
|
-
const normalizedKey = normalizeStorageKey(key);
|
|
58
|
-
if (!prefix)
|
|
59
|
-
return normalizedKey;
|
|
60
|
-
const normalizedPrefix = normalizeStorageKey(prefix);
|
|
61
|
-
return `${normalizedPrefix}/${normalizedKey}`;
|
|
62
|
-
}
|
|
63
145
|
/**
|
|
64
|
-
*
|
|
146
|
+
* Deterministic JSON serialization with object keys sorted at every depth.
|
|
147
|
+
* Unlike `JSON.stringify(value, sortedKeys)` (a recursive replacer allow-list
|
|
148
|
+
* that drops nested values), this preserves the full nested structure, so two
|
|
149
|
+
* values are equal-keyed iff they are deeply equal.
|
|
65
150
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const normalized = normalizeStorageKey(key);
|
|
70
|
-
this._store.set(normalized, new Uint8Array(data));
|
|
71
|
-
return `memory://${normalized}`;
|
|
72
|
-
}
|
|
73
|
-
async retrieve(uri) {
|
|
74
|
-
if (!uri.startsWith("memory://"))
|
|
75
|
-
return null;
|
|
76
|
-
const key = uri.slice("memory://".length);
|
|
77
|
-
const value = this._store.get(key);
|
|
78
|
-
return value ? new Uint8Array(value) : null;
|
|
151
|
+
function stableStringifyDeep(value) {
|
|
152
|
+
if (!isObjectLike(value)) {
|
|
153
|
+
return JSON.stringify(value ?? null);
|
|
79
154
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return false;
|
|
83
|
-
const key = uri.slice("memory://".length);
|
|
84
|
-
return this._store.has(key);
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
return `[${value.map((v) => stableStringifyDeep(v)).join(",")}]`;
|
|
85
157
|
}
|
|
158
|
+
const entries = Object.keys(value)
|
|
159
|
+
.sort()
|
|
160
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringifyDeep(value[k])}`);
|
|
161
|
+
return `{${entries.join(",")}}`;
|
|
86
162
|
}
|
|
87
163
|
/**
|
|
88
|
-
*
|
|
164
|
+
* Normalize the image input for image-to-image / image-to-video predictions
|
|
165
|
+
* into a list of byte buffers. Accepts the modern `images` array as well as
|
|
166
|
+
* the legacy singular `image` field so older callers keep working.
|
|
89
167
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
resolvePathFromKey(key) {
|
|
96
|
-
const normalized = normalizeStorageKey(key);
|
|
97
|
-
const absolute = resolve(join(this.rootDir, normalized));
|
|
98
|
-
if (!isWithinRoot(this.rootDir, absolute)) {
|
|
99
|
-
throw new Error(`Storage key escapes root: ${key}`);
|
|
100
|
-
}
|
|
101
|
-
return absolute;
|
|
102
|
-
}
|
|
103
|
-
resolvePathFromUri(uri) {
|
|
104
|
-
let absolute = null;
|
|
105
|
-
if (uri.startsWith("file://")) {
|
|
106
|
-
try {
|
|
107
|
-
absolute = resolve(fileURLToPath(uri));
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// Invalid file:// URI — treat as unresolvable.
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else if (uri.startsWith("/api/storage/") ||
|
|
115
|
-
uri.startsWith("api/storage/")) {
|
|
116
|
-
const key = uri.replace(/^\/?api\/storage\//, "");
|
|
117
|
-
absolute = this.resolvePathFromKey(key);
|
|
118
|
-
}
|
|
119
|
-
else if (/^https?:\/\//.test(uri)) {
|
|
120
|
-
try {
|
|
121
|
-
const parsed = new URL(uri);
|
|
122
|
-
if (parsed.pathname.startsWith("/api/storage/")) {
|
|
123
|
-
const key = parsed.pathname.replace(/^\/api\/storage\//, "");
|
|
124
|
-
absolute = this.resolvePathFromKey(key);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
// Malformed URL — treat as unresolvable.
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
if (absolute === null || !isWithinRoot(this.rootDir, absolute)) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
return absolute;
|
|
139
|
-
}
|
|
140
|
-
async store(key, data, _contentType) {
|
|
141
|
-
const absolutePath = this.resolvePathFromKey(key);
|
|
142
|
-
await mkdir(dirname(absolutePath), { recursive: true });
|
|
143
|
-
await writeFile(absolutePath, data);
|
|
144
|
-
return pathToFileURL(absolutePath).toString();
|
|
145
|
-
}
|
|
146
|
-
async retrieve(uri) {
|
|
147
|
-
const absolutePath = this.resolvePathFromUri(uri);
|
|
148
|
-
if (!absolutePath)
|
|
149
|
-
return null;
|
|
150
|
-
try {
|
|
151
|
-
return await readFile(absolutePath);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// File not found or unreadable — caller handles null.
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async exists(uri) {
|
|
159
|
-
const absolutePath = this.resolvePathFromUri(uri);
|
|
160
|
-
if (!absolutePath)
|
|
161
|
-
return false;
|
|
162
|
-
try {
|
|
163
|
-
await access(absolutePath);
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// File does not exist or is inaccessible.
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
168
|
+
function coerceImageList(params) {
|
|
169
|
+
const list = params.images;
|
|
170
|
+
if (Array.isArray(list)) {
|
|
171
|
+
return list.filter((b) => b instanceof Uint8Array);
|
|
170
172
|
}
|
|
173
|
+
const single = params.image;
|
|
174
|
+
return single instanceof Uint8Array ? [single] : [];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Read an outpaint padding bag off the loose params. Only the four side keys
|
|
178
|
+
* are read, and only when numeric: a caller that passes nothing gets
|
|
179
|
+
* `undefined`, which lets the provider fall back to its endpoint's default
|
|
180
|
+
* expansion rather than sending zeros that would outpaint nothing.
|
|
181
|
+
*/
|
|
182
|
+
function coerceOutpaintPadding(value) {
|
|
183
|
+
if (!isObjectLike(value))
|
|
184
|
+
return undefined;
|
|
185
|
+
const side = (key) => isNumber(value[key]) ? value[key] : undefined;
|
|
186
|
+
const padding = {
|
|
187
|
+
left: side("left"),
|
|
188
|
+
right: side("right"),
|
|
189
|
+
top: side("top"),
|
|
190
|
+
bottom: side("bottom")
|
|
191
|
+
};
|
|
192
|
+
return Object.values(padding).some((v) => v !== undefined)
|
|
193
|
+
? padding
|
|
194
|
+
: undefined;
|
|
195
|
+
}
|
|
196
|
+
function coerceByteList(value) {
|
|
197
|
+
if (!Array.isArray(value))
|
|
198
|
+
return [];
|
|
199
|
+
return value.filter((item) => item instanceof Uint8Array && item.byteLength > 0);
|
|
200
|
+
}
|
|
201
|
+
function coerceStringList(value) {
|
|
202
|
+
if (!Array.isArray(value))
|
|
203
|
+
return [];
|
|
204
|
+
return value.filter(isNonEmptyString);
|
|
205
|
+
}
|
|
206
|
+
function firstNonEmptyByteList(value) {
|
|
207
|
+
return value.find((bytes) => bytes.byteLength > 0) ?? new Uint8Array();
|
|
208
|
+
}
|
|
209
|
+
function isAbortSignal(value) {
|
|
210
|
+
return (isObjectLike(value) &&
|
|
211
|
+
isBoolean(value.aborted) &&
|
|
212
|
+
isCallable(value.addEventListener));
|
|
171
213
|
}
|
|
172
214
|
/**
|
|
173
|
-
*
|
|
215
|
+
* Normalize `params.entities` into provider-level {@link EntityReference}s.
|
|
174
216
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
217
|
+
* Consumers send entities in whatever shape they hold: a full protocol Entity
|
|
218
|
+
* (`reference_images` list), a lean `{name, descriptor, image}` where `image`
|
|
219
|
+
* is bytes, an ImageRef-like object, or a bare URI string. Images are resolved
|
|
220
|
+
* to bytes here — the last point with a ProcessingContext — so providers only
|
|
221
|
+
* ever see `Uint8Array`s. Entities without a name are dropped; an unresolvable
|
|
222
|
+
* image degrades to a text-only entity (the descriptor still applies).
|
|
177
223
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
|
|
224
|
+
async function coerceEntityList(params, context) {
|
|
225
|
+
const raw = params.entities;
|
|
226
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
227
|
+
return undefined;
|
|
228
|
+
const out = [];
|
|
229
|
+
for (const item of raw) {
|
|
230
|
+
if (!isObjectLike(item))
|
|
231
|
+
continue;
|
|
232
|
+
const entity = item;
|
|
233
|
+
const name = isString(entity.name) ? entity.name.trim() : "";
|
|
234
|
+
if (!name)
|
|
235
|
+
continue;
|
|
236
|
+
const descriptor = isString(entity.descriptor)
|
|
237
|
+
? entity.descriptor
|
|
238
|
+
: undefined;
|
|
239
|
+
const imageSource = entity.image ??
|
|
240
|
+
(Array.isArray(entity.reference_images)
|
|
241
|
+
? entity.reference_images[0]
|
|
242
|
+
: undefined);
|
|
243
|
+
let image;
|
|
244
|
+
if (imageSource instanceof Uint8Array) {
|
|
245
|
+
image = imageSource;
|
|
246
|
+
}
|
|
247
|
+
else if (isNonEmptyString(imageSource)) {
|
|
248
|
+
image =
|
|
249
|
+
(await loadMediaRefBytes({ type: "image", uri: imageSource }, context)) ?? undefined;
|
|
250
|
+
}
|
|
251
|
+
else if (isObjectLike(imageSource)) {
|
|
252
|
+
image =
|
|
253
|
+
(await loadMediaRefBytes(imageSource, context)) ??
|
|
254
|
+
undefined;
|
|
255
|
+
}
|
|
256
|
+
out.push({ name, descriptor, image });
|
|
257
|
+
}
|
|
258
|
+
return out.length > 0 ? out : undefined;
|
|
259
|
+
}
|
|
260
|
+
export function normalizeStorageKey(key) {
|
|
261
|
+
const segments = [];
|
|
262
|
+
for (const segment of key.replaceAll("\\", "/").split("/")) {
|
|
263
|
+
if (!segment || segment === ".")
|
|
264
|
+
continue;
|
|
265
|
+
if (segment === "..") {
|
|
266
|
+
if (segments.length === 0) {
|
|
267
|
+
throw new Error(`Invalid storage key: ${key}`);
|
|
268
|
+
}
|
|
269
|
+
segments.pop();
|
|
270
|
+
continue;
|
|
200
271
|
}
|
|
201
|
-
|
|
202
|
-
const key = withoutScheme.slice(slashIndex + 1);
|
|
203
|
-
return { bucket, key };
|
|
204
|
-
}
|
|
205
|
-
async store(key, data, contentType) {
|
|
206
|
-
const objectKey = this.keyForStore(key);
|
|
207
|
-
await this.client.putObject({
|
|
208
|
-
bucket: this.bucket,
|
|
209
|
-
key: objectKey,
|
|
210
|
-
body: data,
|
|
211
|
-
contentType
|
|
212
|
-
});
|
|
213
|
-
return `s3://${this.bucket}/${objectKey}`;
|
|
214
|
-
}
|
|
215
|
-
async retrieve(uri) {
|
|
216
|
-
const parsed = this.parseUri(uri);
|
|
217
|
-
if (!parsed)
|
|
218
|
-
return null;
|
|
219
|
-
if (parsed.bucket !== this.bucket)
|
|
220
|
-
return null;
|
|
221
|
-
return this.client.getObject(parsed);
|
|
272
|
+
segments.push(segment);
|
|
222
273
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return false;
|
|
227
|
-
if (parsed.bucket !== this.bucket)
|
|
228
|
-
return false;
|
|
229
|
-
return this.client.headObject(parsed);
|
|
274
|
+
const cleaned = segments.join("/");
|
|
275
|
+
if (!cleaned) {
|
|
276
|
+
throw new Error(`Invalid storage key: ${key}`);
|
|
230
277
|
}
|
|
278
|
+
return cleaned;
|
|
231
279
|
}
|
|
232
280
|
/**
|
|
233
281
|
* Resolve paths relative to a configured workspace root.
|
|
@@ -272,32 +320,158 @@ export function resolveWorkspacePath(workspaceDir, path) {
|
|
|
272
320
|
}
|
|
273
321
|
return absPath;
|
|
274
322
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
323
|
+
let workspaceFactory = (opts) => opts.workspace ?? null;
|
|
324
|
+
/** Installed once by `storage-workspace.ts`; see {@link ProcessingContext.workspace}. */
|
|
325
|
+
export function setWorkspaceFactory(factory) {
|
|
326
|
+
workspaceFactory = factory;
|
|
327
|
+
}
|
|
328
|
+
function resolveWorkspaceOption(opts) {
|
|
329
|
+
return workspaceFactory(opts);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The listed entry holding the asset `bareId`, or undefined.
|
|
333
|
+
*
|
|
334
|
+
* A short resource id is a prefix of the stored id, so the key continues with
|
|
335
|
+
* more hex before its extension; a full id is followed by the extension
|
|
336
|
+
* itself. A hierarchical id (`user-1/image`) lives in the full key, a flat id
|
|
337
|
+
* in the last segment — both forms are matched. Generated thumbnails
|
|
338
|
+
* (`<id>_thumb.<ext>`) are skipped unless they are the id being resolved, so a
|
|
339
|
+
* legit `banner_thumb` still matches instead of being dropped by a substring
|
|
340
|
+
* test. Preview proxies (`<id>_proxy.<ext>`) are skipped the same way.
|
|
341
|
+
*/
|
|
342
|
+
function findListedAsset(entries, bareId) {
|
|
343
|
+
const bareEndsWithThumb = bareId.endsWith("_thumb");
|
|
344
|
+
const needle = isShortResourceId(bareId) ? bareId : `${bareId}.`;
|
|
345
|
+
return entries.find((entry) => {
|
|
346
|
+
const key = entry.key;
|
|
347
|
+
const lastSegment = key.split("/").pop() ?? "";
|
|
348
|
+
if (!key.startsWith(needle) && !lastSegment.startsWith(needle)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
// A preview proxy (`<id>_proxy.mp4`) and its manifest sit next to the
|
|
352
|
+
// asset. A short id must never resolve to them: renders read the original.
|
|
353
|
+
if (/_proxy\.[^./]+$/.test(lastSegment) && !bareId.endsWith("_proxy")) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const isThumb = /_thumb\.[^./]+$/.test(lastSegment);
|
|
357
|
+
return !isThumb || bareEndsWithThumb;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
278
360
|
export class ProcessingContext {
|
|
279
361
|
jobId;
|
|
280
362
|
workflowId;
|
|
363
|
+
/** Chat thread id, when this context is serving a chat-driven agent run. */
|
|
364
|
+
threadId;
|
|
281
365
|
userId;
|
|
282
366
|
workspaceDir;
|
|
283
367
|
assetOutputMode;
|
|
368
|
+
/**
|
|
369
|
+
* Whether Output nodes should turn transient media into durable Asset rows.
|
|
370
|
+
* Hosts can disable this for explicitly temporary SDK executions.
|
|
371
|
+
*/
|
|
372
|
+
persistOutputAssets;
|
|
284
373
|
environment;
|
|
374
|
+
/** Bearer token for authenticated calls back to the owning NodeTool API. */
|
|
375
|
+
authToken;
|
|
376
|
+
/** Read-only sandbox module catalog supplied by the execution host. */
|
|
377
|
+
sandboxModuleCatalog;
|
|
378
|
+
/**
|
|
379
|
+
* Run-level cancellation. Set by the kernel to the signal
|
|
380
|
+
* `WorkflowRunner.cancel()` aborts, so long-running node work (agent loops,
|
|
381
|
+
* provider calls) can be interrupted rather than merely having its result
|
|
382
|
+
* discarded. Nodes reached through the streaming `run()` path can also read
|
|
383
|
+
* the same signal via `inputs.signal`.
|
|
384
|
+
*
|
|
385
|
+
* Defaults to a never-aborted signal so contexts built outside a kernel run
|
|
386
|
+
* (tools, tests, CLI) need not supply one.
|
|
387
|
+
*/
|
|
388
|
+
signal = new AbortController().signal;
|
|
389
|
+
/**
|
|
390
|
+
* Wake-up payload for a trigger-driven run. Set by the kernel
|
|
391
|
+
* (`WorkflowRunner._runImpl`) from `RunJobRequest.trigger_event` at the
|
|
392
|
+
* start of each run, alongside {@link signal} — so it must be a mutable
|
|
393
|
+
* property, not a constructor-only value. The trigger node's actor reads
|
|
394
|
+
* `triggerEvent?.node_id === this.id` to enter its event-emitting entry
|
|
395
|
+
* point instead of the normal streaming path. `null` for interactive/
|
|
396
|
+
* live-test runs.
|
|
397
|
+
*/
|
|
398
|
+
triggerEvent;
|
|
285
399
|
/** Message queue: all emitted processing messages. */
|
|
286
400
|
_messages = [];
|
|
401
|
+
/** Whether emit() feeds the pull queue (see constructor option). */
|
|
402
|
+
_retainMessageQueue = true;
|
|
287
403
|
/** Latest node status by node id. */
|
|
288
404
|
_nodeStatuses = new Map();
|
|
289
405
|
/** Latest edge status by edge id. */
|
|
290
406
|
_edgeStatuses = new Map();
|
|
291
|
-
/**
|
|
292
|
-
|
|
407
|
+
/** Message listeners (for real-time streaming). */
|
|
408
|
+
_messageListeners = new Set();
|
|
409
|
+
/**
|
|
410
|
+
* One media object can appear in both a live output_update and the
|
|
411
|
+
* authoritative terminal result. Reuse its materialization within this
|
|
412
|
+
* execution instead of encoding/storing the same large payload twice.
|
|
413
|
+
*/
|
|
414
|
+
_normalizedOutputAssets = new WeakMap();
|
|
293
415
|
/** Cache adapter. */
|
|
294
416
|
cache;
|
|
295
417
|
/** Storage adapter (optional, for asset handling). */
|
|
296
418
|
storage;
|
|
419
|
+
/**
|
|
420
|
+
* Asset storage adapter — where user assets (`asset://<id>.<ext>`) live.
|
|
421
|
+
* Separate from {@link storage}, which servers point at the *temp* store:
|
|
422
|
+
* without this, an `asset://` reference could only be resolved over HTTP
|
|
423
|
+
* through `/api/storage`, which authorizes by `x-user-id` and 404s for
|
|
424
|
+
* anyone but user `1`. Null falls back to {@link storage}.
|
|
425
|
+
*/
|
|
426
|
+
assetStorage;
|
|
427
|
+
/**
|
|
428
|
+
* Workspace storage adapter — the agent's working directory abstracted
|
|
429
|
+
* behind {@link StorageAdapter}. Tools that read/write/list files use
|
|
430
|
+
* this adapter so the same code paths work locally (FS-backed) and in
|
|
431
|
+
* cloud deployments (S3/Supabase-backed).
|
|
432
|
+
*
|
|
433
|
+
* If null, file-tool operations should return a clear error rather than
|
|
434
|
+
* fall back to direct FS access — there is no implicit workspace.
|
|
435
|
+
*/
|
|
436
|
+
/**
|
|
437
|
+
* @deprecated Use {@link workspace}, which works on a cloud deployment too.
|
|
438
|
+
* Only set when a host passed a bare adapter; a host that passes a
|
|
439
|
+
* `workspace` leaves this null.
|
|
440
|
+
*/
|
|
441
|
+
workspaceStorage;
|
|
442
|
+
/**
|
|
443
|
+
* The run's workspace as an interface — the way to read and write run files.
|
|
444
|
+
*
|
|
445
|
+
* Null when the host wired none, in which case a file operation must report
|
|
446
|
+
* that plainly rather than reaching for the process working directory.
|
|
447
|
+
* Prefer this over {@link workspaceDir}, which is only non-null when the
|
|
448
|
+
* workspace happens to be a real folder: a cloud run's workspace lives in
|
|
449
|
+
* object storage and has no path.
|
|
450
|
+
*/
|
|
451
|
+
workspace;
|
|
452
|
+
/** Project captured when this run started. */
|
|
453
|
+
projectId;
|
|
454
|
+
/**
|
|
455
|
+
* Unified, structured agent memory. The single source of truth for results
|
|
456
|
+
* shared between agents, tasks, steps and tools. Keys use the namespaces
|
|
457
|
+
* `step:`, `task:`, `input:`, `shared:` (see {@link memoryKeys}).
|
|
458
|
+
*/
|
|
459
|
+
memory = new AgentMemory();
|
|
297
460
|
/** Variables shared across node execution. */
|
|
298
461
|
_variables;
|
|
462
|
+
/** Named async channels backing Set/Get Variable (see {@link getChannel}). */
|
|
463
|
+
_channels = new Map();
|
|
464
|
+
/** Remaining Set Variable writers per channel; channel closes at zero. */
|
|
465
|
+
_channelWriters = new Map();
|
|
299
466
|
/** Optional async secret resolver. */
|
|
300
467
|
_secretResolver = null;
|
|
468
|
+
/** Every secret value the resolver handed out this run (see {@link ProcessingContext.getResolvedSecretValues}). */
|
|
469
|
+
_resolvedSecrets = new Set();
|
|
470
|
+
/** Identity shared by all work beneath an app operation. */
|
|
471
|
+
appRunContext;
|
|
472
|
+
runTraceContext;
|
|
473
|
+
appRunCostAccount;
|
|
474
|
+
_appRunDocuments = new Map();
|
|
301
475
|
/** Fetch function used by HTTP helpers. */
|
|
302
476
|
_fetch;
|
|
303
477
|
/** Optional temporary URL resolver for stored assets. */
|
|
@@ -308,10 +482,21 @@ export class ProcessingContext {
|
|
|
308
482
|
_modelInterfaces = null;
|
|
309
483
|
/** Provider cache keyed by provider id. */
|
|
310
484
|
_providers = new Map();
|
|
485
|
+
_providerPromises = new Map();
|
|
311
486
|
/** In-context memory URI cache (memory:// key-value objects). */
|
|
312
487
|
_memory = new Map();
|
|
313
488
|
/** Optional control event dispatcher (set by workflow runner). */
|
|
314
489
|
_sendControlEvent = null;
|
|
490
|
+
/**
|
|
491
|
+
* Executable tools supplied by the caller that owns this run (set by the
|
|
492
|
+
* agent workflow runner). Agent nodes merge these into their own tool list
|
|
493
|
+
* by name, so a node that selects `read_file` gets the caller's live,
|
|
494
|
+
* fully-wired tool instead of a builtin stub. Tools the caller does not
|
|
495
|
+
* provide fall back to builtin hydration.
|
|
496
|
+
*/
|
|
497
|
+
_injectedTools = [];
|
|
498
|
+
/** Provider charge (USD) reported by the current node execution (e.g. FAL/KIE generation). */
|
|
499
|
+
_providerCost = null;
|
|
315
500
|
/** Optional executor resolver for sub-workflow execution. */
|
|
316
501
|
_resolveExecutor = null;
|
|
317
502
|
/** Optional node type resolver for sub-workflow graph hydration. */
|
|
@@ -320,48 +505,119 @@ export class ProcessingContext {
|
|
|
320
505
|
_totalCost = 0;
|
|
321
506
|
/** Per-operation cost entries. */
|
|
322
507
|
_operationCosts = [];
|
|
508
|
+
_generationLifecycle;
|
|
323
509
|
constructor(opts) {
|
|
324
510
|
this.jobId = opts.jobId;
|
|
325
511
|
this.workflowId = opts.workflowId ?? null;
|
|
512
|
+
this.projectId = opts.projectId ?? null;
|
|
513
|
+
this.threadId = opts.threadId ?? null;
|
|
326
514
|
this.userId = opts.userId ?? "default";
|
|
327
|
-
this.
|
|
328
|
-
this.
|
|
515
|
+
this.workspace = resolveWorkspaceOption(opts);
|
|
516
|
+
this.workspaceDir = this.workspace?.localDir ?? opts.workspaceDir ?? null;
|
|
517
|
+
this.assetOutputMode = opts.assetOutputMode ?? "native";
|
|
518
|
+
this.persistOutputAssets = opts.persistOutputAssets ?? true;
|
|
519
|
+
this._generationLifecycle = opts.generationLifecycle ?? null;
|
|
520
|
+
this.appRunContext = opts.appRunContext ? Object.freeze({ ...opts.appRunContext }) : null;
|
|
521
|
+
this.runTraceContext = opts.runTraceContext ?? getRunTraceScope() ?? null;
|
|
522
|
+
if (this.runTraceContext?.secretValues instanceof Set) {
|
|
523
|
+
this._resolvedSecrets = this.runTraceContext.secretValues;
|
|
524
|
+
}
|
|
525
|
+
this.appRunCostAccount = opts.appRunCostAccount ?? (this.appRunContext ? { llmCostUsd: 0, unpriced: false } : null);
|
|
329
526
|
this.cache = opts.cache ?? new MemoryCache();
|
|
330
527
|
this.storage = opts.storage ?? null;
|
|
331
|
-
this.
|
|
528
|
+
this.assetStorage = opts.assetStorage ?? null;
|
|
529
|
+
this.workspaceStorage = opts.workspaceStorage ?? null;
|
|
530
|
+
if (opts.onMessage) {
|
|
531
|
+
this._messageListeners.add(opts.onMessage);
|
|
532
|
+
}
|
|
332
533
|
this._variables = { ...(opts.variables ?? {}) };
|
|
333
534
|
const env = {};
|
|
334
|
-
for (const [k, v] of Object.entries(
|
|
335
|
-
if (
|
|
535
|
+
for (const [k, v] of Object.entries(safeProcessEnv())) {
|
|
536
|
+
if (v != null)
|
|
336
537
|
env[k] = v;
|
|
337
538
|
}
|
|
338
539
|
this.environment = { ...env, ...(opts.environment ?? {}) };
|
|
540
|
+
this.authToken = opts.authToken ?? null;
|
|
541
|
+
// An explicit option — including null — wins over the host default.
|
|
542
|
+
this.sandboxModuleCatalog =
|
|
543
|
+
opts.sandboxModuleCatalog !== undefined
|
|
544
|
+
? opts.sandboxModuleCatalog
|
|
545
|
+
: getProcessSandboxModuleCatalog();
|
|
339
546
|
this._secretResolver = opts.secretResolver ?? null;
|
|
340
547
|
this._fetch =
|
|
341
548
|
opts.fetchFn ??
|
|
342
549
|
((input, init) => fetch(input, init));
|
|
343
550
|
this._tempUrlResolver = opts.tempUrlResolver ?? null;
|
|
344
551
|
this._modelInterfaces = opts.modelInterfaces ?? null;
|
|
552
|
+
this._retainMessageQueue = opts.retainMessageQueue ?? true;
|
|
553
|
+
this.triggerEvent = opts.triggerEvent ?? null;
|
|
345
554
|
}
|
|
346
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Derive a child context.
|
|
557
|
+
*
|
|
558
|
+
* Defaults to full isolation: fresh agent memory, fresh message queue, the
|
|
559
|
+
* parent's message listeners inherited. Two opt-ins exist because a child
|
|
560
|
+
* that must *not* be isolated in one respect should not have to reach around
|
|
561
|
+
* the copy:
|
|
562
|
+
*
|
|
563
|
+
* - `shareMemory` shares the parent's {@link AgentMemory} instance, so a
|
|
564
|
+
* scoped run (own injected tools, own emit fan-out) still reads and
|
|
565
|
+
* writes the same step/task results.
|
|
566
|
+
* - `inheritMessageListeners: false` drops the parent's listeners, for a
|
|
567
|
+
* child that forwards messages itself and would otherwise double-notify.
|
|
568
|
+
*
|
|
569
|
+
* The run-level cancellation {@link signal} always carries over: a child that
|
|
570
|
+
* cannot observe the parent's Stop keeps burning provider calls after the
|
|
571
|
+
* user cancelled.
|
|
572
|
+
*/
|
|
573
|
+
copy(opts) {
|
|
347
574
|
const next = new ProcessingContext({
|
|
348
|
-
jobId: this.jobId,
|
|
349
|
-
|
|
575
|
+
jobId: opts?.jobId ?? this.jobId,
|
|
576
|
+
projectId: this.projectId,
|
|
577
|
+
workflowId: opts?.workflowId !== undefined ? opts.workflowId : this.workflowId,
|
|
578
|
+
threadId: this.threadId,
|
|
350
579
|
userId: this.userId,
|
|
351
|
-
|
|
580
|
+
workspace: opts?.workspace !== undefined ? opts.workspace : this.workspace,
|
|
352
581
|
assetOutputMode: this.assetOutputMode,
|
|
582
|
+
persistOutputAssets: this.persistOutputAssets,
|
|
353
583
|
cache: this.cache,
|
|
354
584
|
storage: this.storage,
|
|
355
|
-
|
|
585
|
+
assetStorage: this.assetStorage,
|
|
586
|
+
workspaceStorage: this.workspaceStorage,
|
|
356
587
|
variables: { ...this._variables },
|
|
357
588
|
environment: { ...this.environment },
|
|
589
|
+
authToken: this.authToken,
|
|
590
|
+
sandboxModuleCatalog: this.sandboxModuleCatalog,
|
|
358
591
|
fetchFn: this._fetch,
|
|
359
592
|
secretResolver: this._secretResolver ?? undefined,
|
|
360
|
-
tempUrlResolver: this._tempUrlResolver ?? undefined
|
|
593
|
+
tempUrlResolver: this._tempUrlResolver ?? undefined,
|
|
594
|
+
modelInterfaces: this._modelInterfaces ?? undefined,
|
|
595
|
+
retainMessageQueue: this._retainMessageQueue,
|
|
596
|
+
triggerEvent: this.triggerEvent,
|
|
597
|
+
generationLifecycle: opts?.generationLifecycle ?? this._generationLifecycle ?? undefined,
|
|
598
|
+
appRunContext: opts?.appRunContext ?? this.appRunContext ?? undefined,
|
|
599
|
+
runTraceContext: opts?.runTraceContext ?? this.runTraceContext ?? undefined,
|
|
600
|
+
appRunCostAccount: !opts?.appRunContext || opts.appRunContext.appRunId === this.appRunContext?.appRunId ? this.appRunCostAccount ?? undefined : undefined
|
|
361
601
|
});
|
|
602
|
+
if (opts?.inheritMessageListeners !== false) {
|
|
603
|
+
for (const listener of this._messageListeners) {
|
|
604
|
+
next.addMessageListener(listener);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (opts?.shareMemory) {
|
|
608
|
+
next.memory = this.memory;
|
|
609
|
+
}
|
|
610
|
+
if (!opts?.runTraceContext) {
|
|
611
|
+
next._resolvedSecrets = this._resolvedSecrets;
|
|
612
|
+
}
|
|
613
|
+
if (next.appRunContext?.appRunId === this.appRunContext?.appRunId) {
|
|
614
|
+
next._appRunDocuments = this._appRunDocuments;
|
|
615
|
+
}
|
|
616
|
+
next.signal = this.signal;
|
|
362
617
|
next._providerResolver = this._providerResolver;
|
|
363
618
|
next._modelInterfaces = this._modelInterfaces;
|
|
364
619
|
next._sendControlEvent = this._sendControlEvent;
|
|
620
|
+
next._injectedTools = this._injectedTools;
|
|
365
621
|
next._providers = new Map(this._providers);
|
|
366
622
|
next._totalCost = this._totalCost;
|
|
367
623
|
next._operationCosts = this._operationCosts.map((c) => ({ ...c }));
|
|
@@ -373,9 +629,66 @@ export class ProcessingContext {
|
|
|
373
629
|
setProviderResolver(resolver) {
|
|
374
630
|
this._providerResolver = resolver;
|
|
375
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Whether this context serves providers from an injected resolver rather
|
|
634
|
+
* than the process-wide registry.
|
|
635
|
+
*
|
|
636
|
+
* When it does, {@link getProvider} never reaches the registry and never
|
|
637
|
+
* reads a credential — so a credential the run will not use is not a reason
|
|
638
|
+
* to refuse the run. The execution preflight reads this to decide whether
|
|
639
|
+
* its credential check applies at all.
|
|
640
|
+
*/
|
|
641
|
+
get hasProviderResolver() {
|
|
642
|
+
return this._providerResolver !== null;
|
|
643
|
+
}
|
|
376
644
|
setModelInterfaces(modelInterfaces) {
|
|
377
645
|
this._modelInterfaces = modelInterfaces;
|
|
378
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* The interfaces this context answers with: its own when a caller wired
|
|
649
|
+
* them, else the host's process-wide default.
|
|
650
|
+
*
|
|
651
|
+
* The default is what keeps a run from depending on which entrance built
|
|
652
|
+
* its context. Six hosts assembled these by hand and three of them forgot
|
|
653
|
+
* `createAsset`, so the same workflow saved an image under `workflows run`
|
|
654
|
+
* and died under `debug` with "model interface is not configured".
|
|
655
|
+
*
|
|
656
|
+
* The two compose key by key, the context's own winning, so wiring one
|
|
657
|
+
* interface does not uninstall the rest of the floor.
|
|
658
|
+
*/
|
|
659
|
+
modelInterfaces() {
|
|
660
|
+
const own = this._modelInterfaces;
|
|
661
|
+
const floor = getDefaultModelInterfaces();
|
|
662
|
+
if (!own)
|
|
663
|
+
return floor;
|
|
664
|
+
if (!floor)
|
|
665
|
+
return own;
|
|
666
|
+
// Per key, not per object: a host that wires two interfaces of its own
|
|
667
|
+
// keeps the other twelve. Replacing wholesale is how the HTTP run route
|
|
668
|
+
// lost `getTimelineSequence` while setting only the asset pair.
|
|
669
|
+
return { ...floor, ...own };
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Whether a given model interface is wired for this context — its own or
|
|
673
|
+
* the host default. Lets callers decide between the DB-backed path
|
|
674
|
+
* (interface present — let errors propagate) and an in-memory fallback
|
|
675
|
+
* (interface absent — hermetic CLI runs, tests) without invoking a method
|
|
676
|
+
* that would throw when unwired.
|
|
677
|
+
*/
|
|
678
|
+
hasModelInterface(name) {
|
|
679
|
+
return isCallable(this.modelInterfaces()?.[name]);
|
|
680
|
+
}
|
|
681
|
+
/** Read a user setting, falling back to this run's environment. */
|
|
682
|
+
async getSetting(key) {
|
|
683
|
+
const fn = this.modelInterfaces()?.getSetting;
|
|
684
|
+
if (fn) {
|
|
685
|
+
const value = await fn({ userId: this.userId, key });
|
|
686
|
+
if (value !== null && value.length > 0)
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
const value = this.environment[key];
|
|
690
|
+
return value !== undefined && value.length > 0 ? value : null;
|
|
691
|
+
}
|
|
379
692
|
// -----------------------------------------------------------------------
|
|
380
693
|
// Control event dispatch
|
|
381
694
|
// -----------------------------------------------------------------------
|
|
@@ -396,6 +709,19 @@ export class ProcessingContext {
|
|
|
396
709
|
return null;
|
|
397
710
|
return this._sendControlEvent(targetNodeId, properties);
|
|
398
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Supply the live tool set for this run. Agent nodes match their selected
|
|
714
|
+
* tool names against these and prefer them over builtin hydration, so tools
|
|
715
|
+
* that only the caller can wire (MCP, workspace, skills, security-gated
|
|
716
|
+
* wrappers) reach nodes the caller did not author.
|
|
717
|
+
*/
|
|
718
|
+
setInjectedTools(tools) {
|
|
719
|
+
this._injectedTools = tools;
|
|
720
|
+
}
|
|
721
|
+
/** Injected tool matching `name`, or null when the caller supplied none. */
|
|
722
|
+
getInjectedTool(name) {
|
|
723
|
+
return this._injectedTools.find((tool) => tool.name === name) ?? null;
|
|
724
|
+
}
|
|
399
725
|
// -----------------------------------------------------------------------
|
|
400
726
|
// Sub-workflow executor resolution
|
|
401
727
|
// -----------------------------------------------------------------------
|
|
@@ -411,9 +737,6 @@ export class ProcessingContext {
|
|
|
411
737
|
get resolveNodeType() {
|
|
412
738
|
return this._resolveNodeType;
|
|
413
739
|
}
|
|
414
|
-
/**
|
|
415
|
-
* Check if control event dispatch is available.
|
|
416
|
-
*/
|
|
417
740
|
get hasControlEventSupport() {
|
|
418
741
|
return this._sendControlEvent !== null;
|
|
419
742
|
}
|
|
@@ -427,40 +750,32 @@ export class ProcessingContext {
|
|
|
427
750
|
const cached = this._providers.get(providerId);
|
|
428
751
|
if (cached)
|
|
429
752
|
return cached;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const kwargs = { ...reg.kwargs };
|
|
438
|
-
for (const [key, value] of Object.entries(kwargs)) {
|
|
439
|
-
if (!value) {
|
|
440
|
-
const envVal = process.env[key];
|
|
441
|
-
if (envVal) {
|
|
442
|
-
kwargs[key] = envVal;
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
const secret = await this.getSecret(key);
|
|
446
|
-
if (secret) {
|
|
447
|
-
kwargs[key] = secret;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
const resolved = new reg.cls(kwargs);
|
|
753
|
+
const pending = this._providerPromises.get(providerId);
|
|
754
|
+
if (pending)
|
|
755
|
+
return pending;
|
|
756
|
+
const resolution = (async () => {
|
|
757
|
+
const resolved = this._providerResolver
|
|
758
|
+
? await this._providerResolver(providerId)
|
|
759
|
+
: await import("./providers/index.js").then(({ getProvider }) => getProvider(providerId, (key) => this.getSecret(key)));
|
|
453
760
|
this._providers.set(providerId, resolved);
|
|
454
761
|
resolved.setMessageEmitter((msg) => this.postMessage(msg));
|
|
455
762
|
return resolved;
|
|
763
|
+
})();
|
|
764
|
+
this._providerPromises.set(providerId, resolution);
|
|
765
|
+
try {
|
|
766
|
+
return await resolution;
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
this._providerPromises.delete(providerId);
|
|
456
770
|
}
|
|
457
|
-
const resolved = await this._providerResolver(providerId);
|
|
458
|
-
this._providers.set(providerId, resolved);
|
|
459
|
-
resolved.setMessageEmitter((msg) => this.postMessage(msg));
|
|
460
|
-
return resolved;
|
|
461
771
|
}
|
|
462
|
-
|
|
463
|
-
|
|
772
|
+
/**
|
|
773
|
+
* Check whether a registered provider has all required credentials
|
|
774
|
+
* resolvable through this context (DB/keychain/env).
|
|
775
|
+
*/
|
|
776
|
+
async isProviderConfigured(providerId) {
|
|
777
|
+
const { isProviderConfigured: check } = await import("./providers/index.js");
|
|
778
|
+
return check(providerId, (key) => this.getSecret(key));
|
|
464
779
|
}
|
|
465
780
|
// -----------------------------------------------------------------------
|
|
466
781
|
// Secrets / Variables API
|
|
@@ -471,12 +786,55 @@ export class ProcessingContext {
|
|
|
471
786
|
setTempUrlResolver(resolver) {
|
|
472
787
|
this._tempUrlResolver = resolver;
|
|
473
788
|
}
|
|
789
|
+
/**
|
|
790
|
+
* Resolve a raw storage URI (e.g. `file:///.../storage/<key>`) to a URL the
|
|
791
|
+
* UI can fetch (e.g. `/api/storage/<key>`). Falls back to returning the URI
|
|
792
|
+
* unchanged when no resolver is wired (DSL / tests).
|
|
793
|
+
*
|
|
794
|
+
* Use this from tools that write binary output via `context.storage.store()`
|
|
795
|
+
* and want to surface a UI-renderable URL in their result.
|
|
796
|
+
*/
|
|
797
|
+
async resolveTempUrl(uri) {
|
|
798
|
+
if (!this._tempUrlResolver)
|
|
799
|
+
return uri;
|
|
800
|
+
const resolvedUri = await this._tempUrlResolver(uri);
|
|
801
|
+
if (ProcessingContext.isClientFetchableResolvedAssetUri(uri, resolvedUri)) {
|
|
802
|
+
return resolvedUri;
|
|
803
|
+
}
|
|
804
|
+
throw new Error(`Temp URL resolver returned an unsafe URL for '${uri}': '${resolvedUri}'`);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Whether this run can reach a secret store at all.
|
|
808
|
+
*
|
|
809
|
+
* {@link getSecret} answers `null` both for "no store is wired" and for "the
|
|
810
|
+
* store does not hold this key", which a caller asking *which* of a list of
|
|
811
|
+
* credentials exists cannot tell apart — and reporting every declared
|
|
812
|
+
* credential as missing because no store was reachable is exactly the
|
|
813
|
+
* false alarm the graph validator's `missing_secret` check refuses to raise.
|
|
814
|
+
* A host installs the availability callback only when this is true.
|
|
815
|
+
*/
|
|
816
|
+
get hasSecretResolver() {
|
|
817
|
+
return this._secretResolver !== null;
|
|
818
|
+
}
|
|
474
819
|
async getSecret(key) {
|
|
475
820
|
if (!this._secretResolver)
|
|
476
821
|
return null;
|
|
477
822
|
const value = await this._secretResolver(key, this.userId);
|
|
823
|
+
if (value != null && value.length > 0) {
|
|
824
|
+
this._resolvedSecrets.add(value);
|
|
825
|
+
}
|
|
478
826
|
return value ?? null;
|
|
479
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Every secret value handed out during this run. The supervisor's escalation
|
|
830
|
+
* constructor masks against this set — without it, "mask every secret" has no
|
|
831
|
+
* subject, because the resolver forgets each key as soon as it answers.
|
|
832
|
+
* Values shorter than `MIN_MASKABLE_SECRET_LENGTH` are not recorded: masking
|
|
833
|
+
* a two-character value would redact unrelated text everywhere it occurs.
|
|
834
|
+
*/
|
|
835
|
+
getResolvedSecretValues() {
|
|
836
|
+
return this._resolvedSecrets;
|
|
837
|
+
}
|
|
480
838
|
async getSecretRequired(key) {
|
|
481
839
|
const value = await this.getSecret(key);
|
|
482
840
|
if (value === null || value === "") {
|
|
@@ -484,12 +842,6 @@ export class ProcessingContext {
|
|
|
484
842
|
}
|
|
485
843
|
return value;
|
|
486
844
|
}
|
|
487
|
-
async get_secret(key) {
|
|
488
|
-
return this.getSecret(key);
|
|
489
|
-
}
|
|
490
|
-
async get_secret_required(key) {
|
|
491
|
-
return this.getSecretRequired(key);
|
|
492
|
-
}
|
|
493
845
|
get(key, defaultValue) {
|
|
494
846
|
if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
|
|
495
847
|
return this._variables[key];
|
|
@@ -498,72 +850,93 @@ export class ProcessingContext {
|
|
|
498
850
|
}
|
|
499
851
|
set(key, value) {
|
|
500
852
|
this._variables[key] = value;
|
|
501
|
-
void this.persistVariableIfNeeded(key, value);
|
|
502
853
|
if (key.startsWith("memory://")) {
|
|
503
854
|
this._memory.set(key, value);
|
|
504
855
|
}
|
|
505
856
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
await writeFile(path, JSON.stringify(value, null, 2), "utf8");
|
|
510
|
-
this._variables[key] = { __workspace_result__: `${key}.json` };
|
|
511
|
-
return path;
|
|
857
|
+
/** Whether a shared variable with this name has been set. */
|
|
858
|
+
hasVariable(key) {
|
|
859
|
+
return Object.prototype.hasOwnProperty.call(this._variables, key);
|
|
512
860
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Snapshot of all shared variables (name → value). Used by nodes that
|
|
863
|
+
* resolve variable references against the workflow's shared context — e.g.
|
|
864
|
+
* the Prompt node merges these into its `{{ name }}` template variables so a
|
|
865
|
+
* Set Variable node upstream can supply values without an explicit wire.
|
|
866
|
+
*/
|
|
867
|
+
getVariables() {
|
|
868
|
+
return { ...this._variables };
|
|
869
|
+
}
|
|
870
|
+
// -----------------------------------------------------------------------
|
|
871
|
+
// Variable channels (Set/Get Variable)
|
|
872
|
+
// -----------------------------------------------------------------------
|
|
873
|
+
/**
|
|
874
|
+
* Declare how many Set Variable nodes write to `name`. The runner calls this
|
|
875
|
+
* before any actor runs so a channel with no writers can close immediately
|
|
876
|
+
* (readers get end-of-stream instead of hanging).
|
|
877
|
+
*/
|
|
878
|
+
registerChannelWriters(name, count) {
|
|
879
|
+
this._channelWriters.set(name, (this._channelWriters.get(name) ?? 0) + count);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Get (or lazily create) the named variable channel. A channel created for a
|
|
883
|
+
* name with no registered writers is returned already closed, so a reader
|
|
884
|
+
* referencing an undefined variable resolves immediately rather than waiting
|
|
885
|
+
* forever.
|
|
886
|
+
*/
|
|
887
|
+
getChannel(name) {
|
|
888
|
+
let channel = this._channels.get(name);
|
|
889
|
+
if (!channel) {
|
|
890
|
+
channel = new VariableChannel();
|
|
891
|
+
this._channels.set(name, channel);
|
|
892
|
+
if ((this._channelWriters.get(name) ?? 0) === 0) {
|
|
893
|
+
channel.close();
|
|
528
894
|
}
|
|
529
895
|
}
|
|
530
|
-
|
|
531
|
-
return this._variables[key];
|
|
532
|
-
}
|
|
533
|
-
return defaultValue;
|
|
896
|
+
return channel;
|
|
534
897
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
898
|
+
/**
|
|
899
|
+
* Record that one Set Variable writer for `name` has finished. When the last
|
|
900
|
+
* writer finishes, the channel closes — readers drain the buffered values
|
|
901
|
+
* then end. The runner calls this from each Set Variable actor's completion.
|
|
902
|
+
*/
|
|
903
|
+
markChannelWriterDone(name) {
|
|
904
|
+
const remaining = (this._channelWriters.get(name) ?? 0) - 1;
|
|
905
|
+
this._channelWriters.set(name, Math.max(0, remaining));
|
|
906
|
+
if (remaining <= 0) {
|
|
907
|
+
this.getChannel(name).close();
|
|
538
908
|
}
|
|
539
|
-
return resolveWorkspacePath(this.workspaceDir, fileName);
|
|
540
909
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return;
|
|
546
|
-
if (typeof value === "function" ||
|
|
547
|
-
typeof value === "symbol" ||
|
|
548
|
-
typeof value === "bigint")
|
|
549
|
-
return;
|
|
550
|
-
const filePath = this.workspacePathFor(`var_${key}.json`);
|
|
551
|
-
try {
|
|
552
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
553
|
-
await writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
|
|
910
|
+
/** Close every channel — a teardown backstop so no reader is left waiting. */
|
|
911
|
+
closeAllChannels() {
|
|
912
|
+
for (const channel of this._channels.values()) {
|
|
913
|
+
channel.close();
|
|
554
914
|
}
|
|
555
|
-
|
|
556
|
-
|
|
915
|
+
}
|
|
916
|
+
/** Whether any Set Variable node was registered as writing to `name`. */
|
|
917
|
+
hasChannelWriters(name) {
|
|
918
|
+
return (this._channelWriters.get(name) ?? 0) > 0;
|
|
919
|
+
}
|
|
920
|
+
async storeStepResult(key, value) {
|
|
921
|
+
this._variables[key] = value;
|
|
922
|
+
return "";
|
|
923
|
+
}
|
|
924
|
+
async loadStepResult(key, defaultValue) {
|
|
925
|
+
if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
|
|
926
|
+
return this._variables[key];
|
|
557
927
|
}
|
|
928
|
+
return defaultValue;
|
|
558
929
|
}
|
|
559
930
|
// -----------------------------------------------------------------------
|
|
560
931
|
// Node result cache helpers
|
|
561
932
|
// -----------------------------------------------------------------------
|
|
562
933
|
generateNodeCacheKey(nodeType, nodeProps) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
934
|
+
// Deep, deterministic serialization: keys are sorted at EVERY nesting
|
|
935
|
+
// level. The previous `JSON.stringify(props, sortedTopLevelKeys)` form used
|
|
936
|
+
// the key array as a recursive replacer allow-list, which silently dropped
|
|
937
|
+
// every nested value — so nodes differing only in a nested prop collided on
|
|
938
|
+
// the same key and returned each other's cached (wrong) results.
|
|
939
|
+
return `${this.userId}:${nodeType}:${stableStringifyDeep(nodeProps ?? null)}`;
|
|
567
940
|
}
|
|
568
941
|
async getCachedResult(nodeType, nodeProps) {
|
|
569
942
|
const key = this.generateNodeCacheKey(nodeType, nodeProps);
|
|
@@ -576,12 +949,20 @@ export class ProcessingContext {
|
|
|
576
949
|
// -----------------------------------------------------------------------
|
|
577
950
|
// Message queue API
|
|
578
951
|
// -----------------------------------------------------------------------
|
|
952
|
+
addMessageListener(listener) {
|
|
953
|
+
this._messageListeners.add(listener);
|
|
954
|
+
return () => {
|
|
955
|
+
this._messageListeners.delete(listener);
|
|
956
|
+
};
|
|
957
|
+
}
|
|
579
958
|
/**
|
|
580
959
|
* Emit a processing message.
|
|
581
|
-
* Appended to the internal queue and forwarded to
|
|
960
|
+
* Appended to the internal queue and forwarded to listeners if set.
|
|
582
961
|
*/
|
|
583
962
|
emit(msg) {
|
|
584
|
-
this.
|
|
963
|
+
if (this._retainMessageQueue) {
|
|
964
|
+
this._messages.push(msg);
|
|
965
|
+
}
|
|
585
966
|
this._notifyMessage();
|
|
586
967
|
if (msg.type === "node_update" && msg.node_id) {
|
|
587
968
|
this._nodeStatuses.set(msg.node_id, msg);
|
|
@@ -589,17 +970,29 @@ export class ProcessingContext {
|
|
|
589
970
|
if (msg.type === "edge_update" && msg.edge_id) {
|
|
590
971
|
this._edgeStatuses.set(msg.edge_id, msg);
|
|
591
972
|
}
|
|
592
|
-
|
|
593
|
-
|
|
973
|
+
for (const listener of this._messageListeners) {
|
|
974
|
+
try {
|
|
975
|
+
listener(msg);
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
// Listeners are observers and must not interrupt workflow execution.
|
|
979
|
+
}
|
|
594
980
|
}
|
|
595
981
|
}
|
|
596
982
|
postMessage(msg) {
|
|
597
983
|
this.emit(msg);
|
|
598
984
|
}
|
|
599
|
-
|
|
600
|
-
|
|
985
|
+
/** Record actual provider charge for the current node run (attached to completed NodeUpdate). */
|
|
986
|
+
setProviderCost(provider, amount, unit, details) {
|
|
987
|
+
this._providerCost = { provider, amount, unit, ...details };
|
|
988
|
+
recordInvocationCost(amount);
|
|
989
|
+
}
|
|
990
|
+
getProviderCost() {
|
|
991
|
+
return this._providerCost;
|
|
992
|
+
}
|
|
993
|
+
clearProviderCost() {
|
|
994
|
+
this._providerCost = null;
|
|
601
995
|
}
|
|
602
|
-
/** Get all emitted messages. */
|
|
603
996
|
getMessages() {
|
|
604
997
|
return this._messages;
|
|
605
998
|
}
|
|
@@ -609,39 +1002,31 @@ export class ProcessingContext {
|
|
|
609
1002
|
popMessage() {
|
|
610
1003
|
return this._messages.shift();
|
|
611
1004
|
}
|
|
612
|
-
|
|
1005
|
+
_messageWaiters = [];
|
|
613
1006
|
/** Notify that a new message has been pushed. */
|
|
614
1007
|
_notifyMessage() {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1008
|
+
const waiters = this._messageWaiters;
|
|
1009
|
+
this._messageWaiters = [];
|
|
1010
|
+
for (const resolve of waiters)
|
|
618
1011
|
resolve();
|
|
619
|
-
}
|
|
620
1012
|
}
|
|
621
1013
|
async popMessageAsync() {
|
|
622
1014
|
while (this._messages.length === 0) {
|
|
623
1015
|
await new Promise((r) => {
|
|
624
|
-
this.
|
|
1016
|
+
this._messageWaiters.push(r);
|
|
625
1017
|
});
|
|
626
1018
|
}
|
|
627
1019
|
return this._messages.shift();
|
|
628
1020
|
}
|
|
629
|
-
async pop_message_async() {
|
|
630
|
-
return this.popMessageAsync();
|
|
631
|
-
}
|
|
632
1021
|
getNodeStatuses() {
|
|
633
1022
|
return Object.fromEntries(this._nodeStatuses);
|
|
634
1023
|
}
|
|
635
1024
|
getEdgeStatuses() {
|
|
636
1025
|
return Object.fromEntries(this._edgeStatuses);
|
|
637
1026
|
}
|
|
638
|
-
/** Clear the message queue. */
|
|
639
1027
|
clearMessages() {
|
|
640
1028
|
this._messages = [];
|
|
641
1029
|
}
|
|
642
|
-
clear_messages() {
|
|
643
|
-
this.clearMessages();
|
|
644
|
-
}
|
|
645
1030
|
trackOperationCost(operation, cost, metadata) {
|
|
646
1031
|
const safeCost = Number.isFinite(cost) ? cost : 0;
|
|
647
1032
|
this._totalCost += safeCost;
|
|
@@ -663,6 +1048,26 @@ export class ProcessingContext {
|
|
|
663
1048
|
getTotalCost() {
|
|
664
1049
|
return this._totalCost;
|
|
665
1050
|
}
|
|
1051
|
+
/** Null means at least one LLM call could not be priced. */
|
|
1052
|
+
getAppRunLlmCost() {
|
|
1053
|
+
return this.appRunCostAccount && !this.appRunCostAccount.unpriced
|
|
1054
|
+
? this.appRunCostAccount.llmCostUsd
|
|
1055
|
+
: null;
|
|
1056
|
+
}
|
|
1057
|
+
getAppRunDocuments() {
|
|
1058
|
+
return [...this._appRunDocuments.values()].map((reference) => ({
|
|
1059
|
+
...reference
|
|
1060
|
+
}));
|
|
1061
|
+
}
|
|
1062
|
+
recordAppRunDocument(kind, document) {
|
|
1063
|
+
if (this.appRunContext && document) {
|
|
1064
|
+
this._appRunDocuments.set(`${kind}:${document.id}`, {
|
|
1065
|
+
kind,
|
|
1066
|
+
id: document.id
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return document;
|
|
1070
|
+
}
|
|
666
1071
|
getOperationCosts() {
|
|
667
1072
|
return this._operationCosts;
|
|
668
1073
|
}
|
|
@@ -694,6 +1099,10 @@ export class ProcessingContext {
|
|
|
694
1099
|
// -----------------------------------------------------------------------
|
|
695
1100
|
// HTTP helpers
|
|
696
1101
|
// -----------------------------------------------------------------------
|
|
1102
|
+
/**
|
|
1103
|
+
* Issue an HTTP request with the retry/backoff policy every node shares.
|
|
1104
|
+
* `httpGet` is the GET shorthand; other verbs go through this directly.
|
|
1105
|
+
*/
|
|
697
1106
|
async httpRequestWithRetries(method, url, opts = {}) {
|
|
698
1107
|
const retry = opts.retry ?? {};
|
|
699
1108
|
const maxRetries = retry.maxRetries ?? 3;
|
|
@@ -719,22 +1128,38 @@ export class ProcessingContext {
|
|
|
719
1128
|
const retryAfter = response.headers.get("Retry-After");
|
|
720
1129
|
const retryDelay = retryAfter ? Number(retryAfter) * 1000 : NaN;
|
|
721
1130
|
const delayMs = Number.isFinite(retryDelay)
|
|
722
|
-
? retryDelay
|
|
1131
|
+
? Math.min(Math.max(0, retryDelay), 30_000)
|
|
723
1132
|
: backoffMs * 2 ** attempt;
|
|
724
|
-
|
|
1133
|
+
// Drain/cancel the unconsumed body before retrying, or under undici
|
|
1134
|
+
// the socket stays checked out of the connection pool (leaked until
|
|
1135
|
+
// GC) and emits a "body not consumed" warning.
|
|
1136
|
+
await response.body?.cancel().catch(() => { });
|
|
1137
|
+
await waitForRetry(Math.max(0, delayMs), opts.signal);
|
|
725
1138
|
continue;
|
|
726
1139
|
}
|
|
727
1140
|
if (!response.ok) {
|
|
728
|
-
|
|
1141
|
+
// Non-retryable HTTP error (status not in retryStatuses): fail fast.
|
|
1142
|
+
// Throwing into the generic catch below would treat it like a network
|
|
1143
|
+
// error and retry it, hammering 4xx responses maxRetries times.
|
|
1144
|
+
const nonRetryable = new Error(`HTTP ${response.status} ${response.statusText} for ${method} ${url}`);
|
|
1145
|
+
nonRetryable.nonRetryable = true;
|
|
1146
|
+
await response.body?.cancel().catch(() => { });
|
|
1147
|
+
throw nonRetryable;
|
|
729
1148
|
}
|
|
730
1149
|
return response;
|
|
731
1150
|
}
|
|
732
1151
|
catch (error) {
|
|
733
1152
|
lastError = error;
|
|
1153
|
+
if (opts.signal?.aborted)
|
|
1154
|
+
break;
|
|
1155
|
+
// A non-retryable HTTP error must not loop; only thrown fetch/network
|
|
1156
|
+
// errors and retryable statuses are retried.
|
|
1157
|
+
if (error?.nonRetryable)
|
|
1158
|
+
break;
|
|
734
1159
|
if (attempt >= maxRetries - 1)
|
|
735
1160
|
break;
|
|
736
1161
|
const delayMs = backoffMs * 2 ** attempt;
|
|
737
|
-
await
|
|
1162
|
+
await waitForRetry(delayMs, opts.signal);
|
|
738
1163
|
}
|
|
739
1164
|
}
|
|
740
1165
|
throw lastError instanceof Error
|
|
@@ -744,50 +1169,13 @@ export class ProcessingContext {
|
|
|
744
1169
|
async httpGet(url, opts = {}) {
|
|
745
1170
|
return this.httpRequestWithRetries("GET", url, opts);
|
|
746
1171
|
}
|
|
747
|
-
async httpPost(url, opts = {}) {
|
|
748
|
-
return this.httpRequestWithRetries("POST", url, opts);
|
|
749
|
-
}
|
|
750
|
-
async httpPatch(url, opts = {}) {
|
|
751
|
-
return this.httpRequestWithRetries("PATCH", url, opts);
|
|
752
|
-
}
|
|
753
|
-
async httpPut(url, opts = {}) {
|
|
754
|
-
return this.httpRequestWithRetries("PUT", url, opts);
|
|
755
|
-
}
|
|
756
|
-
async httpDelete(url, opts = {}) {
|
|
757
|
-
return this.httpRequestWithRetries("DELETE", url, opts);
|
|
758
|
-
}
|
|
759
|
-
async httpHead(url, opts = {}) {
|
|
760
|
-
return this.httpRequestWithRetries("HEAD", url, opts);
|
|
761
|
-
}
|
|
762
1172
|
async downloadFile(url, opts = {}) {
|
|
763
1173
|
const response = await this.httpGet(url, opts);
|
|
764
1174
|
const arr = await response.arrayBuffer();
|
|
765
1175
|
return new Uint8Array(arr);
|
|
766
1176
|
}
|
|
767
|
-
async downloadText(url, opts = {}) {
|
|
768
|
-
const response = await this.httpGet(url, opts);
|
|
769
|
-
return response.text();
|
|
770
|
-
}
|
|
771
|
-
async http_get(url, opts = {}) {
|
|
772
|
-
return this.httpGet(url, opts);
|
|
773
|
-
}
|
|
774
|
-
async http_post(url, opts = {}) {
|
|
775
|
-
return this.httpPost(url, opts);
|
|
776
|
-
}
|
|
777
|
-
async http_patch(url, opts = {}) {
|
|
778
|
-
return this.httpPatch(url, opts);
|
|
779
|
-
}
|
|
780
|
-
async http_put(url, opts = {}) {
|
|
781
|
-
return this.httpPut(url, opts);
|
|
782
|
-
}
|
|
783
|
-
async http_delete(url, opts = {}) {
|
|
784
|
-
return this.httpDelete(url, opts);
|
|
785
|
-
}
|
|
786
|
-
async http_head(url, opts = {}) {
|
|
787
|
-
return this.httpHead(url, opts);
|
|
788
|
-
}
|
|
789
1177
|
requireModelInterface(name) {
|
|
790
|
-
const fn = this.
|
|
1178
|
+
const fn = this.modelInterfaces()?.[name];
|
|
791
1179
|
if (!fn) {
|
|
792
1180
|
throw new Error(`ProcessingContext model interface '${String(name)}' is not configured`);
|
|
793
1181
|
}
|
|
@@ -797,34 +1185,155 @@ export class ProcessingContext {
|
|
|
797
1185
|
const fn = this.requireModelInterface("getJob");
|
|
798
1186
|
return fn({ userId: this.userId, jobId });
|
|
799
1187
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1188
|
+
/**
|
|
1189
|
+
* HOLDOUT (anti-slop/no-unknown-returns): see the `createAsset` port above —
|
|
1190
|
+
* narrowing the result breaks the `as Record<string, unknown>` assertions in
|
|
1191
|
+
* `image-nodes` and `audio-nodes`.
|
|
1192
|
+
*/
|
|
803
1193
|
async createAsset(args) {
|
|
804
1194
|
const fn = this.requireModelInterface("createAsset");
|
|
805
1195
|
const content = args.content ?? args.instructions;
|
|
806
1196
|
if (!content) {
|
|
807
1197
|
throw new Error("Asset content is required");
|
|
808
1198
|
}
|
|
1199
|
+
recordInvocationAsset();
|
|
809
1200
|
return fn({
|
|
810
1201
|
userId: this.userId,
|
|
811
1202
|
workflowId: this.workflowId,
|
|
812
1203
|
jobId: this.jobId,
|
|
1204
|
+
projectId: this.projectId,
|
|
813
1205
|
name: args.name,
|
|
814
1206
|
contentType: args.contentType,
|
|
815
1207
|
content,
|
|
816
1208
|
parentId: args.parentId ?? null,
|
|
817
|
-
nodeId: args.nodeId ?? null
|
|
1209
|
+
nodeId: args.nodeId ?? null,
|
|
1210
|
+
metadata: args.metadata ?? null
|
|
818
1211
|
});
|
|
819
1212
|
}
|
|
820
|
-
|
|
821
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* Replace an owned asset's bytes, keeping its id. Null when the asset is
|
|
1215
|
+
* missing or belongs to someone else.
|
|
1216
|
+
*/
|
|
1217
|
+
async updateAssetBytes(args) {
|
|
1218
|
+
const fn = this.requireModelInterface("updateAssetBytes");
|
|
1219
|
+
return fn({ userId: this.userId, ...args });
|
|
1220
|
+
}
|
|
1221
|
+
/** Load a persisted sketch (image document) owned by the current user. */
|
|
1222
|
+
async getImageDocument(id) {
|
|
1223
|
+
const fn = this.requireModelInterface("getImageDocument");
|
|
1224
|
+
return fn({ userId: this.userId, id });
|
|
1225
|
+
}
|
|
1226
|
+
/** Create a persisted sketch (image document) owned by the current user. */
|
|
1227
|
+
async createImageDocument(args) {
|
|
1228
|
+
const fn = this.requireModelInterface("createImageDocument");
|
|
1229
|
+
const document = await fn({ userId: this.userId, ...args });
|
|
1230
|
+
return this.recordAppRunDocument("sketch", document);
|
|
1231
|
+
}
|
|
1232
|
+
/** Load a persisted timeline sequence owned by the current user. */
|
|
1233
|
+
async getTimelineSequence(id) {
|
|
1234
|
+
const fn = this.requireModelInterface("getTimelineSequence");
|
|
1235
|
+
return fn({ userId: this.userId, id });
|
|
1236
|
+
}
|
|
1237
|
+
/** Create a persisted timeline sequence owned by the current user. */
|
|
1238
|
+
async createTimelineSequence(sequence) {
|
|
1239
|
+
const fn = this.requireModelInterface("createTimelineSequence");
|
|
1240
|
+
const document = await fn({ userId: this.userId, sequence });
|
|
1241
|
+
return this.recordAppRunDocument("timeline", document);
|
|
1242
|
+
}
|
|
1243
|
+
/** Replace a persisted timeline sequence's document. */
|
|
1244
|
+
async updateTimelineSequence(id, sequence) {
|
|
1245
|
+
const fn = this.requireModelInterface("updateTimelineSequence");
|
|
1246
|
+
const document = await fn({ userId: this.userId, id, sequence });
|
|
1247
|
+
return this.recordAppRunDocument("timeline", document);
|
|
1248
|
+
}
|
|
1249
|
+
/** Load a persisted script owned by the current user. */
|
|
1250
|
+
async getScript(id) {
|
|
1251
|
+
const fn = this.requireModelInterface("getScript");
|
|
1252
|
+
return fn({ userId: this.userId, id });
|
|
1253
|
+
}
|
|
1254
|
+
/** Create a persisted script owned by the current user. */
|
|
1255
|
+
async createScript(args) {
|
|
1256
|
+
const fn = this.requireModelInterface("createScript");
|
|
1257
|
+
const document = await fn({ userId: this.userId, ...args });
|
|
1258
|
+
return this.recordAppRunDocument("script", document);
|
|
1259
|
+
}
|
|
1260
|
+
/** Replace a persisted script's document (and optional timeline link). */
|
|
1261
|
+
async updateScript(id, args) {
|
|
1262
|
+
const fn = this.requireModelInterface("updateScript");
|
|
1263
|
+
const document = await fn({ userId: this.userId, id, ...args });
|
|
1264
|
+
return this.recordAppRunDocument("script", document);
|
|
1265
|
+
}
|
|
1266
|
+
/** Load a persisted storyboard owned by the current user. */
|
|
1267
|
+
async getStoryboard(id) {
|
|
1268
|
+
const fn = this.requireModelInterface("getStoryboard");
|
|
1269
|
+
return fn({ userId: this.userId, id });
|
|
1270
|
+
}
|
|
1271
|
+
/** The current user's storyboards, narrowed by the given filters. */
|
|
1272
|
+
async listStoryboards(args = {}) {
|
|
1273
|
+
const fn = this.requireModelInterface("listStoryboards");
|
|
1274
|
+
return fn({ userId: this.userId, ...args });
|
|
1275
|
+
}
|
|
1276
|
+
/** The copy this user derived from a template for one recast mapping. */
|
|
1277
|
+
async findRecastStoryboard(args) {
|
|
1278
|
+
const fn = this.requireModelInterface("findRecastStoryboard");
|
|
1279
|
+
return fn({ userId: this.userId, ...args });
|
|
1280
|
+
}
|
|
1281
|
+
/** Create a persisted storyboard owned by the current user. */
|
|
1282
|
+
async createStoryboard(args) {
|
|
1283
|
+
const fn = this.requireModelInterface("createStoryboard");
|
|
1284
|
+
const document = await fn({ userId: this.userId, ...args });
|
|
1285
|
+
return this.recordAppRunDocument("storyboard", document);
|
|
1286
|
+
}
|
|
1287
|
+
/** Replace a persisted storyboard's document (and optional timeline link). */
|
|
1288
|
+
async updateStoryboard(id, args) {
|
|
1289
|
+
const fn = this.requireModelInterface("updateStoryboard");
|
|
1290
|
+
const document = await fn({ userId: this.userId, id, ...args });
|
|
1291
|
+
return this.recordAppRunDocument("storyboard", document);
|
|
1292
|
+
}
|
|
1293
|
+
/** The current user's entity library, narrowed by the given filters. */
|
|
1294
|
+
async listEntities(args = {}) {
|
|
1295
|
+
const fn = this.requireModelInterface("listEntities");
|
|
1296
|
+
return fn({ userId: this.userId, ...args });
|
|
1297
|
+
}
|
|
1298
|
+
/** Read one entity owned by the current user. */
|
|
1299
|
+
async getEntity(id) {
|
|
1300
|
+
const fn = this.requireModelInterface("getEntity");
|
|
1301
|
+
return fn({ userId: this.userId, id });
|
|
1302
|
+
}
|
|
1303
|
+
/** Create or update one entity in the current user's library. */
|
|
1304
|
+
async upsertEntity(args) {
|
|
1305
|
+
const fn = this.requireModelInterface("upsertEntity");
|
|
1306
|
+
return fn({ userId: this.userId, ...args });
|
|
822
1307
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1308
|
+
/**
|
|
1309
|
+
* Recursively list the non-folder assets in `folderId`, or `null` when the id
|
|
1310
|
+
* is not a folder. Backed by the optional `listFolderAssets` model interface;
|
|
1311
|
+
* returns `null` when that interface is not configured.
|
|
1312
|
+
*/
|
|
1313
|
+
async listFolderAssets(folderId) {
|
|
1314
|
+
const fn = this._modelInterfaces?.listFolderAssets;
|
|
1315
|
+
if (!fn) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
return fn({ userId: this.userId, folderId });
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Look up one owned asset's identity + metadata, or `null` when missing /
|
|
1322
|
+
* not owned. Backed by the optional `getAssetInfo` model interface; returns
|
|
1323
|
+
* `null` when that interface is not configured.
|
|
1324
|
+
*/
|
|
1325
|
+
async getAssetInfo(assetId) {
|
|
1326
|
+
const fn = this.modelInterfaces()?.getAssetInfo;
|
|
1327
|
+
if (!fn) {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
return fn({ userId: this.userId, assetId });
|
|
1331
|
+
}
|
|
1332
|
+
resolveSandboxFilePath(path) {
|
|
1333
|
+
if (this.workspaceDir == null || this.workspaceDir === "") {
|
|
1334
|
+
throw new Error("workspaceDir is required for sandbox file operations");
|
|
1335
|
+
}
|
|
1336
|
+
const workspaceRoot = resolve(this.workspaceDir);
|
|
828
1337
|
const normalizedPath = path.replaceAll("\\", "/");
|
|
829
1338
|
if ((isAbsolute(normalizedPath) || /^[A-Za-z]:\//.test(normalizedPath)) &&
|
|
830
1339
|
isWithinRoot(workspaceRoot, resolve(normalizedPath))) {
|
|
@@ -876,110 +1385,393 @@ export class ProcessingContext {
|
|
|
876
1385
|
if (!trimmed) {
|
|
877
1386
|
return [];
|
|
878
1387
|
}
|
|
879
|
-
|
|
880
|
-
|
|
1388
|
+
// Strip a leading reference prefix down to the bare storage key. `asset://`
|
|
1389
|
+
// is the ref scheme; `/api/storage/` (and its slash-less variant) is the
|
|
1390
|
+
// browser-facing storage route that older messages / callers still carry —
|
|
1391
|
+
// treating that whole path as the id probes a bogus key and builds a
|
|
1392
|
+
// double-prefixed, percent-encoded HTTP url that 404s.
|
|
1393
|
+
let raw = trimmed;
|
|
1394
|
+
if (raw.startsWith("asset://")) {
|
|
1395
|
+
raw = raw.slice("asset://".length);
|
|
1396
|
+
}
|
|
1397
|
+
else if (raw.startsWith("/api/storage/")) {
|
|
1398
|
+
raw = raw.slice("/api/storage/".length);
|
|
1399
|
+
}
|
|
1400
|
+
else if (raw.startsWith("api/storage/")) {
|
|
1401
|
+
raw = raw.slice("api/storage/".length);
|
|
1402
|
+
}
|
|
1403
|
+
// Preserve sub-paths: `asset://user-1/image.png` -> primary `user-1/image.png`,
|
|
1404
|
+
// so storage keys with hierarchical layouts still resolve. Drop any
|
|
1405
|
+
// `?query`/`#hash` before deriving the key.
|
|
1406
|
+
const primary = raw.split(/[?#]/)[0];
|
|
881
1407
|
if (!primary) {
|
|
882
1408
|
return [];
|
|
883
1409
|
}
|
|
884
|
-
|
|
1410
|
+
// Strip the extension from the LAST path segment only. A `.` in an earlier
|
|
1411
|
+
// segment (`folder.v2/img`) is part of the id, not an extension — slicing
|
|
1412
|
+
// across `/` would yield a wrong-bytes candidate (`folder`).
|
|
1413
|
+
const slash = primary.lastIndexOf("/");
|
|
1414
|
+
const dir = slash >= 0 ? primary.slice(0, slash + 1) : "";
|
|
1415
|
+
const lastSegment = slash >= 0 ? primary.slice(slash + 1) : primary;
|
|
1416
|
+
const withoutExt = dir + lastSegment.replace(/\.[^.]+$/, "");
|
|
885
1417
|
return Array.from(new Set([primary, withoutExt].filter(Boolean)));
|
|
886
1418
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1419
|
+
/**
|
|
1420
|
+
* The adapters an asset reference is looked up in. Assets live in the asset
|
|
1421
|
+
* store; `this.storage` is the *temp* store on server paths. The asset
|
|
1422
|
+
* adapter goes first and the temp adapter stays as the fallback so
|
|
1423
|
+
* runtime-materialized refs still resolve.
|
|
1424
|
+
*/
|
|
1425
|
+
assetAdapters() {
|
|
1426
|
+
return [this.assetStorage, this.storage].filter((adapter, index, all) => adapter !== null && all.indexOf(adapter) === index);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Keys an asset id candidate may be stored under: `<userId>/<id>.<ext>`
|
|
1430
|
+
* (uploads — the current owner-prefixed layout), `<id>.<ext>` (the flat
|
|
1431
|
+
* legacy layout) and `assets/<id>` (runtime-materialized refs). The
|
|
1432
|
+
* owner-prefixed key goes first: without it every reference misses all the
|
|
1433
|
+
* exact lookups and falls through to the prefix listing, which degrades to
|
|
1434
|
+
* `list("")` — a full recursive walk / whole-bucket `listObjectsV2` across
|
|
1435
|
+
* every tenant, on every asset reference.
|
|
1436
|
+
*/
|
|
1437
|
+
assetKeysFor(candidate) {
|
|
1438
|
+
const keys = [candidate, `assets/${candidate}`];
|
|
1439
|
+
if (this.userId && !candidate.startsWith(`${this.userId}/`)) {
|
|
1440
|
+
keys.unshift(`${this.userId}/${candidate}`);
|
|
895
1441
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1442
|
+
return keys;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Prefixes to list, narrowest first, when no exact key matched.
|
|
1446
|
+
*
|
|
1447
|
+
* S3 treats `list(<owner>/<bareId>)` as a raw-string prefix match — bounded
|
|
1448
|
+
* to a handful of entries. `list("")` is the last resort: it is a
|
|
1449
|
+
* multi-thousand-object scan on production S3 backends, and on Supabase it
|
|
1450
|
+
* lists only the bucket's immediate children — which under the
|
|
1451
|
+
* owner-prefixed layout are folders, so it never sees an asset at all.
|
|
1452
|
+
*
|
|
1453
|
+
* The owner prefix is what makes this work for uploads. They are stored at
|
|
1454
|
+
* `<userId>/<id>.<ext>`, and a ref that carries no extension (`asset://<id>`,
|
|
1455
|
+
* or a bare `asset_id`) misses every exact-key lookup. Listing the owner's
|
|
1456
|
+
* own folder finds it on every adapter: hierarchical ones
|
|
1457
|
+
* (file/supabase/memory) list its children, and prefix-matching ones (S3)
|
|
1458
|
+
* stay scoped to that one user.
|
|
1459
|
+
*/
|
|
1460
|
+
assetListingPrefixes(bareId) {
|
|
1461
|
+
const owner = this.userId;
|
|
1462
|
+
return owner && !bareId.startsWith(`${owner}/`)
|
|
1463
|
+
? [`${owner}/${bareId}`, bareId, owner, ""]
|
|
1464
|
+
: [bareId, ""];
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Resolve an asset reference to a local file holding its bytes — the path
|
|
1468
|
+
* counterpart of {@link resolveAssetBytes}, for callers such as ffmpeg that
|
|
1469
|
+
* read a file in place instead of loading it into memory.
|
|
1470
|
+
*
|
|
1471
|
+
* Looks the reference up in the same adapters and under the same keys,
|
|
1472
|
+
* including the extension-tolerant listing, and asks each adapter for
|
|
1473
|
+
* `localPath`. That finds a managed file under the local asset root and an
|
|
1474
|
+
* external asset's in-place file. Returns null when no adapter keeps a local
|
|
1475
|
+
* file for it (object stores, the in-memory store, HTTP-only and
|
|
1476
|
+
* `package://` references); the caller then falls back to
|
|
1477
|
+
* {@link resolveAssetBytes}. The path is for reading only, and it is an
|
|
1478
|
+
* asset-store path, not a workspace file.
|
|
1479
|
+
*/
|
|
1480
|
+
async localPath(uri) {
|
|
1481
|
+
const trimmed = uri.trim();
|
|
1482
|
+
if (!trimmed || parsePackageAssetUri(trimmed)) {
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
const adapters = this.assetAdapters();
|
|
1486
|
+
const tryLocalPath = async (adapter, storageUri) => {
|
|
1487
|
+
try {
|
|
1488
|
+
return await adapter.localPath(storageUri);
|
|
1489
|
+
}
|
|
1490
|
+
catch {
|
|
1491
|
+
// An adapter that cannot answer is treated as having no local file.
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
if (trimmed.includes("://") && !trimmed.startsWith("asset://")) {
|
|
1496
|
+
for (const adapter of adapters) {
|
|
1497
|
+
const direct = await tryLocalPath(adapter, trimmed);
|
|
1498
|
+
if (direct) {
|
|
1499
|
+
return direct;
|
|
1500
|
+
}
|
|
900
1501
|
}
|
|
901
1502
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
for (const
|
|
1503
|
+
const idCandidates = this.parseAssetIdCandidates(trimmed);
|
|
1504
|
+
for (const adapter of adapters) {
|
|
1505
|
+
for (const candidate of idCandidates) {
|
|
1506
|
+
for (const key of this.assetKeysFor(candidate)) {
|
|
1507
|
+
const found = await tryLocalPath(adapter, adapter.uriForKey(key));
|
|
1508
|
+
if (found) {
|
|
1509
|
+
return found;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const bareId = idCandidates[idCandidates.length - 1];
|
|
1514
|
+
if (!bareId) {
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
for (const prefix of this.assetListingPrefixes(bareId)) {
|
|
905
1518
|
try {
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1519
|
+
const listing = await adapter.list(prefix);
|
|
1520
|
+
const match = findListedAsset(listing.entries, bareId);
|
|
1521
|
+
const found = match ? await tryLocalPath(adapter, match.uri) : null;
|
|
1522
|
+
if (found) {
|
|
1523
|
+
return found;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
// A listing that fails leaves the next, wider prefix to try.
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Resolve an `asset://<id>[.ext]` reference (or bare id / storage URI) to its
|
|
1535
|
+
* raw bytes.
|
|
1536
|
+
*
|
|
1537
|
+
* Server-uploaded assets are stored under the key `<id>.<ext>` and served at
|
|
1538
|
+
* `/api/storage/<id>.<ext>`; runtime-materialized refs live under `assets/`.
|
|
1539
|
+
* Resolution order: the in-process storage adapter (file/s3/supabase/memory),
|
|
1540
|
+
* a prefix listing that tolerates extension mismatches, then an HTTP download
|
|
1541
|
+
* from the server's `/api/storage` route. Returns the bytes plus an `attempts`
|
|
1542
|
+
* trail for callers that build detailed errors; `null` bytes means unresolved.
|
|
1543
|
+
*/
|
|
1544
|
+
async resolveAssetBytes(assetId, options) {
|
|
1545
|
+
let idCandidates = this.parseAssetIdCandidates(assetId);
|
|
1546
|
+
const trimmed = assetId.trim();
|
|
1547
|
+
const attempts = [];
|
|
1548
|
+
const adapters = this.assetAdapters();
|
|
1549
|
+
const tryStorageUri = async (adapter, uri) => {
|
|
1550
|
+
try {
|
|
1551
|
+
const retrieved = await adapter.retrieve(uri);
|
|
1552
|
+
if (retrieved) {
|
|
1553
|
+
return retrieved;
|
|
1554
|
+
}
|
|
1555
|
+
attempts.push(`storage miss: ${uri}`);
|
|
1556
|
+
}
|
|
1557
|
+
catch (error) {
|
|
1558
|
+
attempts.push(`storage error: ${uri} (${error instanceof Error ? error.message : String(error)})`);
|
|
1559
|
+
}
|
|
1560
|
+
return null;
|
|
1561
|
+
};
|
|
1562
|
+
// Constant package assets (`package://<pkg>/<path>`) ship with the install
|
|
1563
|
+
// — they have stable URIs and don't live in per-user storage. Resolve from
|
|
1564
|
+
// the bundled package-assets dir when configured, else over HTTP from the
|
|
1565
|
+
// server's package-asset route.
|
|
1566
|
+
const packageRef = parsePackageAssetUri(trimmed);
|
|
1567
|
+
if (packageRef) {
|
|
1568
|
+
const root = this.environment.NODETOOL_PACKAGE_ASSETS_DIR ??
|
|
1569
|
+
safeProcessEnv().NODETOOL_PACKAGE_ASSETS_DIR;
|
|
1570
|
+
if (root) {
|
|
1571
|
+
const segments = `${packageRef.packageName}/${packageRef.path}`
|
|
1572
|
+
.split("/")
|
|
1573
|
+
.filter((s) => s && s !== ".");
|
|
1574
|
+
if (!segments.some((s) => s === "..")) {
|
|
1575
|
+
const filePath = resolve(join(root, ...segments));
|
|
1576
|
+
const within = !relative(resolve(root), filePath).startsWith("..");
|
|
1577
|
+
if (within) {
|
|
1578
|
+
try {
|
|
1579
|
+
const bytes = (await readFile(filePath));
|
|
1580
|
+
attempts.push(`package asset: ${filePath}`);
|
|
1581
|
+
return { bytes, attempts };
|
|
1582
|
+
}
|
|
1583
|
+
catch (error) {
|
|
1584
|
+
attempts.push(`package asset miss: ${filePath} (${error instanceof Error ? error.message : String(error)})`);
|
|
1585
|
+
}
|
|
910
1586
|
}
|
|
911
|
-
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const httpPath = packageAssetHttpPath(trimmed);
|
|
1590
|
+
if (httpPath) {
|
|
1591
|
+
let pkgBaseUrl = this.environment.NODETOOL_API_URL ??
|
|
1592
|
+
safeProcessEnv().NODETOOL_API_URL ??
|
|
1593
|
+
"http://localhost:7777";
|
|
1594
|
+
while (pkgBaseUrl.endsWith("/")) {
|
|
1595
|
+
pkgBaseUrl = pkgBaseUrl.slice(0, -1);
|
|
1596
|
+
}
|
|
1597
|
+
const url = `${pkgBaseUrl}${httpPath}`;
|
|
1598
|
+
try {
|
|
1599
|
+
const bytes = await this.downloadFile(url, {
|
|
1600
|
+
retry: { maxRetries: 1, backoffMs: 200 }
|
|
1601
|
+
});
|
|
1602
|
+
attempts.push(`downloaded: ${url}`);
|
|
1603
|
+
return { bytes, attempts };
|
|
912
1604
|
}
|
|
913
1605
|
catch (error) {
|
|
914
|
-
|
|
1606
|
+
attempts.push(`package asset http miss: ${url} (${error instanceof Error ? error.message : String(error)})`);
|
|
915
1607
|
}
|
|
916
1608
|
}
|
|
1609
|
+
return { bytes: null, attempts };
|
|
917
1610
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1611
|
+
// A concrete storage URI / http URL handed in directly.
|
|
1612
|
+
if (trimmed.includes("://") && !trimmed.startsWith("asset://")) {
|
|
1613
|
+
for (const adapter of adapters) {
|
|
1614
|
+
const direct = await tryStorageUri(adapter, trimmed);
|
|
1615
|
+
if (direct) {
|
|
1616
|
+
return { bytes: direct, attempts };
|
|
1617
|
+
}
|
|
924
1618
|
}
|
|
925
|
-
|
|
1619
|
+
if (/^https?:\/\//.test(trimmed)) {
|
|
926
1620
|
try {
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1621
|
+
const bytes = await this.downloadFile(trimmed, {
|
|
1622
|
+
retry: { maxRetries: 1, backoffMs: 200 }
|
|
1623
|
+
});
|
|
1624
|
+
attempts.push(`downloaded: ${trimmed}`);
|
|
1625
|
+
return { bytes, attempts };
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
attempts.push(`http error: ${trimmed} (${error instanceof Error ? error.message : String(error)})`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
// An asset row names the stored file exactly (`<owner>/<id>.<ext>`), so a
|
|
1633
|
+
// reference that resolves to one is read by key and never by listing.
|
|
1634
|
+
let assetRowFound = false;
|
|
1635
|
+
if (this.hasModelInterface("getAssetInfo")) {
|
|
1636
|
+
let asset = null;
|
|
1637
|
+
const ownerPrefix = `${this.userId}/`;
|
|
1638
|
+
const rowIdCandidates = new Set(idCandidates);
|
|
1639
|
+
for (const candidate of idCandidates) {
|
|
1640
|
+
if (candidate.startsWith(ownerPrefix)) {
|
|
1641
|
+
rowIdCandidates.add(candidate.slice(ownerPrefix.length));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
for (const candidate of rowIdCandidates) {
|
|
1645
|
+
asset = await this.getAssetInfo(candidate);
|
|
1646
|
+
if (asset) {
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (asset) {
|
|
1651
|
+
assetRowFound = true;
|
|
1652
|
+
idCandidates = assetFileNameCandidates(asset.id, asset.content_type);
|
|
1653
|
+
}
|
|
1654
|
+
else if (options?.requireOwnedAsset) {
|
|
1655
|
+
attempts.push(`asset unavailable or not owned: ${trimmed}`);
|
|
1656
|
+
return { bytes: null, attempts };
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
for (const adapter of adapters) {
|
|
1660
|
+
for (const candidate of idCandidates) {
|
|
1661
|
+
for (const key of this.assetKeysFor(candidate)) {
|
|
1662
|
+
const bytes = await tryStorageUri(adapter, adapter.uriForKey(key));
|
|
1663
|
+
if (bytes) {
|
|
1664
|
+
return { bytes, attempts };
|
|
940
1665
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
// Extension-tolerant fallback: locate the stored file whose name starts
|
|
1669
|
+
// with the id, since the URN extension may differ from the one on disk
|
|
1670
|
+
// (e.g. jpeg vs jpg). Only reached when the exact-key lookups all miss
|
|
1671
|
+
// and no asset row named the file.
|
|
1672
|
+
const bareId = idCandidates[idCandidates.length - 1];
|
|
1673
|
+
if (bareId && !assetRowFound) {
|
|
1674
|
+
for (const prefix of this.assetListingPrefixes(bareId)) {
|
|
1675
|
+
try {
|
|
1676
|
+
const listing = await adapter.list(prefix);
|
|
1677
|
+
const match = findListedAsset(listing.entries, bareId);
|
|
1678
|
+
if (match) {
|
|
1679
|
+
const bytes = await tryStorageUri(adapter, match.uri);
|
|
1680
|
+
if (bytes) {
|
|
1681
|
+
return { bytes, attempts };
|
|
1682
|
+
}
|
|
947
1683
|
}
|
|
948
|
-
|
|
1684
|
+
}
|
|
1685
|
+
catch (error) {
|
|
1686
|
+
attempts.push(`storage list error (${error instanceof Error ? error.message : String(error)})`);
|
|
949
1687
|
}
|
|
950
1688
|
}
|
|
951
|
-
|
|
952
|
-
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
// HTTP fallback: the server streams bytes at `/api/storage/<key>`.
|
|
1692
|
+
let baseUrl = this.environment.NODETOOL_API_URL ??
|
|
1693
|
+
safeProcessEnv().NODETOOL_API_URL ??
|
|
1694
|
+
"http://localhost:7777";
|
|
1695
|
+
while (baseUrl.endsWith("/")) {
|
|
1696
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
1697
|
+
}
|
|
1698
|
+
for (const candidate of idCandidates) {
|
|
1699
|
+
if (!candidate.includes(".")) {
|
|
1700
|
+
continue; // the storage route is keyed by filename (`<id>.<ext>`)
|
|
1701
|
+
}
|
|
1702
|
+
const url = `${baseUrl}/api/storage/${encodeURIComponent(candidate)}`;
|
|
1703
|
+
try {
|
|
1704
|
+
const bytes = await this.downloadFile(url, {
|
|
1705
|
+
retry: { maxRetries: 1, backoffMs: 200 }
|
|
1706
|
+
});
|
|
1707
|
+
attempts.push(`downloaded: ${url}`);
|
|
1708
|
+
return { bytes, attempts };
|
|
1709
|
+
}
|
|
1710
|
+
catch (error) {
|
|
1711
|
+
attempts.push(`http miss: ${url} (${error instanceof Error ? error.message : String(error)})`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
// Last resort: ask the asset API for metadata and follow its `get_url`.
|
|
1715
|
+
// Handles bare ids (no extension, so the storage route can't be addressed
|
|
1716
|
+
// directly) and backends that expose a single-asset metadata endpoint.
|
|
1717
|
+
for (const candidate of idCandidates) {
|
|
1718
|
+
try {
|
|
1719
|
+
const metaResponse = await this.httpGet(`${baseUrl}/api/assets/${encodeURIComponent(candidate)}`);
|
|
1720
|
+
const metadata = (await metaResponse.json());
|
|
1721
|
+
const getUrl = metadata.get_url;
|
|
1722
|
+
if (isNonEmptyString(getUrl)) {
|
|
1723
|
+
const downloadUrl = getUrl.startsWith("/")
|
|
1724
|
+
? `${baseUrl}${getUrl}`
|
|
1725
|
+
: getUrl;
|
|
1726
|
+
const bytes = await this.downloadFile(downloadUrl, {
|
|
1727
|
+
retry: { maxRetries: 1, backoffMs: 200 }
|
|
1728
|
+
});
|
|
1729
|
+
attempts.push(`downloaded: ${downloadUrl}`);
|
|
1730
|
+
return { bytes, attempts };
|
|
953
1731
|
}
|
|
1732
|
+
const uri = metadata.uri;
|
|
1733
|
+
if (isString(uri)) {
|
|
1734
|
+
for (const adapter of adapters) {
|
|
1735
|
+
const bytes = await tryStorageUri(adapter, uri);
|
|
1736
|
+
if (bytes) {
|
|
1737
|
+
return { bytes, attempts };
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
attempts.push(`api lookup error for ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
954
1744
|
}
|
|
955
1745
|
}
|
|
1746
|
+
return { bytes: null, attempts };
|
|
1747
|
+
}
|
|
1748
|
+
async assetToSandbox(assetId, path) {
|
|
1749
|
+
const outputPath = this.resolveSandboxFilePath(path);
|
|
1750
|
+
const { bytes, attempts } = await this.resolveAssetBytes(assetId);
|
|
956
1751
|
if (!bytes) {
|
|
957
|
-
const details =
|
|
958
|
-
? ` Attempts: ${resolutionAttempts.join("; ")}`
|
|
959
|
-
: "";
|
|
1752
|
+
const details = attempts.length > 0 ? ` Attempts: ${attempts.join("; ")}` : "";
|
|
960
1753
|
throw new Error(`Unable to resolve asset '${assetId}' to sandbox bytes.${details}`);
|
|
961
1754
|
}
|
|
962
1755
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
963
1756
|
await writeFile(outputPath, bytes);
|
|
964
1757
|
return outputPath;
|
|
965
1758
|
}
|
|
966
|
-
async asset_to_sandbox(asset_id, path) {
|
|
967
|
-
return this.assetToSandbox(asset_id, path);
|
|
968
|
-
}
|
|
969
1759
|
async sandboxToAsset(path) {
|
|
970
1760
|
const filePath = this.resolveSandboxFilePath(path);
|
|
971
|
-
const bytes = await readFile(filePath);
|
|
1761
|
+
const bytes = (await readFile(filePath));
|
|
972
1762
|
const contentType = ProcessingContext.guessMimeFromPath(filePath);
|
|
1763
|
+
// SAFETY: the host's asset persistence answers with its own asset record;
|
|
1764
|
+
// both reads below re-check the field before using it.
|
|
973
1765
|
const created = (await this.createAsset({
|
|
974
1766
|
name: basename(filePath),
|
|
975
1767
|
contentType,
|
|
976
1768
|
content: bytes
|
|
977
1769
|
}));
|
|
978
1770
|
const type = ProcessingContext.assetTypeForMime(contentType);
|
|
979
|
-
const assetId =
|
|
1771
|
+
const assetId = isString(created.id) ? created.id : null;
|
|
980
1772
|
const uri = assetId !== null
|
|
981
1773
|
? `asset://${assetId}`
|
|
982
|
-
:
|
|
1774
|
+
: isString(created.uri)
|
|
983
1775
|
? created.uri
|
|
984
1776
|
: pathToFileURL(filePath).toString();
|
|
985
1777
|
return {
|
|
@@ -988,9 +1780,6 @@ export class ProcessingContext {
|
|
|
988
1780
|
asset_id: assetId
|
|
989
1781
|
};
|
|
990
1782
|
}
|
|
991
|
-
async sandbox_to_asset(path) {
|
|
992
|
-
return this.sandboxToAsset(path);
|
|
993
|
-
}
|
|
994
1783
|
async createMessage(req) {
|
|
995
1784
|
if (!req.thread_id) {
|
|
996
1785
|
throw new Error("Thread ID is required");
|
|
@@ -998,9 +1787,6 @@ export class ProcessingContext {
|
|
|
998
1787
|
const fn = this.requireModelInterface("createMessage");
|
|
999
1788
|
return fn({ userId: this.userId, req });
|
|
1000
1789
|
}
|
|
1001
|
-
async create_message(req) {
|
|
1002
|
-
return this.createMessage(req);
|
|
1003
|
-
}
|
|
1004
1790
|
async getThreadMessages(threadId, limit = 10, startKey = null, reverse = false) {
|
|
1005
1791
|
const fn = this.requireModelInterface("getMessages");
|
|
1006
1792
|
return fn({
|
|
@@ -1011,9 +1797,10 @@ export class ProcessingContext {
|
|
|
1011
1797
|
reverse
|
|
1012
1798
|
});
|
|
1013
1799
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1800
|
+
// The four wrappers below and `normalizeOutputValue` are HOLDOUTs
|
|
1801
|
+
// (anti-slop/no-unknown-returns): they rewrite an arbitrary workflow value —
|
|
1802
|
+
// any node output, at any nesting depth — and NodeTool has no named union
|
|
1803
|
+
// for that value domain.
|
|
1017
1804
|
async assetsToDataUri(value) {
|
|
1018
1805
|
return this.normalizeOutputValue(value, "data_uri");
|
|
1019
1806
|
}
|
|
@@ -1029,8 +1816,8 @@ export class ProcessingContext {
|
|
|
1029
1816
|
// -----------------------------------------------------------------------
|
|
1030
1817
|
// Provider prediction pipeline
|
|
1031
1818
|
// -----------------------------------------------------------------------
|
|
1032
|
-
emitPrediction(status, req, id, data, error, startedAt) {
|
|
1033
|
-
|
|
1819
|
+
emitPrediction(status, req, id, data, error, startedAt, extra) {
|
|
1820
|
+
const message = {
|
|
1034
1821
|
type: "prediction",
|
|
1035
1822
|
id,
|
|
1036
1823
|
user_id: this.userId,
|
|
@@ -1038,20 +1825,233 @@ export class ProcessingContext {
|
|
|
1038
1825
|
workflow_id: req.workflowId ?? this.workflowId ?? null,
|
|
1039
1826
|
provider: req.provider,
|
|
1040
1827
|
model: req.model,
|
|
1828
|
+
capability: req.capability,
|
|
1041
1829
|
status,
|
|
1042
|
-
|
|
1830
|
+
// Redacted: bytes and data URLs become their length. The row stores
|
|
1831
|
+
// this bag and every listener receives it.
|
|
1832
|
+
params: redactGenerationParams(req.params),
|
|
1043
1833
|
data: data ?? null,
|
|
1044
1834
|
error: error ?? null,
|
|
1045
|
-
duration: startedAt ? Date.now() - startedAt : null
|
|
1046
|
-
|
|
1835
|
+
duration: startedAt ? Date.now() - startedAt : null,
|
|
1836
|
+
origin: extra?.origin ?? this.generationOrigin(req),
|
|
1837
|
+
receipt: extra?.receipt ?? null
|
|
1838
|
+
};
|
|
1839
|
+
// Only a terminal message names assets; `running` has none to name.
|
|
1840
|
+
if (extra?.asset_ids)
|
|
1841
|
+
message.asset_ids = extra.asset_ids;
|
|
1842
|
+
this.emit(message);
|
|
1047
1843
|
}
|
|
1048
1844
|
/**
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1845
|
+
* Who asked, defaulted from the context: a chat turn names its thread, a
|
|
1846
|
+
* run names its job and node. A capability passes `surface` and its
|
|
1847
|
+
* `tool_call_id` explicitly, the one id the context does not hold.
|
|
1848
|
+
*/
|
|
1849
|
+
generationOrigin(req, override) {
|
|
1850
|
+
const base = this.threadId
|
|
1851
|
+
? {
|
|
1852
|
+
surface: "chat",
|
|
1853
|
+
thread_id: this.threadId,
|
|
1854
|
+
job_id: this.jobId || null,
|
|
1855
|
+
node_id: req.nodeId ?? null
|
|
1856
|
+
}
|
|
1857
|
+
: {
|
|
1858
|
+
surface: "workflow",
|
|
1859
|
+
job_id: this.jobId || null,
|
|
1860
|
+
node_id: req.nodeId ?? null
|
|
1861
|
+
};
|
|
1862
|
+
if (!override)
|
|
1863
|
+
return base;
|
|
1864
|
+
const merged = { ...base };
|
|
1865
|
+
if (override.surface !== undefined)
|
|
1866
|
+
merged.surface = override.surface;
|
|
1867
|
+
if (override.thread_id !== undefined)
|
|
1868
|
+
merged.thread_id = override.thread_id;
|
|
1869
|
+
if (override.tool_call_id !== undefined)
|
|
1870
|
+
merged.tool_call_id = override.tool_call_id;
|
|
1871
|
+
if (override.request_id !== undefined)
|
|
1872
|
+
merged.request_id = override.request_id;
|
|
1873
|
+
if (override.job_id !== undefined)
|
|
1874
|
+
merged.job_id = override.job_id;
|
|
1875
|
+
if (override.node_id !== undefined)
|
|
1876
|
+
merged.node_id = override.node_id;
|
|
1877
|
+
return merged;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Retrieve raw bytes for a media URI referenced in message content. Handles
|
|
1881
|
+
* the `asset://<id>` reference scheme (via {@link resolveAssetBytes}) as well
|
|
1882
|
+
* as opaque storage URIs (memory/file/s3) via the storage adapter.
|
|
1883
|
+
*/
|
|
1884
|
+
async retrieveMediaBytes(uri) {
|
|
1885
|
+
// `/api/storage/<key>` (and its slash-less `api/storage/<key>` form) is the
|
|
1886
|
+
// browser-facing route, not a storage-adapter uri — the InMemory/S3 adapters
|
|
1887
|
+
// only accept `memory://`/`s3://`, so route it through resolveAssetBytes
|
|
1888
|
+
// (which parses the key) like `asset://`.
|
|
1889
|
+
if (uri.startsWith("asset://") ||
|
|
1890
|
+
uri.startsWith("/api/storage/") ||
|
|
1891
|
+
uri.startsWith("api/storage/")) {
|
|
1892
|
+
const { bytes } = await this.resolveAssetBytes(uri);
|
|
1893
|
+
return bytes;
|
|
1894
|
+
}
|
|
1895
|
+
for (const adapter of [this.assetStorage, this.storage]) {
|
|
1896
|
+
if (!adapter)
|
|
1897
|
+
continue;
|
|
1898
|
+
const bytes = await adapter.retrieve(uri);
|
|
1899
|
+
if (bytes)
|
|
1900
|
+
return bytes;
|
|
1901
|
+
}
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Resolve non-data, non-http media URIs in message content to data URIs so
|
|
1906
|
+
* providers can consume them directly. Covers `/api/storage/` and other
|
|
1907
|
+
* storage URIs as well as the `asset://<id>` reference scheme (assets
|
|
1908
|
+
* mentioned inline in a prompt). Text parts that contain `asset://<id>.<ext>`
|
|
1909
|
+
* image / audio tokens are first split into proper blocks via
|
|
1910
|
+
* {@link expandAssetReferences} so the same provider path handles chat
|
|
1911
|
+
* surfaces (where the user typed / dropped a reference) and workflow node
|
|
1912
|
+
* surfaces (where the agent node builds the message). Text document mentions
|
|
1913
|
+
* (`asset://doc.md`, `.txt`, `.csv`, …) are inlined as their decoded
|
|
1914
|
+
* contents in place of the token.
|
|
1051
1915
|
*/
|
|
1052
1916
|
async resolveMessageMediaUris(messages) {
|
|
1053
|
-
|
|
1054
|
-
|
|
1917
|
+
const IMAGE_MIME = {
|
|
1918
|
+
png: "image/png",
|
|
1919
|
+
jpg: "image/jpeg",
|
|
1920
|
+
jpeg: "image/jpeg",
|
|
1921
|
+
gif: "image/gif",
|
|
1922
|
+
webp: "image/webp"
|
|
1923
|
+
};
|
|
1924
|
+
const AUDIO_MIME = {
|
|
1925
|
+
mp3: "audio/mpeg",
|
|
1926
|
+
wav: "audio/wav",
|
|
1927
|
+
ogg: "audio/ogg",
|
|
1928
|
+
m4a: "audio/mp4",
|
|
1929
|
+
flac: "audio/flac"
|
|
1930
|
+
};
|
|
1931
|
+
const VIDEO_MIME = {
|
|
1932
|
+
mp4: "video/mp4",
|
|
1933
|
+
m4v: "video/mp4",
|
|
1934
|
+
webm: "video/webm",
|
|
1935
|
+
mov: "video/quicktime",
|
|
1936
|
+
avi: "video/x-msvideo"
|
|
1937
|
+
};
|
|
1938
|
+
// Strict, anchored sanitizer for the URIs that may flow into the asset /
|
|
1939
|
+
// storage retrieval path. Accept asset references, the browser-facing
|
|
1940
|
+
// storage route, and URIs from configured storage adapters. External URLs
|
|
1941
|
+
// and unrecognized schemes stay out of the storage adapter. `:` is allowed
|
|
1942
|
+
// for named owner ids such as `projects/personal:<user_id>/assets/…`.
|
|
1943
|
+
const RESOLVABLE_URI_RE = /^(?:asset:\/\/|\/?api\/storage\/|(?:memory|file|s3|supabase):\/\/)[A-Za-z0-9._~/%:-]+$/;
|
|
1944
|
+
const isResolvableUri = (uri) => RESOLVABLE_URI_RE.test(uri);
|
|
1945
|
+
// An `asset://` reference that cannot be dereferenced must not reach the
|
|
1946
|
+
// provider: providers `fetch` a non-data URI, and the SSRF guard then
|
|
1947
|
+
// rejects `asset://…` with "Refusing to fetch unsafe URL", which fails the
|
|
1948
|
+
// whole turn on a message the user cannot fix. Replace the block with a
|
|
1949
|
+
// note the model can act on instead — a stale attachment in the history
|
|
1950
|
+
// must not break every later turn in the thread.
|
|
1951
|
+
const unresolvedNote = (kind, uri) => ({
|
|
1952
|
+
type: "text",
|
|
1953
|
+
text: `[attached ${kind} could not be ${kind === "image" ? "shown" : "loaded"}: ${uri}]`
|
|
1954
|
+
});
|
|
1955
|
+
// A resolved block reaches the provider as a `data:` URI, which the model
|
|
1956
|
+
// cannot pass to a tool. Emit the stored handle beside it so "make it like
|
|
1957
|
+
// this" can become `edit_image({input_file: "asset://…"})` — only for the
|
|
1958
|
+
// URIs `isResolvableUri` admits, never for a `data:` URI the caller already
|
|
1959
|
+
// inlined.
|
|
1960
|
+
const attachedNote = (kind, uri) => ({
|
|
1961
|
+
type: "text",
|
|
1962
|
+
text: `[attached ${kind} ${uri}]`
|
|
1963
|
+
});
|
|
1964
|
+
const resolvePart = async (part,
|
|
1965
|
+
// Parts split out of a text block already carry the literal token next
|
|
1966
|
+
// to them (see `expandAssetReferences`), so a note there would repeat it.
|
|
1967
|
+
fromText = false) => {
|
|
1968
|
+
if (part.type === "image_url" &&
|
|
1969
|
+
part.image.uri &&
|
|
1970
|
+
isResolvableUri(part.image.uri)) {
|
|
1971
|
+
const bytes = await this.retrieveMediaBytes(part.image.uri);
|
|
1972
|
+
if (bytes) {
|
|
1973
|
+
const ext = part.image.uri.split(".").pop()?.toLowerCase() ?? "png";
|
|
1974
|
+
const mimeType = IMAGE_MIME[ext] ?? part.image.mimeType ?? "image/png";
|
|
1975
|
+
const b64 = Buffer.from(bytes).toString("base64");
|
|
1976
|
+
const block = {
|
|
1977
|
+
type: "image_url",
|
|
1978
|
+
image: { uri: `data:${mimeType};base64,${b64}`, mimeType }
|
|
1979
|
+
};
|
|
1980
|
+
return fromText
|
|
1981
|
+
? [block]
|
|
1982
|
+
: [block, attachedNote("image", part.image.uri)];
|
|
1983
|
+
}
|
|
1984
|
+
return [unresolvedNote("image", part.image.uri)];
|
|
1985
|
+
}
|
|
1986
|
+
if (part.type === "audio" &&
|
|
1987
|
+
part.audio.uri &&
|
|
1988
|
+
isResolvableUri(part.audio.uri)) {
|
|
1989
|
+
const bytes = await this.retrieveMediaBytes(part.audio.uri);
|
|
1990
|
+
if (bytes) {
|
|
1991
|
+
const ext = part.audio.uri.split(".").pop()?.toLowerCase() ?? "mp3";
|
|
1992
|
+
const mimeType = AUDIO_MIME[ext] ?? part.audio.mimeType ?? "audio/mpeg";
|
|
1993
|
+
const b64 = Buffer.from(bytes).toString("base64");
|
|
1994
|
+
const block = {
|
|
1995
|
+
type: "audio",
|
|
1996
|
+
audio: { uri: `data:${mimeType};base64,${b64}`, mimeType }
|
|
1997
|
+
};
|
|
1998
|
+
return fromText
|
|
1999
|
+
? [block]
|
|
2000
|
+
: [block, attachedNote("audio", part.audio.uri)];
|
|
2001
|
+
}
|
|
2002
|
+
return [unresolvedNote("audio", part.audio.uri)];
|
|
2003
|
+
}
|
|
2004
|
+
if (part.type === "video" &&
|
|
2005
|
+
part.video.uri &&
|
|
2006
|
+
isResolvableUri(part.video.uri)) {
|
|
2007
|
+
const bytes = await this.retrieveMediaBytes(part.video.uri);
|
|
2008
|
+
if (bytes) {
|
|
2009
|
+
const ext = part.video.uri.split(".").pop()?.toLowerCase() ?? "mp4";
|
|
2010
|
+
const mimeType = VIDEO_MIME[ext] ?? part.video.mimeType ?? "video/mp4";
|
|
2011
|
+
const b64 = Buffer.from(bytes).toString("base64");
|
|
2012
|
+
const block = {
|
|
2013
|
+
type: "video",
|
|
2014
|
+
video: { uri: `data:${mimeType};base64,${b64}`, mimeType }
|
|
2015
|
+
};
|
|
2016
|
+
return fromText
|
|
2017
|
+
? [block]
|
|
2018
|
+
: [block, attachedNote("video", part.video.uri)];
|
|
2019
|
+
}
|
|
2020
|
+
return [unresolvedNote("video", part.video.uri)];
|
|
2021
|
+
}
|
|
2022
|
+
if (part.type === "text") {
|
|
2023
|
+
// Entity mentions resolve first: each `entity://<id>` becomes the
|
|
2024
|
+
// entity's name inline, a "Consistency references" line carrying its
|
|
2025
|
+
// descriptor, and an `asset://` token for its reference image — which
|
|
2026
|
+
// the asset pass below then turns into a real image block. Resolving
|
|
2027
|
+
// here, per turn, is what makes a later rename or re-description
|
|
2028
|
+
// reach every message that already mentions the entity.
|
|
2029
|
+
const text = await expandEntityRefs(part.text, this, true);
|
|
2030
|
+
if (!text.includes("asset://")) {
|
|
2031
|
+
return text === part.text ? [part] : [{ type: "text", text }];
|
|
2032
|
+
}
|
|
2033
|
+
// First split out image / audio mentions into their own blocks; what
|
|
2034
|
+
// stays as text is then run through the document inliner so any
|
|
2035
|
+
// .md/.txt/.csv mentions become their decoded contents. The image and
|
|
2036
|
+
// audio blocks fall through to the URI-resolution branches above on a
|
|
2037
|
+
// second pass so the provider receives data URIs.
|
|
2038
|
+
const expanded = expandAssetReferences(text);
|
|
2039
|
+
const out = [];
|
|
2040
|
+
for (const sub of expanded) {
|
|
2041
|
+
if (sub.type === "text") {
|
|
2042
|
+
const inlined = await inlineTextAssetRefs(sub.text, this);
|
|
2043
|
+
if (inlined)
|
|
2044
|
+
out.push({ type: "text", text: inlined });
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
const resolved = await resolvePart(sub, true);
|
|
2048
|
+
out.push(...resolved);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return out;
|
|
2052
|
+
}
|
|
2053
|
+
return [part];
|
|
2054
|
+
};
|
|
1055
2055
|
const resolved = [];
|
|
1056
2056
|
for (const msg of messages) {
|
|
1057
2057
|
if (!Array.isArray(msg.content)) {
|
|
@@ -1060,64 +2060,88 @@ export class ProcessingContext {
|
|
|
1060
2060
|
}
|
|
1061
2061
|
const parts = [];
|
|
1062
2062
|
for (const part of msg.content) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
continue;
|
|
1084
|
-
}
|
|
2063
|
+
parts.push(...(await resolvePart(part)));
|
|
2064
|
+
}
|
|
2065
|
+
resolved.push({ ...msg, content: parts });
|
|
2066
|
+
}
|
|
2067
|
+
return resolved;
|
|
2068
|
+
}
|
|
2069
|
+
async resolveRequiredImageEvidence(messages, required = []) {
|
|
2070
|
+
const evidence = new Map();
|
|
2071
|
+
const resolve = async (ref) => {
|
|
2072
|
+
const identity = ref.uri?.startsWith("data:")
|
|
2073
|
+
? "inline image"
|
|
2074
|
+
: ref.uri ?? ref.asset_id ?? "inline image";
|
|
2075
|
+
const cached = ref.uri ? evidence.get(ref.uri) : undefined;
|
|
2076
|
+
if (cached) {
|
|
2077
|
+
return cached;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
const bytes = await loadMediaRefBytes(ref, this);
|
|
2081
|
+
if (!bytes?.length) {
|
|
2082
|
+
throw new Error("no image bytes");
|
|
1085
2083
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
!
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
m4a: "audio/mp4",
|
|
1098
|
-
flac: "audio/flac"
|
|
1099
|
-
};
|
|
1100
|
-
const mimeType = mime[ext] ?? part.audio.mimeType ?? "audio/mpeg";
|
|
1101
|
-
const b64 = Buffer.from(bytes).toString("base64");
|
|
1102
|
-
parts.push({
|
|
1103
|
-
type: "audio",
|
|
1104
|
-
audio: { uri: `data:${mimeType};base64,${b64}`, mimeType }
|
|
1105
|
-
});
|
|
1106
|
-
continue;
|
|
2084
|
+
const mimeType = sniffImageMime(bytes);
|
|
2085
|
+
if (!mimeType ||
|
|
2086
|
+
!["image/png", "image/jpeg", "image/webp", "image/gif"].includes(mimeType)) {
|
|
2087
|
+
throw new Error("unsupported image format");
|
|
2088
|
+
}
|
|
2089
|
+
await validateImageEvidence(bytes);
|
|
2090
|
+
const block = {
|
|
2091
|
+
type: "image_url",
|
|
2092
|
+
image: {
|
|
2093
|
+
uri: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
|
|
2094
|
+
mimeType
|
|
1107
2095
|
}
|
|
2096
|
+
};
|
|
2097
|
+
if (ref.uri) {
|
|
2098
|
+
evidence.set(ref.uri, block);
|
|
1108
2099
|
}
|
|
1109
|
-
|
|
2100
|
+
return block;
|
|
1110
2101
|
}
|
|
1111
|
-
|
|
2102
|
+
catch (error) {
|
|
2103
|
+
throw new Error(`Required image evidence unavailable: ${identity} (${error instanceof Error ? error.message : String(error)})`);
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
for (const uri of required) {
|
|
2107
|
+
await resolve({ type: "image", uri });
|
|
1112
2108
|
}
|
|
1113
|
-
|
|
2109
|
+
const result = [];
|
|
2110
|
+
for (const message of messages) {
|
|
2111
|
+
if (!Array.isArray(message.content)) {
|
|
2112
|
+
result.push(message);
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
const content = [];
|
|
2116
|
+
let imageIndex = 0;
|
|
2117
|
+
for (const part of message.content) {
|
|
2118
|
+
if (part.type === "image_url") {
|
|
2119
|
+
imageIndex++;
|
|
2120
|
+
content.push(await resolve(part.image), {
|
|
2121
|
+
type: "text",
|
|
2122
|
+
text: `[required image ${part.image.uri && !part.image.uri.startsWith("data:")
|
|
2123
|
+
? part.image.uri
|
|
2124
|
+
: `inline image ${imageIndex}`}]`
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
else {
|
|
2128
|
+
content.push(part);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
result.push({ ...message, content });
|
|
2132
|
+
}
|
|
2133
|
+
return result;
|
|
1114
2134
|
}
|
|
1115
2135
|
async dispatchCapability(provider, req) {
|
|
1116
2136
|
const params = req.params ?? {};
|
|
1117
2137
|
switch (req.capability) {
|
|
1118
2138
|
case "generate_message":
|
|
1119
2139
|
return provider.generateMessageTraced({
|
|
1120
|
-
messages:
|
|
2140
|
+
messages: params.strict_image_evidence === true
|
|
2141
|
+
? await this.resolveRequiredImageEvidence(params.messages ?? [], Array.isArray(params.required_images)
|
|
2142
|
+
? params.required_images.filter(isString)
|
|
2143
|
+
: [])
|
|
2144
|
+
: await this.resolveMessageMediaUris(params.messages ?? []),
|
|
1121
2145
|
model: req.model,
|
|
1122
2146
|
tools: params.tools,
|
|
1123
2147
|
toolChoice: params.tool_choice,
|
|
@@ -1129,7 +2153,9 @@ export class ProcessingContext {
|
|
|
1129
2153
|
});
|
|
1130
2154
|
case "text_to_image":
|
|
1131
2155
|
return provider.textToImage({
|
|
2156
|
+
signal: params.signal,
|
|
1132
2157
|
prompt: String(params.prompt ?? ""),
|
|
2158
|
+
entities: await coerceEntityList(params, this),
|
|
1133
2159
|
model: { id: req.model, name: req.model, provider: req.provider },
|
|
1134
2160
|
width: params.width,
|
|
1135
2161
|
height: params.height,
|
|
@@ -1139,9 +2165,25 @@ export class ProcessingContext {
|
|
|
1139
2165
|
quality: params.quality
|
|
1140
2166
|
});
|
|
1141
2167
|
case "image_to_image":
|
|
1142
|
-
return provider.imageToImage(params
|
|
2168
|
+
return provider.imageToImage(coerceImageList(params), {
|
|
2169
|
+
signal: params.signal,
|
|
2170
|
+
prompt: String(params.prompt ?? ""),
|
|
2171
|
+
entities: await coerceEntityList(params, this),
|
|
2172
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2173
|
+
negativePrompt: params.negative_prompt,
|
|
2174
|
+
targetWidth: params.target_width,
|
|
2175
|
+
targetHeight: params.target_height,
|
|
2176
|
+
aspectRatio: params.aspect_ratio,
|
|
2177
|
+
resolution: params.resolution,
|
|
2178
|
+
strength: params.strength,
|
|
2179
|
+
quality: params.quality
|
|
2180
|
+
});
|
|
2181
|
+
case "inpainting":
|
|
2182
|
+
return provider.inpaint(coerceImageList(params), {
|
|
1143
2183
|
prompt: String(params.prompt ?? ""),
|
|
2184
|
+
entities: await coerceEntityList(params, this),
|
|
1144
2185
|
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2186
|
+
mask: params.mask,
|
|
1145
2187
|
negativePrompt: params.negative_prompt,
|
|
1146
2188
|
targetWidth: params.target_width,
|
|
1147
2189
|
targetHeight: params.target_height,
|
|
@@ -1152,23 +2194,267 @@ export class ProcessingContext {
|
|
|
1152
2194
|
});
|
|
1153
2195
|
case "text_to_video":
|
|
1154
2196
|
return provider.textToVideo({
|
|
2197
|
+
signal: params.signal,
|
|
1155
2198
|
prompt: String(params.prompt ?? ""),
|
|
2199
|
+
entities: await coerceEntityList(params, this),
|
|
1156
2200
|
model: { id: req.model, name: req.model, provider: req.provider },
|
|
1157
2201
|
negativePrompt: params.negative_prompt,
|
|
1158
2202
|
numFrames: params.num_frames,
|
|
1159
2203
|
durationSeconds: params.duration_seconds,
|
|
1160
2204
|
aspectRatio: params.aspect_ratio,
|
|
1161
|
-
resolution: params.resolution
|
|
2205
|
+
resolution: params.resolution,
|
|
2206
|
+
timeoutSeconds: params.timeout_seconds
|
|
1162
2207
|
});
|
|
1163
2208
|
case "image_to_video":
|
|
1164
|
-
return provider.imageToVideo(params
|
|
2209
|
+
return provider.imageToVideo(firstNonEmptyByteList(coerceImageList(params)), {
|
|
2210
|
+
signal: params.signal,
|
|
1165
2211
|
prompt: params.prompt,
|
|
2212
|
+
entities: await coerceEntityList(params, this),
|
|
1166
2213
|
model: { id: req.model, name: req.model, provider: req.provider },
|
|
1167
2214
|
negativePrompt: params.negative_prompt,
|
|
1168
2215
|
numFrames: params.num_frames,
|
|
1169
2216
|
durationSeconds: params.duration_seconds,
|
|
1170
2217
|
aspectRatio: params.aspect_ratio,
|
|
1171
|
-
resolution: params.resolution
|
|
2218
|
+
resolution: params.resolution,
|
|
2219
|
+
timeoutSeconds: params.timeout_seconds
|
|
2220
|
+
});
|
|
2221
|
+
case "reference_to_video": {
|
|
2222
|
+
const images = coerceByteList(params.reference_images);
|
|
2223
|
+
const videos = coerceByteList(params.reference_videos);
|
|
2224
|
+
if (images.length === 0 && videos.length === 0) {
|
|
2225
|
+
throw new Error("reference_to_video requires at least one reference image or video");
|
|
2226
|
+
}
|
|
2227
|
+
return provider.referenceToVideo({ images, videos }, {
|
|
2228
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2229
|
+
prompt: isString(params.prompt) ? params.prompt : "",
|
|
2230
|
+
entities: await coerceEntityList(params, this),
|
|
2231
|
+
useReferenceVideoAudio: isBoolean(params.use_reference_video_audio)
|
|
2232
|
+
? params.use_reference_video_audio
|
|
2233
|
+
: undefined,
|
|
2234
|
+
negativePrompt: isString(params.negative_prompt)
|
|
2235
|
+
? params.negative_prompt
|
|
2236
|
+
: undefined,
|
|
2237
|
+
numFrames: isNumber(params.num_frames)
|
|
2238
|
+
? params.num_frames
|
|
2239
|
+
: undefined,
|
|
2240
|
+
durationSeconds: isNumber(params.duration_seconds)
|
|
2241
|
+
? params.duration_seconds
|
|
2242
|
+
: undefined,
|
|
2243
|
+
aspectRatio: isString(params.aspect_ratio)
|
|
2244
|
+
? params.aspect_ratio
|
|
2245
|
+
: undefined,
|
|
2246
|
+
resolution: isString(params.resolution)
|
|
2247
|
+
? params.resolution
|
|
2248
|
+
: undefined,
|
|
2249
|
+
timeoutSeconds: isNumber(params.timeout_seconds)
|
|
2250
|
+
? params.timeout_seconds
|
|
2251
|
+
: undefined,
|
|
2252
|
+
signal: isAbortSignal(params.signal) ? params.signal : undefined
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
case "outpaint_image":
|
|
2256
|
+
return provider.outpaintImage(coerceImageList(params), {
|
|
2257
|
+
signal: params.signal,
|
|
2258
|
+
prompt: params.prompt,
|
|
2259
|
+
entities: await coerceEntityList(params, this),
|
|
2260
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2261
|
+
negativePrompt: params.negative_prompt,
|
|
2262
|
+
padding: coerceOutpaintPadding(params.padding),
|
|
2263
|
+
aspectRatio: params.aspect_ratio,
|
|
2264
|
+
seed: params.seed
|
|
2265
|
+
});
|
|
2266
|
+
case "upscale_image":
|
|
2267
|
+
return provider.upscaleImage(params.image, {
|
|
2268
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2269
|
+
scale: params.scale,
|
|
2270
|
+
prompt: params.prompt,
|
|
2271
|
+
creativity: params.creativity,
|
|
2272
|
+
seed: params.seed
|
|
2273
|
+
});
|
|
2274
|
+
case "remove_background":
|
|
2275
|
+
return provider.removeBackground(params.image, {
|
|
2276
|
+
model: { id: req.model, name: req.model, provider: req.provider }
|
|
2277
|
+
});
|
|
2278
|
+
case "relight_image":
|
|
2279
|
+
return provider.relightImage(params.image, {
|
|
2280
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2281
|
+
prompt: params.prompt,
|
|
2282
|
+
negativePrompt: params.negative_prompt,
|
|
2283
|
+
seed: params.seed
|
|
2284
|
+
});
|
|
2285
|
+
case "segment_image":
|
|
2286
|
+
return provider.segmentImage(params.image, {
|
|
2287
|
+
signal: params.signal,
|
|
2288
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2289
|
+
prompt: params.prompt,
|
|
2290
|
+
points: params.points,
|
|
2291
|
+
box: params.box,
|
|
2292
|
+
maxMasks: params.max_masks,
|
|
2293
|
+
minConfidence: params.min_confidence
|
|
2294
|
+
});
|
|
2295
|
+
case "vectorize_image":
|
|
2296
|
+
return provider.vectorizeImage(params.image, {
|
|
2297
|
+
model: { id: req.model, name: req.model, provider: req.provider }
|
|
2298
|
+
});
|
|
2299
|
+
case "estimate_depth":
|
|
2300
|
+
return provider.estimateDepth(params.image, {
|
|
2301
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2302
|
+
signal: isAbortSignal(params.signal) ? params.signal : this.signal
|
|
2303
|
+
});
|
|
2304
|
+
case "rerank":
|
|
2305
|
+
return provider.rerank({
|
|
2306
|
+
model: req.model,
|
|
2307
|
+
query: isString(params.query) ? params.query : "",
|
|
2308
|
+
// Every entry stays, so result indices point into the caller's list.
|
|
2309
|
+
documents: Array.isArray(params.documents)
|
|
2310
|
+
? params.documents.map((doc) => (isString(doc) ? doc : String(doc)))
|
|
2311
|
+
: [],
|
|
2312
|
+
topK: isNumber(params.top_k) ? params.top_k : undefined,
|
|
2313
|
+
signal: isAbortSignal(params.signal) ? params.signal : this.signal
|
|
2314
|
+
});
|
|
2315
|
+
case "track_object": {
|
|
2316
|
+
const request = objectTrackingRequestSchema.parse(params);
|
|
2317
|
+
if (!provider.getCapabilities().includes("track_object")) {
|
|
2318
|
+
throw new Error("The selected provider does not support object tracking.");
|
|
2319
|
+
}
|
|
2320
|
+
const signal = params.signal instanceof AbortSignal ? params.signal : this.signal;
|
|
2321
|
+
signal.throwIfAborted();
|
|
2322
|
+
const video = await loadMediaRefBytes({ type: "video", asset_id: request.sourceAssetId }, this);
|
|
2323
|
+
if (!video?.byteLength) {
|
|
2324
|
+
throw new Error("The tracking source video could not be loaded.");
|
|
2325
|
+
}
|
|
2326
|
+
signal.throwIfAborted();
|
|
2327
|
+
const output = await provider.trackObject(video, {
|
|
2328
|
+
...request,
|
|
2329
|
+
model: req.model,
|
|
2330
|
+
signal
|
|
2331
|
+
});
|
|
2332
|
+
signal.throwIfAborted();
|
|
2333
|
+
return parseObjectTrackingResult(output, request);
|
|
2334
|
+
}
|
|
2335
|
+
case "video_to_video": {
|
|
2336
|
+
const signal = isAbortSignal(params.signal)
|
|
2337
|
+
? params.signal
|
|
2338
|
+
: this.signal;
|
|
2339
|
+
const referenceImages = coerceByteList(params.reference_images ?? params.referenceImages);
|
|
2340
|
+
const referenceAssetIds = coerceStringList(params.reference_asset_ids ?? params.referenceAssetIds);
|
|
2341
|
+
signal.throwIfAborted();
|
|
2342
|
+
return provider.videoToVideo(params.video, {
|
|
2343
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2344
|
+
prompt: params.prompt,
|
|
2345
|
+
referenceImages: referenceImages.length > 0 ? referenceImages : undefined,
|
|
2346
|
+
referenceAssetIds: referenceAssetIds.length > 0 ? referenceAssetIds : undefined,
|
|
2347
|
+
signal,
|
|
2348
|
+
entities: await coerceEntityList(params, this),
|
|
2349
|
+
negativePrompt: params.negative_prompt,
|
|
2350
|
+
strength: params.strength,
|
|
2351
|
+
durationSeconds: params.duration_seconds,
|
|
2352
|
+
resolution: params.resolution,
|
|
2353
|
+
seed: params.seed
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
case "video_to_audio": {
|
|
2357
|
+
const source = isRecord(params.source)
|
|
2358
|
+
? {
|
|
2359
|
+
assetId: params.source.assetId ?? params.source.asset_id,
|
|
2360
|
+
durationSeconds: params.source.durationSeconds ?? params.source.duration_seconds,
|
|
2361
|
+
startSeconds: params.source.startSeconds ?? params.source.start_seconds,
|
|
2362
|
+
endSeconds: params.source.endSeconds ?? params.source.end_seconds
|
|
2363
|
+
}
|
|
2364
|
+
: params.source;
|
|
2365
|
+
const request = VideoToAudioRequest.parse({
|
|
2366
|
+
task: VIDEO_TO_AUDIO_TASK,
|
|
2367
|
+
model: {
|
|
2368
|
+
id: req.model,
|
|
2369
|
+
provider: req.provider,
|
|
2370
|
+
supportedTasks: [VIDEO_TO_AUDIO_TASK]
|
|
2371
|
+
},
|
|
2372
|
+
source,
|
|
2373
|
+
sceneContext: params.sceneContext ?? params.scene_context
|
|
2374
|
+
});
|
|
2375
|
+
const video = params.video instanceof Uint8Array
|
|
2376
|
+
? params.video
|
|
2377
|
+
: await loadMediaRefBytes({ type: "video", asset_id: request.source.assetId }, this);
|
|
2378
|
+
if (!video?.byteLength) {
|
|
2379
|
+
throw new Error("video_to_audio requires nonempty source video bytes");
|
|
2380
|
+
}
|
|
2381
|
+
return requestVideoToAudio(provider, video, request, {
|
|
2382
|
+
signal: params.signal
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
case "extend_video": {
|
|
2386
|
+
if ((params.mode !== "start" && params.mode !== "end") ||
|
|
2387
|
+
typeof params.duration_seconds !== "number" ||
|
|
2388
|
+
!Number.isFinite(params.duration_seconds) ||
|
|
2389
|
+
params.duration_seconds <= 0) {
|
|
2390
|
+
throw new Error("extend_video requires start/end mode and positive duration_seconds");
|
|
2391
|
+
}
|
|
2392
|
+
return provider.extendVideo(params.video, {
|
|
2393
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2394
|
+
prompt: typeof params.prompt === "string" ? params.prompt : "",
|
|
2395
|
+
mode: params.mode,
|
|
2396
|
+
durationSeconds: params.duration_seconds
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
case "upscale_video":
|
|
2400
|
+
return provider.upscaleVideo(params.video, {
|
|
2401
|
+
signal: params.signal,
|
|
2402
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2403
|
+
scale: params.scale,
|
|
2404
|
+
targetResolution: params.target_resolution,
|
|
2405
|
+
prompt: params.prompt,
|
|
2406
|
+
creativity: params.creativity,
|
|
2407
|
+
seed: params.seed,
|
|
2408
|
+
timeoutSeconds: params.timeout_seconds
|
|
2409
|
+
});
|
|
2410
|
+
case "interpolate_video":
|
|
2411
|
+
return provider.interpolateVideo(params.video, {
|
|
2412
|
+
signal: params.signal,
|
|
2413
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2414
|
+
targetFps: params.target_fps,
|
|
2415
|
+
factor: params.factor,
|
|
2416
|
+
seed: params.seed,
|
|
2417
|
+
timeoutSeconds: params.timeout_seconds
|
|
2418
|
+
});
|
|
2419
|
+
case "outpaint_video":
|
|
2420
|
+
return provider.outpaintVideo(params.video, {
|
|
2421
|
+
signal: params.signal,
|
|
2422
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2423
|
+
prompt: params.prompt,
|
|
2424
|
+
entities: await coerceEntityList(params, this),
|
|
2425
|
+
negativePrompt: params.negative_prompt,
|
|
2426
|
+
padding: coerceOutpaintPadding(params.padding),
|
|
2427
|
+
expandRatio: params.expand_ratio,
|
|
2428
|
+
aspectRatio: params.aspect_ratio,
|
|
2429
|
+
resolution: params.resolution,
|
|
2430
|
+
seed: params.seed,
|
|
2431
|
+
timeoutSeconds: params.timeout_seconds
|
|
2432
|
+
});
|
|
2433
|
+
case "lip_sync":
|
|
2434
|
+
return provider.lipSync(params.video, {
|
|
2435
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2436
|
+
audio: params.audio,
|
|
2437
|
+
seed: params.seed
|
|
2438
|
+
});
|
|
2439
|
+
case "text_to_music":
|
|
2440
|
+
return provider.textToMusic({
|
|
2441
|
+
prompt: String(params.prompt ?? ""),
|
|
2442
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2443
|
+
lyrics: params.lyrics,
|
|
2444
|
+
durationSeconds: params.duration_seconds,
|
|
2445
|
+
seed: params.seed,
|
|
2446
|
+
audioFormat: params.audioFormat,
|
|
2447
|
+
timeoutSeconds: params.timeout_seconds
|
|
2448
|
+
});
|
|
2449
|
+
case "audio_to_audio":
|
|
2450
|
+
return provider.audioToAudio(params.audio, {
|
|
2451
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2452
|
+
prompt: params.prompt,
|
|
2453
|
+
voice: params.voice,
|
|
2454
|
+
strength: params.strength,
|
|
2455
|
+
seed: params.seed,
|
|
2456
|
+
audioFormat: params.audioFormat,
|
|
2457
|
+
timeoutSeconds: params.timeout_seconds
|
|
1172
2458
|
});
|
|
1173
2459
|
case "automatic_speech_recognition":
|
|
1174
2460
|
return provider.automaticSpeechRecognition({
|
|
@@ -1184,30 +2470,605 @@ export class ProcessingContext {
|
|
|
1184
2470
|
model: req.model,
|
|
1185
2471
|
dimensions: params.dimensions
|
|
1186
2472
|
});
|
|
2473
|
+
case "text_to_3d":
|
|
2474
|
+
return provider.textTo3D({
|
|
2475
|
+
model: model3dOf(params, req),
|
|
2476
|
+
prompt: String(params.prompt ?? ""),
|
|
2477
|
+
negativePrompt: params.negative_prompt ?? null,
|
|
2478
|
+
artStyle: params.art_style ?? null,
|
|
2479
|
+
outputFormat: params.output_format,
|
|
2480
|
+
seed: params.seed ?? null,
|
|
2481
|
+
timeoutSeconds: params.timeout_seconds ?? null,
|
|
2482
|
+
enableTextures: params.enable_textures
|
|
2483
|
+
});
|
|
2484
|
+
case "image_to_3d":
|
|
2485
|
+
return provider.imageTo3D(coerceImageList(params)[0] ?? new Uint8Array(), {
|
|
2486
|
+
model: model3dOf(params, req),
|
|
2487
|
+
prompt: params.prompt ?? null,
|
|
2488
|
+
outputFormat: params.output_format,
|
|
2489
|
+
seed: params.seed ?? null,
|
|
2490
|
+
timeoutSeconds: params.timeout_seconds ?? null
|
|
2491
|
+
});
|
|
1187
2492
|
default:
|
|
1188
2493
|
throw new Error(`Capability '${req.capability}' requires streaming API`);
|
|
1189
2494
|
}
|
|
1190
2495
|
}
|
|
2496
|
+
/**
|
|
2497
|
+
* HOLDOUT (anti-slop/no-unknown-returns): the honest return type is
|
|
2498
|
+
* {@link ProviderPredictionResult} (see {@link dispatchCapability}), but the
|
|
2499
|
+
* node and capability call sites assert their own capability's result off it
|
|
2500
|
+
* (`as Uint8Array`, `as number[][]`, …), and those assertions do not compile
|
|
2501
|
+
* against the union. Typing it means editing every one of them. `text-nodes`
|
|
2502
|
+
* is done — it parses each result with a predicate instead.
|
|
2503
|
+
*/
|
|
1191
2504
|
async runProviderPrediction(req) {
|
|
1192
|
-
|
|
2505
|
+
return (await this.runGeneration(req)).output;
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Run one generation: open the record, call the provider inside a receipt
|
|
2509
|
+
* scope, persist what came back, close the record. Every terminal state is
|
|
2510
|
+
* a message — `completed`, `failed`, `cancelled` — and the tracker turns
|
|
2511
|
+
* those into the row. Design: docs/media-generation-tracking-design.md § 5.
|
|
2512
|
+
*/
|
|
2513
|
+
async runGeneration(req) {
|
|
2514
|
+
return this.runGenerationWith(req, (provider, signal) => {
|
|
2515
|
+
if (!provider) {
|
|
2516
|
+
throw new Error("Provider generation requires a provider.");
|
|
2517
|
+
}
|
|
2518
|
+
const dispatchReq = {
|
|
2519
|
+
...req,
|
|
2520
|
+
params: { ...(req.params ?? {}), signal }
|
|
2521
|
+
};
|
|
2522
|
+
return this.retryContentFilterRefusal(req, () => this.dispatchCapability(provider, dispatchReq));
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* The generation envelope around an arbitrary provider call: the same
|
|
2527
|
+
* record, receipt scope, persistence and terminal messages as
|
|
2528
|
+
* {@link runGeneration}, for a caller whose call shape the capability
|
|
2529
|
+
* switch has no case for — N images in one request, streamed PCM wrapped
|
|
2530
|
+
* into a WAV. `call` receives the provider and the generation's abort
|
|
2531
|
+
* signal; what it returns is the output, and its media buffers are what
|
|
2532
|
+
* `persist` saves.
|
|
2533
|
+
*/
|
|
2534
|
+
async runGenerationWith(req, call, opts) {
|
|
2535
|
+
const id = req.id ?? randomUUID();
|
|
2536
|
+
return withSpan("generation", { "generation.id": id, "generation.provider": req.provider, "generation.kind": req.capability }, async () => {
|
|
2537
|
+
const startedAt = Date.now();
|
|
2538
|
+
const origin = this.generationOrigin(req, req.origin);
|
|
2539
|
+
const controller = new AbortController();
|
|
2540
|
+
const signals = [controller.signal, this.signal];
|
|
2541
|
+
if (req.signal)
|
|
2542
|
+
signals.push(req.signal);
|
|
2543
|
+
const signal = AbortSignal.any(signals);
|
|
2544
|
+
const accept = opts?.onGenerationAccepted ??
|
|
2545
|
+
this._generationLifecycle?.onGenerationAccepted;
|
|
2546
|
+
const terminal = opts?.onGenerationTerminal ??
|
|
2547
|
+
this._generationLifecycle?.onGenerationTerminal;
|
|
2548
|
+
let durableAccepted = false;
|
|
2549
|
+
let terminalAttempted = false;
|
|
2550
|
+
try {
|
|
2551
|
+
const accepted = await accept?.({ generationId: id, request: req });
|
|
2552
|
+
if (accepted?.skipProvider) {
|
|
2553
|
+
throw new GenerationAlreadyAcceptedError(accepted.existingGenerationId ?? id);
|
|
2554
|
+
}
|
|
2555
|
+
durableAccepted = accepted?.durable === true;
|
|
2556
|
+
generationRegistry.register(id, {
|
|
2557
|
+
userId: this.userId,
|
|
2558
|
+
abort: () => controller.abort(new Error("Generation cancelled"))
|
|
2559
|
+
});
|
|
2560
|
+
this.emitPrediction("running", req, id, null, undefined, startedAt, {
|
|
2561
|
+
origin
|
|
2562
|
+
});
|
|
2563
|
+
const provider = opts?.withoutProvider
|
|
2564
|
+
? null
|
|
2565
|
+
: await this.getProvider(req.provider);
|
|
2566
|
+
const { value: output, receipt: recorded, providerResult } = await runWithGenerationReceipt(() => call(provider, signal), {
|
|
2567
|
+
providerRequestOptions: accepted ?? undefined,
|
|
2568
|
+
onProviderRequestAccepted: opts?.onProviderRequestAccepted ??
|
|
2569
|
+
this._generationLifecycle?.onProviderRequestAccepted,
|
|
2570
|
+
onProviderRequestBound: opts?.onProviderRequestBound ??
|
|
2571
|
+
this._generationLifecycle?.onProviderRequestBound
|
|
2572
|
+
});
|
|
2573
|
+
const encoded = encodedAudioOf(output);
|
|
2574
|
+
const persistedAssets = req.persist
|
|
2575
|
+
? await this.persistGenerationBytes(id, req, encoded ? [encoded.data] : mediaBuffers(output), encoded?.mimeType)
|
|
2576
|
+
: [];
|
|
2577
|
+
const assets = persistedAssets.filter((asset) => asset !== null);
|
|
2578
|
+
let delivery;
|
|
2579
|
+
if (opts?.finalizeOutput) {
|
|
2580
|
+
try {
|
|
2581
|
+
delivery = await opts.finalizeOutput(output, assets);
|
|
2582
|
+
}
|
|
2583
|
+
catch (error) {
|
|
2584
|
+
delivery = {
|
|
2585
|
+
status: "failed",
|
|
2586
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
const deliveryFields = {};
|
|
2591
|
+
if (delivery) {
|
|
2592
|
+
deliveryFields.delivery = delivery;
|
|
2593
|
+
}
|
|
2594
|
+
const late = opts?.receiptAfterPersist?.() ?? null;
|
|
2595
|
+
const receipt = late || recorded ? { ...(recorded ?? {}), ...(late ?? {}) } : null;
|
|
2596
|
+
const assetIds = assets
|
|
2597
|
+
.map((asset) => asset.asset_id)
|
|
2598
|
+
.filter((assetId) => typeof assetId === "string");
|
|
2599
|
+
const durableAssetIds = persistedAssets.map((asset) => asset?.asset_id ?? null);
|
|
2600
|
+
try {
|
|
2601
|
+
terminalAttempted = true;
|
|
2602
|
+
await terminal?.({
|
|
2603
|
+
generationId: id,
|
|
2604
|
+
request: req,
|
|
2605
|
+
status: "completed",
|
|
2606
|
+
output: providerResult ?? output,
|
|
2607
|
+
receipt,
|
|
2608
|
+
assetIds: durableAssetIds,
|
|
2609
|
+
...deliveryFields
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
catch (terminalError) {
|
|
2613
|
+
this.emit({
|
|
2614
|
+
type: "log_update",
|
|
2615
|
+
node_id: req.nodeId ?? "",
|
|
2616
|
+
node_name: req.nodeId ?? "",
|
|
2617
|
+
workflow_id: req.workflowId ?? this.workflowId ?? null,
|
|
2618
|
+
severity: "warning",
|
|
2619
|
+
content: `Generation ${id} completed but durable finalization will be recovered: ${terminalError instanceof Error ? terminalError.message : String(terminalError)}`
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
this.emitPrediction("completed", req, id, delivery ? { delivery } : generationResultData(req.capability, output), undefined, startedAt, { origin, asset_ids: assetIds, receipt });
|
|
2623
|
+
generationRegistry.settle(id, {
|
|
2624
|
+
status: "completed",
|
|
2625
|
+
asset_ids: assetIds,
|
|
2626
|
+
receipt,
|
|
2627
|
+
...deliveryFields
|
|
2628
|
+
});
|
|
2629
|
+
return {
|
|
2630
|
+
id,
|
|
2631
|
+
output,
|
|
2632
|
+
assets,
|
|
2633
|
+
receipt,
|
|
2634
|
+
duration_ms: Date.now() - startedAt,
|
|
2635
|
+
...deliveryFields
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
catch (error) {
|
|
2639
|
+
const cause = error instanceof GenerationScopeError ? error.cause : error;
|
|
2640
|
+
const receipt = error instanceof GenerationScopeError ? error.receipt : null;
|
|
2641
|
+
const providerResult = error instanceof GenerationScopeError
|
|
2642
|
+
? error.providerResult
|
|
2643
|
+
: undefined;
|
|
2644
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2645
|
+
if (cause instanceof GenerationAlreadyAcceptedError) {
|
|
2646
|
+
throw cause;
|
|
2647
|
+
}
|
|
2648
|
+
const status = signal.aborted
|
|
2649
|
+
? durableAccepted
|
|
2650
|
+
? "running"
|
|
2651
|
+
: "cancelled"
|
|
2652
|
+
: durableAccepted && !isAuthoritativeProviderTerminalError(cause)
|
|
2653
|
+
? "running"
|
|
2654
|
+
: "failed";
|
|
2655
|
+
if (durableAccepted && !terminalAttempted) {
|
|
2656
|
+
try {
|
|
2657
|
+
terminalAttempted = true;
|
|
2658
|
+
await terminal?.({
|
|
2659
|
+
generationId: id,
|
|
2660
|
+
request: req,
|
|
2661
|
+
status: isAuthoritativeProviderTerminalError(cause)
|
|
2662
|
+
? "failed"
|
|
2663
|
+
: "recovering",
|
|
2664
|
+
output: providerResult,
|
|
2665
|
+
error: message,
|
|
2666
|
+
receipt,
|
|
2667
|
+
assetIds: []
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
catch {
|
|
2671
|
+
// The provider observation remains authoritative. The durable row
|
|
2672
|
+
// can be finalized by the recovery worker.
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
if (this.appRunContext && !durableAccepted && !terminalAttempted) {
|
|
2676
|
+
try {
|
|
2677
|
+
terminalAttempted = true;
|
|
2678
|
+
await terminal?.({
|
|
2679
|
+
generationId: id,
|
|
2680
|
+
request: req,
|
|
2681
|
+
status: signal.aborted ? "cancelled" : "failed",
|
|
2682
|
+
error: message,
|
|
2683
|
+
receipt,
|
|
2684
|
+
assetIds: []
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
catch {
|
|
2688
|
+
// Preserve the provider failure; the live ledger can retry its write.
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
this.emitPrediction(status, req, id, null, message, startedAt, {
|
|
2692
|
+
origin,
|
|
2693
|
+
receipt
|
|
2694
|
+
});
|
|
2695
|
+
generationRegistry.settle(id, {
|
|
2696
|
+
status,
|
|
2697
|
+
error: message,
|
|
2698
|
+
asset_ids: [],
|
|
2699
|
+
receipt
|
|
2700
|
+
});
|
|
2701
|
+
throw cause;
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
/**
|
|
2706
|
+
* Save a generation's media output as assets, stamping the generation id
|
|
2707
|
+
* on each. Bytes only: a segmentation result, a transcript or an embedding
|
|
2708
|
+
* is a result and stays on the message.
|
|
2709
|
+
*/
|
|
2710
|
+
async persistGenerationBytes(id, req, buffers, mimeOverride) {
|
|
2711
|
+
if (!this.hasModelInterface("createAsset")) {
|
|
2712
|
+
return buffers.map(() => null);
|
|
2713
|
+
}
|
|
2714
|
+
const assets = [];
|
|
2715
|
+
// The prompt and settings ride on the asset, not only on the ledger row:
|
|
2716
|
+
// the row has its own retention and lives in another table, while the
|
|
2717
|
+
// asset is what somebody opens weeks later to make a variant.
|
|
2718
|
+
const provenance = buildAssetGenerationMetadata({
|
|
2719
|
+
prompt: req.params?.prompt,
|
|
2720
|
+
provider: req.provider,
|
|
2721
|
+
model: req.model,
|
|
2722
|
+
capability: req.capability,
|
|
2723
|
+
params: req.params
|
|
2724
|
+
});
|
|
2725
|
+
const metadata = { generation_id: id, ...provenance };
|
|
2726
|
+
for (const [index, bytes] of buffers.entries()) {
|
|
2727
|
+
const mime = req.persist?.mime ??
|
|
2728
|
+
mimeOverride ??
|
|
2729
|
+
generationMime(req.capability, bytes);
|
|
2730
|
+
const ext = extForGenerationMime(mime);
|
|
2731
|
+
const name = buffers.length === 1 || !req.persist?.name
|
|
2732
|
+
? (req.persist?.name ?? `${req.capability}-${startedName(id)}.${ext}`)
|
|
2733
|
+
: req.persist.name.replace(/(\.[a-z0-9]+)?$/i, `-${index + 1}$1`);
|
|
2734
|
+
try {
|
|
2735
|
+
const created = await this.createAsset({
|
|
2736
|
+
name,
|
|
2737
|
+
contentType: mime,
|
|
2738
|
+
content: bytes,
|
|
2739
|
+
parentId: req.persist?.parentId ?? null,
|
|
2740
|
+
nodeId: req.nodeId ?? null,
|
|
2741
|
+
metadata: { ...metadata }
|
|
2742
|
+
});
|
|
2743
|
+
const assetId = isRecord(created) && typeof created.id === "string"
|
|
2744
|
+
? created.id
|
|
2745
|
+
: null;
|
|
2746
|
+
if (!assetId) {
|
|
2747
|
+
assets.push(null);
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
assets.push({
|
|
2751
|
+
type: assetRefType(mime),
|
|
2752
|
+
uri: `asset://${assetId}.${ext}`,
|
|
2753
|
+
asset_id: assetId,
|
|
2754
|
+
metadata: { ...metadata }
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
catch (error) {
|
|
2758
|
+
assets.push(null);
|
|
2759
|
+
// The generation happened and was billed; a failed save must not turn
|
|
2760
|
+
// it into a failed generation. The caller still gets the bytes.
|
|
2761
|
+
this.emit({
|
|
2762
|
+
type: "log_update",
|
|
2763
|
+
node_id: req.nodeId ?? "",
|
|
2764
|
+
node_name: req.nodeId ?? "",
|
|
2765
|
+
workflow_id: req.workflowId ?? this.workflowId ?? null,
|
|
2766
|
+
severity: "warning",
|
|
2767
|
+
content: `Generated ${mime} could not be saved as an asset: ${error instanceof Error ? error.message : String(error)}`
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return assets;
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Run a prediction, retrying a content-filter refusal a bounded number of
|
|
2775
|
+
* times. The filter is non-deterministic — Veo refused one shot of a
|
|
2776
|
+
* five-shot trailer on an ordinary cinematic prompt and passed the same
|
|
2777
|
+
* prompt on retry — and a refused take is not charged, so the cheap answer to
|
|
2778
|
+
* a refusal is to ask again before failing the node that asked.
|
|
2779
|
+
*
|
|
2780
|
+
* A refusal that survives the retries is rethrown as-is, so the kernel can
|
|
2781
|
+
* still drop the item rather than the run.
|
|
2782
|
+
*/
|
|
2783
|
+
async retryContentFilterRefusal(req, attemptFn) {
|
|
2784
|
+
for (let attempt = 1;; attempt++) {
|
|
2785
|
+
try {
|
|
2786
|
+
return await attemptFn();
|
|
2787
|
+
}
|
|
2788
|
+
catch (error) {
|
|
2789
|
+
if (!isContentFilterRefusal(error) ||
|
|
2790
|
+
attempt > CONTENT_FILTER_MAX_RETRIES) {
|
|
2791
|
+
throw error;
|
|
2792
|
+
}
|
|
2793
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2794
|
+
this.emit({
|
|
2795
|
+
type: "log_update",
|
|
2796
|
+
node_id: req.nodeId ?? "",
|
|
2797
|
+
node_name: req.nodeId ?? "",
|
|
2798
|
+
workflow_id: req.workflowId ?? this.workflowId ?? null,
|
|
2799
|
+
severity: "warning",
|
|
2800
|
+
content: `${req.provider}/${req.model} content-filtered this request (attempt ${attempt} of ${CONTENT_FILTER_MAX_RETRIES + 1}); retrying. ${message}`
|
|
2801
|
+
});
|
|
2802
|
+
await waitForRetry(contentFilterRetryDelayMs(attempt), this.signal);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Whether a provider streams raw PCM for text-to-speech. When false, the
|
|
2808
|
+
* caller should use {@link textToSpeechEncoded} (the provider returns an
|
|
2809
|
+
* encoded audio file rather than samples).
|
|
2810
|
+
*/
|
|
2811
|
+
async providerSupportsStreamingTTS(providerId) {
|
|
2812
|
+
const provider = await this.getProvider(providerId);
|
|
2813
|
+
return provider.supportsStreamingTextToSpeech();
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Whether a provider's chat API accepts a `video` content part. Lets a
|
|
2817
|
+
* caller refuse before paying for a prediction that the provider would
|
|
2818
|
+
* reject while resolving the message, which is where the video bytes have
|
|
2819
|
+
* already been fetched and inlined.
|
|
2820
|
+
*/
|
|
2821
|
+
async providerSupportsVideoInput(providerId) {
|
|
2822
|
+
const provider = await this.getProvider(providerId);
|
|
2823
|
+
return provider.supportsVideoInput;
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Generate speech as an encoded audio file (mp3/wav/…). Returns null when the
|
|
2827
|
+
* provider doesn't implement the encoded path. Used for providers (FAL, KIE,
|
|
2828
|
+
* ElevenLabs) that return audio files instead of streaming PCM.
|
|
2829
|
+
*/
|
|
2830
|
+
async textToSpeechEncoded(req) {
|
|
2831
|
+
return this.runEncodedGeneration(req, async (provider) => {
|
|
2832
|
+
const params = req.params ?? {};
|
|
2833
|
+
return provider.textToSpeechEncoded({
|
|
2834
|
+
text: String(params.text ?? ""),
|
|
2835
|
+
model: req.model,
|
|
2836
|
+
voice: params.voice,
|
|
2837
|
+
speed: params.speed,
|
|
2838
|
+
referenceAudio: params.reference_audio,
|
|
2839
|
+
referenceText: params.reference_text,
|
|
2840
|
+
language: params.language,
|
|
2841
|
+
instructions: params.instructions,
|
|
2842
|
+
audioFormat: params.audioFormat
|
|
2843
|
+
});
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* The generation lifecycle for the encoded-audio paths: same id on every
|
|
2848
|
+
* message, origin on `running`, the receipt on the terminal message,
|
|
2849
|
+
* `cancelled` on abort. These return an encoded result rather than bytes,
|
|
2850
|
+
* so the caller persists.
|
|
2851
|
+
*/
|
|
2852
|
+
async runEncodedGeneration(req, call) {
|
|
2853
|
+
const id = req.id ?? randomUUID();
|
|
1193
2854
|
const startedAt = Date.now();
|
|
1194
|
-
this.
|
|
2855
|
+
const origin = this.generationOrigin(req, req.origin);
|
|
2856
|
+
const controller = new AbortController();
|
|
2857
|
+
const signals = [controller.signal, this.signal];
|
|
2858
|
+
if (req.signal)
|
|
2859
|
+
signals.push(req.signal);
|
|
2860
|
+
const signal = AbortSignal.any(signals);
|
|
2861
|
+
const accept = this._generationLifecycle?.onGenerationAccepted;
|
|
2862
|
+
const terminal = this._generationLifecycle?.onGenerationTerminal;
|
|
2863
|
+
let durableAccepted = false;
|
|
2864
|
+
let terminalAttempted = false;
|
|
2865
|
+
let accepted;
|
|
1195
2866
|
try {
|
|
2867
|
+
accepted = await accept?.({ generationId: id, request: req });
|
|
2868
|
+
if (accepted?.skipProvider) {
|
|
2869
|
+
throw new GenerationAlreadyAcceptedError(accepted.existingGenerationId ?? id);
|
|
2870
|
+
}
|
|
2871
|
+
durableAccepted = accepted?.durable === true;
|
|
2872
|
+
generationRegistry.register(id, {
|
|
2873
|
+
userId: this.userId,
|
|
2874
|
+
abort: () => controller.abort(new Error("Generation cancelled"))
|
|
2875
|
+
});
|
|
2876
|
+
this.emitPrediction("running", req, id, null, undefined, startedAt, {
|
|
2877
|
+
origin
|
|
2878
|
+
});
|
|
1196
2879
|
const provider = await this.getProvider(req.provider);
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
2880
|
+
const { value, receipt, providerResult } = await runWithGenerationReceipt(() => call(provider, signal), {
|
|
2881
|
+
providerRequestOptions: accepted ?? undefined,
|
|
2882
|
+
onProviderRequestAccepted: this._generationLifecycle?.onProviderRequestAccepted,
|
|
2883
|
+
onProviderRequestBound: this._generationLifecycle?.onProviderRequestBound
|
|
2884
|
+
});
|
|
2885
|
+
const encoded = encodedAudioOf(value);
|
|
2886
|
+
const persistedAssets = req.persist && encoded
|
|
2887
|
+
? await this.persistGenerationBytes(id, req, [encoded.data], encoded.mimeType)
|
|
2888
|
+
: [];
|
|
2889
|
+
const assets = persistedAssets.filter((asset) => asset !== null);
|
|
2890
|
+
const assetIds = assets
|
|
2891
|
+
.map((asset) => asset.asset_id)
|
|
2892
|
+
.filter((assetId) => typeof assetId === "string");
|
|
2893
|
+
const durableAssetIds = persistedAssets.map((asset) => asset?.asset_id ?? null);
|
|
2894
|
+
try {
|
|
2895
|
+
terminalAttempted = true;
|
|
2896
|
+
await terminal?.({
|
|
2897
|
+
generationId: id,
|
|
2898
|
+
request: req,
|
|
2899
|
+
status: "completed",
|
|
2900
|
+
output: providerResult ?? value,
|
|
2901
|
+
receipt,
|
|
2902
|
+
assetIds: durableAssetIds
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
catch {
|
|
2906
|
+
// Provider success remains authoritative even when durable finalization
|
|
2907
|
+
// needs a later recovery attempt.
|
|
2908
|
+
}
|
|
2909
|
+
this.emitPrediction("completed", req, id, null, undefined, startedAt, {
|
|
2910
|
+
origin,
|
|
2911
|
+
asset_ids: assetIds,
|
|
2912
|
+
receipt
|
|
2913
|
+
});
|
|
2914
|
+
generationRegistry.settle(id, {
|
|
2915
|
+
status: "completed",
|
|
2916
|
+
asset_ids: assetIds,
|
|
2917
|
+
receipt
|
|
2918
|
+
});
|
|
2919
|
+
return value;
|
|
1200
2920
|
}
|
|
1201
2921
|
catch (error) {
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
2922
|
+
const cause = error instanceof GenerationScopeError ? error.cause : error;
|
|
2923
|
+
const receipt = error instanceof GenerationScopeError ? error.receipt : null;
|
|
2924
|
+
const providerResult = error instanceof GenerationScopeError
|
|
2925
|
+
? error.providerResult
|
|
2926
|
+
: undefined;
|
|
2927
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2928
|
+
if (cause instanceof GenerationAlreadyAcceptedError) {
|
|
2929
|
+
throw cause;
|
|
2930
|
+
}
|
|
2931
|
+
const status = signal.aborted
|
|
2932
|
+
? durableAccepted
|
|
2933
|
+
? "running"
|
|
2934
|
+
: "cancelled"
|
|
2935
|
+
: durableAccepted && !isAuthoritativeProviderTerminalError(cause)
|
|
2936
|
+
? "running"
|
|
2937
|
+
: "failed";
|
|
2938
|
+
if (durableAccepted && !terminalAttempted) {
|
|
2939
|
+
try {
|
|
2940
|
+
terminalAttempted = true;
|
|
2941
|
+
await terminal?.({
|
|
2942
|
+
generationId: id,
|
|
2943
|
+
request: req,
|
|
2944
|
+
status: isAuthoritativeProviderTerminalError(cause)
|
|
2945
|
+
? "failed"
|
|
2946
|
+
: "recovering",
|
|
2947
|
+
output: providerResult,
|
|
2948
|
+
error: message,
|
|
2949
|
+
receipt,
|
|
2950
|
+
assetIds: []
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
catch {
|
|
2954
|
+
// Keep the provider outcome visible while durable finalization is
|
|
2955
|
+
// retried by the recovery worker.
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (this.appRunContext && !durableAccepted && !terminalAttempted) {
|
|
2959
|
+
try {
|
|
2960
|
+
terminalAttempted = true;
|
|
2961
|
+
await terminal?.({
|
|
2962
|
+
generationId: id,
|
|
2963
|
+
request: req,
|
|
2964
|
+
status: signal.aborted ? "cancelled" : "failed",
|
|
2965
|
+
error: message,
|
|
2966
|
+
receipt,
|
|
2967
|
+
assetIds: []
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
catch {
|
|
2971
|
+
// Preserve the provider failure; the live ledger can retry its write.
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
this.emitPrediction(status, req, id, null, message, startedAt, {
|
|
2975
|
+
origin,
|
|
2976
|
+
receipt
|
|
2977
|
+
});
|
|
2978
|
+
generationRegistry.settle(id, {
|
|
2979
|
+
status,
|
|
2980
|
+
error: message,
|
|
2981
|
+
asset_ids: [],
|
|
2982
|
+
receipt
|
|
2983
|
+
});
|
|
2984
|
+
throw cause;
|
|
1205
2985
|
}
|
|
1206
2986
|
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Generate music as an encoded audio file (mp3/wav/flac). Mirrors
|
|
2989
|
+
* {@link textToSpeechEncoded} for the `text_to_music` capability — the
|
|
2990
|
+
* provider returns a fully-encoded audio file rather than streaming PCM.
|
|
2991
|
+
*/
|
|
2992
|
+
async textToMusic(req) {
|
|
2993
|
+
return this.runEncodedGeneration(req, async (provider) => {
|
|
2994
|
+
const params = req.params ?? {};
|
|
2995
|
+
return provider.textToMusic({
|
|
2996
|
+
prompt: String(params.prompt ?? ""),
|
|
2997
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
2998
|
+
lyrics: params.lyrics,
|
|
2999
|
+
durationSeconds: params.duration_seconds,
|
|
3000
|
+
seed: params.seed,
|
|
3001
|
+
audioFormat: params.audioFormat,
|
|
3002
|
+
timeoutSeconds: params.timeout_seconds
|
|
3003
|
+
});
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Rewrite a recording into another one. Mirrors {@link textToMusic} for the
|
|
3008
|
+
* `audio_to_audio` capability: the provider hands back an encoded file, and
|
|
3009
|
+
* the source bytes ride in `params.audio`.
|
|
3010
|
+
*/
|
|
3011
|
+
async audioToAudio(req) {
|
|
3012
|
+
return this.runEncodedGeneration(req, async (provider) => {
|
|
3013
|
+
const params = req.params ?? {};
|
|
3014
|
+
const audio = params.audio;
|
|
3015
|
+
if (!(audio instanceof Uint8Array) || audio.length === 0) {
|
|
3016
|
+
throw new Error("audio_to_audio needs source audio bytes");
|
|
3017
|
+
}
|
|
3018
|
+
return provider.audioToAudio(audio, {
|
|
3019
|
+
model: { id: req.model, name: req.model, provider: req.provider },
|
|
3020
|
+
prompt: params.prompt,
|
|
3021
|
+
voice: params.voice,
|
|
3022
|
+
strength: params.strength,
|
|
3023
|
+
seed: params.seed,
|
|
3024
|
+
audioFormat: params.audioFormat,
|
|
3025
|
+
timeoutSeconds: params.timeout_seconds
|
|
3026
|
+
});
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Generate an audio result conditioned on a captured video window and scene
|
|
3031
|
+
* context. The encoded-audio lifecycle persists the result without touching
|
|
3032
|
+
* the source video's soundtrack or timeline placement.
|
|
3033
|
+
*/
|
|
3034
|
+
async videoToAudio(req) {
|
|
3035
|
+
return this.runEncodedGeneration(req, async (provider, signal) => {
|
|
3036
|
+
const params = req.params ?? {};
|
|
3037
|
+
const source = isRecord(params.source)
|
|
3038
|
+
? {
|
|
3039
|
+
assetId: params.source.assetId ?? params.source.asset_id,
|
|
3040
|
+
durationSeconds: params.source.durationSeconds ?? params.source.duration_seconds,
|
|
3041
|
+
startSeconds: params.source.startSeconds ?? params.source.start_seconds,
|
|
3042
|
+
endSeconds: params.source.endSeconds ?? params.source.end_seconds
|
|
3043
|
+
}
|
|
3044
|
+
: params.source;
|
|
3045
|
+
const request = VideoToAudioRequest.parse({
|
|
3046
|
+
task: VIDEO_TO_AUDIO_TASK,
|
|
3047
|
+
model: {
|
|
3048
|
+
id: req.model,
|
|
3049
|
+
provider: req.provider,
|
|
3050
|
+
supportedTasks: [VIDEO_TO_AUDIO_TASK]
|
|
3051
|
+
},
|
|
3052
|
+
source,
|
|
3053
|
+
sceneContext: params.sceneContext ?? params.scene_context
|
|
3054
|
+
});
|
|
3055
|
+
const video = params.video instanceof Uint8Array
|
|
3056
|
+
? params.video
|
|
3057
|
+
: await loadMediaRefBytes({ type: "video", asset_id: request.source.assetId }, this);
|
|
3058
|
+
if (!video?.byteLength) {
|
|
3059
|
+
throw new Error("video_to_audio requires nonempty source video bytes");
|
|
3060
|
+
}
|
|
3061
|
+
return requestVideoToAudio(provider, video, request, { signal });
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
1207
3064
|
async *streamProviderPrediction(req) {
|
|
1208
|
-
const id = randomUUID();
|
|
3065
|
+
const id = req.id ?? randomUUID();
|
|
1209
3066
|
const startedAt = Date.now();
|
|
1210
|
-
this.
|
|
3067
|
+
const origin = this.generationOrigin(req, req.origin);
|
|
3068
|
+
this.emitPrediction("running", req, id, null, undefined, startedAt, {
|
|
3069
|
+
origin
|
|
3070
|
+
});
|
|
3071
|
+
let terminalStatusEmitted = false;
|
|
1211
3072
|
try {
|
|
1212
3073
|
const provider = await this.getProvider(req.provider);
|
|
1213
3074
|
const params = req.params ?? {};
|
|
@@ -1232,7 +3093,11 @@ export class ProcessingContext {
|
|
|
1232
3093
|
text: String(params.text ?? ""),
|
|
1233
3094
|
model: req.model,
|
|
1234
3095
|
voice: params.voice,
|
|
1235
|
-
speed: params.speed
|
|
3096
|
+
speed: params.speed,
|
|
3097
|
+
referenceAudio: params.reference_audio,
|
|
3098
|
+
referenceText: params.reference_text,
|
|
3099
|
+
language: params.language,
|
|
3100
|
+
instructions: params.instructions
|
|
1236
3101
|
})) {
|
|
1237
3102
|
yield item;
|
|
1238
3103
|
}
|
|
@@ -1240,13 +3105,29 @@ export class ProcessingContext {
|
|
|
1240
3105
|
else {
|
|
1241
3106
|
throw new Error(`Capability '${req.capability}' is not streamable`);
|
|
1242
3107
|
}
|
|
1243
|
-
this.emitPrediction("completed", req, id, null, undefined, startedAt
|
|
3108
|
+
this.emitPrediction("completed", req, id, null, undefined, startedAt, {
|
|
3109
|
+
origin,
|
|
3110
|
+
asset_ids: []
|
|
3111
|
+
});
|
|
3112
|
+
terminalStatusEmitted = true;
|
|
1244
3113
|
}
|
|
1245
3114
|
catch (error) {
|
|
1246
3115
|
const message = error instanceof Error ? error.message : String(error);
|
|
1247
|
-
this.
|
|
3116
|
+
const status = this.signal.aborted || req.signal?.aborted ? "cancelled" : "failed";
|
|
3117
|
+
this.emitPrediction(status, req, id, null, message, startedAt, {
|
|
3118
|
+
origin
|
|
3119
|
+
});
|
|
3120
|
+
terminalStatusEmitted = true;
|
|
1248
3121
|
throw error;
|
|
1249
3122
|
}
|
|
3123
|
+
finally {
|
|
3124
|
+
if (!terminalStatusEmitted) {
|
|
3125
|
+
this.emitPrediction("completed", req, id, null, undefined, startedAt, {
|
|
3126
|
+
origin,
|
|
3127
|
+
asset_ids: []
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
1250
3131
|
}
|
|
1251
3132
|
// -----------------------------------------------------------------------
|
|
1252
3133
|
// Output normalization
|
|
@@ -1257,6 +3138,8 @@ export class ProcessingContext {
|
|
|
1257
3138
|
*
|
|
1258
3139
|
* Port of sanitize_memory_uris_for_client() from types.py.
|
|
1259
3140
|
*/
|
|
3141
|
+
// HOLDOUT (anti-slop/no-unknown-returns): same value domain as
|
|
3142
|
+
// `normalizeOutputValue` — an arbitrary node output, rewritten in place.
|
|
1260
3143
|
static sanitizeForClient(value) {
|
|
1261
3144
|
if (value === null || value === undefined)
|
|
1262
3145
|
return value;
|
|
@@ -1266,11 +3149,11 @@ export class ProcessingContext {
|
|
|
1266
3149
|
if (Array.isArray(value)) {
|
|
1267
3150
|
return value.map((v) => ProcessingContext.sanitizeForClient(v));
|
|
1268
3151
|
}
|
|
1269
|
-
if (
|
|
3152
|
+
if (!isRecord(value))
|
|
1270
3153
|
return value;
|
|
1271
3154
|
const obj = value;
|
|
1272
3155
|
const uri = obj.uri;
|
|
1273
|
-
const isAssetLike = "type" in obj &&
|
|
3156
|
+
const isAssetLike = "type" in obj && isString(uri);
|
|
1274
3157
|
if (isAssetLike && uri.startsWith("memory://")) {
|
|
1275
3158
|
const sanitized = { ...obj };
|
|
1276
3159
|
if (sanitized.data !== undefined && sanitized.data !== null) {
|
|
@@ -1320,13 +3203,22 @@ export class ProcessingContext {
|
|
|
1320
3203
|
const v = value;
|
|
1321
3204
|
return "type" in v && ("uri" in v || "data" in v || "asset_id" in v);
|
|
1322
3205
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
3206
|
+
/**
|
|
3207
|
+
* `mimeType` comes first because it is the field `ImageRef` declares — this
|
|
3208
|
+
* used to read only `mime_type`, which nothing sets, so a node's sniffed
|
|
3209
|
+
* `image/webp` was discarded and every provider's WebP stored as PNG.
|
|
3210
|
+
* `image/unknown` is `image-nodes` reporting its own sniff found nothing, so
|
|
3211
|
+
* it counts as undeclared here rather than as a type to forward.
|
|
3212
|
+
*/
|
|
3213
|
+
static guessAssetMime(asset, bytes) {
|
|
3214
|
+
const declared = asset.mimeType ?? asset.mime_type ?? asset.content_type;
|
|
3215
|
+
if (isNonEmptyString(declared) && declared !== "image/unknown") {
|
|
3216
|
+
return declared;
|
|
3217
|
+
}
|
|
1327
3218
|
const type = String(asset.type ?? "").toLowerCase();
|
|
1328
|
-
if (type.includes("image"))
|
|
1329
|
-
return "image/png";
|
|
3219
|
+
if (type.includes("image")) {
|
|
3220
|
+
return (bytes && sniffImageMime(bytes)) ?? "image/png";
|
|
3221
|
+
}
|
|
1330
3222
|
if (type.includes("audio"))
|
|
1331
3223
|
return "audio/wav";
|
|
1332
3224
|
if (type.includes("video"))
|
|
@@ -1338,10 +3230,10 @@ export class ProcessingContext {
|
|
|
1338
3230
|
return "application/octet-stream";
|
|
1339
3231
|
}
|
|
1340
3232
|
static extForMime(mime) {
|
|
3233
|
+
const imageExt = extForImageMime(mime);
|
|
3234
|
+
if (imageExt)
|
|
3235
|
+
return imageExt;
|
|
1341
3236
|
const map = {
|
|
1342
|
-
"image/png": "png",
|
|
1343
|
-
"image/jpeg": "jpg",
|
|
1344
|
-
"image/webp": "webp",
|
|
1345
3237
|
"audio/wav": "wav",
|
|
1346
3238
|
"audio/mpeg": "mp3",
|
|
1347
3239
|
"video/mp4": "mp4",
|
|
@@ -1359,33 +3251,87 @@ export class ProcessingContext {
|
|
|
1359
3251
|
if (Array.isArray(data) && data.every((v) => Number.isInteger(v))) {
|
|
1360
3252
|
return new Uint8Array(data);
|
|
1361
3253
|
}
|
|
1362
|
-
if (
|
|
3254
|
+
if (isString(data)) {
|
|
1363
3255
|
return Uint8Array.from(Buffer.from(data, "base64"));
|
|
1364
3256
|
}
|
|
1365
3257
|
return null;
|
|
1366
3258
|
}
|
|
3259
|
+
static isClientFetchableResolvedAssetUri(sourceUri, resolvedUri) {
|
|
3260
|
+
return (resolvedUri.startsWith("/api/storage/") ||
|
|
3261
|
+
resolvedUri.startsWith("api/storage/") ||
|
|
3262
|
+
(/^(memory|file|s3|supabase):\/\//.test(sourceUri) &&
|
|
3263
|
+
resolvedUri !== sourceUri &&
|
|
3264
|
+
/^https?:\/\//.test(resolvedUri)));
|
|
3265
|
+
}
|
|
1367
3266
|
async getAssetBytes(asset) {
|
|
1368
3267
|
const decoded = ProcessingContext.decodeAssetData(asset.data);
|
|
1369
3268
|
if (decoded)
|
|
1370
3269
|
return decoded;
|
|
1371
3270
|
const uri = asset.uri;
|
|
1372
|
-
if (
|
|
1373
|
-
|
|
1374
|
-
|
|
3271
|
+
if (isString(uri) && this.storage) {
|
|
3272
|
+
const stored = await this.storage.retrieve(uri);
|
|
3273
|
+
if (stored)
|
|
3274
|
+
return stored;
|
|
3275
|
+
}
|
|
3276
|
+
if (isString(uri) &&
|
|
3277
|
+
(uri.startsWith("asset://") ||
|
|
3278
|
+
uri.startsWith("/api/storage/") ||
|
|
3279
|
+
uri.startsWith("api/storage/"))) {
|
|
3280
|
+
const { bytes } = await this.resolveAssetBytes(uri);
|
|
3281
|
+
if (bytes)
|
|
3282
|
+
return bytes;
|
|
3283
|
+
}
|
|
3284
|
+
// No usable uri but an asset_id is present: resolve directly by id.
|
|
3285
|
+
// resolveAssetBytes has its own HTTP fallback, so this works even without a
|
|
3286
|
+
// storage adapter (which the early `!this.storage` return used to preclude).
|
|
3287
|
+
const assetId = asset.asset_id;
|
|
3288
|
+
if (isNonEmptyString(assetId)) {
|
|
3289
|
+
const { bytes } = await this.resolveAssetBytes(`asset://${assetId}`);
|
|
3290
|
+
if (bytes)
|
|
3291
|
+
return bytes;
|
|
3292
|
+
}
|
|
3293
|
+
return null;
|
|
1375
3294
|
}
|
|
1376
|
-
async materializeAsset(
|
|
1377
|
-
if (mode === "
|
|
1378
|
-
|
|
3295
|
+
async materializeAsset(rawAsset, mode) {
|
|
3296
|
+
if (mode === "native" || mode === "raw") {
|
|
3297
|
+
// In-process consumers handle the raw asset as-is (incl. raw RGBA).
|
|
3298
|
+
return rawAsset;
|
|
3299
|
+
}
|
|
3300
|
+
// A fetchable reference with no inline bytes is already materialized for
|
|
3301
|
+
// an HTTP client. Reuse it instead of downloading the asset into this
|
|
3302
|
+
// process and writing an identical second temp object. This is especially
|
|
3303
|
+
// important for SDK pass-through workflows: their large input was already
|
|
3304
|
+
// uploaded once and should not be copied again on output.
|
|
3305
|
+
if (mode === "temp_url") {
|
|
3306
|
+
const uri = isString(rawAsset.uri) ? rawAsset.uri : "";
|
|
3307
|
+
const hasInlineBytes = ProcessingContext.decodeAssetData(rawAsset.data) !== null;
|
|
3308
|
+
const isStorageRoute = uri.startsWith("/api/storage/") || uri.startsWith("api/storage/");
|
|
3309
|
+
const isAdapterUri = /^(file|s3|supabase):\/\//.test(uri);
|
|
3310
|
+
if (uri && !hasInlineBytes && (isStorageRoute || isAdapterUri)) {
|
|
3311
|
+
const resolvedUri = this._tempUrlResolver
|
|
3312
|
+
? await this._tempUrlResolver(uri)
|
|
3313
|
+
: uri;
|
|
3314
|
+
if (ProcessingContext.isClientFetchableResolvedAssetUri(uri, resolvedUri)) {
|
|
3315
|
+
return {
|
|
3316
|
+
...rawAsset,
|
|
3317
|
+
uri: resolvedUri,
|
|
3318
|
+
data: undefined
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
1379
3322
|
}
|
|
3323
|
+
// Raw in-flight RGBA → encode to PNG up front so every downstream mode
|
|
3324
|
+
// treats it as an ordinary image (correct mime, extension, and bytes).
|
|
3325
|
+
const asset = (await encodeRawImageRef(rawAsset));
|
|
1380
3326
|
const bytes = await this.getAssetBytes(asset);
|
|
1381
3327
|
if (!bytes)
|
|
1382
3328
|
return asset;
|
|
1383
|
-
const mime = ProcessingContext.guessAssetMime(asset);
|
|
3329
|
+
const mime = ProcessingContext.guessAssetMime(asset, bytes);
|
|
1384
3330
|
if (mode === "data_uri") {
|
|
1385
|
-
const
|
|
3331
|
+
const base64 = Buffer.from(bytes).toString("base64");
|
|
1386
3332
|
return {
|
|
1387
3333
|
...asset,
|
|
1388
|
-
uri: `data:${mime};base64,${
|
|
3334
|
+
uri: `data:${mime};base64,${base64}`
|
|
1389
3335
|
};
|
|
1390
3336
|
}
|
|
1391
3337
|
if (mode === "storage_url") {
|
|
@@ -1404,12 +3350,9 @@ export class ProcessingContext {
|
|
|
1404
3350
|
return asset;
|
|
1405
3351
|
const key = `temp/${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
|
|
1406
3352
|
const storedUri = await this.storage.store(key, bytes, mime);
|
|
1407
|
-
const resolvedUri = this._tempUrlResolver
|
|
1408
|
-
? await this._tempUrlResolver(storedUri)
|
|
1409
|
-
: storedUri;
|
|
1410
3353
|
return {
|
|
1411
3354
|
...asset,
|
|
1412
|
-
uri:
|
|
3355
|
+
uri: await this.resolveTempUrl(storedUri),
|
|
1413
3356
|
data: undefined
|
|
1414
3357
|
};
|
|
1415
3358
|
}
|
|
@@ -1417,6 +3360,10 @@ export class ProcessingContext {
|
|
|
1417
3360
|
if (!this.workspaceDir) {
|
|
1418
3361
|
throw new Error("workspace_dir is required for workspace asset output");
|
|
1419
3362
|
}
|
|
3363
|
+
// Imported here rather than at module scope: this module also loads in
|
|
3364
|
+
// browser / Edge runtimes, and `@nodetool-ai/storage` reaches `node:fs`.
|
|
3365
|
+
// Only this branch, which already requires a workspace directory, needs it.
|
|
3366
|
+
const { FileStorageAdapter } = await import("@nodetool-ai/storage");
|
|
1420
3367
|
const workspaceAssets = new FileStorageAdapter(resolveWorkspacePath(this.workspaceDir, "assets"));
|
|
1421
3368
|
const key = `${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
|
|
1422
3369
|
const uri = await workspaceAssets.store(key, bytes, mime);
|
|
@@ -1432,6 +3379,9 @@ export class ProcessingContext {
|
|
|
1432
3379
|
* Recursively normalize workflow outputs, materializing asset-like values
|
|
1433
3380
|
* according to the selected output mode.
|
|
1434
3381
|
*/
|
|
3382
|
+
// HOLDOUT (anti-slop/no-unknown-returns): the input and the result are an
|
|
3383
|
+
// arbitrary workflow value; naming that domain is a bigger change than a
|
|
3384
|
+
// typing pass.
|
|
1435
3385
|
async normalizeOutputValue(value, mode = this.assetOutputMode) {
|
|
1436
3386
|
if (value === null || value === undefined)
|
|
1437
3387
|
return value;
|
|
@@ -1442,9 +3392,27 @@ export class ProcessingContext {
|
|
|
1442
3392
|
return Promise.all(value.map((item) => this.normalizeOutputValue(item, mode)));
|
|
1443
3393
|
}
|
|
1444
3394
|
if (ProcessingContext.isAssetLike(value)) {
|
|
1445
|
-
|
|
3395
|
+
let byMode = this._normalizedOutputAssets.get(value);
|
|
3396
|
+
if (!byMode) {
|
|
3397
|
+
byMode = new Map();
|
|
3398
|
+
this._normalizedOutputAssets.set(value, byMode);
|
|
3399
|
+
}
|
|
3400
|
+
let normalized = byMode.get(mode);
|
|
3401
|
+
if (!normalized) {
|
|
3402
|
+
normalized = this.materializeAsset(value, mode);
|
|
3403
|
+
byMode.set(mode, normalized);
|
|
3404
|
+
}
|
|
3405
|
+
try {
|
|
3406
|
+
return await normalized;
|
|
3407
|
+
}
|
|
3408
|
+
catch (error) {
|
|
3409
|
+
// A transient storage failure must remain retryable.
|
|
3410
|
+
if (byMode.get(mode) === normalized)
|
|
3411
|
+
byMode.delete(mode);
|
|
3412
|
+
throw error;
|
|
3413
|
+
}
|
|
1446
3414
|
}
|
|
1447
|
-
if (
|
|
3415
|
+
if (isRecord(value)) {
|
|
1448
3416
|
const entries = Object.entries(value);
|
|
1449
3417
|
const normalized = await Promise.all(entries.map(async ([k, v]) => [k, await this.normalizeOutputValue(v, mode)]));
|
|
1450
3418
|
return Object.fromEntries(normalized);
|
|
@@ -1452,4 +3420,155 @@ export class ProcessingContext {
|
|
|
1452
3420
|
return value;
|
|
1453
3421
|
}
|
|
1454
3422
|
}
|
|
3423
|
+
/**
|
|
3424
|
+
* The media resolver to hand `BaseProvider.generateLoop`, or undefined when
|
|
3425
|
+
* this context cannot dereference one.
|
|
3426
|
+
*
|
|
3427
|
+
* `view_image` answers with an `asset://` reference and the loop turns it into
|
|
3428
|
+
* pixels for the one request that needs them, so every host that drives a tool
|
|
3429
|
+
* loop passes this. A partial context — a test double, a host object built
|
|
3430
|
+
* from a subset of the interface — has no method to call, and the loop then
|
|
3431
|
+
* sends the reference through untouched rather than throwing mid-turn.
|
|
3432
|
+
*/
|
|
3433
|
+
export function mediaResolverFor(context) {
|
|
3434
|
+
if (!context || !isCallable(context.resolveMessageMediaUris))
|
|
3435
|
+
return undefined;
|
|
3436
|
+
return (messages) => context.resolveMessageMediaUris(messages);
|
|
3437
|
+
}
|
|
3438
|
+
// ---------------------------------------------------------------------------
|
|
3439
|
+
// Generation seam helpers
|
|
3440
|
+
// ---------------------------------------------------------------------------
|
|
3441
|
+
/**
|
|
3442
|
+
* The 3D model a request names: the full `Model3D` the node resolved (with
|
|
3443
|
+
* its supported tasks and formats) when it passed one, else the minimal ref.
|
|
3444
|
+
*/
|
|
3445
|
+
function model3dOf(params, req) {
|
|
3446
|
+
const given = params.model;
|
|
3447
|
+
if (isRecord(given) &&
|
|
3448
|
+
typeof given.id === "string" &&
|
|
3449
|
+
typeof given.provider === "string") {
|
|
3450
|
+
return given;
|
|
3451
|
+
}
|
|
3452
|
+
return {
|
|
3453
|
+
id: req.model,
|
|
3454
|
+
name: req.model,
|
|
3455
|
+
provider: req.provider
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
/** The media buffers a capability result carries, if any. */
|
|
3459
|
+
function mediaBuffers(output) {
|
|
3460
|
+
if (output instanceof Uint8Array)
|
|
3461
|
+
return [output];
|
|
3462
|
+
if (Array.isArray(output) &&
|
|
3463
|
+
output.length > 0 &&
|
|
3464
|
+
output.every((item) => item instanceof Uint8Array)) {
|
|
3465
|
+
return output;
|
|
3466
|
+
}
|
|
3467
|
+
return [];
|
|
3468
|
+
}
|
|
3469
|
+
/** An encoded-audio result (`{data, mimeType}`), or null for anything else. */
|
|
3470
|
+
function encodedAudioOf(value) {
|
|
3471
|
+
if (!isRecord(value))
|
|
3472
|
+
return null;
|
|
3473
|
+
const data = value.data;
|
|
3474
|
+
if (!(data instanceof Uint8Array) || data.length === 0)
|
|
3475
|
+
return null;
|
|
3476
|
+
const mimeType = value.mimeType;
|
|
3477
|
+
return {
|
|
3478
|
+
data,
|
|
3479
|
+
mimeType: typeof mimeType === "string" ? mimeType : undefined
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
/** What a `completed` prediction message carries as `data`: never bytes. */
|
|
3483
|
+
function generationResultData(capability, output) {
|
|
3484
|
+
if (output instanceof Uint8Array)
|
|
3485
|
+
return null;
|
|
3486
|
+
if (encodedAudioOf(output))
|
|
3487
|
+
return null;
|
|
3488
|
+
if (Array.isArray(output)) {
|
|
3489
|
+
if (output.every((item) => item instanceof Uint8Array)) {
|
|
3490
|
+
return { count: output.length };
|
|
3491
|
+
}
|
|
3492
|
+
if (capability === "segment_image") {
|
|
3493
|
+
return output.map((item) => {
|
|
3494
|
+
if (!isRecord(item))
|
|
3495
|
+
return item;
|
|
3496
|
+
const { mask: _mask, ...rest } = item;
|
|
3497
|
+
return rest;
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
if (capability === "generate_embedding") {
|
|
3501
|
+
const first = output[0];
|
|
3502
|
+
return {
|
|
3503
|
+
count: output.length,
|
|
3504
|
+
dimensions: Array.isArray(first) ? first.length : null
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
return { count: output.length };
|
|
3508
|
+
}
|
|
3509
|
+
return output ?? null;
|
|
3510
|
+
}
|
|
3511
|
+
const VIDEO_CAPABILITIES = new Set([
|
|
3512
|
+
"text_to_video",
|
|
3513
|
+
"image_to_video",
|
|
3514
|
+
"reference_to_video",
|
|
3515
|
+
"video_to_video",
|
|
3516
|
+
"extend_video",
|
|
3517
|
+
"lip_sync"
|
|
3518
|
+
]);
|
|
3519
|
+
const AUDIO_CAPABILITIES = new Set([
|
|
3520
|
+
"text_to_speech",
|
|
3521
|
+
"text_to_music",
|
|
3522
|
+
"audio_to_audio",
|
|
3523
|
+
"video_to_audio"
|
|
3524
|
+
]);
|
|
3525
|
+
const MODEL3D_CAPABILITIES = new Set([
|
|
3526
|
+
"text_to_3d",
|
|
3527
|
+
"image_to_3d"
|
|
3528
|
+
]);
|
|
3529
|
+
/** The MIME a generation's bytes carry when the caller stated none. */
|
|
3530
|
+
function generationMime(capability, bytes) {
|
|
3531
|
+
if (VIDEO_CAPABILITIES.has(capability))
|
|
3532
|
+
return "video/mp4";
|
|
3533
|
+
if (AUDIO_CAPABILITIES.has(capability))
|
|
3534
|
+
return "audio/mpeg";
|
|
3535
|
+
if (MODEL3D_CAPABILITIES.has(capability))
|
|
3536
|
+
return "model/gltf-binary";
|
|
3537
|
+
if (capability === "vectorize_image")
|
|
3538
|
+
return "image/svg+xml";
|
|
3539
|
+
return sniffImageMime(bytes) ?? "image/png";
|
|
3540
|
+
}
|
|
3541
|
+
const GENERATION_MIME_EXT = {
|
|
3542
|
+
"video/mp4": "mp4",
|
|
3543
|
+
"video/webm": "webm",
|
|
3544
|
+
"audio/mpeg": "mp3",
|
|
3545
|
+
"audio/mp3": "mp3",
|
|
3546
|
+
"audio/wav": "wav",
|
|
3547
|
+
"audio/x-wav": "wav",
|
|
3548
|
+
"audio/flac": "flac",
|
|
3549
|
+
"audio/ogg": "ogg",
|
|
3550
|
+
"model/gltf-binary": "glb",
|
|
3551
|
+
"model/gltf+json": "gltf",
|
|
3552
|
+
"image/svg+xml": "svg"
|
|
3553
|
+
};
|
|
3554
|
+
function extForGenerationMime(mime) {
|
|
3555
|
+
if (mime.startsWith("image/") && mime !== "image/svg+xml") {
|
|
3556
|
+
return extForImageMime(mime) ?? "bin";
|
|
3557
|
+
}
|
|
3558
|
+
return GENERATION_MIME_EXT[mime] ?? "bin";
|
|
3559
|
+
}
|
|
3560
|
+
function assetRefType(mime) {
|
|
3561
|
+
if (mime.startsWith("image/"))
|
|
3562
|
+
return "image";
|
|
3563
|
+
if (mime.startsWith("video/"))
|
|
3564
|
+
return "video";
|
|
3565
|
+
if (mime.startsWith("audio/"))
|
|
3566
|
+
return "audio";
|
|
3567
|
+
return "asset";
|
|
3568
|
+
}
|
|
3569
|
+
/** A short, sortable name fragment for an unnamed generation asset. */
|
|
3570
|
+
function startedName(id) {
|
|
3571
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
3572
|
+
return `${stamp}-${id.slice(0, 8)}`;
|
|
3573
|
+
}
|
|
1455
3574
|
//# sourceMappingURL=context.js.map
|