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