@nodetool-ai/websocket 0.7.0-rc.22 → 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 +50 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/agent/run-node-tool.d.ts +39 -0
- package/dist/agent/run-node-tool.d.ts.map +1 -0
- package/dist/agent/run-node-tool.js +55 -0
- package/dist/agent/run-node-tool.js.map +1 -0
- package/dist/chat-prediction-forwarder.d.ts +13 -0
- package/dist/chat-prediction-forwarder.d.ts.map +1 -0
- package/dist/chat-prediction-forwarder.js +22 -0
- package/dist/chat-prediction-forwarder.js.map +1 -0
- package/dist/chat-tool-call-repair.d.ts +45 -0
- package/dist/chat-tool-call-repair.d.ts.map +1 -0
- package/dist/chat-tool-call-repair.js +100 -0
- package/dist/chat-tool-call-repair.js.map +1 -0
- package/dist/chat-turn-registry.d.ts +169 -0
- package/dist/chat-turn-registry.d.ts.map +1 -0
- package/dist/chat-turn-registry.js +326 -0
- package/dist/chat-turn-registry.js.map +1 -0
- package/dist/collection-api.d.ts +2 -2
- package/dist/collection-api.d.ts.map +1 -1
- package/dist/collection-api.js +44 -13
- package/dist/collection-api.js.map +1 -1
- package/dist/configured-providers.d.ts +52 -0
- package/dist/configured-providers.d.ts.map +1 -0
- package/dist/configured-providers.js +85 -0
- package/dist/configured-providers.js.map +1 -0
- package/dist/cors.d.ts +22 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +91 -0
- package/dist/cors.js.map +1 -0
- package/dist/credit-gate.d.ts +36 -0
- package/dist/credit-gate.d.ts.map +1 -0
- package/dist/credit-gate.js +76 -0
- package/dist/credit-gate.js.map +1 -0
- package/dist/custom-providers.d.ts +40 -0
- package/dist/custom-providers.d.ts.map +1 -0
- package/dist/custom-providers.js +134 -0
- package/dist/custom-providers.js.map +1 -0
- package/dist/debug-sessions.d.ts +10 -0
- package/dist/debug-sessions.d.ts.map +1 -0
- package/dist/debug-sessions.js +9 -0
- package/dist/debug-sessions.js.map +1 -0
- package/dist/drain.d.ts +34 -0
- package/dist/drain.d.ts.map +1 -0
- package/dist/drain.js +68 -0
- package/dist/drain.js.map +1 -0
- package/dist/e2e-server.d.ts +30 -0
- package/dist/e2e-server.d.ts.map +1 -0
- package/dist/e2e-server.js +111 -0
- package/dist/e2e-server.js.map +1 -0
- package/dist/example-workflows.d.ts +20 -0
- package/dist/example-workflows.d.ts.map +1 -0
- package/dist/example-workflows.js +123 -0
- package/dist/example-workflows.js.map +1 -0
- package/dist/extension-cdp-bridge.d.ts +85 -0
- package/dist/extension-cdp-bridge.d.ts.map +1 -0
- package/dist/extension-cdp-bridge.js +163 -0
- package/dist/extension-cdp-bridge.js.map +1 -0
- package/dist/external-mcp.d.ts +58 -0
- package/dist/external-mcp.d.ts.map +1 -0
- package/dist/external-mcp.js +288 -0
- package/dist/external-mcp.js.map +1 -0
- package/dist/fake-runtime.d.ts +114 -0
- package/dist/fake-runtime.d.ts.map +1 -0
- package/dist/fake-runtime.js +270 -0
- package/dist/fake-runtime.js.map +1 -0
- package/dist/file-api.d.ts +4 -7
- package/dist/file-api.d.ts.map +1 -1
- package/dist/file-api.js +122 -39
- package/dist/file-api.js.map +1 -1
- package/dist/frontend-renderer-registry.d.ts +75 -0
- package/dist/frontend-renderer-registry.d.ts.map +1 -0
- package/dist/frontend-renderer-registry.js +111 -0
- package/dist/frontend-renderer-registry.js.map +1 -0
- package/dist/generation-recovery.d.ts +13 -0
- package/dist/generation-recovery.d.ts.map +1 -0
- package/dist/generation-recovery.js +197 -0
- package/dist/generation-recovery.js.map +1 -0
- package/dist/headless-job-runner.d.ts +71 -0
- package/dist/headless-job-runner.d.ts.map +1 -0
- package/dist/headless-job-runner.js +198 -0
- package/dist/headless-job-runner.js.map +1 -0
- package/dist/http-api.d.ts +157 -24
- package/dist/http-api.d.ts.map +1 -1
- package/dist/http-api.js +613 -646
- package/dist/http-api.js.map +1 -1
- package/dist/http-body-schemas.d.ts +109 -0
- package/dist/http-body-schemas.d.ts.map +1 -0
- package/dist/http-body-schemas.js +146 -0
- package/dist/http-body-schemas.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/job-control.d.ts +57 -0
- package/dist/job-control.d.ts.map +1 -0
- package/dist/job-control.js +134 -0
- package/dist/job-control.js.map +1 -0
- package/dist/job-queue.d.ts +47 -0
- package/dist/job-queue.d.ts.map +1 -0
- package/dist/job-queue.js +52 -0
- package/dist/job-queue.js.map +1 -0
- package/dist/job-run-registry.d.ts +176 -0
- package/dist/job-run-registry.d.ts.map +1 -0
- package/dist/job-run-registry.js +312 -0
- package/dist/job-run-registry.js.map +1 -0
- package/dist/lib/app-build-service.d.ts +16 -0
- package/dist/lib/app-build-service.d.ts.map +1 -0
- package/dist/lib/app-build-service.js +39 -0
- package/dist/lib/app-build-service.js.map +1 -0
- package/dist/lib/app-debug-service.d.ts +16 -0
- package/dist/lib/app-debug-service.d.ts.map +1 -0
- package/dist/lib/app-debug-service.js +48 -0
- package/dist/lib/app-debug-service.js.map +1 -0
- package/dist/lib/app-deployment-service.d.ts +82 -0
- package/dist/lib/app-deployment-service.d.ts.map +1 -0
- package/dist/lib/app-deployment-service.js +282 -0
- package/dist/lib/app-deployment-service.js.map +1 -0
- package/dist/lib/app-run-server.d.ts +7 -0
- package/dist/lib/app-run-server.d.ts.map +1 -0
- package/dist/lib/app-run-server.js +6 -0
- package/dist/lib/app-run-server.js.map +1 -0
- package/dist/lib/app-session-scope.d.ts +91 -0
- package/dist/lib/app-session-scope.d.ts.map +1 -0
- package/dist/lib/app-session-scope.js +91 -0
- package/dist/lib/app-session-scope.js.map +1 -0
- package/dist/lib/applications-service.d.ts +173 -0
- package/dist/lib/applications-service.d.ts.map +1 -0
- package/dist/lib/applications-service.js +491 -0
- package/dist/lib/applications-service.js.map +1 -0
- package/dist/lib/asset-duration.d.ts +7 -0
- package/dist/lib/asset-duration.d.ts.map +1 -0
- package/dist/lib/asset-duration.js +24 -0
- package/dist/lib/asset-duration.js.map +1 -0
- package/dist/lib/asset-export.d.ts +34 -0
- package/dist/lib/asset-export.d.ts.map +1 -0
- package/dist/lib/asset-export.js +113 -0
- package/dist/lib/asset-export.js.map +1 -0
- package/dist/lib/asset-model-interface.d.ts +41 -0
- package/dist/lib/asset-model-interface.d.ts.map +1 -0
- package/dist/lib/asset-model-interface.js +74 -0
- package/dist/lib/asset-model-interface.js.map +1 -0
- package/dist/lib/asset-paths.d.ts +52 -0
- package/dist/lib/asset-paths.d.ts.map +1 -1
- package/dist/lib/asset-paths.js +145 -1
- package/dist/lib/asset-paths.js.map +1 -1
- package/dist/lib/asset-response.d.ts +12 -0
- package/dist/lib/asset-response.d.ts.map +1 -0
- package/dist/lib/asset-response.js +88 -0
- package/dist/lib/asset-response.js.map +1 -0
- package/dist/lib/audio-peaks.d.ts +23 -0
- package/dist/lib/audio-peaks.d.ts.map +1 -0
- package/dist/lib/audio-peaks.js +331 -0
- package/dist/lib/audio-peaks.js.map +1 -0
- package/dist/lib/bridge.d.ts +9 -1
- package/dist/lib/bridge.d.ts.map +1 -1
- package/dist/lib/bridge.js +27 -4
- package/dist/lib/bridge.js.map +1 -1
- package/dist/lib/compression.d.ts +3 -0
- package/dist/lib/compression.d.ts.map +1 -0
- package/dist/lib/compression.js +3 -0
- package/dist/lib/compression.js.map +1 -0
- package/dist/lib/document-model-interfaces.d.ts +30 -0
- package/dist/lib/document-model-interfaces.d.ts.map +1 -0
- package/dist/lib/document-model-interfaces.js +295 -0
- package/dist/lib/document-model-interfaces.js.map +1 -0
- package/dist/lib/env-config.d.ts +9 -0
- package/dist/lib/env-config.d.ts.map +1 -0
- package/dist/lib/env-config.js +18 -0
- package/dist/lib/env-config.js.map +1 -0
- package/dist/lib/example-apps.d.ts +43 -0
- package/dist/lib/example-apps.d.ts.map +1 -0
- package/dist/lib/example-apps.js +145 -0
- package/dist/lib/example-apps.js.map +1 -0
- package/dist/lib/example-games.d.ts +5 -0
- package/dist/lib/example-games.d.ts.map +1 -0
- package/dist/lib/example-games.js +13 -0
- package/dist/lib/example-games.js.map +1 -0
- package/dist/lib/example-recipes.d.ts +33 -0
- package/dist/lib/example-recipes.d.ts.map +1 -0
- package/dist/lib/example-recipes.js +213 -0
- package/dist/lib/example-recipes.js.map +1 -0
- package/dist/lib/example-storyboards.d.ts +40 -0
- package/dist/lib/example-storyboards.d.ts.map +1 -0
- package/dist/lib/example-storyboards.js +131 -0
- package/dist/lib/example-storyboards.js.map +1 -0
- package/dist/lib/example-thumbnail.d.ts +21 -0
- package/dist/lib/example-thumbnail.d.ts.map +1 -0
- package/dist/lib/example-thumbnail.js +50 -0
- package/dist/lib/example-thumbnail.js.map +1 -0
- package/dist/lib/example-timelines.d.ts +9 -0
- package/dist/lib/example-timelines.d.ts.map +1 -0
- package/dist/lib/example-timelines.js +25 -0
- package/dist/lib/example-timelines.js.map +1 -0
- package/dist/lib/extension-dist.d.ts +24 -0
- package/dist/lib/extension-dist.d.ts.map +1 -0
- package/dist/lib/extension-dist.js +67 -0
- package/dist/lib/extension-dist.js.map +1 -0
- package/dist/lib/external-asset-lookup.d.ts +30 -0
- package/dist/lib/external-asset-lookup.d.ts.map +1 -0
- package/dist/lib/external-asset-lookup.js +99 -0
- package/dist/lib/external-asset-lookup.js.map +1 -0
- package/dist/lib/external-assets.d.ts +13 -0
- package/dist/lib/external-assets.d.ts.map +1 -0
- package/dist/lib/external-assets.js +46 -0
- package/dist/lib/external-assets.js.map +1 -0
- package/dist/lib/fly-replay.d.ts +34 -0
- package/dist/lib/fly-replay.d.ts.map +1 -0
- package/dist/lib/fly-replay.js +111 -0
- package/dist/lib/fly-replay.js.map +1 -0
- package/dist/lib/http-rate-limit.d.ts +32 -0
- package/dist/lib/http-rate-limit.d.ts.map +1 -0
- package/dist/lib/http-rate-limit.js +47 -0
- package/dist/lib/http-rate-limit.js.map +1 -0
- package/dist/lib/instance-id.d.ts +25 -0
- package/dist/lib/instance-id.d.ts.map +1 -0
- package/dist/lib/instance-id.js +44 -0
- package/dist/lib/instance-id.js.map +1 -0
- package/dist/lib/link-codes.d.ts +82 -0
- package/dist/lib/link-codes.d.ts.map +1 -0
- package/dist/lib/link-codes.js +100 -0
- package/dist/lib/link-codes.js.map +1 -0
- package/dist/lib/local-asset-upload.d.ts +5 -0
- package/dist/lib/local-asset-upload.d.ts.map +1 -0
- package/dist/lib/local-asset-upload.js +69 -0
- package/dist/lib/local-asset-upload.js.map +1 -0
- package/dist/lib/local-file-access.d.ts +31 -0
- package/dist/lib/local-file-access.d.ts.map +1 -0
- package/dist/lib/local-file-access.js +169 -0
- package/dist/lib/local-file-access.js.map +1 -0
- package/dist/lib/localhost-trust.d.ts +59 -0
- package/dist/lib/localhost-trust.d.ts.map +1 -0
- package/dist/lib/localhost-trust.js +139 -0
- package/dist/lib/localhost-trust.js.map +1 -0
- package/dist/lib/mcp-mount.d.ts +25 -0
- package/dist/lib/mcp-mount.d.ts.map +1 -0
- package/dist/lib/mcp-mount.js +32 -0
- package/dist/lib/mcp-mount.js.map +1 -0
- package/dist/lib/media-size.d.ts +21 -0
- package/dist/lib/media-size.d.ts.map +1 -0
- package/dist/lib/media-size.js +48 -0
- package/dist/lib/media-size.js.map +1 -0
- package/dist/lib/media.d.ts +38 -0
- package/dist/lib/media.d.ts.map +1 -0
- package/dist/lib/media.js +146 -0
- package/dist/lib/media.js.map +1 -0
- package/dist/lib/package-asset-export.d.ts +83 -0
- package/dist/lib/package-asset-export.d.ts.map +1 -0
- package/dist/lib/package-asset-export.js +212 -0
- package/dist/lib/package-asset-export.js.map +1 -0
- package/dist/lib/package-assets.d.ts +41 -0
- package/dist/lib/package-assets.d.ts.map +1 -0
- package/dist/lib/package-assets.js +155 -0
- package/dist/lib/package-assets.js.map +1 -0
- package/dist/lib/project-delete.d.ts +11 -0
- package/dist/lib/project-delete.d.ts.map +1 -0
- package/dist/lib/project-delete.js +67 -0
- package/dist/lib/project-delete.js.map +1 -0
- package/dist/lib/project-document-copy.d.ts +31 -0
- package/dist/lib/project-document-copy.d.ts.map +1 -0
- package/dist/lib/project-document-copy.js +473 -0
- package/dist/lib/project-document-copy.js.map +1 -0
- package/dist/lib/public-app-assets.d.ts +8 -0
- package/dist/lib/public-app-assets.d.ts.map +1 -0
- package/dist/lib/public-app-assets.js +77 -0
- package/dist/lib/public-app-assets.js.map +1 -0
- package/dist/lib/public-routes.d.ts +95 -0
- package/dist/lib/public-routes.d.ts.map +1 -0
- package/dist/lib/public-routes.js +154 -0
- package/dist/lib/public-routes.js.map +1 -0
- package/dist/lib/runtime-detection.d.ts +17 -0
- package/dist/lib/runtime-detection.d.ts.map +1 -0
- package/dist/lib/runtime-detection.js +90 -0
- package/dist/lib/runtime-detection.js.map +1 -0
- package/dist/lib/storage-access.d.ts +21 -0
- package/dist/lib/storage-access.d.ts.map +1 -0
- package/dist/lib/storage-access.js +74 -0
- package/dist/lib/storage-access.js.map +1 -0
- package/dist/lib/storage.d.ts +9 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +28 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/storyboard-export.d.ts +64 -0
- package/dist/lib/storyboard-export.d.ts.map +1 -0
- package/dist/lib/storyboard-export.js +183 -0
- package/dist/lib/storyboard-export.js.map +1 -0
- package/dist/lib/style-presets.d.ts +34 -0
- package/dist/lib/style-presets.d.ts.map +1 -0
- package/dist/lib/style-presets.js +62 -0
- package/dist/lib/style-presets.js.map +1 -0
- package/dist/lib/temp-url-resolver.d.ts +35 -0
- package/dist/lib/temp-url-resolver.d.ts.map +1 -0
- package/dist/lib/temp-url-resolver.js +88 -0
- package/dist/lib/temp-url-resolver.js.map +1 -0
- package/dist/lib/thumbnail.d.ts +74 -0
- package/dist/lib/thumbnail.d.ts.map +1 -0
- package/dist/lib/thumbnail.js +302 -0
- package/dist/lib/thumbnail.js.map +1 -0
- package/dist/lib/timeline-bundle.d.ts +1689 -0
- package/dist/lib/timeline-bundle.d.ts.map +1 -0
- package/dist/lib/timeline-bundle.js +330 -0
- package/dist/lib/timeline-bundle.js.map +1 -0
- package/dist/lib/video-proxy.d.ts +114 -0
- package/dist/lib/video-proxy.d.ts.map +1 -0
- package/dist/lib/video-proxy.js +545 -0
- package/dist/lib/video-proxy.js.map +1 -0
- package/dist/lib/wire-values.d.ts +31 -0
- package/dist/lib/wire-values.d.ts.map +1 -0
- package/dist/lib/wire-values.js +49 -0
- package/dist/lib/wire-values.js.map +1 -0
- package/dist/lib/workflow-bundle.d.ts +102 -0
- package/dist/lib/workflow-bundle.d.ts.map +1 -0
- package/dist/lib/workflow-bundle.js +306 -0
- package/dist/lib/workflow-bundle.js.map +1 -0
- package/dist/lib/workflow-service.d.ts +82 -0
- package/dist/lib/workflow-service.d.ts.map +1 -0
- package/dist/lib/workflow-service.js +194 -0
- package/dist/lib/workflow-service.js.map +1 -0
- package/dist/lib/workflow-workspace.d.ts +20 -0
- package/dist/lib/workflow-workspace.d.ts.map +1 -0
- package/dist/lib/workflow-workspace.js +65 -0
- package/dist/lib/workflow-workspace.js.map +1 -0
- package/dist/lib/ws-connection-health.d.ts +10 -0
- package/dist/lib/ws-connection-health.d.ts.map +1 -0
- package/dist/lib/ws-connection-health.js +38 -0
- package/dist/lib/ws-connection-health.js.map +1 -0
- package/dist/lib/ws-rate-limit.d.ts +22 -0
- package/dist/lib/ws-rate-limit.d.ts.map +1 -0
- package/dist/lib/ws-rate-limit.js +45 -0
- package/dist/lib/ws-rate-limit.js.map +1 -0
- package/dist/lib/ws-upgrade.d.ts +40 -0
- package/dist/lib/ws-upgrade.d.ts.map +1 -0
- package/dist/lib/ws-upgrade.js +66 -0
- package/dist/lib/ws-upgrade.js.map +1 -0
- package/dist/lib/zip-stream.d.ts +46 -0
- package/dist/lib/zip-stream.d.ts.map +1 -0
- package/dist/lib/zip-stream.js +140 -0
- package/dist/lib/zip-stream.js.map +1 -0
- package/dist/mcp-agent-tools.d.ts +56 -0
- package/dist/mcp-agent-tools.d.ts.map +1 -0
- package/dist/mcp-agent-tools.js +1216 -0
- package/dist/mcp-agent-tools.js.map +1 -0
- package/dist/mcp-server.d.ts +56 -7
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +255 -762
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tool-deps.d.ts +20 -0
- package/dist/mcp-tool-deps.d.ts.map +1 -0
- package/dist/mcp-tool-deps.js +61 -0
- package/dist/mcp-tool-deps.js.map +1 -0
- package/dist/messagepack.d.ts +7 -0
- package/dist/messagepack.d.ts.map +1 -0
- package/dist/messagepack.js +19 -0
- package/dist/messagepack.js.map +1 -0
- package/dist/model-download-runtime.d.ts +9 -0
- package/dist/model-download-runtime.d.ts.map +1 -0
- package/dist/model-download-runtime.js +77 -0
- package/dist/model-download-runtime.js.map +1 -0
- package/dist/model-execution-availability.d.ts +14 -0
- package/dist/model-execution-availability.d.ts.map +1 -0
- package/dist/model-execution-availability.js +161 -0
- package/dist/model-execution-availability.js.map +1 -0
- package/dist/models-api.d.ts +51 -5
- package/dist/models-api.d.ts.map +1 -1
- package/dist/models-api.js +160 -690
- package/dist/models-api.js.map +1 -1
- package/dist/node-registry-setup.d.ts +84 -0
- package/dist/node-registry-setup.d.ts.map +1 -0
- package/dist/node-registry-setup.js +256 -0
- package/dist/node-registry-setup.js.map +1 -0
- package/dist/oauth/cimd.d.ts +28 -0
- package/dist/oauth/cimd.d.ts.map +1 -0
- package/dist/oauth/cimd.js +177 -0
- package/dist/oauth/cimd.js.map +1 -0
- package/dist/oauth/gate.d.ts +38 -0
- package/dist/oauth/gate.d.ts.map +1 -0
- package/dist/oauth/gate.js +77 -0
- package/dist/oauth/gate.js.map +1 -0
- package/dist/oauth/metadata.d.ts +35 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +56 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/oauth/pending-store.d.ts +79 -0
- package/dist/oauth/pending-store.d.ts.map +1 -0
- package/dist/oauth/pending-store.js +133 -0
- package/dist/oauth/pending-store.js.map +1 -0
- package/dist/oauth/pkce.d.ts +16 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +29 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/validate.d.ts +25 -0
- package/dist/oauth/validate.d.ts.map +1 -0
- package/dist/oauth/validate.js +62 -0
- package/dist/oauth/validate.js.map +1 -0
- package/dist/oauth/www-authenticate.d.ts +9 -0
- package/dist/oauth/www-authenticate.d.ts.map +1 -0
- package/dist/oauth/www-authenticate.js +33 -0
- package/dist/oauth/www-authenticate.js.map +1 -0
- package/dist/oauth-api.d.ts +19 -0
- package/dist/oauth-api.d.ts.map +1 -1
- package/dist/oauth-api.js +531 -12
- package/dist/oauth-api.js.map +1 -1
- package/dist/openai-api.d.ts.map +1 -1
- package/dist/openai-api.js +4 -9
- package/dist/openai-api.js.map +1 -1
- package/dist/pack-snapshot.d.ts +24 -0
- package/dist/pack-snapshot.d.ts.map +1 -0
- package/dist/pack-snapshot.js +35 -0
- package/dist/pack-snapshot.js.map +1 -0
- package/dist/plugins/websocket.d.ts +21 -4
- package/dist/plugins/websocket.d.ts.map +1 -1
- package/dist/plugins/websocket.js +136 -290
- package/dist/plugins/websocket.js.map +1 -1
- package/dist/resolve-media-urls.d.ts +17 -3
- package/dist/resolve-media-urls.d.ts.map +1 -1
- package/dist/resolve-media-urls.js +117 -31
- package/dist/resolve-media-urls.js.map +1 -1
- package/dist/resource-events.d.ts +28 -0
- package/dist/resource-events.d.ts.map +1 -0
- package/dist/resource-events.js +20 -0
- package/dist/resource-events.js.map +1 -0
- package/dist/routes/account.d.ts +100 -0
- package/dist/routes/account.d.ts.map +1 -0
- package/dist/routes/account.js +197 -0
- package/dist/routes/account.js.map +1 -0
- package/dist/routes/applications.d.ts +36 -0
- package/dist/routes/applications.d.ts.map +1 -0
- package/dist/routes/applications.js +183 -0
- package/dist/routes/applications.js.map +1 -0
- package/dist/routes/assets.d.ts +3 -3
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +78 -44
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/atlascloud-webhook.d.ts +93 -0
- package/dist/routes/atlascloud-webhook.d.ts.map +1 -0
- package/dist/routes/atlascloud-webhook.js +369 -0
- package/dist/routes/atlascloud-webhook.js.map +1 -0
- package/dist/routes/collections.d.ts.map +1 -1
- package/dist/routes/config.d.ts +26 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +64 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/documents.d.ts +27 -0
- package/dist/routes/documents.d.ts.map +1 -0
- package/dist/routes/documents.js +155 -0
- package/dist/routes/documents.js.map +1 -0
- package/dist/routes/fal-credits.d.ts +4 -0
- package/dist/routes/fal-credits.d.ts.map +1 -0
- package/dist/routes/fal-credits.js +101 -0
- package/dist/routes/fal-credits.js.map +1 -0
- package/dist/routes/fal-pricing-estimate.d.ts +4 -0
- package/dist/routes/fal-pricing-estimate.d.ts.map +1 -0
- package/dist/routes/fal-pricing-estimate.js +114 -0
- package/dist/routes/fal-pricing-estimate.js.map +1 -0
- package/dist/routes/fal-pricing.d.ts +4 -0
- package/dist/routes/fal-pricing.d.ts.map +1 -0
- package/dist/routes/fal-pricing.js +93 -0
- package/dist/routes/fal-pricing.js.map +1 -0
- package/dist/routes/fal-webhook.d.ts +102 -0
- package/dist/routes/fal-webhook.d.ts.map +1 -0
- package/dist/routes/fal-webhook.js +379 -0
- package/dist/routes/fal-webhook.js.map +1 -0
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +4 -3
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/health.d.ts +1 -0
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +41 -7
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/integrations.d.ts +39 -0
- package/dist/routes/integrations.d.ts.map +1 -0
- package/dist/routes/integrations.js +211 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/js-scripts.d.ts +26 -0
- package/dist/routes/js-scripts.d.ts.map +1 -0
- package/dist/routes/js-scripts.js +116 -0
- package/dist/routes/js-scripts.js.map +1 -0
- package/dist/routes/kie-credits.d.ts +4 -0
- package/dist/routes/kie-credits.d.ts.map +1 -0
- package/dist/routes/kie-credits.js +93 -0
- package/dist/routes/kie-credits.js.map +1 -0
- package/dist/routes/kie-pricing.d.ts +4 -0
- package/dist/routes/kie-pricing.d.ts.map +1 -0
- package/dist/routes/kie-pricing.js +45 -0
- package/dist/routes/kie-pricing.js.map +1 -0
- package/dist/routes/kie-webhook.d.ts +4 -0
- package/dist/routes/kie-webhook.d.ts.map +1 -0
- package/dist/routes/kie-webhook.js +70 -0
- package/dist/routes/kie-webhook.js.map +1 -0
- package/dist/routes/nodes.d.ts.map +1 -1
- package/dist/routes/nodes.js +6 -59
- package/dist/routes/nodes.js.map +1 -1
- package/dist/routes/oauth-as.d.ts +30 -0
- package/dist/routes/oauth-as.d.ts.map +1 -0
- package/dist/routes/oauth-as.js +412 -0
- package/dist/routes/oauth-as.js.map +1 -0
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/openai.d.ts.map +1 -1
- package/dist/routes/public-apps.d.ts +27 -0
- package/dist/routes/public-apps.d.ts.map +1 -0
- package/dist/routes/public-apps.js +99 -0
- package/dist/routes/public-apps.js.map +1 -0
- package/dist/routes/sandbox-modules.d.ts +17 -0
- package/dist/routes/sandbox-modules.d.ts.map +1 -0
- package/dist/routes/sandbox-modules.js +85 -0
- package/dist/routes/sandbox-modules.js.map +1 -0
- package/dist/routes/sdk-v1.d.ts +24 -0
- package/dist/routes/sdk-v1.d.ts.map +1 -0
- package/dist/routes/sdk-v1.js +320 -0
- package/dist/routes/sdk-v1.js.map +1 -0
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/storyboards.d.ts +16 -0
- package/dist/routes/storyboards.d.ts.map +1 -0
- package/dist/routes/storyboards.js +97 -0
- package/dist/routes/storyboards.js.map +1 -0
- package/dist/routes/timeline-animations.d.ts +23 -0
- package/dist/routes/timeline-animations.d.ts.map +1 -0
- package/dist/routes/timeline-animations.js +99 -0
- package/dist/routes/timeline-animations.js.map +1 -0
- package/dist/routes/timeline-audio-bake.d.ts +51 -0
- package/dist/routes/timeline-audio-bake.d.ts.map +1 -0
- package/dist/routes/timeline-audio-bake.js +110 -0
- package/dist/routes/timeline-audio-bake.js.map +1 -0
- package/dist/routes/timeline-fonts.d.ts +29 -0
- package/dist/routes/timeline-fonts.d.ts.map +1 -0
- package/dist/routes/timeline-fonts.js +64 -0
- package/dist/routes/timeline-fonts.js.map +1 -0
- package/dist/routes/timeline-isolate-subject.d.ts +55 -0
- package/dist/routes/timeline-isolate-subject.d.ts.map +1 -0
- package/dist/routes/timeline-isolate-subject.js +128 -0
- package/dist/routes/timeline-isolate-subject.js.map +1 -0
- package/dist/routes/timeline-track-object.d.ts +39 -0
- package/dist/routes/timeline-track-object.d.ts.map +1 -0
- package/dist/routes/timeline-track-object.js +97 -0
- package/dist/routes/timeline-track-object.js.map +1 -0
- package/dist/routes/timelines.d.ts +18 -0
- package/dist/routes/timelines.d.ts.map +1 -0
- package/dist/routes/timelines.js +191 -0
- package/dist/routes/timelines.js.map +1 -0
- package/dist/routes/workflows.d.ts +6 -9
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +100 -11
- package/dist/routes/workflows.js.map +1 -1
- package/dist/routes/workspace.d.ts.map +1 -1
- package/dist/run-supervisor.d.ts +30 -0
- package/dist/run-supervisor.d.ts.map +1 -0
- package/dist/run-supervisor.js +86 -0
- package/dist/run-supervisor.js.map +1 -0
- package/dist/sandbox-catalog.d.ts +28 -0
- package/dist/sandbox-catalog.d.ts.map +1 -0
- package/dist/sandbox-catalog.js +74 -0
- package/dist/sandbox-catalog.js.map +1 -0
- package/dist/screenshot-projects.d.ts +27 -0
- package/dist/screenshot-projects.d.ts.map +1 -0
- package/dist/screenshot-projects.js +880 -0
- package/dist/screenshot-projects.js.map +1 -0
- package/dist/screenshot-server.js +947 -43
- package/dist/screenshot-server.js.map +1 -1
- package/dist/sdk/sdk-cached-model-inventory.d.ts +21 -0
- package/dist/sdk/sdk-cached-model-inventory.d.ts.map +1 -0
- package/dist/sdk/sdk-cached-model-inventory.js +51 -0
- package/dist/sdk/sdk-cached-model-inventory.js.map +1 -0
- package/dist/sdk/sdk-capabilities-service.d.ts +32 -0
- package/dist/sdk/sdk-capabilities-service.d.ts.map +1 -0
- package/dist/sdk/sdk-capabilities-service.js +47 -0
- package/dist/sdk/sdk-capabilities-service.js.map +1 -0
- package/dist/sdk/sdk-execution-readiness-probe.d.ts +19 -0
- package/dist/sdk/sdk-execution-readiness-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-execution-readiness-probe.js +90 -0
- package/dist/sdk/sdk-execution-readiness-probe.js.map +1 -0
- package/dist/sdk/sdk-execution-target-readiness.d.ts +15 -0
- package/dist/sdk/sdk-execution-target-readiness.d.ts.map +1 -0
- package/dist/sdk/sdk-execution-target-readiness.js +50 -0
- package/dist/sdk/sdk-execution-target-readiness.js.map +1 -0
- package/dist/sdk/sdk-feature-flags.d.ts +18 -0
- package/dist/sdk/sdk-feature-flags.d.ts.map +1 -0
- package/dist/sdk/sdk-feature-flags.js +30 -0
- package/dist/sdk/sdk-feature-flags.js.map +1 -0
- package/dist/sdk/sdk-huggingface-download-state.d.ts +15 -0
- package/dist/sdk/sdk-huggingface-download-state.d.ts.map +1 -0
- package/dist/sdk/sdk-huggingface-download-state.js +25 -0
- package/dist/sdk/sdk-huggingface-download-state.js.map +1 -0
- package/dist/sdk/sdk-live-runner-registry.d.ts +21 -0
- package/dist/sdk/sdk-live-runner-registry.d.ts.map +1 -0
- package/dist/sdk/sdk-live-runner-registry.js +26 -0
- package/dist/sdk/sdk-live-runner-registry.js.map +1 -0
- package/dist/sdk/sdk-model-catalog-service.d.ts +20 -0
- package/dist/sdk/sdk-model-catalog-service.d.ts.map +1 -0
- package/dist/sdk/sdk-model-catalog-service.js +219 -0
- package/dist/sdk/sdk-model-catalog-service.js.map +1 -0
- package/dist/sdk/sdk-model-download-service.d.ts +39 -0
- package/dist/sdk/sdk-model-download-service.d.ts.map +1 -0
- package/dist/sdk/sdk-model-download-service.js +274 -0
- package/dist/sdk/sdk-model-download-service.js.map +1 -0
- package/dist/sdk/sdk-model-requirement-probe.d.ts +30 -0
- package/dist/sdk/sdk-model-requirement-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-model-requirement-probe.js +120 -0
- package/dist/sdk/sdk-model-requirement-probe.js.map +1 -0
- package/dist/sdk/sdk-node-package-requirement-probe.d.ts +20 -0
- package/dist/sdk/sdk-node-package-requirement-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-node-package-requirement-probe.js +44 -0
- package/dist/sdk/sdk-node-package-requirement-probe.js.map +1 -0
- package/dist/sdk/sdk-node-type-inventory-service.d.ts +11 -0
- package/dist/sdk/sdk-node-type-inventory-service.d.ts.map +1 -0
- package/dist/sdk/sdk-node-type-inventory-service.js +36 -0
- package/dist/sdk/sdk-node-type-inventory-service.js.map +1 -0
- package/dist/sdk/sdk-preflight-orchestrator.d.ts +54 -0
- package/dist/sdk/sdk-preflight-orchestrator.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-orchestrator.js +63 -0
- package/dist/sdk/sdk-preflight-orchestrator.js.map +1 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.d.ts +35 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.js +67 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.js.map +1 -0
- package/dist/sdk/sdk-preflight-service.d.ts +61 -0
- package/dist/sdk/sdk-preflight-service.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-service.js +104 -0
- package/dist/sdk/sdk-preflight-service.js.map +1 -0
- package/dist/sdk/sdk-preflight-workflow-source.d.ts +18 -0
- package/dist/sdk/sdk-preflight-workflow-source.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-workflow-source.js +42 -0
- package/dist/sdk/sdk-preflight-workflow-source.js.map +1 -0
- package/dist/sdk/sdk-route-policy.d.ts +3 -0
- package/dist/sdk/sdk-route-policy.d.ts.map +1 -0
- package/dist/sdk/sdk-route-policy.js +18 -0
- package/dist/sdk/sdk-route-policy.js.map +1 -0
- package/dist/sdk/sdk-runtime-capabilities-service.d.ts +60 -0
- package/dist/sdk/sdk-runtime-capabilities-service.d.ts.map +1 -0
- package/dist/sdk/sdk-runtime-capabilities-service.js +30 -0
- package/dist/sdk/sdk-runtime-capabilities-service.js.map +1 -0
- package/dist/sdk/sdk-runtime-requirement-probe.d.ts +13 -0
- package/dist/sdk/sdk-runtime-requirement-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-runtime-requirement-probe.js +55 -0
- package/dist/sdk/sdk-runtime-requirement-probe.js.map +1 -0
- package/dist/sdk/sdk-static-preflight-service.d.ts +66 -0
- package/dist/sdk/sdk-static-preflight-service.d.ts.map +1 -0
- package/dist/sdk/sdk-static-preflight-service.js +492 -0
- package/dist/sdk/sdk-static-preflight-service.js.map +1 -0
- package/dist/sdk/sdk-temporary-asset-service.d.ts +18 -0
- package/dist/sdk/sdk-temporary-asset-service.d.ts.map +1 -0
- package/dist/sdk/sdk-temporary-asset-service.js +33 -0
- package/dist/sdk/sdk-temporary-asset-service.js.map +1 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.d.ts +9 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.d.ts.map +1 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.js +65 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.js.map +1 -0
- package/dist/sdk/sdk-v1-handler-map.d.ts +44 -0
- package/dist/sdk/sdk-v1-handler-map.d.ts.map +1 -0
- package/dist/sdk/sdk-v1-handler-map.js +37 -0
- package/dist/sdk/sdk-v1-handler-map.js.map +1 -0
- package/dist/sdk/sdk-v1-service-error.d.ts +21 -0
- package/dist/sdk/sdk-v1-service-error.d.ts.map +1 -0
- package/dist/sdk/sdk-v1-service-error.js +62 -0
- package/dist/sdk/sdk-v1-service-error.js.map +1 -0
- package/dist/sdk/sdk-v1-service.d.ts +74 -0
- package/dist/sdk/sdk-v1-service.d.ts.map +1 -0
- package/dist/sdk/sdk-v1-service.js +219 -0
- package/dist/sdk/sdk-v1-service.js.map +1 -0
- package/dist/sdk/sdk-worker-readiness-adapter.d.ts +22 -0
- package/dist/sdk/sdk-worker-readiness-adapter.d.ts.map +1 -0
- package/dist/sdk/sdk-worker-readiness-adapter.js +29 -0
- package/dist/sdk/sdk-worker-readiness-adapter.js.map +1 -0
- package/dist/server.js +1019 -201
- package/dist/server.js.map +1 -1
- package/dist/session/asset-autosave.d.ts +80 -0
- package/dist/session/asset-autosave.d.ts.map +1 -0
- package/dist/session/asset-autosave.js +456 -0
- package/dist/session/asset-autosave.js.map +1 -0
- package/dist/session/chat-compaction.d.ts +107 -0
- package/dist/session/chat-compaction.d.ts.map +1 -0
- package/dist/session/chat-compaction.js +191 -0
- package/dist/session/chat-compaction.js.map +1 -0
- package/dist/session/chat-history.d.ts +75 -0
- package/dist/session/chat-history.d.ts.map +1 -0
- package/dist/session/chat-history.js +194 -0
- package/dist/session/chat-history.js.map +1 -0
- package/dist/session/chat-prompt.d.ts +45 -0
- package/dist/session/chat-prompt.d.ts.map +1 -0
- package/dist/session/chat-prompt.js +480 -0
- package/dist/session/chat-prompt.js.map +1 -0
- package/dist/session/chat-turn.d.ts +336 -0
- package/dist/session/chat-turn.d.ts.map +1 -0
- package/dist/session/chat-turn.js +3516 -0
- package/dist/session/chat-turn.js.map +1 -0
- package/dist/session/client-session.d.ts +59 -0
- package/dist/session/client-session.d.ts.map +1 -0
- package/dist/session/client-session.js +2 -0
- package/dist/session/client-session.js.map +1 -0
- package/dist/session/commands.d.ts +133 -0
- package/dist/session/commands.d.ts.map +1 -0
- package/dist/session/commands.js +778 -0
- package/dist/session/commands.js.map +1 -0
- package/dist/session/inference.d.ts +213 -0
- package/dist/session/inference.d.ts.map +1 -0
- package/dist/session/inference.js +1072 -0
- package/dist/session/inference.js.map +1 -0
- package/dist/session/job-execution.d.ts +477 -0
- package/dist/session/job-execution.d.ts.map +1 -0
- package/dist/session/job-execution.js +2089 -0
- package/dist/session/job-execution.js.map +1 -0
- package/dist/session/media-generation.d.ts +94 -0
- package/dist/session/media-generation.d.ts.map +1 -0
- package/dist/session/media-generation.js +234 -0
- package/dist/session/media-generation.js.map +1 -0
- package/dist/session/model-interfaces.d.ts +27 -0
- package/dist/session/model-interfaces.d.ts.map +1 -0
- package/dist/session/model-interfaces.js +132 -0
- package/dist/session/model-interfaces.js.map +1 -0
- package/dist/session/sanitize.d.ts +7 -0
- package/dist/session/sanitize.d.ts.map +1 -0
- package/dist/session/sanitize.js +64 -0
- package/dist/session/sanitize.js.map +1 -0
- package/dist/settings-registry.d.ts +6 -15
- package/dist/settings-registry.d.ts.map +1 -1
- package/dist/settings-registry.js +6 -103
- package/dist/settings-registry.js.map +1 -1
- package/dist/storage-api.d.ts +20 -0
- package/dist/storage-api.d.ts.map +1 -1
- package/dist/storage-api.js +260 -77
- package/dist/storage-api.js.map +1 -1
- package/dist/storage-retention.d.ts +9 -0
- package/dist/storage-retention.d.ts.map +1 -0
- package/dist/storage-retention.js +74 -0
- package/dist/storage-retention.js.map +1 -0
- package/dist/system-stats.d.ts +52 -0
- package/dist/system-stats.d.ts.map +1 -0
- package/dist/system-stats.js +134 -0
- package/dist/system-stats.js.map +1 -0
- package/dist/test-ui-server.d.ts +46 -0
- package/dist/test-ui-server.d.ts.map +1 -1
- package/dist/test-ui-server.js +188 -41
- package/dist/test-ui-server.js.map +1 -1
- package/dist/triggers/boot.d.ts +73 -0
- package/dist/triggers/boot.d.ts.map +1 -0
- package/dist/triggers/boot.js +149 -0
- package/dist/triggers/boot.js.map +1 -0
- package/dist/triggers/dispatcher.d.ts +234 -0
- package/dist/triggers/dispatcher.d.ts.map +1 -0
- package/dist/triggers/dispatcher.js +525 -0
- package/dist/triggers/dispatcher.js.map +1 -0
- package/dist/triggers/file-watch.d.ts +90 -0
- package/dist/triggers/file-watch.d.ts.map +1 -0
- package/dist/triggers/file-watch.js +405 -0
- package/dist/triggers/file-watch.js.map +1 -0
- package/dist/triggers/registration-sync.d.ts +34 -0
- package/dist/triggers/registration-sync.d.ts.map +1 -0
- package/dist/triggers/registration-sync.js +162 -0
- package/dist/triggers/registration-sync.js.map +1 -0
- package/dist/triggers/schedule-timing.d.ts +52 -0
- package/dist/triggers/schedule-timing.d.ts.map +1 -0
- package/dist/triggers/schedule-timing.js +90 -0
- package/dist/triggers/schedule-timing.js.map +1 -0
- package/dist/triggers/scheduler.d.ts +55 -0
- package/dist/triggers/scheduler.d.ts.map +1 -0
- package/dist/triggers/scheduler.js +122 -0
- package/dist/triggers/scheduler.js.map +1 -0
- package/dist/triggers/settle.d.ts +66 -0
- package/dist/triggers/settle.d.ts.map +1 -0
- package/dist/triggers/settle.js +99 -0
- package/dist/triggers/settle.js.map +1 -0
- package/dist/triggers/stores.d.ts +38 -0
- package/dist/triggers/stores.d.ts.map +1 -0
- package/dist/triggers/stores.js +168 -0
- package/dist/triggers/stores.js.map +1 -0
- package/dist/triggers/webhook-route.d.ts +49 -0
- package/dist/triggers/webhook-route.d.ts.map +1 -0
- package/dist/triggers/webhook-route.js +148 -0
- package/dist/triggers/webhook-route.js.map +1 -0
- package/dist/trpc/context.d.ts +52 -5
- package/dist/trpc/context.d.ts.map +1 -1
- package/dist/trpc/context.js +4 -1
- package/dist/trpc/context.js.map +1 -1
- package/dist/trpc/error-formatter.d.ts.map +1 -1
- package/dist/trpc/error-formatter.js +23 -2
- package/dist/trpc/error-formatter.js.map +1 -1
- package/dist/trpc/error-logging.d.ts +14 -0
- package/dist/trpc/error-logging.d.ts.map +1 -0
- package/dist/trpc/error-logging.js +27 -0
- package/dist/trpc/error-logging.js.map +1 -0
- package/dist/trpc/index.d.ts +2 -2
- package/dist/trpc/index.d.ts.map +1 -1
- package/dist/trpc/index.js +0 -2
- package/dist/trpc/index.js.map +1 -1
- package/dist/trpc/middleware.d.ts +6 -3
- package/dist/trpc/middleware.d.ts.map +1 -1
- package/dist/trpc/router.d.ts +29791 -1303
- package/dist/trpc/router.d.ts.map +1 -1
- package/dist/trpc/router.js +46 -3
- package/dist/trpc/router.js.map +1 -1
- package/dist/trpc/routers/agent-access.d.ts +149 -0
- package/dist/trpc/routers/agent-access.d.ts.map +1 -0
- package/dist/trpc/routers/agent-access.js +224 -0
- package/dist/trpc/routers/agent-access.js.map +1 -0
- package/dist/trpc/routers/applications.d.ts +1117 -0
- package/dist/trpc/routers/applications.d.ts.map +1 -0
- package/dist/trpc/routers/applications.js +218 -0
- package/dist/trpc/routers/applications.js.map +1 -0
- package/dist/trpc/routers/assets.d.ts +184 -42
- package/dist/trpc/routers/assets.d.ts.map +1 -1
- package/dist/trpc/routers/assets.js +499 -105
- package/dist/trpc/routers/assets.js.map +1 -1
- package/dist/trpc/routers/code-gen.d.ts +99 -0
- package/dist/trpc/routers/code-gen.d.ts.map +1 -0
- package/dist/trpc/routers/code-gen.js +164 -0
- package/dist/trpc/routers/code-gen.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts +1 -26
- package/dist/trpc/routers/collections.d.ts.map +1 -1
- package/dist/trpc/routers/collections.js +145 -78
- package/dist/trpc/routers/collections.js.map +1 -1
- package/dist/trpc/routers/costs.d.ts +40 -96
- package/dist/trpc/routers/costs.d.ts.map +1 -1
- package/dist/trpc/routers/costs.js +14 -89
- package/dist/trpc/routers/costs.js.map +1 -1
- package/dist/trpc/routers/credits.d.ts +110 -0
- package/dist/trpc/routers/credits.d.ts.map +1 -0
- package/dist/trpc/routers/credits.js +69 -0
- package/dist/trpc/routers/credits.js.map +1 -0
- package/dist/trpc/routers/custom-providers.d.ts +88 -0
- package/dist/trpc/routers/custom-providers.d.ts.map +1 -0
- package/dist/trpc/routers/custom-providers.js +140 -0
- package/dist/trpc/routers/custom-providers.js.map +1 -0
- package/dist/trpc/routers/documents.d.ts +61 -0
- package/dist/trpc/routers/documents.d.ts.map +1 -0
- package/dist/trpc/routers/documents.js +23 -0
- package/dist/trpc/routers/documents.js.map +1 -0
- package/dist/trpc/routers/extension.d.ts +24 -0
- package/dist/trpc/routers/extension.d.ts.map +1 -0
- package/dist/trpc/routers/extension.js +28 -0
- package/dist/trpc/routers/extension.js.map +1 -0
- package/dist/trpc/routers/external-mcp.d.ts +108 -0
- package/dist/trpc/routers/external-mcp.d.ts.map +1 -0
- package/dist/trpc/routers/external-mcp.js +70 -0
- package/dist/trpc/routers/external-mcp.js.map +1 -0
- package/dist/trpc/routers/files.d.ts +9 -2
- package/dist/trpc/routers/files.d.ts.map +1 -1
- package/dist/trpc/routers/files.js +67 -37
- package/dist/trpc/routers/files.js.map +1 -1
- package/dist/trpc/routers/fonts.d.ts +30 -0
- package/dist/trpc/routers/fonts.d.ts.map +1 -0
- package/dist/trpc/routers/fonts.js +112 -0
- package/dist/trpc/routers/fonts.js.map +1 -0
- package/dist/trpc/routers/games.d.ts +7610 -0
- package/dist/trpc/routers/games.d.ts.map +1 -0
- package/dist/trpc/routers/games.js +456 -0
- package/dist/trpc/routers/games.js.map +1 -0
- package/dist/trpc/routers/integrations.d.ts +86 -0
- package/dist/trpc/routers/integrations.d.ts.map +1 -0
- package/dist/trpc/routers/integrations.js +129 -0
- package/dist/trpc/routers/integrations.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +88 -27
- package/dist/trpc/routers/jobs.d.ts.map +1 -1
- package/dist/trpc/routers/jobs.js +113 -35
- package/dist/trpc/routers/jobs.js.map +1 -1
- package/dist/trpc/routers/js-scripts.d.ts +410 -0
- package/dist/trpc/routers/js-scripts.d.ts.map +1 -0
- package/dist/trpc/routers/js-scripts.js +303 -0
- package/dist/trpc/routers/js-scripts.js.map +1 -0
- package/dist/trpc/routers/mcp-config.d.ts +1 -1
- package/dist/trpc/routers/mcp-config.js +5 -3
- package/dist/trpc/routers/mcp-config.js.map +1 -1
- package/dist/trpc/routers/memories.d.ts +79 -0
- package/dist/trpc/routers/memories.d.ts.map +1 -0
- package/dist/trpc/routers/memories.js +66 -0
- package/dist/trpc/routers/memories.js.map +1 -0
- package/dist/trpc/routers/messages.d.ts +4 -77
- package/dist/trpc/routers/messages.d.ts.map +1 -1
- package/dist/trpc/routers/messages.js +9 -58
- package/dist/trpc/routers/messages.js.map +1 -1
- package/dist/trpc/routers/models.d.ts +1403 -238
- package/dist/trpc/routers/models.d.ts.map +1 -1
- package/dist/trpc/routers/models.js +532 -294
- package/dist/trpc/routers/models.js.map +1 -1
- package/dist/trpc/routers/nodes.d.ts +32 -16
- package/dist/trpc/routers/nodes.d.ts.map +1 -1
- package/dist/trpc/routers/nodes.js +70 -31
- package/dist/trpc/routers/nodes.js.map +1 -1
- package/dist/trpc/routers/packs.d.ts +184 -0
- package/dist/trpc/routers/packs.d.ts.map +1 -0
- package/dist/trpc/routers/packs.js +259 -0
- package/dist/trpc/routers/packs.js.map +1 -0
- package/dist/trpc/routers/projects.d.ts +422 -0
- package/dist/trpc/routers/projects.d.ts.map +1 -0
- package/dist/trpc/routers/projects.js +373 -0
- package/dist/trpc/routers/projects.js.map +1 -0
- package/dist/trpc/routers/resources.d.ts +120 -0
- package/dist/trpc/routers/resources.d.ts.map +1 -0
- package/dist/trpc/routers/resources.js +212 -0
- package/dist/trpc/routers/resources.js.map +1 -0
- package/dist/trpc/routers/scripts.d.ts +1116 -0
- package/dist/trpc/routers/scripts.d.ts.map +1 -0
- package/dist/trpc/routers/scripts.js +131 -0
- package/dist/trpc/routers/scripts.js.map +1 -0
- package/dist/trpc/routers/segmentation.d.ts +62 -0
- package/dist/trpc/routers/segmentation.d.ts.map +1 -0
- package/dist/trpc/routers/segmentation.js +146 -0
- package/dist/trpc/routers/segmentation.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +177 -10
- package/dist/trpc/routers/settings.d.ts.map +1 -1
- package/dist/trpc/routers/settings.js +123 -33
- package/dist/trpc/routers/settings.js.map +1 -1
- package/dist/trpc/routers/sketch.d.ts +854 -0
- package/dist/trpc/routers/sketch.d.ts.map +1 -0
- package/dist/trpc/routers/sketch.js +617 -0
- package/dist/trpc/routers/sketch.js.map +1 -0
- package/dist/trpc/routers/skills.d.ts +76 -22
- package/dist/trpc/routers/skills.d.ts.map +1 -1
- package/dist/trpc/routers/skills.js +195 -170
- package/dist/trpc/routers/skills.js.map +1 -1
- package/dist/trpc/routers/storage.d.ts +3 -30
- package/dist/trpc/routers/storage.d.ts.map +1 -1
- package/dist/trpc/routers/storage.js +60 -117
- package/dist/trpc/routers/storage.js.map +1 -1
- package/dist/trpc/routers/storyboards.d.ts +5907 -0
- package/dist/trpc/routers/storyboards.d.ts.map +1 -0
- package/dist/trpc/routers/storyboards.js +156 -0
- package/dist/trpc/routers/storyboards.js.map +1 -0
- package/dist/trpc/routers/threads.d.ts +33 -21
- package/dist/trpc/routers/threads.d.ts.map +1 -1
- package/dist/trpc/routers/threads.js +36 -30
- package/dist/trpc/routers/threads.js.map +1 -1
- package/dist/trpc/routers/timeline.d.ts +8516 -0
- package/dist/trpc/routers/timeline.d.ts.map +1 -0
- package/dist/trpc/routers/timeline.js +493 -0
- package/dist/trpc/routers/timeline.js.map +1 -0
- package/dist/trpc/routers/triggers.d.ts +46 -0
- package/dist/trpc/routers/triggers.d.ts.map +1 -0
- package/dist/trpc/routers/triggers.js +117 -0
- package/dist/trpc/routers/triggers.js.map +1 -0
- package/dist/trpc/routers/users.d.ts +1 -14
- package/dist/trpc/routers/users.d.ts.map +1 -1
- package/dist/trpc/routers/users.js +1 -12
- package/dist/trpc/routers/users.js.map +1 -1
- package/dist/trpc/routers/worker.d.ts +242 -0
- package/dist/trpc/routers/worker.d.ts.map +1 -0
- package/dist/trpc/routers/worker.js +220 -0
- package/dist/trpc/routers/worker.js.map +1 -0
- package/dist/trpc/routers/workflows.d.ts +385 -197
- package/dist/trpc/routers/workflows.d.ts.map +1 -1
- package/dist/trpc/routers/workflows.js +326 -345
- package/dist/trpc/routers/workflows.js.map +1 -1
- package/dist/trpc/routers/workspace.d.ts +59 -33
- package/dist/trpc/routers/workspace.d.ts.map +1 -1
- package/dist/trpc/routers/workspace.js +230 -93
- package/dist/trpc/routers/workspace.js.map +1 -1
- package/dist/trpc/sandbox-coverage.d.ts +64 -0
- package/dist/trpc/sandbox-coverage.d.ts.map +1 -0
- package/dist/trpc/sandbox-coverage.js +1049 -0
- package/dist/trpc/sandbox-coverage.js.map +1 -0
- package/dist/websocket-client-session.d.ts +320 -0
- package/dist/websocket-client-session.d.ts.map +1 -0
- package/dist/websocket-client-session.js +1253 -0
- package/dist/websocket-client-session.js.map +1 -0
- package/dist/workflow-interface-service.d.ts +87 -0
- package/dist/workflow-interface-service.d.ts.map +1 -0
- package/dist/workflow-interface-service.js +131 -0
- package/dist/workflow-interface-service.js.map +1 -0
- package/dist/workspace-api.d.ts.map +1 -1
- package/dist/workspace-api.js +33 -23
- package/dist/workspace-api.js.map +1 -1
- package/dist/ws-adapter.d.ts +46 -3
- package/dist/ws-adapter.d.ts.map +1 -1
- package/dist/ws-adapter.js +164 -26
- package/dist/ws-adapter.js.map +1 -1
- package/package.json +81 -35
- package/dist/agent/agent-runtime.d.ts +0 -47
- package/dist/agent/agent-runtime.d.ts.map +0 -1
- package/dist/agent/agent-runtime.js +0 -579
- package/dist/agent/agent-runtime.js.map +0 -1
- package/dist/agent/codex-agent.d.ts +0 -41
- package/dist/agent/codex-agent.d.ts.map +0 -1
- package/dist/agent/codex-agent.js +0 -334
- package/dist/agent/codex-agent.js.map +0 -1
- package/dist/agent/index.d.ts +0 -6
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -4
- package/dist/agent/index.js.map +0 -1
- package/dist/agent/llm-agent.d.ts +0 -51
- package/dist/agent/llm-agent.d.ts.map +0 -1
- package/dist/agent/llm-agent.js +0 -526
- package/dist/agent/llm-agent.js.map +0 -1
- package/dist/agent/mcp-tool-server.d.ts +0 -33
- package/dist/agent/mcp-tool-server.d.ts.map +0 -1
- package/dist/agent/mcp-tool-server.js +0 -292
- package/dist/agent/mcp-tool-server.js.map +0 -1
- package/dist/agent/opencode-agent.d.ts +0 -32
- package/dist/agent/opencode-agent.d.ts.map +0 -1
- package/dist/agent/opencode-agent.js +0 -332
- package/dist/agent/opencode-agent.js.map +0 -1
- package/dist/agent/pi-agent.d.ts +0 -42
- package/dist/agent/pi-agent.d.ts.map +0 -1
- package/dist/agent/pi-agent.js +0 -429
- package/dist/agent/pi-agent.js.map +0 -1
- package/dist/agent/sdk-provider.d.ts +0 -46
- package/dist/agent/sdk-provider.d.ts.map +0 -1
- package/dist/agent/sdk-provider.js +0 -50
- package/dist/agent/sdk-provider.js.map +0 -1
- package/dist/agent/socket-route.d.ts +0 -11
- package/dist/agent/socket-route.d.ts.map +0 -1
- package/dist/agent/socket-route.js +0 -262
- package/dist/agent/socket-route.js.map +0 -1
- package/dist/agent/transport.d.ts +0 -39
- package/dist/agent/transport.d.ts.map +0 -1
- package/dist/agent/transport.js +0 -17
- package/dist/agent/transport.js.map +0 -1
- package/dist/agent/types.d.ts +0 -11
- package/dist/agent/types.d.ts.map +0 -1
- package/dist/agent/types.js +0 -10
- package/dist/agent/types.js.map +0 -1
- package/dist/routes/jobs.d.ts +0 -16
- package/dist/routes/jobs.d.ts.map +0 -1
- package/dist/routes/jobs.js +0 -25
- package/dist/routes/jobs.js.map +0 -1
- package/dist/trpc/routers/sandboxes.d.ts +0 -64
- package/dist/trpc/routers/sandboxes.d.ts.map +0 -1
- package/dist/trpc/routers/sandboxes.js +0 -327
- package/dist/trpc/routers/sandboxes.js.map +0 -1
- package/dist/unified-websocket-runner.d.ts +0 -280
- package/dist/unified-websocket-runner.d.ts.map +0 -1
- package/dist/unified-websocket-runner.js +0 -3840
- package/dist/unified-websocket-runner.js.map +0 -1
|
@@ -1,3840 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { getSecret } from "@nodetool-ai/models";
|
|
5
|
-
import { getSetting } from "./settings-registry.js";
|
|
6
|
-
import { pack, unpack } from "msgpackr";
|
|
7
|
-
import { createLogger, getDefaultAssetsPath, buildAssetUrl } from "@nodetool-ai/config";
|
|
8
|
-
import { resolveContentUrls, resolveContentForProvider } from "./resolve-media-urls.js";
|
|
9
|
-
import { Graph, WorkflowRunner } from "@nodetool-ai/kernel";
|
|
10
|
-
import { Asset, Job, Message, ModelObserver, Prediction, Thread, Workflow } from "@nodetool-ai/models";
|
|
11
|
-
import { FileStorageAdapter, ProcessingContext as RuntimeProcessingContext, executeComfy } from "@nodetool-ai/runtime";
|
|
12
|
-
import { Tool } from "@nodetool-ai/agents";
|
|
13
|
-
const log = createLogger("nodetool.websocket.runner");
|
|
14
|
-
const DATA_URI_PATTERN = /data:([^;,]+)?;base64,[A-Za-z0-9+/=\r\n]+/gi;
|
|
15
|
-
const MAX_ERROR_TEXT_LENGTH = 4000;
|
|
16
|
-
/**
|
|
17
|
-
* Return `true` when the given http(s) URL appears to point at a public
|
|
18
|
-
* destination (not a loopback, link-local, or RFC1918 private address).
|
|
19
|
-
*
|
|
20
|
-
* Used before `fetch`ing URLs supplied by chat clients to resolve source
|
|
21
|
-
* images — without this gate, an authenticated user could coerce the server
|
|
22
|
-
* into reading internal services via `http://169.254.169.254/...`,
|
|
23
|
-
* `http://localhost:6379/...`, etc. The check is conservative: unparseable
|
|
24
|
-
* URLs and literal IP addresses in private ranges are refused. DNS-based
|
|
25
|
-
* bypass is still possible, so this is a defense-in-depth measure and not a
|
|
26
|
-
* full SSRF mitigation; intended for complementing network-level egress
|
|
27
|
-
* filtering.
|
|
28
|
-
*/
|
|
29
|
-
function isSafeExternalUrl(url) {
|
|
30
|
-
let parsed;
|
|
31
|
-
try {
|
|
32
|
-
parsed = new URL(url);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
// Normalise IPv6 hostnames: WHATWG URL may return them with or without
|
|
41
|
-
// surrounding brackets depending on the runtime.
|
|
42
|
-
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
43
|
-
if (!host)
|
|
44
|
-
return false;
|
|
45
|
-
if (host === "localhost" ||
|
|
46
|
-
host === "ip6-localhost" ||
|
|
47
|
-
host === "ip6-loopback" ||
|
|
48
|
-
host.endsWith(".localhost") ||
|
|
49
|
-
host.endsWith(".local") ||
|
|
50
|
-
host.endsWith(".internal")) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
// IPv4 literal check
|
|
54
|
-
const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
55
|
-
if (ipv4) {
|
|
56
|
-
const [a, b] = ipv4.slice(1).map((n) => parseInt(n, 10));
|
|
57
|
-
// 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16,
|
|
58
|
-
// 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 (CGNAT)
|
|
59
|
-
if (a === 0 || a === 10 || a === 127)
|
|
60
|
-
return false;
|
|
61
|
-
if (a === 169 && b === 254)
|
|
62
|
-
return false;
|
|
63
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
64
|
-
return false;
|
|
65
|
-
if (a === 192 && b === 168)
|
|
66
|
-
return false;
|
|
67
|
-
if (a === 100 && b >= 64 && b <= 127)
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
// IPv6 literal — refuse loopback / ULA / link-local / unspecified ranges.
|
|
71
|
-
if (host.includes(":")) {
|
|
72
|
-
if (host === "::" || host === "::1")
|
|
73
|
-
return false;
|
|
74
|
-
if (host.startsWith("fc") || host.startsWith("fd"))
|
|
75
|
-
return false; // ULA fc00::/7
|
|
76
|
-
if (host.startsWith("fe80:"))
|
|
77
|
-
return false; // link-local
|
|
78
|
-
}
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
function sanitizeLargeText(text, maxLength = MAX_ERROR_TEXT_LENGTH) {
|
|
82
|
-
const sanitized = text.replace(DATA_URI_PATTERN, (match, mimeType) => {
|
|
83
|
-
const mime = typeof mimeType === "string" && mimeType !== "" ? mimeType : "data";
|
|
84
|
-
return `[${mime} base64 omitted, ${match.length} chars]`;
|
|
85
|
-
});
|
|
86
|
-
if (sanitized.length <= maxLength) {
|
|
87
|
-
return sanitized;
|
|
88
|
-
}
|
|
89
|
-
const truncatedChars = sanitized.length - maxLength;
|
|
90
|
-
return `${sanitized.slice(0, maxLength)}... (truncated ${truncatedChars} chars)`;
|
|
91
|
-
}
|
|
92
|
-
function sanitizeErrorValue(value, seen = new WeakSet()) {
|
|
93
|
-
if (typeof value === "string") {
|
|
94
|
-
return sanitizeLargeText(value);
|
|
95
|
-
}
|
|
96
|
-
if (value instanceof Error) {
|
|
97
|
-
return sanitizeLargeText(value.message);
|
|
98
|
-
}
|
|
99
|
-
if (Array.isArray(value)) {
|
|
100
|
-
return value.map((item) => sanitizeErrorValue(item, seen));
|
|
101
|
-
}
|
|
102
|
-
if (value && typeof value === "object") {
|
|
103
|
-
if (seen.has(value)) {
|
|
104
|
-
return "[circular]";
|
|
105
|
-
}
|
|
106
|
-
seen.add(value);
|
|
107
|
-
const result = {};
|
|
108
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
109
|
-
result[key] = sanitizeErrorValue(nested, seen);
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
function formatSanitizedError(error) {
|
|
116
|
-
if (typeof error === "string") {
|
|
117
|
-
return sanitizeLargeText(error);
|
|
118
|
-
}
|
|
119
|
-
if (error instanceof Error) {
|
|
120
|
-
return sanitizeLargeText(error.message);
|
|
121
|
-
}
|
|
122
|
-
const sanitized = sanitizeErrorValue(error);
|
|
123
|
-
if (typeof sanitized === "string") {
|
|
124
|
-
return sanitized;
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
return sanitizeLargeText(JSON.stringify(sanitized));
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return sanitizeLargeText(String(error));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function getAssetStoragePath() {
|
|
134
|
-
return getDefaultAssetsPath();
|
|
135
|
-
}
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Auto-save assets — persists generated media as Asset records
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
const ASSET_MEDIA_TYPES = new Set(["image", "audio", "video"]);
|
|
140
|
-
const ASSET_TYPE_MIME = {
|
|
141
|
-
image: "image/png",
|
|
142
|
-
audio: "audio/wav",
|
|
143
|
-
video: "video/mp4"
|
|
144
|
-
};
|
|
145
|
-
const ASSET_TYPE_EXT = {
|
|
146
|
-
image: "png",
|
|
147
|
-
audio: "wav",
|
|
148
|
-
video: "mp4"
|
|
149
|
-
};
|
|
150
|
-
function isAssetLikeValue(value) {
|
|
151
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
152
|
-
return false;
|
|
153
|
-
const v = value;
|
|
154
|
-
return (typeof v.type === "string" &&
|
|
155
|
-
ASSET_MEDIA_TYPES.has(v.type) &&
|
|
156
|
-
("data" in v || "uri" in v));
|
|
157
|
-
}
|
|
158
|
-
function decodeAssetBytes(data) {
|
|
159
|
-
if (data === null || data === undefined)
|
|
160
|
-
return null;
|
|
161
|
-
if (data instanceof Uint8Array)
|
|
162
|
-
return data;
|
|
163
|
-
if (Buffer.isBuffer(data))
|
|
164
|
-
return new Uint8Array(data);
|
|
165
|
-
if (Array.isArray(data) && data.every((v) => Number.isInteger(v))) {
|
|
166
|
-
return new Uint8Array(data);
|
|
167
|
-
}
|
|
168
|
-
if (typeof data === "string") {
|
|
169
|
-
return Uint8Array.from(Buffer.from(data, "base64"));
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
async function readBytesFromUri(uri) {
|
|
174
|
-
if (!uri)
|
|
175
|
-
return null;
|
|
176
|
-
try {
|
|
177
|
-
if (uri.startsWith("file://")) {
|
|
178
|
-
const { readFile } = await import("node:fs/promises");
|
|
179
|
-
const { fileURLToPath } = await import("node:url");
|
|
180
|
-
return new Uint8Array(await readFile(fileURLToPath(uri)));
|
|
181
|
-
}
|
|
182
|
-
if (uri.startsWith("data:")) {
|
|
183
|
-
const commaIdx = uri.indexOf(",");
|
|
184
|
-
if (commaIdx < 0)
|
|
185
|
-
return null;
|
|
186
|
-
return Uint8Array.from(Buffer.from(uri.slice(commaIdx + 1), "base64"));
|
|
187
|
-
}
|
|
188
|
-
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
189
|
-
const resp = await fetch(uri);
|
|
190
|
-
if (!resp.ok)
|
|
191
|
-
return null;
|
|
192
|
-
return new Uint8Array(await resp.arrayBuffer());
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
// Failed to read bytes — non-fatal
|
|
197
|
-
}
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Recursively find asset-like values in a result object and persist them as
|
|
202
|
-
* Asset records in the database + on disk.
|
|
203
|
-
*
|
|
204
|
-
* Mutates the result in-place: sets `asset_id` and updates `uri` to
|
|
205
|
-
* `asset://{id}.{ext}`.
|
|
206
|
-
*/
|
|
207
|
-
async function autoSaveAssets(result, opts) {
|
|
208
|
-
const { join } = await import("node:path");
|
|
209
|
-
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
210
|
-
const queue = [];
|
|
211
|
-
// Collect all asset-like values from the result (may be nested)
|
|
212
|
-
function collect(value) {
|
|
213
|
-
if (value === null || value === undefined)
|
|
214
|
-
return;
|
|
215
|
-
if (Array.isArray(value)) {
|
|
216
|
-
for (const item of value)
|
|
217
|
-
collect(item);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (isAssetLikeValue(value)) {
|
|
221
|
-
queue.push(value);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (typeof value === "object") {
|
|
225
|
-
for (const v of Object.values(value)) {
|
|
226
|
-
collect(v);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
collect(result);
|
|
231
|
-
for (const assetValue of queue) {
|
|
232
|
-
// Skip if already saved
|
|
233
|
-
if (assetValue.asset_id)
|
|
234
|
-
continue;
|
|
235
|
-
const assetType = String(assetValue.type);
|
|
236
|
-
// Get bytes from inline data or URI
|
|
237
|
-
let bytes = decodeAssetBytes(assetValue.data);
|
|
238
|
-
if (!bytes && typeof assetValue.uri === "string") {
|
|
239
|
-
bytes = await readBytesFromUri(assetValue.uri);
|
|
240
|
-
}
|
|
241
|
-
if (!bytes)
|
|
242
|
-
continue;
|
|
243
|
-
// Determine mime/ext, preferring explicit content_type
|
|
244
|
-
const explicitMime = assetValue.mime_type ?? assetValue.content_type;
|
|
245
|
-
const contentType = typeof explicitMime === "string" && explicitMime
|
|
246
|
-
? explicitMime
|
|
247
|
-
: (ASSET_TYPE_MIME[assetType] ?? "application/octet-stream");
|
|
248
|
-
const ext = ASSET_TYPE_EXT[assetType] ?? "bin";
|
|
249
|
-
// Create Asset record
|
|
250
|
-
const asset = new Asset({
|
|
251
|
-
user_id: opts.userId,
|
|
252
|
-
workflow_id: opts.workflowId ?? null,
|
|
253
|
-
node_id: opts.nodeId,
|
|
254
|
-
job_id: opts.jobId,
|
|
255
|
-
name: `${assetType}_${opts.nodeId.slice(0, 8)}`,
|
|
256
|
-
content_type: contentType,
|
|
257
|
-
parent_id: null
|
|
258
|
-
});
|
|
259
|
-
const fileName = `${asset.id}.${ext}`;
|
|
260
|
-
try {
|
|
261
|
-
await mkdir(opts.storagePath, { recursive: true });
|
|
262
|
-
await writeFile(join(opts.storagePath, fileName), bytes);
|
|
263
|
-
asset.size = bytes.length;
|
|
264
|
-
await asset.save();
|
|
265
|
-
// Mutate the result value in-place
|
|
266
|
-
assetValue.asset_id = asset.id;
|
|
267
|
-
assetValue.uri = `asset://${fileName}`;
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
270
|
-
log.warn("Auto-save asset failed", {
|
|
271
|
-
nodeId: opts.nodeId,
|
|
272
|
-
error: String(err)
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Returns true if every node in the graph has a type starting with "comfy.".
|
|
279
|
-
*/
|
|
280
|
-
function isComfyGraph(graph) {
|
|
281
|
-
return (graph.nodes.length > 0 &&
|
|
282
|
-
graph.nodes.every((n) => typeof n.type === "string" && n.type.startsWith("comfy.")));
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Converts a NodeTool graph (with comfy.* nodes) into a ComfyUI API prompt dict.
|
|
286
|
-
*/
|
|
287
|
-
function graphToComfyPrompt(graph) {
|
|
288
|
-
const prompt = {};
|
|
289
|
-
for (const node of graph.nodes) {
|
|
290
|
-
const id = String(node.id);
|
|
291
|
-
const classType = node.type.replace(/^comfy\./, "");
|
|
292
|
-
const props = node.properties ??
|
|
293
|
-
node.data ??
|
|
294
|
-
{};
|
|
295
|
-
const inputs = {};
|
|
296
|
-
for (const [key, value] of Object.entries(props)) {
|
|
297
|
-
if (key.startsWith("_") || key === "workflow_id")
|
|
298
|
-
continue;
|
|
299
|
-
// MsgPack may decode large integers as BigInt; ComfyUI expects plain numbers
|
|
300
|
-
inputs[key] = typeof value === "bigint" ? Number(value) : value;
|
|
301
|
-
}
|
|
302
|
-
prompt[id] = { class_type: classType, inputs };
|
|
303
|
-
}
|
|
304
|
-
// Wire edges: set inputs to [sourceId, slotIndex]
|
|
305
|
-
for (const edge of graph.edges) {
|
|
306
|
-
const sourceId = String(edge.source);
|
|
307
|
-
const targetId = String(edge.target);
|
|
308
|
-
const sourceHandle = edge.sourceHandle;
|
|
309
|
-
const targetHandle = edge.targetHandle;
|
|
310
|
-
if (!prompt[targetId])
|
|
311
|
-
continue;
|
|
312
|
-
// Determine output slot index from source handle name.
|
|
313
|
-
// Handles may be "output_0", "output_1", etc. (generic indexed format)
|
|
314
|
-
// or named like "MODEL", "CLIP", "VAE" (resolved via _comfy_metadata).
|
|
315
|
-
let slotIndex = 0;
|
|
316
|
-
const outputMatch = /^output_(\d+)$/.exec(sourceHandle);
|
|
317
|
-
if (outputMatch) {
|
|
318
|
-
slotIndex = parseInt(outputMatch[1], 10);
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
const sourceNode = graph.nodes.find((n) => String(n.id) === sourceId);
|
|
322
|
-
if (sourceNode) {
|
|
323
|
-
const nodeProps = sourceNode.properties ??
|
|
324
|
-
sourceNode.data ??
|
|
325
|
-
{};
|
|
326
|
-
const meta = (nodeProps._comfy_metadata ??
|
|
327
|
-
sourceNode._comfy_metadata);
|
|
328
|
-
if (meta?.outputs) {
|
|
329
|
-
const idx = meta.outputs.indexOf(sourceHandle);
|
|
330
|
-
if (idx >= 0)
|
|
331
|
-
slotIndex = idx;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
prompt[targetId].inputs[targetHandle] = [sourceId, slotIndex];
|
|
336
|
-
}
|
|
337
|
-
return prompt;
|
|
338
|
-
}
|
|
339
|
-
function createRuntimeContext(opts) {
|
|
340
|
-
const storagePath = getAssetStoragePath();
|
|
341
|
-
const storage = new FileStorageAdapter(storagePath);
|
|
342
|
-
const ctx = new RuntimeProcessingContext({
|
|
343
|
-
...opts,
|
|
344
|
-
secretResolver: getSecret,
|
|
345
|
-
storage,
|
|
346
|
-
tempUrlResolver: (fileUri) => {
|
|
347
|
-
// Convert file:///path/to/storage/temp/uuid.png → public asset URL.
|
|
348
|
-
// When ASSET_DOMAIN / TEMP_DOMAIN are configured the result uses those
|
|
349
|
-
// domains; otherwise it falls back to /api/storage/temp/uuid.png.
|
|
350
|
-
const prefix = pathToFileURL(storagePath).toString();
|
|
351
|
-
if (fileUri.startsWith(prefix)) {
|
|
352
|
-
return buildAssetUrl(fileUri.slice(prefix.length + 1));
|
|
353
|
-
}
|
|
354
|
-
return fileUri;
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
const MIME_TO_EXT = {
|
|
358
|
-
"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif",
|
|
359
|
-
"image/webp": "webp", "image/bmp": "bmp", "image/svg+xml": "svg",
|
|
360
|
-
"audio/mpeg": "mp3", "audio/mp3": "mp3", "audio/wav": "wav",
|
|
361
|
-
"audio/ogg": "ogg", "video/mp4": "mp4", "video/webm": "webm",
|
|
362
|
-
"application/pdf": "pdf", "text/plain": "txt", "text/html": "html",
|
|
363
|
-
"model/gltf-binary": "glb"
|
|
364
|
-
};
|
|
365
|
-
ctx.setModelInterfaces({
|
|
366
|
-
createAsset: async (args) => {
|
|
367
|
-
const { join } = await import("node:path");
|
|
368
|
-
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
369
|
-
const asset = new Asset({
|
|
370
|
-
user_id: args.userId,
|
|
371
|
-
workflow_id: args.workflowId ?? null,
|
|
372
|
-
node_id: args.nodeId ?? null,
|
|
373
|
-
job_id: args.jobId ?? null,
|
|
374
|
-
name: args.name,
|
|
375
|
-
content_type: args.contentType,
|
|
376
|
-
parent_id: args.parentId ?? null
|
|
377
|
-
});
|
|
378
|
-
if (args.content) {
|
|
379
|
-
const ext = MIME_TO_EXT[args.contentType] ?? "bin";
|
|
380
|
-
const fileName = `${asset.id}.${ext}`;
|
|
381
|
-
await mkdir(storagePath, { recursive: true });
|
|
382
|
-
await writeFile(join(storagePath, fileName), args.content);
|
|
383
|
-
asset.size = args.content.length;
|
|
384
|
-
}
|
|
385
|
-
await asset.save();
|
|
386
|
-
return asset;
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
return ctx;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Default system prompt for regular chat — matches Python's REGULAR_SYSTEM_PROMPT.
|
|
393
|
-
*/
|
|
394
|
-
const REGULAR_SYSTEM_PROMPT = `You are a helpful assistant.
|
|
395
|
-
|
|
396
|
-
# IMAGE TOOLS
|
|
397
|
-
When using image tools, you will get an image url as result.
|
|
398
|
-
ALWAYS EMBED THE IMAGE AS MARKDOWN IMAGE TAG.
|
|
399
|
-
|
|
400
|
-
# File types
|
|
401
|
-
References to documents, images, videos or audio files are objects with following structure:
|
|
402
|
-
- type: either document, image, video, audio
|
|
403
|
-
- uri: either local "file:///path/to/file" or "http://"
|
|
404
|
-
`;
|
|
405
|
-
class ToolBridge {
|
|
406
|
-
waiters = new Map();
|
|
407
|
-
createWaiter(toolCallId, timeoutMs = 300_000) {
|
|
408
|
-
return new Promise((resolve, reject) => {
|
|
409
|
-
let timer = null;
|
|
410
|
-
const cleanup = () => {
|
|
411
|
-
if (timer)
|
|
412
|
-
clearTimeout(timer);
|
|
413
|
-
this.waiters.delete(toolCallId);
|
|
414
|
-
};
|
|
415
|
-
const wrappedResolve = (value) => {
|
|
416
|
-
cleanup();
|
|
417
|
-
resolve(value);
|
|
418
|
-
};
|
|
419
|
-
const wrappedReject = (reason) => {
|
|
420
|
-
cleanup();
|
|
421
|
-
reject(reason);
|
|
422
|
-
};
|
|
423
|
-
this.waiters.set(toolCallId, {
|
|
424
|
-
resolve: wrappedResolve,
|
|
425
|
-
reject: wrappedReject
|
|
426
|
-
});
|
|
427
|
-
if (timeoutMs > 0) {
|
|
428
|
-
timer = setTimeout(() => {
|
|
429
|
-
if (this.waiters.has(toolCallId)) {
|
|
430
|
-
wrappedReject(new Error(`Tool call ${toolCallId} timed out after ${timeoutMs}ms`));
|
|
431
|
-
}
|
|
432
|
-
}, timeoutMs);
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
resolveResult(toolCallId, payload) {
|
|
437
|
-
const waiter = this.waiters.get(toolCallId);
|
|
438
|
-
if (!waiter)
|
|
439
|
-
return;
|
|
440
|
-
waiter.resolve(payload);
|
|
441
|
-
}
|
|
442
|
-
cancelAll() {
|
|
443
|
-
const error = new Error("All pending tool calls cancelled");
|
|
444
|
-
for (const waiter of this.waiters.values()) {
|
|
445
|
-
waiter.reject(error);
|
|
446
|
-
}
|
|
447
|
-
this.waiters.clear();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Proxy tool that forwards execution to the frontend via ToolBridge.
|
|
452
|
-
* Mirrors Python's UIToolProxy from messaging/ui_tool_proxy.py.
|
|
453
|
-
*/
|
|
454
|
-
class UIToolProxy extends Tool {
|
|
455
|
-
name;
|
|
456
|
-
description;
|
|
457
|
-
inputSchema;
|
|
458
|
-
bridge;
|
|
459
|
-
sendMsg;
|
|
460
|
-
constructor(manifest, bridge, sendMsg) {
|
|
461
|
-
super();
|
|
462
|
-
this.name = typeof manifest.name === "string" ? manifest.name : "";
|
|
463
|
-
this.description =
|
|
464
|
-
typeof manifest.description === "string"
|
|
465
|
-
? manifest.description
|
|
466
|
-
: "UI tool";
|
|
467
|
-
this.inputSchema =
|
|
468
|
-
typeof manifest.parameters === "object" && manifest.parameters !== null
|
|
469
|
-
? manifest.parameters
|
|
470
|
-
: {};
|
|
471
|
-
this.bridge = bridge;
|
|
472
|
-
this.sendMsg = sendMsg;
|
|
473
|
-
}
|
|
474
|
-
async process(_context, params) {
|
|
475
|
-
const toolCallId = randomUUID();
|
|
476
|
-
await this.sendMsg({
|
|
477
|
-
type: "tool_call",
|
|
478
|
-
tool_call_id: toolCallId,
|
|
479
|
-
name: this.name,
|
|
480
|
-
args: params
|
|
481
|
-
});
|
|
482
|
-
try {
|
|
483
|
-
const payload = await this.bridge.createWaiter(toolCallId, 60_000);
|
|
484
|
-
if (payload.ok) {
|
|
485
|
-
return payload.result ?? {};
|
|
486
|
-
}
|
|
487
|
-
return {
|
|
488
|
-
error: `Frontend tool execution failed: ${payload.error ?? "Unknown error"}`
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
492
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
userMessage(_params) {
|
|
496
|
-
return `Executing frontend tool: ${this.name}`;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
export class UnifiedWebSocketRunner {
|
|
500
|
-
websocket = null;
|
|
501
|
-
mode = "binary";
|
|
502
|
-
userId;
|
|
503
|
-
authToken;
|
|
504
|
-
defaultModel;
|
|
505
|
-
defaultProvider;
|
|
506
|
-
resolveExecutor;
|
|
507
|
-
resolveNodeType;
|
|
508
|
-
resolveProvider;
|
|
509
|
-
resolveTools;
|
|
510
|
-
getSystemStats;
|
|
511
|
-
workspaceResolver;
|
|
512
|
-
beforeRunJob;
|
|
513
|
-
getNodeMetadata;
|
|
514
|
-
validateNode;
|
|
515
|
-
nodeRegistry;
|
|
516
|
-
configuredProvidersCache = new Map();
|
|
517
|
-
sendLock = Promise.resolve();
|
|
518
|
-
activeJobs = new Map();
|
|
519
|
-
currentTask = null;
|
|
520
|
-
heartbeatTimer = null;
|
|
521
|
-
statsTimer = null;
|
|
522
|
-
chatRequestSeq = 0;
|
|
523
|
-
clientToolsManifest = {};
|
|
524
|
-
toolBridge = new ToolBridge();
|
|
525
|
-
observerRegistered = false;
|
|
526
|
-
logError(context, error) {
|
|
527
|
-
log.error(context, formatSanitizedError(error));
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Extract text from message content that may be a string or array of content items.
|
|
531
|
-
* Mirrors Python's _extract_query_text / _extract_objective / _extract_text_content.
|
|
532
|
-
*/
|
|
533
|
-
extractTextContent(content, fallback = "") {
|
|
534
|
-
if (typeof content === "string")
|
|
535
|
-
return content;
|
|
536
|
-
if (Array.isArray(content)) {
|
|
537
|
-
const texts = content
|
|
538
|
-
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
539
|
-
.map((c) => c.text);
|
|
540
|
-
return texts.length > 0 ? texts.join(" ") : fallback;
|
|
541
|
-
}
|
|
542
|
-
return fallback;
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Run one final LLM call to present structured agent results as readable
|
|
546
|
-
* markdown. Falls back to formatted JSON if the call fails.
|
|
547
|
-
*/
|
|
548
|
-
async presentStructuredResults(provider, model, objective, resultsJson, threadId) {
|
|
549
|
-
const messages = [
|
|
550
|
-
{
|
|
551
|
-
role: "user",
|
|
552
|
-
content: `You completed the following task:\n"${objective}"\n\nHere are the structured results:\n\`\`\`json\n${resultsJson}\n\`\`\`\n\nPresent these results to the user in clear, well-formatted markdown. Be concise — do not repeat the raw JSON. Focus on readability.`
|
|
553
|
-
}
|
|
554
|
-
];
|
|
555
|
-
try {
|
|
556
|
-
let content = "";
|
|
557
|
-
for await (const item of provider.generateMessagesTraced({
|
|
558
|
-
messages,
|
|
559
|
-
model
|
|
560
|
-
})) {
|
|
561
|
-
if ("type" in item && item.type === "chunk") {
|
|
562
|
-
const chunk = item;
|
|
563
|
-
content += chunk.content ?? "";
|
|
564
|
-
// Stream chunks to client so the user sees the response forming
|
|
565
|
-
await this.sendMessage({
|
|
566
|
-
type: "chunk",
|
|
567
|
-
content: chunk.content ?? "",
|
|
568
|
-
done: false,
|
|
569
|
-
thread_id: threadId
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return content || resultsJson;
|
|
574
|
-
}
|
|
575
|
-
catch (err) {
|
|
576
|
-
log.warn("Failed to present structured results via LLM, using JSON fallback", {
|
|
577
|
-
error: err instanceof Error ? err.message : String(err)
|
|
578
|
-
});
|
|
579
|
-
return resultsJson;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
inferOutputType(value) {
|
|
583
|
-
if (value === null || value === undefined)
|
|
584
|
-
return "any";
|
|
585
|
-
if (typeof value === "string")
|
|
586
|
-
return "str";
|
|
587
|
-
if (typeof value === "number")
|
|
588
|
-
return Number.isInteger(value) ? "int" : "float";
|
|
589
|
-
if (typeof value === "boolean")
|
|
590
|
-
return "bool";
|
|
591
|
-
if (Array.isArray(value))
|
|
592
|
-
return "list";
|
|
593
|
-
if (value && typeof value === "object")
|
|
594
|
-
return "dict";
|
|
595
|
-
return "any";
|
|
596
|
-
}
|
|
597
|
-
resolveOutputNodeForKey(active, outputKey) {
|
|
598
|
-
let fallback = null;
|
|
599
|
-
for (const raw of active.graph.nodes) {
|
|
600
|
-
const node = raw;
|
|
601
|
-
const id = typeof node.id === "string" ? node.id : null;
|
|
602
|
-
if (!id)
|
|
603
|
-
continue;
|
|
604
|
-
const name = typeof node.name === "string" ? node.name : id;
|
|
605
|
-
const type = typeof node.type === "string" ? node.type : "";
|
|
606
|
-
if (name === outputKey || id === outputKey)
|
|
607
|
-
return { id, name };
|
|
608
|
-
if (type === "nodetool.output.Output" && !fallback)
|
|
609
|
-
fallback = { id, name };
|
|
610
|
-
}
|
|
611
|
-
return fallback;
|
|
612
|
-
}
|
|
613
|
-
async sendOutputUpdates(active, outputs) {
|
|
614
|
-
for (const [outputKey, values] of Object.entries(outputs)) {
|
|
615
|
-
const nodeRef = this.resolveOutputNodeForKey(active, outputKey) ?? {
|
|
616
|
-
id: outputKey,
|
|
617
|
-
name: outputKey
|
|
618
|
-
};
|
|
619
|
-
const seq = Array.isArray(values) ? values : [];
|
|
620
|
-
for (const rawValue of seq) {
|
|
621
|
-
const value = await active.context.normalizeOutputValue(rawValue);
|
|
622
|
-
await this.sendMessage({
|
|
623
|
-
type: "output_update",
|
|
624
|
-
node_id: nodeRef.id,
|
|
625
|
-
node_name: nodeRef.name,
|
|
626
|
-
output_name: "output",
|
|
627
|
-
value,
|
|
628
|
-
output_type: this.inferOutputType(value),
|
|
629
|
-
metadata: {},
|
|
630
|
-
workflow_id: active.workflowId,
|
|
631
|
-
job_id: active.jobId
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
constructor(options) {
|
|
637
|
-
this.userId = options.userId ?? null;
|
|
638
|
-
this.authToken = options.authToken ?? null;
|
|
639
|
-
this.defaultModel = options.defaultModel ?? "gpt-oss:20b";
|
|
640
|
-
this.defaultProvider = options.defaultProvider ?? "ollama";
|
|
641
|
-
this.resolveExecutor = options.resolveExecutor;
|
|
642
|
-
this.resolveNodeType = options.resolveNodeType;
|
|
643
|
-
this.resolveProvider = options.resolveProvider;
|
|
644
|
-
this.resolveTools = options.resolveTools;
|
|
645
|
-
this.workspaceResolver = options.workspaceResolver;
|
|
646
|
-
this.beforeRunJob = options.beforeRunJob;
|
|
647
|
-
this.getNodeMetadata = options.getNodeMetadata;
|
|
648
|
-
this.validateNode = options.validateNode;
|
|
649
|
-
this.nodeRegistry = options.nodeRegistry;
|
|
650
|
-
this.getSystemStats =
|
|
651
|
-
options.getSystemStats ??
|
|
652
|
-
(() => ({
|
|
653
|
-
timestamp: Date.now(),
|
|
654
|
-
process_uptime_sec: process.uptime(),
|
|
655
|
-
memory: process.memoryUsage()
|
|
656
|
-
}));
|
|
657
|
-
}
|
|
658
|
-
async connect(websocket, userId, authToken) {
|
|
659
|
-
if (userId)
|
|
660
|
-
this.userId = userId;
|
|
661
|
-
if (authToken)
|
|
662
|
-
this.authToken = authToken;
|
|
663
|
-
this.userId = this.userId ?? "1";
|
|
664
|
-
await websocket.accept();
|
|
665
|
-
this.websocket = websocket;
|
|
666
|
-
log.info("Client connected", { userId: this.userId });
|
|
667
|
-
this.startHeartbeat();
|
|
668
|
-
// Only broadcast system stats in development — unnecessary overhead in production
|
|
669
|
-
if (process.env.NODE_ENV !== "production") {
|
|
670
|
-
this.startStatsBroadcast();
|
|
671
|
-
}
|
|
672
|
-
this.registerObserver();
|
|
673
|
-
}
|
|
674
|
-
async disconnect() {
|
|
675
|
-
log.info("Client disconnected");
|
|
676
|
-
this.stopHeartbeat();
|
|
677
|
-
this.stopStatsBroadcast();
|
|
678
|
-
this.unregisterObserver();
|
|
679
|
-
this.toolBridge.cancelAll();
|
|
680
|
-
this.currentTask = null;
|
|
681
|
-
for (const [jobId, job] of this.activeJobs) {
|
|
682
|
-
if (job.comfyHandle) {
|
|
683
|
-
job.comfyHandle.cancel();
|
|
684
|
-
}
|
|
685
|
-
else if (job.runner) {
|
|
686
|
-
job.runner.cancel();
|
|
687
|
-
}
|
|
688
|
-
this.activeJobs.delete(jobId);
|
|
689
|
-
}
|
|
690
|
-
if (this.websocket) {
|
|
691
|
-
try {
|
|
692
|
-
await this.websocket.close();
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
this.logError("disconnect websocket.close failed", error);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
this.websocket = null;
|
|
699
|
-
}
|
|
700
|
-
serializeForJson(value) {
|
|
701
|
-
if (value instanceof Uint8Array)
|
|
702
|
-
return Array.from(value);
|
|
703
|
-
if (value instanceof Date)
|
|
704
|
-
return value.toISOString();
|
|
705
|
-
if (Array.isArray(value))
|
|
706
|
-
return value.map((v) => this.serializeForJson(v));
|
|
707
|
-
if (value && typeof value === "object") {
|
|
708
|
-
const out = {};
|
|
709
|
-
for (const [k, v] of Object.entries(value)) {
|
|
710
|
-
out[k] = this.serializeForJson(v);
|
|
711
|
-
}
|
|
712
|
-
return out;
|
|
713
|
-
}
|
|
714
|
-
return value;
|
|
715
|
-
}
|
|
716
|
-
async sendMessage(message) {
|
|
717
|
-
if (!this.websocket)
|
|
718
|
-
return;
|
|
719
|
-
if (this.websocket.clientState === "disconnected" ||
|
|
720
|
-
this.websocket.applicationState === "disconnected") {
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
// Resolve storage keys in content to browser-accessible URLs before
|
|
724
|
-
// sending over the wire. This keeps DB storage URL-agnostic while
|
|
725
|
-
// delivering ready-to-use URLs to the client.
|
|
726
|
-
if (Array.isArray(message.content)) {
|
|
727
|
-
message = {
|
|
728
|
-
...message,
|
|
729
|
-
content: resolveContentUrls(message.content)
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
const payload = this.mode === "text"
|
|
733
|
-
? this.serializeForJson(message)
|
|
734
|
-
: message;
|
|
735
|
-
const prev = this.sendLock;
|
|
736
|
-
let release;
|
|
737
|
-
this.sendLock = new Promise((resolve) => {
|
|
738
|
-
release = resolve;
|
|
739
|
-
});
|
|
740
|
-
await prev;
|
|
741
|
-
try {
|
|
742
|
-
if (this.mode === "binary") {
|
|
743
|
-
await this.websocket.sendBytes(pack(payload));
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
await this.websocket.sendText(JSON.stringify(payload));
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
finally {
|
|
750
|
-
release();
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
async receiveMessage() {
|
|
754
|
-
if (!this.websocket) {
|
|
755
|
-
throw new Error("WebSocket is not connected");
|
|
756
|
-
}
|
|
757
|
-
const message = await this.websocket.receive();
|
|
758
|
-
if (message.type === "websocket.disconnect")
|
|
759
|
-
return null;
|
|
760
|
-
if (message.bytes) {
|
|
761
|
-
return unpack(message.bytes);
|
|
762
|
-
}
|
|
763
|
-
if (message.text) {
|
|
764
|
-
return JSON.parse(message.text);
|
|
765
|
-
}
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* Normalize a raw graph so that the kernel's NodeDescriptor contract is met.
|
|
770
|
-
* The web-UI / Python serialisation stores node properties under `data`;
|
|
771
|
-
* the kernel expects them under `properties`.
|
|
772
|
-
*/
|
|
773
|
-
normalizeGraph(graph) {
|
|
774
|
-
const nodes = graph.nodes.map((n) => {
|
|
775
|
-
if (n.properties === undefined && n.data !== undefined) {
|
|
776
|
-
const { data, ...rest } = n;
|
|
777
|
-
return { ...rest, properties: data };
|
|
778
|
-
}
|
|
779
|
-
return n;
|
|
780
|
-
});
|
|
781
|
-
const edges = graph.edges.map((edge) => {
|
|
782
|
-
const rawEdgeType = edge.edge_type ?? edge.type;
|
|
783
|
-
const edge_type = rawEdgeType === "control" ? "control" : "data";
|
|
784
|
-
const { type, ...rest } = edge;
|
|
785
|
-
return { ...rest, edge_type };
|
|
786
|
-
});
|
|
787
|
-
return { nodes, edges };
|
|
788
|
-
}
|
|
789
|
-
async hydrateGraph(graph) {
|
|
790
|
-
const normalized = this.normalizeGraph(graph);
|
|
791
|
-
if (!this.resolveNodeType) {
|
|
792
|
-
return normalized;
|
|
793
|
-
}
|
|
794
|
-
const hydrated = await Graph.loadFromDict(normalized, {
|
|
795
|
-
resolver: this.resolveNodeType
|
|
796
|
-
});
|
|
797
|
-
return {
|
|
798
|
-
nodes: [...hydrated.nodes],
|
|
799
|
-
edges: [...hydrated.edges]
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
getRawGraph(req) {
|
|
803
|
-
if (req.graph) {
|
|
804
|
-
return this.normalizeGraph(req.graph);
|
|
805
|
-
}
|
|
806
|
-
if (req.workflow_id && this.userId) {
|
|
807
|
-
const userId = this.userId;
|
|
808
|
-
const workflowId = req.workflow_id;
|
|
809
|
-
return (async () => {
|
|
810
|
-
const workflow = await Workflow.find(userId, workflowId);
|
|
811
|
-
if (!workflow)
|
|
812
|
-
throw new Error(`Workflow not found: ${workflowId}`);
|
|
813
|
-
return this.normalizeGraph(workflow.graph);
|
|
814
|
-
})();
|
|
815
|
-
}
|
|
816
|
-
throw new Error("workflow_id or graph is required");
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Surface a clean terminal job_update when pre-run setup fails (typically
|
|
820
|
-
* because the Python bridge could not start). Without this the error would
|
|
821
|
-
* bubble up to handleCommand and be sent as a generic `invalid_command`
|
|
822
|
-
* envelope, which the UI does not associate with the job — the workflow
|
|
823
|
-
* appears to spin forever instead of failing.
|
|
824
|
-
*/
|
|
825
|
-
async emitBeforeRunFailure(jobId, workflowId, err) {
|
|
826
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
827
|
-
this.logError("beforeRunJob failed", err);
|
|
828
|
-
await this.sendMessage({
|
|
829
|
-
type: "job_update",
|
|
830
|
-
status: "failed",
|
|
831
|
-
job_id: jobId,
|
|
832
|
-
workflow_id: workflowId,
|
|
833
|
-
error: errorMessage
|
|
834
|
-
});
|
|
835
|
-
try {
|
|
836
|
-
const job = (await Job.get(jobId));
|
|
837
|
-
if (job) {
|
|
838
|
-
job.markFailed(errorMessage);
|
|
839
|
-
await job.save();
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
catch (persistErr) {
|
|
843
|
-
this.logError("beforeRunJob failure persistence failed", persistErr);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
async runJob(req) {
|
|
847
|
-
const userId = req.user_id ?? this.userId ?? "1";
|
|
848
|
-
const workflowId = req.workflow_id ?? null;
|
|
849
|
-
const jobId = req.job_id ?? randomUUID();
|
|
850
|
-
// Get the normalized (but not hydrated) graph first so we can check
|
|
851
|
-
// for comfy nodes before hydration strips unregistered node types.
|
|
852
|
-
const rawGraph = await this.getRawGraph(req);
|
|
853
|
-
// Route ComfyUI workflows to the dedicated comfy executor
|
|
854
|
-
if (isComfyGraph(rawGraph)) {
|
|
855
|
-
if (this.beforeRunJob) {
|
|
856
|
-
try {
|
|
857
|
-
await this.beforeRunJob(rawGraph);
|
|
858
|
-
}
|
|
859
|
-
catch (err) {
|
|
860
|
-
await this.emitBeforeRunFailure(jobId, workflowId, err);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
await this.runComfyJob(jobId, workflowId, userId, rawGraph, req.settings ?? {});
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
// Hydrate non-comfy graphs (resolves node types from the registry)
|
|
868
|
-
const graph = await this.hydrateGraph(rawGraph);
|
|
869
|
-
if (this.beforeRunJob) {
|
|
870
|
-
try {
|
|
871
|
-
await this.beforeRunJob(graph);
|
|
872
|
-
}
|
|
873
|
-
catch (err) {
|
|
874
|
-
await this.emitBeforeRunFailure(jobId, workflowId, err);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
const workspaceDir = workflowId && this.workspaceResolver
|
|
879
|
-
? await this.workspaceResolver(workflowId, userId)
|
|
880
|
-
: null;
|
|
881
|
-
const context = createRuntimeContext({
|
|
882
|
-
jobId,
|
|
883
|
-
workflowId,
|
|
884
|
-
userId,
|
|
885
|
-
workspaceDir,
|
|
886
|
-
assetOutputMode: this.mode === "text" ? "data_uri" : "temp_url"
|
|
887
|
-
});
|
|
888
|
-
// Expose executor/node-type resolution on the context so that
|
|
889
|
-
// sub-workflow nodes (WorkflowNode) can create child runners.
|
|
890
|
-
context.setResolveExecutor((node) => this.resolveExecutor(node));
|
|
891
|
-
if (this.resolveNodeType) {
|
|
892
|
-
const resolverObj = typeof this.resolveNodeType === "function"
|
|
893
|
-
? { resolveNodeType: this.resolveNodeType }
|
|
894
|
-
: this.resolveNodeType;
|
|
895
|
-
context.setResolveNodeType((nodeType) => resolverObj.resolveNodeType(nodeType));
|
|
896
|
-
}
|
|
897
|
-
const runner = new WorkflowRunner(jobId, {
|
|
898
|
-
resolveExecutor: (node) => this.resolveExecutor(node),
|
|
899
|
-
executionContext: context,
|
|
900
|
-
validateNode: this.validateNode
|
|
901
|
-
});
|
|
902
|
-
const active = {
|
|
903
|
-
jobId,
|
|
904
|
-
workflowId,
|
|
905
|
-
context,
|
|
906
|
-
runner,
|
|
907
|
-
graph,
|
|
908
|
-
finished: false,
|
|
909
|
-
status: "running"
|
|
910
|
-
};
|
|
911
|
-
this.activeJobs.set(jobId, active);
|
|
912
|
-
log.info("Job started", { jobId, workflowId });
|
|
913
|
-
try {
|
|
914
|
-
const existing = await Job.get(jobId);
|
|
915
|
-
if (!existing) {
|
|
916
|
-
await Job.create({
|
|
917
|
-
id: jobId,
|
|
918
|
-
workflow_id: workflowId ?? "",
|
|
919
|
-
user_id: userId,
|
|
920
|
-
status: "running",
|
|
921
|
-
params: req.params ?? {},
|
|
922
|
-
graph
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
catch (error) {
|
|
927
|
-
this.logError("runJob persistence failed", error);
|
|
928
|
-
// Persistence is best-effort in TS runtime mode.
|
|
929
|
-
}
|
|
930
|
-
const executePromise = runner.run({
|
|
931
|
-
job_id: jobId,
|
|
932
|
-
workflow_id: workflowId ?? undefined,
|
|
933
|
-
params: req.params ?? {}
|
|
934
|
-
}, graph);
|
|
935
|
-
active.streamTask = this.streamJobMessages(active, executePromise);
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Execute a ComfyUI workflow via the comfy executor (local or RunPod).
|
|
939
|
-
*/
|
|
940
|
-
async runComfyJob(jobId, workflowId, _userId, graph, _settings) {
|
|
941
|
-
const active = {
|
|
942
|
-
jobId,
|
|
943
|
-
workflowId,
|
|
944
|
-
context: null,
|
|
945
|
-
runner: null,
|
|
946
|
-
graph,
|
|
947
|
-
finished: false,
|
|
948
|
-
status: "running"
|
|
949
|
-
};
|
|
950
|
-
this.activeJobs.set(jobId, active);
|
|
951
|
-
// Build node metadata lookup for status messages
|
|
952
|
-
const nodeLookup = new Map();
|
|
953
|
-
for (const node of graph.nodes) {
|
|
954
|
-
const id = String(node.id);
|
|
955
|
-
const type = node.type ?? "comfy.unknown";
|
|
956
|
-
const props = (node.properties ?? node.data ?? {});
|
|
957
|
-
const name = props.title ?? type;
|
|
958
|
-
nodeLookup.set(id, { name, type });
|
|
959
|
-
}
|
|
960
|
-
const getNode = (id) => nodeLookup.get(id) ?? { name: `Node ${id}`, type: "comfy.unknown" };
|
|
961
|
-
try {
|
|
962
|
-
await this.sendMessage({
|
|
963
|
-
type: "job_update",
|
|
964
|
-
status: "running",
|
|
965
|
-
job_id: jobId,
|
|
966
|
-
workflow_id: workflowId
|
|
967
|
-
});
|
|
968
|
-
const prompt = graphToComfyPrompt(graph);
|
|
969
|
-
const host = await getSetting("COMFYUI_ADDR");
|
|
970
|
-
if (!host) {
|
|
971
|
-
throw new Error("COMFYUI_ADDR is not configured. Set it in Settings.");
|
|
972
|
-
}
|
|
973
|
-
// Track active node so we can mark it completed when the next one starts
|
|
974
|
-
let activeNodeId = null;
|
|
975
|
-
const completeNode = (nodeId) => {
|
|
976
|
-
const n = getNode(nodeId);
|
|
977
|
-
void this.sendMessage({
|
|
978
|
-
type: "node_update",
|
|
979
|
-
node_id: nodeId,
|
|
980
|
-
node_name: n.name,
|
|
981
|
-
node_type: n.type,
|
|
982
|
-
status: "completed",
|
|
983
|
-
workflow_id: workflowId
|
|
984
|
-
});
|
|
985
|
-
};
|
|
986
|
-
const onProgress = (event) => {
|
|
987
|
-
switch (event.type) {
|
|
988
|
-
case "executing": {
|
|
989
|
-
// When a new node starts, the previous one is implicitly done
|
|
990
|
-
if (activeNodeId && activeNodeId !== event.node) {
|
|
991
|
-
completeNode(activeNodeId);
|
|
992
|
-
activeNodeId = null;
|
|
993
|
-
}
|
|
994
|
-
if (event.node) {
|
|
995
|
-
activeNodeId = event.node;
|
|
996
|
-
const n = getNode(event.node);
|
|
997
|
-
void this.sendMessage({
|
|
998
|
-
type: "node_update",
|
|
999
|
-
node_id: event.node,
|
|
1000
|
-
node_name: n.name,
|
|
1001
|
-
node_type: n.type,
|
|
1002
|
-
status: "running",
|
|
1003
|
-
workflow_id: workflowId
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
else {
|
|
1007
|
-
// null node = execution finished, complete any lingering active node
|
|
1008
|
-
if (activeNodeId) {
|
|
1009
|
-
completeNode(activeNodeId);
|
|
1010
|
-
activeNodeId = null;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
break;
|
|
1014
|
-
}
|
|
1015
|
-
case "progress":
|
|
1016
|
-
if (event.node) {
|
|
1017
|
-
void this.sendMessage({
|
|
1018
|
-
type: "node_progress",
|
|
1019
|
-
node_id: event.node,
|
|
1020
|
-
progress: event.progress ?? 0,
|
|
1021
|
-
total: event.total ?? 1,
|
|
1022
|
-
workflow_id: workflowId
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
break;
|
|
1026
|
-
case "executed":
|
|
1027
|
-
if (event.node) {
|
|
1028
|
-
// Explicit completion with output — mark done and clear active
|
|
1029
|
-
if (activeNodeId === event.node)
|
|
1030
|
-
activeNodeId = null;
|
|
1031
|
-
const n = getNode(event.node);
|
|
1032
|
-
void this.sendMessage({
|
|
1033
|
-
type: "node_update",
|
|
1034
|
-
node_id: event.node,
|
|
1035
|
-
node_name: n.name,
|
|
1036
|
-
node_type: n.type,
|
|
1037
|
-
status: "completed",
|
|
1038
|
-
result: event.output ?? null,
|
|
1039
|
-
workflow_id: workflowId
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
break;
|
|
1043
|
-
case "execution_cached":
|
|
1044
|
-
if (event.cached_nodes) {
|
|
1045
|
-
for (const nodeId of event.cached_nodes) {
|
|
1046
|
-
completeNode(nodeId);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
break;
|
|
1050
|
-
case "execution_error":
|
|
1051
|
-
if (event.node) {
|
|
1052
|
-
activeNodeId = null;
|
|
1053
|
-
const n = getNode(event.node);
|
|
1054
|
-
void this.sendMessage({
|
|
1055
|
-
type: "node_update",
|
|
1056
|
-
node_id: event.node,
|
|
1057
|
-
node_name: n.name,
|
|
1058
|
-
node_type: n.type,
|
|
1059
|
-
status: "error",
|
|
1060
|
-
error: event.error ?? "Execution error",
|
|
1061
|
-
workflow_id: workflowId
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
break;
|
|
1065
|
-
}
|
|
1066
|
-
};
|
|
1067
|
-
const handle = executeComfy(prompt, host, onProgress);
|
|
1068
|
-
active.comfyHandle = handle;
|
|
1069
|
-
const result = await handle.result;
|
|
1070
|
-
if (result.status === "completed") {
|
|
1071
|
-
if (result.images && result.images.length > 0) {
|
|
1072
|
-
await this.sendMessage({
|
|
1073
|
-
type: "output_update",
|
|
1074
|
-
job_id: jobId,
|
|
1075
|
-
workflow_id: workflowId,
|
|
1076
|
-
result: { images: result.images }
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
active.status = "completed";
|
|
1080
|
-
active.finished = true;
|
|
1081
|
-
await this.sendMessage({
|
|
1082
|
-
type: "job_update",
|
|
1083
|
-
status: "completed",
|
|
1084
|
-
job_id: jobId,
|
|
1085
|
-
workflow_id: workflowId
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
else {
|
|
1089
|
-
throw new Error(result.error ?? "ComfyUI execution failed");
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
catch (err) {
|
|
1093
|
-
active.status = "failed";
|
|
1094
|
-
active.finished = true;
|
|
1095
|
-
active.error = err instanceof Error ? err.message : String(err);
|
|
1096
|
-
log.error("ComfyUI job failed", { jobId, error: active.error });
|
|
1097
|
-
await this.sendMessage({
|
|
1098
|
-
type: "job_update",
|
|
1099
|
-
status: "failed",
|
|
1100
|
-
job_id: jobId,
|
|
1101
|
-
workflow_id: workflowId,
|
|
1102
|
-
error: active.error
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
finally {
|
|
1106
|
-
this.activeJobs.delete(jobId);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
async streamJobMessages(active, executePromise) {
|
|
1110
|
-
let terminalSeen = false;
|
|
1111
|
-
let terminalWithResultSeen = false;
|
|
1112
|
-
let outputUpdateSeen = false;
|
|
1113
|
-
let finalOutputs = {};
|
|
1114
|
-
await this.sendMessage({
|
|
1115
|
-
type: "job_update",
|
|
1116
|
-
status: "running",
|
|
1117
|
-
job_id: active.jobId,
|
|
1118
|
-
workflow_id: active.workflowId
|
|
1119
|
-
});
|
|
1120
|
-
void executePromise
|
|
1121
|
-
.then((result) => {
|
|
1122
|
-
active.status = result.status;
|
|
1123
|
-
active.error = result.error;
|
|
1124
|
-
finalOutputs = result.outputs ?? {};
|
|
1125
|
-
})
|
|
1126
|
-
.catch((err) => {
|
|
1127
|
-
this.logError("job execution failed", err);
|
|
1128
|
-
active.status = "failed";
|
|
1129
|
-
active.error = formatSanitizedError(err);
|
|
1130
|
-
})
|
|
1131
|
-
.finally(() => {
|
|
1132
|
-
active.finished = true;
|
|
1133
|
-
});
|
|
1134
|
-
while (!active.finished || active.context.hasMessages()) {
|
|
1135
|
-
while (active.context.hasMessages()) {
|
|
1136
|
-
const msg = active.context.popMessage();
|
|
1137
|
-
if (!msg)
|
|
1138
|
-
break;
|
|
1139
|
-
const outbound = {
|
|
1140
|
-
...msg,
|
|
1141
|
-
job_id: msg.job_id ?? active.jobId,
|
|
1142
|
-
workflow_id: msg.workflow_id ??
|
|
1143
|
-
active.workflowId
|
|
1144
|
-
};
|
|
1145
|
-
if (outbound.error !== undefined) {
|
|
1146
|
-
outbound.error = formatSanitizedError(outbound.error);
|
|
1147
|
-
}
|
|
1148
|
-
if (outbound.type === "notification" &&
|
|
1149
|
-
typeof outbound.content === "string") {
|
|
1150
|
-
outbound.content = sanitizeLargeText(outbound.content);
|
|
1151
|
-
}
|
|
1152
|
-
if (outbound.type === "node_update" && outbound.status === "error") {
|
|
1153
|
-
log.error("Node error", {
|
|
1154
|
-
jobId: active.jobId,
|
|
1155
|
-
nodeId: outbound.node_id,
|
|
1156
|
-
error: outbound.error
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
else if (outbound.type === "job_update" &&
|
|
1160
|
-
outbound.status === "failed") {
|
|
1161
|
-
log.error("Job failed", {
|
|
1162
|
-
jobId: active.jobId,
|
|
1163
|
-
error: outbound.error
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
// Skip messages for constant/input nodes — they produce trivial
|
|
1167
|
-
// outputs that don't need to be relayed to the frontend.
|
|
1168
|
-
if (outbound.type === "output_update" ||
|
|
1169
|
-
outbound.type === "node_update") {
|
|
1170
|
-
const nodeId = String(outbound.node_id ?? "");
|
|
1171
|
-
const graphNodes = active.graph.nodes ?? [];
|
|
1172
|
-
const node = graphNodes.find((n) => n.id === nodeId);
|
|
1173
|
-
const nodeType = typeof node?.type === "string" ? node.type : "";
|
|
1174
|
-
// Skip constant and input nodes entirely
|
|
1175
|
-
if (nodeType.startsWith("nodetool.constant.") ||
|
|
1176
|
-
nodeType.startsWith("nodetool.input.")) {
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1179
|
-
// Only relay output_update for Output-type nodes
|
|
1180
|
-
if (outbound.type === "output_update") {
|
|
1181
|
-
if (!nodeType.includes("Output"))
|
|
1182
|
-
continue;
|
|
1183
|
-
outputUpdateSeen = true;
|
|
1184
|
-
}
|
|
1185
|
-
// Auto-save generated assets before normalization strips inline data
|
|
1186
|
-
if (outbound.type === "node_update" &&
|
|
1187
|
-
outbound.status === "completed" &&
|
|
1188
|
-
outbound.result != null &&
|
|
1189
|
-
this.getNodeMetadata) {
|
|
1190
|
-
const meta = this.getNodeMetadata(nodeType);
|
|
1191
|
-
if (meta?.auto_save_asset) {
|
|
1192
|
-
try {
|
|
1193
|
-
await autoSaveAssets(outbound.result, {
|
|
1194
|
-
userId: this.userId ?? "1",
|
|
1195
|
-
workflowId: active.workflowId,
|
|
1196
|
-
jobId: active.jobId,
|
|
1197
|
-
nodeId: String(outbound.node_id ?? ""),
|
|
1198
|
-
storagePath: getAssetStoragePath()
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
catch (err) {
|
|
1202
|
-
log.warn("autoSaveAssets error", { error: String(err) });
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
// Materialize binary assets to temp URLs before sending over WebSocket
|
|
1207
|
-
if (outbound.type === "node_update" && outbound.result != null) {
|
|
1208
|
-
outbound.result = await active.context.normalizeOutputValue(outbound.result);
|
|
1209
|
-
}
|
|
1210
|
-
if (outbound.type === "output_update" && outbound.value != null) {
|
|
1211
|
-
outbound.value = await active.context.normalizeOutputValue(outbound.value);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
await this.sendMessage(outbound);
|
|
1215
|
-
if (outbound.type === "job_update") {
|
|
1216
|
-
const status = String(outbound.status ?? "");
|
|
1217
|
-
if (["completed", "failed", "cancelled", "error", "suspended"].includes(status)) {
|
|
1218
|
-
terminalSeen = true;
|
|
1219
|
-
if (outbound.result !== undefined) {
|
|
1220
|
-
terminalWithResultSeen = true;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (!active.finished) {
|
|
1226
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (!outputUpdateSeen && Object.keys(finalOutputs).length > 0) {
|
|
1230
|
-
await this.sendOutputUpdates(active, finalOutputs);
|
|
1231
|
-
}
|
|
1232
|
-
log.info("Job completed", { jobId: active.jobId, status: active.status });
|
|
1233
|
-
if (!terminalSeen ||
|
|
1234
|
-
(!terminalWithResultSeen && Object.keys(finalOutputs).length > 0)) {
|
|
1235
|
-
await this.sendMessage({
|
|
1236
|
-
type: "job_update",
|
|
1237
|
-
status: active.status,
|
|
1238
|
-
job_id: active.jobId,
|
|
1239
|
-
workflow_id: active.workflowId,
|
|
1240
|
-
error: active.error,
|
|
1241
|
-
result: { outputs: finalOutputs }
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
// Persist final job status
|
|
1245
|
-
try {
|
|
1246
|
-
const job = (await Job.get(active.jobId));
|
|
1247
|
-
if (job) {
|
|
1248
|
-
if (active.status === "completed") {
|
|
1249
|
-
job.markCompleted();
|
|
1250
|
-
}
|
|
1251
|
-
else if (active.status === "failed") {
|
|
1252
|
-
job.markFailed(active.error ?? "Unknown error");
|
|
1253
|
-
}
|
|
1254
|
-
else if (active.status === "cancelled") {
|
|
1255
|
-
job.markCancelled();
|
|
1256
|
-
}
|
|
1257
|
-
await job.save();
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
catch (error) {
|
|
1261
|
-
this.logError("job persistence (final status) failed", error);
|
|
1262
|
-
}
|
|
1263
|
-
this.activeJobs.delete(active.jobId);
|
|
1264
|
-
}
|
|
1265
|
-
async reconnectJob(jobId, workflowId) {
|
|
1266
|
-
const active = this.activeJobs.get(jobId);
|
|
1267
|
-
if (!active) {
|
|
1268
|
-
throw new Error(`Job ${jobId} not found`);
|
|
1269
|
-
}
|
|
1270
|
-
await this.sendMessage({
|
|
1271
|
-
type: "job_update",
|
|
1272
|
-
status: active.status,
|
|
1273
|
-
job_id: jobId,
|
|
1274
|
-
workflow_id: workflowId ?? active.workflowId
|
|
1275
|
-
});
|
|
1276
|
-
for (const status of Object.values(active.context.getNodeStatuses())) {
|
|
1277
|
-
await this.sendMessage({
|
|
1278
|
-
...status,
|
|
1279
|
-
job_id: jobId,
|
|
1280
|
-
workflow_id: workflowId ?? active.workflowId
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
for (const status of Object.values(active.context.getEdgeStatuses())) {
|
|
1284
|
-
await this.sendMessage({
|
|
1285
|
-
...status,
|
|
1286
|
-
job_id: jobId,
|
|
1287
|
-
workflow_id: workflowId ?? active.workflowId
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
async resumeJob(jobId, workflowId) {
|
|
1292
|
-
await this.reconnectJob(jobId, workflowId);
|
|
1293
|
-
}
|
|
1294
|
-
async cancelJob(jobId, workflowId) {
|
|
1295
|
-
if (!jobId) {
|
|
1296
|
-
return { error: "No job_id provided" };
|
|
1297
|
-
}
|
|
1298
|
-
const active = this.activeJobs.get(jobId);
|
|
1299
|
-
if (!active) {
|
|
1300
|
-
return {
|
|
1301
|
-
error: "Job not found or already completed",
|
|
1302
|
-
job_id: jobId,
|
|
1303
|
-
workflow_id: workflowId ?? ""
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
if (active.comfyHandle) {
|
|
1307
|
-
active.comfyHandle.cancel();
|
|
1308
|
-
}
|
|
1309
|
-
else if (active.runner) {
|
|
1310
|
-
active.runner.cancel();
|
|
1311
|
-
}
|
|
1312
|
-
active.status = "cancelled";
|
|
1313
|
-
// Do NOT set active.finished = true here. Let the runner's cancellation
|
|
1314
|
-
// propagate through executePromise's .finally() callback so that
|
|
1315
|
-
// streamJobMessages can drain remaining messages and persist job state.
|
|
1316
|
-
return {
|
|
1317
|
-
message: "Job cancellation requested",
|
|
1318
|
-
job_id: jobId,
|
|
1319
|
-
workflow_id: workflowId ?? active.workflowId ?? ""
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
getStatus(jobId) {
|
|
1323
|
-
if (jobId) {
|
|
1324
|
-
const active = this.activeJobs.get(jobId);
|
|
1325
|
-
if (!active) {
|
|
1326
|
-
return { status: "not_found", job_id: jobId };
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
status: active.status,
|
|
1330
|
-
job_id: active.jobId,
|
|
1331
|
-
workflow_id: active.workflowId
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
return {
|
|
1335
|
-
active_jobs: Array.from(this.activeJobs.values()).map((job) => ({
|
|
1336
|
-
job_id: job.jobId,
|
|
1337
|
-
workflow_id: job.workflowId,
|
|
1338
|
-
status: job.status
|
|
1339
|
-
}))
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
async clearModels() {
|
|
1343
|
-
return {
|
|
1344
|
-
message: "Model clearing is managed by provider implementations in TS runtime"
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
async ensureThreadExists(threadId) {
|
|
1348
|
-
const userId = this.userId ?? "1";
|
|
1349
|
-
if (!threadId) {
|
|
1350
|
-
const thread = await Thread.create({ user_id: userId, title: "" });
|
|
1351
|
-
return thread.id;
|
|
1352
|
-
}
|
|
1353
|
-
const existing = await Thread.find(userId, threadId);
|
|
1354
|
-
if (existing)
|
|
1355
|
-
return existing.id;
|
|
1356
|
-
const thread = await Thread.create({
|
|
1357
|
-
id: threadId,
|
|
1358
|
-
user_id: userId,
|
|
1359
|
-
title: ""
|
|
1360
|
-
});
|
|
1361
|
-
return thread.id;
|
|
1362
|
-
}
|
|
1363
|
-
dbMessageToProviderMessage(m) {
|
|
1364
|
-
const role = m.role;
|
|
1365
|
-
// Filter out non-standard roles (e.g. "agent_execution") that providers can't handle
|
|
1366
|
-
if (!role || !["user", "assistant", "system", "tool"].includes(role)) {
|
|
1367
|
-
return null;
|
|
1368
|
-
}
|
|
1369
|
-
const rawContent = Array.isArray(m.content)
|
|
1370
|
-
? resolveContentForProvider(m.content)
|
|
1371
|
-
: m.content;
|
|
1372
|
-
return {
|
|
1373
|
-
role,
|
|
1374
|
-
content: rawContent ?? "",
|
|
1375
|
-
toolCallId: typeof m.tool_call_id === "string" ? m.tool_call_id : null,
|
|
1376
|
-
toolCalls: Array.isArray(m.tool_calls)
|
|
1377
|
-
? m.tool_calls
|
|
1378
|
-
: null,
|
|
1379
|
-
threadId: m.thread_id
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Save a message dict to the database.
|
|
1384
|
-
* Mirrors Python's _save_message_to_db_async: pops id, type, user_id before create.
|
|
1385
|
-
*/
|
|
1386
|
-
async saveMessageToDb(messageData) {
|
|
1387
|
-
const data = { ...messageData };
|
|
1388
|
-
delete data.id;
|
|
1389
|
-
delete data.type;
|
|
1390
|
-
const threadId = typeof data.thread_id === "string" ? data.thread_id : "";
|
|
1391
|
-
delete data.thread_id;
|
|
1392
|
-
const userId = this.userId ?? "1";
|
|
1393
|
-
delete data.user_id;
|
|
1394
|
-
await Message.create({
|
|
1395
|
-
thread_id: threadId,
|
|
1396
|
-
user_id: userId,
|
|
1397
|
-
...data
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
/**
|
|
1401
|
-
* Recursively process tool results, handling asset-like objects.
|
|
1402
|
-
* Mirrors Python's RegularChatProcessor._process_tool_result().
|
|
1403
|
-
*
|
|
1404
|
-
* - Asset-like objects (have type + uri/data): materialized via storage
|
|
1405
|
-
* - Date/datetime: converted to ISO string
|
|
1406
|
-
* - Arrays/objects: recursed into
|
|
1407
|
-
* - Primitives: returned as-is
|
|
1408
|
-
*/
|
|
1409
|
-
async processToolResult(obj, ctx) {
|
|
1410
|
-
if (obj === null || obj === undefined)
|
|
1411
|
-
return obj;
|
|
1412
|
-
// Asset-like objects: { type: "image"|"audio"|"video"|..., uri?: string, data?: ... }
|
|
1413
|
-
if (typeof obj === "object" && !Array.isArray(obj)) {
|
|
1414
|
-
const record = obj;
|
|
1415
|
-
// Check if it's an asset-like object (has type + uri or data)
|
|
1416
|
-
if ("type" in record &&
|
|
1417
|
-
("uri" in record || "data" in record || "asset_id" in record)) {
|
|
1418
|
-
// Use ProcessingContext's normalizeOutputValue to handle asset materialization
|
|
1419
|
-
return ctx.normalizeOutputValue(record, "storage_url");
|
|
1420
|
-
}
|
|
1421
|
-
// Date objects
|
|
1422
|
-
if (obj instanceof Date) {
|
|
1423
|
-
return obj.toISOString();
|
|
1424
|
-
}
|
|
1425
|
-
// Regular objects — recurse into values
|
|
1426
|
-
const result = {};
|
|
1427
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1428
|
-
result[key] = await this.processToolResult(value, ctx);
|
|
1429
|
-
}
|
|
1430
|
-
return result;
|
|
1431
|
-
}
|
|
1432
|
-
// Arrays — recurse into items
|
|
1433
|
-
if (Array.isArray(obj)) {
|
|
1434
|
-
return Promise.all(obj.map((item) => this.processToolResult(item, ctx)));
|
|
1435
|
-
}
|
|
1436
|
-
// Uint8Array/Buffer — store as asset
|
|
1437
|
-
if (obj instanceof Uint8Array) {
|
|
1438
|
-
if (!ctx.storage)
|
|
1439
|
-
return obj;
|
|
1440
|
-
const key = `assets/${randomUUID()}.bin`;
|
|
1441
|
-
const uri = await ctx.storage.store(key, obj);
|
|
1442
|
-
return { type: "asset", uri };
|
|
1443
|
-
}
|
|
1444
|
-
// Primitives
|
|
1445
|
-
return obj;
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Query vector store collections and return concatenated context string.
|
|
1449
|
-
* Mirrors Python's RegularChatProcessor._query_collections().
|
|
1450
|
-
*/
|
|
1451
|
-
async queryCollections(collections, queryText, nResults = 5) {
|
|
1452
|
-
if (!collections.length || !queryText)
|
|
1453
|
-
return "";
|
|
1454
|
-
try {
|
|
1455
|
-
const { getVecStore } = await import("@nodetool-ai/vectorstore");
|
|
1456
|
-
const store = await getVecStore();
|
|
1457
|
-
const allResults = [];
|
|
1458
|
-
for (const collectionName of collections) {
|
|
1459
|
-
try {
|
|
1460
|
-
const collection = await store.getCollection({
|
|
1461
|
-
name: collectionName
|
|
1462
|
-
});
|
|
1463
|
-
const results = await collection.query({
|
|
1464
|
-
queryTexts: [queryText],
|
|
1465
|
-
nResults,
|
|
1466
|
-
include: ["documents", "metadatas"]
|
|
1467
|
-
});
|
|
1468
|
-
if (results.documents?.[0]?.length) {
|
|
1469
|
-
let collectionResults = `\n\n### Results from ${collectionName}:\n`;
|
|
1470
|
-
for (const doc of results.documents[0]) {
|
|
1471
|
-
if (!doc)
|
|
1472
|
-
continue;
|
|
1473
|
-
const preview = doc.length > 200 ? `${doc.slice(0, 200)}...` : doc;
|
|
1474
|
-
collectionResults += `\n- ${preview}`;
|
|
1475
|
-
}
|
|
1476
|
-
allResults.push(collectionResults);
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
catch (err) {
|
|
1480
|
-
log.warn("Collection query failed", {
|
|
1481
|
-
collection: collectionName,
|
|
1482
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
return allResults.join("\n");
|
|
1487
|
-
}
|
|
1488
|
-
catch (err) {
|
|
1489
|
-
log.warn("Vector store init failed", {
|
|
1490
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1491
|
-
});
|
|
1492
|
-
return "";
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Add collection context as a system message before the last user message.
|
|
1497
|
-
* Mirrors Python's RegularChatProcessor._add_collection_context().
|
|
1498
|
-
*/
|
|
1499
|
-
addCollectionContext(messages, collectionContext) {
|
|
1500
|
-
// Find the last user message index
|
|
1501
|
-
let lastUserIndex = -1;
|
|
1502
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1503
|
-
if (messages[i].role === "user") {
|
|
1504
|
-
lastUserIndex = i;
|
|
1505
|
-
break;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
if (lastUserIndex >= 0) {
|
|
1509
|
-
const contextMessage = {
|
|
1510
|
-
role: "system",
|
|
1511
|
-
content: `Context from knowledge base:\n${collectionContext}`,
|
|
1512
|
-
toolCallId: null,
|
|
1513
|
-
toolCalls: null,
|
|
1514
|
-
threadId: null
|
|
1515
|
-
};
|
|
1516
|
-
return [
|
|
1517
|
-
...messages.slice(0, lastUserIndex),
|
|
1518
|
-
contextMessage,
|
|
1519
|
-
...messages.slice(lastUserIndex)
|
|
1520
|
-
];
|
|
1521
|
-
}
|
|
1522
|
-
return messages;
|
|
1523
|
-
}
|
|
1524
|
-
/**
|
|
1525
|
-
* Handle an incoming chat message.
|
|
1526
|
-
*
|
|
1527
|
-
* Mirrors Python's full 3-layer flow:
|
|
1528
|
-
* handle_chat_message → handle_message_impl → process_messages
|
|
1529
|
-
* → _run_processor + RegularChatProcessor.process()
|
|
1530
|
-
*
|
|
1531
|
-
* The processor sends messages to a queue. _run_processor reads them:
|
|
1532
|
-
* - type === "message" → persist to DB AND forward to client
|
|
1533
|
-
* - anything else → forward to client only
|
|
1534
|
-
*
|
|
1535
|
-
* RegularChatProcessor.process():
|
|
1536
|
-
* 1. Prepend system prompt if first message isn't system role
|
|
1537
|
-
* 2. while True: messages_to_send = chat_history + unprocessed_messages
|
|
1538
|
-
* 3. Stream chunks (type: "chunk") — forwarded to client (not persisted)
|
|
1539
|
-
* 4. On tool call: build assistant Message + tool result Message (type: "message")
|
|
1540
|
-
* → persisted to DB AND forwarded to client
|
|
1541
|
-
* 5. If unprocessed_messages empty, break
|
|
1542
|
-
* 6. Send done chunk + final assistant Message
|
|
1543
|
-
*/
|
|
1544
|
-
async handleChatMessage(data, requestSeq) {
|
|
1545
|
-
const threadId = await this.ensureThreadExists(typeof data.thread_id === "string" ? data.thread_id : undefined);
|
|
1546
|
-
data.thread_id = threadId;
|
|
1547
|
-
// Apply defaults — matches Python's handle_chat_message
|
|
1548
|
-
if (!data.model)
|
|
1549
|
-
data.model = this.defaultModel;
|
|
1550
|
-
if (!data.provider)
|
|
1551
|
-
data.provider = this.defaultProvider;
|
|
1552
|
-
const providerId = data.provider;
|
|
1553
|
-
const model = data.model;
|
|
1554
|
-
const workflowId = typeof data.workflow_id === "string" ? data.workflow_id : null;
|
|
1555
|
-
const userId = this.userId ?? "1";
|
|
1556
|
-
log.debug("Chat message", { threadId, model, provider: providerId });
|
|
1557
|
-
// Save user message to DB — matches Python's _save_message_to_db_async(data)
|
|
1558
|
-
await this.saveMessageToDb(data);
|
|
1559
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
1560
|
-
return;
|
|
1561
|
-
if (!this.resolveProvider) {
|
|
1562
|
-
await this.sendMessage({
|
|
1563
|
-
type: "error",
|
|
1564
|
-
message: "No provider resolver configured",
|
|
1565
|
-
thread_id: threadId
|
|
1566
|
-
});
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
// Route to workflow processor when workflow_target or workflow_id is set — matches Python's handle_message_impl
|
|
1570
|
-
// This check comes BEFORE agent_mode, matching Python's routing priority.
|
|
1571
|
-
const workflowTarget = typeof data.workflow_target === "string" ? data.workflow_target : null;
|
|
1572
|
-
if (workflowTarget === "workflow" || workflowId) {
|
|
1573
|
-
await this.handleWorkflowMessage(data, requestSeq);
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1576
|
-
// Route to media generation when the client requests a text-to-image or
|
|
1577
|
-
// text-to-video turn. The composer attaches a `media_generation` field
|
|
1578
|
-
// with mode + params; when mode is a media mode we invoke the provider's
|
|
1579
|
-
// textToImage / textToVideo instead of a regular LLM round and return an
|
|
1580
|
-
// assistant message containing MessageImageContent / MessageVideoContent.
|
|
1581
|
-
const mediaGeneration = data.media_generation && typeof data.media_generation === "object"
|
|
1582
|
-
? data.media_generation
|
|
1583
|
-
: null;
|
|
1584
|
-
if (mediaGeneration &&
|
|
1585
|
-
typeof mediaGeneration.mode === "string" &&
|
|
1586
|
-
mediaGeneration.mode !== "chat") {
|
|
1587
|
-
await this.handleMediaGenerationMessage(data, mediaGeneration, requestSeq);
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
// Route to agent mode if requested — matches Python's handle_message_impl
|
|
1591
|
-
const agentMode = data.agent_mode === true || data.agent_mode === "true";
|
|
1592
|
-
if (agentMode) {
|
|
1593
|
-
await this.handleAgentMessage(data, requestSeq);
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
// Load history from DB, filter out agent_execution — matches Python's get_chat_history_from_db
|
|
1597
|
-
const [dbMessages] = await Message.paginate(threadId, { limit: 1000 });
|
|
1598
|
-
const chatHistory = [];
|
|
1599
|
-
for (const m of dbMessages) {
|
|
1600
|
-
const pm = this.dbMessageToProviderMessage(m);
|
|
1601
|
-
if (pm)
|
|
1602
|
-
chatHistory.push(pm);
|
|
1603
|
-
}
|
|
1604
|
-
const provider = await this.resolveProvider(providerId, userId);
|
|
1605
|
-
// Extract tool names from raw tool data — supports both string[] and object[]
|
|
1606
|
-
const rawTools = Array.isArray(data.tools) ? data.tools : [];
|
|
1607
|
-
const toolNames = rawTools
|
|
1608
|
-
.map((t) => {
|
|
1609
|
-
if (typeof t === "string")
|
|
1610
|
-
return t;
|
|
1611
|
-
const tool = t;
|
|
1612
|
-
return typeof tool.name === "string" ? tool.name : "";
|
|
1613
|
-
})
|
|
1614
|
-
.filter((n) => n.length > 0);
|
|
1615
|
-
log.info("Chat tools from client", {
|
|
1616
|
-
rawToolCount: rawTools.length,
|
|
1617
|
-
rawToolTypes: rawTools.map((t) => typeof t),
|
|
1618
|
-
toolNames
|
|
1619
|
-
});
|
|
1620
|
-
// Resolve server-side Tool instances for execution
|
|
1621
|
-
let serverTools = [];
|
|
1622
|
-
if (toolNames.length > 0 && this.resolveTools) {
|
|
1623
|
-
serverTools = await this.resolveTools(toolNames, userId);
|
|
1624
|
-
}
|
|
1625
|
-
const serverToolMap = new Map(serverTools.map((t) => [t.name, t]));
|
|
1626
|
-
log.info("Resolved server tools", {
|
|
1627
|
-
requested: toolNames,
|
|
1628
|
-
resolved: serverTools.map((t) => t.name),
|
|
1629
|
-
hasResolveTools: !!this.resolveTools
|
|
1630
|
-
});
|
|
1631
|
-
// Build provider-format tool schemas from resolved Tool instances + client tools
|
|
1632
|
-
const providerToolSchemas = serverTools.map((t) => t.toProviderTool());
|
|
1633
|
-
// Only include client tools (ui_*) when a workflow is active
|
|
1634
|
-
const clientToolNames = workflowId
|
|
1635
|
-
? Object.keys(this.clientToolsManifest)
|
|
1636
|
-
: [];
|
|
1637
|
-
if (workflowId) {
|
|
1638
|
-
for (const [name, manifest] of Object.entries(this.clientToolsManifest)) {
|
|
1639
|
-
providerToolSchemas.push({
|
|
1640
|
-
name,
|
|
1641
|
-
description: typeof manifest.description === "string"
|
|
1642
|
-
? manifest.description
|
|
1643
|
-
: undefined,
|
|
1644
|
-
inputSchema: typeof manifest.inputSchema === "object"
|
|
1645
|
-
? manifest.inputSchema
|
|
1646
|
-
: undefined
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
log.info("Provider tool schemas", {
|
|
1651
|
-
serverToolCount: serverTools.length,
|
|
1652
|
-
clientToolCount: clientToolNames.length,
|
|
1653
|
-
clientTools: clientToolNames,
|
|
1654
|
-
totalSchemas: providerToolSchemas.length,
|
|
1655
|
-
schemaNames: providerToolSchemas.map((t) => t.name)
|
|
1656
|
-
});
|
|
1657
|
-
// Create a processing context for tool execution
|
|
1658
|
-
const chatWorkspaceDir = workflowId && this.workspaceResolver
|
|
1659
|
-
? await this.workspaceResolver(workflowId, userId)
|
|
1660
|
-
: tmpdir();
|
|
1661
|
-
const ctx = createRuntimeContext({
|
|
1662
|
-
jobId: randomUUID(),
|
|
1663
|
-
userId,
|
|
1664
|
-
workspaceDir: chatWorkspaceDir
|
|
1665
|
-
});
|
|
1666
|
-
// Prepend system prompt if first message isn't system role — matches Python
|
|
1667
|
-
if (chatHistory.length === 0 || chatHistory[0].role !== "system") {
|
|
1668
|
-
chatHistory.unshift({
|
|
1669
|
-
role: "system",
|
|
1670
|
-
content: REGULAR_SYSTEM_PROMPT,
|
|
1671
|
-
toolCallId: null,
|
|
1672
|
-
toolCalls: null,
|
|
1673
|
-
threadId: null
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
// Query collections for RAG context — matches Python's _query_collections()
|
|
1677
|
-
const collections = Array.isArray(data.collections)
|
|
1678
|
-
? data.collections.filter((c) => typeof c === "string")
|
|
1679
|
-
: [];
|
|
1680
|
-
const userContent = this.extractTextContent(data.content);
|
|
1681
|
-
let collectionContext = "";
|
|
1682
|
-
if (collections.length > 0 && userContent) {
|
|
1683
|
-
collectionContext = await this.queryCollections(collections, userContent);
|
|
1684
|
-
if (collectionContext) {
|
|
1685
|
-
log.debug("Retrieved collection context", {
|
|
1686
|
-
chars: collectionContext.length
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
let content = "";
|
|
1691
|
-
let unprocessedMessages = [];
|
|
1692
|
-
// Tool execution loop — mirrors Python's RegularChatProcessor.process()
|
|
1693
|
-
const MAX_TOOL_ROUNDS = 10;
|
|
1694
|
-
let toolRound = 0;
|
|
1695
|
-
const shouldIncludeTools = true;
|
|
1696
|
-
try {
|
|
1697
|
-
while (true) {
|
|
1698
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
1699
|
-
return;
|
|
1700
|
-
let messagesToSend = [...chatHistory, ...unprocessedMessages];
|
|
1701
|
-
unprocessedMessages = [];
|
|
1702
|
-
// Add collection context on first iteration — matches Python
|
|
1703
|
-
if (collectionContext) {
|
|
1704
|
-
messagesToSend = this.addCollectionContext(messagesToSend, collectionContext);
|
|
1705
|
-
collectionContext = ""; // Clear after first use
|
|
1706
|
-
}
|
|
1707
|
-
const stream = provider.generateMessagesTraced({
|
|
1708
|
-
messages: messagesToSend,
|
|
1709
|
-
model,
|
|
1710
|
-
tools: shouldIncludeTools && providerToolSchemas.length > 0
|
|
1711
|
-
? providerToolSchemas
|
|
1712
|
-
: undefined,
|
|
1713
|
-
threadId,
|
|
1714
|
-
onToolCall: shouldIncludeTools && providerToolSchemas.length > 0
|
|
1715
|
-
? async (name, args) => {
|
|
1716
|
-
let toolResult;
|
|
1717
|
-
const serverTool = serverToolMap.get(name);
|
|
1718
|
-
if (serverTool) {
|
|
1719
|
-
try {
|
|
1720
|
-
toolResult = await serverTool.process(ctx, args);
|
|
1721
|
-
}
|
|
1722
|
-
catch (err) {
|
|
1723
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1724
|
-
toolResult = { error: errMsg };
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
else if (this.clientToolsManifest[name]) {
|
|
1728
|
-
// Client-side tool — send to UI via ToolBridge
|
|
1729
|
-
const callId = `call_${Date.now()}`;
|
|
1730
|
-
await this.sendMessage({
|
|
1731
|
-
type: "tool_call",
|
|
1732
|
-
thread_id: threadId,
|
|
1733
|
-
tool_call_id: callId,
|
|
1734
|
-
name,
|
|
1735
|
-
args
|
|
1736
|
-
});
|
|
1737
|
-
const clientResult = await this.toolBridge.createWaiter(callId, 300_000);
|
|
1738
|
-
toolResult =
|
|
1739
|
-
clientResult.result ?? clientResult.content ?? clientResult;
|
|
1740
|
-
}
|
|
1741
|
-
else {
|
|
1742
|
-
toolResult = { error: `Tool "${name}" not available` };
|
|
1743
|
-
}
|
|
1744
|
-
const processed = await this.processToolResult(toolResult, ctx);
|
|
1745
|
-
return typeof processed === "string"
|
|
1746
|
-
? processed
|
|
1747
|
-
: JSON.stringify(processed);
|
|
1748
|
-
}
|
|
1749
|
-
: undefined
|
|
1750
|
-
});
|
|
1751
|
-
const pendingToolCalls = [];
|
|
1752
|
-
for await (const item of stream) {
|
|
1753
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
1754
|
-
return;
|
|
1755
|
-
if ("type" in item && item.type === "chunk") {
|
|
1756
|
-
// --- Text chunk --- forward to client (not persisted)
|
|
1757
|
-
const chunk = item;
|
|
1758
|
-
const text = chunk.content ?? "";
|
|
1759
|
-
content += text;
|
|
1760
|
-
// Set thread_id if not already set — matches Python
|
|
1761
|
-
if (!chunk.thread_id)
|
|
1762
|
-
chunk.thread_id = threadId;
|
|
1763
|
-
await this.sendMessage({ ...chunk });
|
|
1764
|
-
}
|
|
1765
|
-
else if ("name" in item && "id" in item) {
|
|
1766
|
-
// --- Tool call from provider (collect, don't execute yet) ---
|
|
1767
|
-
const tc = item;
|
|
1768
|
-
log.info("Tool call", { tool: tc.name, args: tc.args });
|
|
1769
|
-
pendingToolCalls.push({ tc });
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
// Build ONE assistant message with ALL tool calls (OpenAI requires this)
|
|
1773
|
-
if (pendingToolCalls.length > 0) {
|
|
1774
|
-
const allToolCalls = pendingToolCalls.map(({ tc }) => ({
|
|
1775
|
-
id: tc.id,
|
|
1776
|
-
name: tc.name,
|
|
1777
|
-
args: tc.args,
|
|
1778
|
-
result: null
|
|
1779
|
-
}));
|
|
1780
|
-
const assistantMsgData = {
|
|
1781
|
-
type: "message",
|
|
1782
|
-
role: "assistant",
|
|
1783
|
-
content: content || null,
|
|
1784
|
-
tool_calls: allToolCalls,
|
|
1785
|
-
thread_id: threadId,
|
|
1786
|
-
workflow_id: workflowId,
|
|
1787
|
-
provider: providerId,
|
|
1788
|
-
model
|
|
1789
|
-
};
|
|
1790
|
-
await this.saveMessageToDb(assistantMsgData);
|
|
1791
|
-
await this.sendMessage(assistantMsgData);
|
|
1792
|
-
unprocessedMessages.push({
|
|
1793
|
-
role: "assistant",
|
|
1794
|
-
content: content || null,
|
|
1795
|
-
toolCalls: pendingToolCalls.map(({ tc }) => ({
|
|
1796
|
-
id: tc.id,
|
|
1797
|
-
name: tc.name,
|
|
1798
|
-
args: tc.args
|
|
1799
|
-
})),
|
|
1800
|
-
toolCallId: null,
|
|
1801
|
-
threadId
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
|
-
// Phase 2: Execute all collected tool calls in parallel
|
|
1805
|
-
if (pendingToolCalls.length > 0) {
|
|
1806
|
-
const executeOne = async ({ tc }) => {
|
|
1807
|
-
try {
|
|
1808
|
-
let toolResult;
|
|
1809
|
-
const serverTool = serverToolMap.get(tc.name);
|
|
1810
|
-
if (serverTool) {
|
|
1811
|
-
try {
|
|
1812
|
-
toolResult = await serverTool.process(ctx, tc.args);
|
|
1813
|
-
}
|
|
1814
|
-
catch (err) {
|
|
1815
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1816
|
-
log.error("Tool execution failed", {
|
|
1817
|
-
tool: tc.name,
|
|
1818
|
-
error: errMsg
|
|
1819
|
-
});
|
|
1820
|
-
toolResult = { error: errMsg };
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
else if (this.clientToolsManifest[tc.name]) {
|
|
1824
|
-
// Client-side tool via ToolBridge
|
|
1825
|
-
await this.sendMessage({
|
|
1826
|
-
type: "tool_call",
|
|
1827
|
-
thread_id: threadId,
|
|
1828
|
-
tool_call_id: tc.id,
|
|
1829
|
-
name: tc.name,
|
|
1830
|
-
args: tc.args
|
|
1831
|
-
});
|
|
1832
|
-
const clientResult = await this.toolBridge.createWaiter(tc.id, 300_000);
|
|
1833
|
-
toolResult =
|
|
1834
|
-
clientResult.result ?? clientResult.content ?? clientResult;
|
|
1835
|
-
}
|
|
1836
|
-
else {
|
|
1837
|
-
toolResult = { error: `Tool "${tc.name}" not available` };
|
|
1838
|
-
}
|
|
1839
|
-
// Process tool result — handle asset-like objects, dates, etc.
|
|
1840
|
-
const processedResult = await this.processToolResult(toolResult, ctx);
|
|
1841
|
-
const toolResultJson = JSON.stringify(processedResult);
|
|
1842
|
-
return { tc, toolResultJson };
|
|
1843
|
-
}
|
|
1844
|
-
catch (err) {
|
|
1845
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1846
|
-
log.error("Tool executeOne failed", {
|
|
1847
|
-
tool: tc.name,
|
|
1848
|
-
error: errMsg
|
|
1849
|
-
});
|
|
1850
|
-
return { tc, toolResultJson: JSON.stringify({ error: errMsg }) };
|
|
1851
|
-
}
|
|
1852
|
-
};
|
|
1853
|
-
const results = await Promise.all(pendingToolCalls.map(executeOne));
|
|
1854
|
-
for (const { tc, toolResultJson } of results) {
|
|
1855
|
-
// Build tool result Message
|
|
1856
|
-
const toolMsgData = {
|
|
1857
|
-
type: "message",
|
|
1858
|
-
role: "tool",
|
|
1859
|
-
tool_call_id: tc.id,
|
|
1860
|
-
name: tc.name,
|
|
1861
|
-
content: toolResultJson,
|
|
1862
|
-
thread_id: threadId,
|
|
1863
|
-
workflow_id: workflowId,
|
|
1864
|
-
provider: providerId,
|
|
1865
|
-
model
|
|
1866
|
-
};
|
|
1867
|
-
await this.saveMessageToDb(toolMsgData);
|
|
1868
|
-
await this.sendMessage(toolMsgData);
|
|
1869
|
-
// Add tool result to unprocessed for next provider round
|
|
1870
|
-
unprocessedMessages.push({
|
|
1871
|
-
role: "tool",
|
|
1872
|
-
content: toolResultJson,
|
|
1873
|
-
toolCallId: tc.id,
|
|
1874
|
-
toolCalls: null,
|
|
1875
|
-
threadId
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
// If no unprocessed messages, generation is complete — matches Python's break condition
|
|
1880
|
-
if (unprocessedMessages.length === 0) {
|
|
1881
|
-
break;
|
|
1882
|
-
}
|
|
1883
|
-
// Reset content for next tool round so the final message only contains
|
|
1884
|
-
// text from the last generation pass (prior rounds were already streamed).
|
|
1885
|
-
content = "";
|
|
1886
|
-
toolRound++;
|
|
1887
|
-
if (toolRound >= MAX_TOOL_ROUNDS) {
|
|
1888
|
-
log.warn("Max tool rounds reached, stopping tool loop", {
|
|
1889
|
-
rounds: toolRound
|
|
1890
|
-
});
|
|
1891
|
-
break;
|
|
1892
|
-
}
|
|
1893
|
-
log.debug("Unprocessed messages", {
|
|
1894
|
-
count: unprocessedMessages.length,
|
|
1895
|
-
round: toolRound
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
// Log provider call for cost tracking — matches Python's _log_provider_call()
|
|
1899
|
-
await this._logProviderCall(userId, provider, providerId, model, workflowId);
|
|
1900
|
-
// Signal completion — matches Python's done chunk + final assistant Message
|
|
1901
|
-
await this.sendMessage({
|
|
1902
|
-
type: "chunk",
|
|
1903
|
-
content: "",
|
|
1904
|
-
done: true,
|
|
1905
|
-
thread_id: threadId
|
|
1906
|
-
});
|
|
1907
|
-
// Final assistant message — persisted and forwarded (type: "message")
|
|
1908
|
-
const finalMsgData = {
|
|
1909
|
-
type: "message",
|
|
1910
|
-
role: "assistant",
|
|
1911
|
-
content: content || null,
|
|
1912
|
-
thread_id: threadId,
|
|
1913
|
-
workflow_id: workflowId,
|
|
1914
|
-
provider: providerId,
|
|
1915
|
-
model
|
|
1916
|
-
};
|
|
1917
|
-
await this.saveMessageToDb(finalMsgData);
|
|
1918
|
-
await this.sendMessage(finalMsgData);
|
|
1919
|
-
log.debug("Chat complete", { threadId, chars: content.length });
|
|
1920
|
-
}
|
|
1921
|
-
catch (err) {
|
|
1922
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1923
|
-
log.error("Chat processing error", { threadId, error: errMsg });
|
|
1924
|
-
// Detect error type — matches Python's separate ConnectError / HTTPStatusError handlers
|
|
1925
|
-
let errorType = "error";
|
|
1926
|
-
let statusCode;
|
|
1927
|
-
let formattedMsg = errMsg;
|
|
1928
|
-
// Connection errors (ECONNREFUSED, ENOTFOUND, etc.)
|
|
1929
|
-
if (errMsg.includes("ECONNREFUSED") ||
|
|
1930
|
-
errMsg.includes("ENOTFOUND") ||
|
|
1931
|
-
errMsg.includes("fetch failed") ||
|
|
1932
|
-
errMsg.includes("nodename nor servname")) {
|
|
1933
|
-
errorType = "connection_error";
|
|
1934
|
-
if (errMsg.includes("ENOTFOUND") ||
|
|
1935
|
-
errMsg.includes("nodename nor servname")) {
|
|
1936
|
-
formattedMsg =
|
|
1937
|
-
"Connection error: Unable to resolve hostname. Please check your network connection and API endpoint configuration.";
|
|
1938
|
-
}
|
|
1939
|
-
else {
|
|
1940
|
-
formattedMsg = `Connection error: ${errMsg}`;
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
// HTTP status errors — check for status code in error
|
|
1944
|
-
else if (err && typeof err === "object" && "status" in err) {
|
|
1945
|
-
const status = err.status;
|
|
1946
|
-
errorType = "http_status_error";
|
|
1947
|
-
statusCode = status;
|
|
1948
|
-
// Try to extract error message from response body
|
|
1949
|
-
let bodyMsg = null;
|
|
1950
|
-
try {
|
|
1951
|
-
if ("body" in err || "response" in err) {
|
|
1952
|
-
const body = err.body ?? err.response;
|
|
1953
|
-
if (body && typeof body === "object" && "error" in body) {
|
|
1954
|
-
const errorDetail = body.error;
|
|
1955
|
-
if (typeof errorDetail === "object" &&
|
|
1956
|
-
errorDetail &&
|
|
1957
|
-
"message" in errorDetail) {
|
|
1958
|
-
bodyMsg = String(errorDetail.message);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
catch {
|
|
1964
|
-
// Intentional: best-effort extraction of error message from response body
|
|
1965
|
-
}
|
|
1966
|
-
if (bodyMsg) {
|
|
1967
|
-
formattedMsg = bodyMsg;
|
|
1968
|
-
}
|
|
1969
|
-
else if (status === 400) {
|
|
1970
|
-
formattedMsg = `Bad request: ${errMsg}`;
|
|
1971
|
-
}
|
|
1972
|
-
else if (status === 401) {
|
|
1973
|
-
formattedMsg = "Authentication failed: Invalid API key or token";
|
|
1974
|
-
}
|
|
1975
|
-
else if (status === 403) {
|
|
1976
|
-
formattedMsg =
|
|
1977
|
-
"Access forbidden: You don't have permission for this resource";
|
|
1978
|
-
}
|
|
1979
|
-
else if (status === 404) {
|
|
1980
|
-
formattedMsg = "Not found: The requested resource was not found";
|
|
1981
|
-
}
|
|
1982
|
-
else if (status === 429) {
|
|
1983
|
-
formattedMsg = "Rate limited: Too many requests, please slow down";
|
|
1984
|
-
}
|
|
1985
|
-
else if (status >= 500) {
|
|
1986
|
-
formattedMsg = `Server error (${status}): The service is temporarily unavailable`;
|
|
1987
|
-
}
|
|
1988
|
-
else {
|
|
1989
|
-
formattedMsg = `HTTP error (${status}): ${errMsg}`;
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
await this.sendMessage({
|
|
1993
|
-
type: "error",
|
|
1994
|
-
message: formattedMsg,
|
|
1995
|
-
error_type: errorType,
|
|
1996
|
-
...(statusCode !== undefined ? { status_code: statusCode } : {}),
|
|
1997
|
-
thread_id: threadId,
|
|
1998
|
-
workflow_id: workflowId
|
|
1999
|
-
});
|
|
2000
|
-
// Signal completion even on error — matches Python
|
|
2001
|
-
await this.sendMessage({
|
|
2002
|
-
type: "chunk",
|
|
2003
|
-
content: "",
|
|
2004
|
-
done: true,
|
|
2005
|
-
thread_id: threadId
|
|
2006
|
-
});
|
|
2007
|
-
const errorMsgData = {
|
|
2008
|
-
type: "message",
|
|
2009
|
-
role: "assistant",
|
|
2010
|
-
content: errorType === "connection_error"
|
|
2011
|
-
? `I encountered a connection error: ${formattedMsg}. Please check your network connection and try again.`
|
|
2012
|
-
: errorType === "http_status_error"
|
|
2013
|
-
? `I encountered an API error (HTTP ${statusCode}): ${formattedMsg}`
|
|
2014
|
-
: `I encountered an error: ${formattedMsg}`,
|
|
2015
|
-
thread_id: threadId,
|
|
2016
|
-
workflow_id: workflowId,
|
|
2017
|
-
provider: providerId,
|
|
2018
|
-
model
|
|
2019
|
-
};
|
|
2020
|
-
await this.saveMessageToDb(errorMsgData);
|
|
2021
|
-
await this.sendMessage(errorMsgData);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* Log a provider call for cost tracking — mirrors Python's _log_provider_call().
|
|
2026
|
-
* Best-effort: never throws, logs warnings on failure.
|
|
2027
|
-
*/
|
|
2028
|
-
async _logProviderCall(userId, provider, providerId, model, workflowId) {
|
|
2029
|
-
if (!providerId || !model) {
|
|
2030
|
-
log.warn("Cannot log provider call: missing provider or model");
|
|
2031
|
-
return;
|
|
2032
|
-
}
|
|
2033
|
-
try {
|
|
2034
|
-
const cost = provider.cost;
|
|
2035
|
-
await Prediction.create({
|
|
2036
|
-
user_id: userId,
|
|
2037
|
-
provider: providerId,
|
|
2038
|
-
model,
|
|
2039
|
-
cost,
|
|
2040
|
-
workflow_id: workflowId,
|
|
2041
|
-
status: "completed",
|
|
2042
|
-
node_id: ""
|
|
2043
|
-
});
|
|
2044
|
-
log.debug("Logged provider call", { provider: providerId, model, cost });
|
|
2045
|
-
}
|
|
2046
|
-
catch (err) {
|
|
2047
|
-
if (err instanceof TypeError || err instanceof ReferenceError) {
|
|
2048
|
-
log.warn("Failed to log provider call due to invalid data", {
|
|
2049
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2050
|
-
});
|
|
2051
|
-
}
|
|
2052
|
-
else {
|
|
2053
|
-
log.error("Unexpected error logging provider call", {
|
|
2054
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2055
|
-
});
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
/**
|
|
2060
|
-
* Detect message input node names from a workflow graph.
|
|
2061
|
-
* Mirrors Python's WorkflowMessageProcessor._detect_message_input_names().
|
|
2062
|
-
*
|
|
2063
|
-
* Scans graph nodes for types ending in .MessageInput / .MessageListInput
|
|
2064
|
-
* and returns their data.name values.
|
|
2065
|
-
*/
|
|
2066
|
-
detectMessageInputNames(graph) {
|
|
2067
|
-
let messageName = null;
|
|
2068
|
-
let messagesName = null;
|
|
2069
|
-
for (const node of graph.nodes) {
|
|
2070
|
-
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
2071
|
-
const data = typeof node.data === "object" && node.data !== null
|
|
2072
|
-
? node.data
|
|
2073
|
-
: {};
|
|
2074
|
-
const nodeName = typeof data.name === "string" ? data.name.trim() : "";
|
|
2075
|
-
if (!nodeName)
|
|
2076
|
-
continue;
|
|
2077
|
-
if (messageName === null &&
|
|
2078
|
-
(nodeType === "nodetool.input.MessageInput" ||
|
|
2079
|
-
nodeType.endsWith(".MessageInput"))) {
|
|
2080
|
-
messageName = nodeName;
|
|
2081
|
-
}
|
|
2082
|
-
if (messagesName === null &&
|
|
2083
|
-
(nodeType === "nodetool.input.MessageListInput" ||
|
|
2084
|
-
nodeType.endsWith(".MessageListInput"))) {
|
|
2085
|
-
messagesName = nodeName;
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return { messageName, messagesName };
|
|
2089
|
-
}
|
|
2090
|
-
/**
|
|
2091
|
-
* Convert workflow result dict into a response message with typed content.
|
|
2092
|
-
* Mirrors Python's WorkflowMessageProcessor._create_response_message().
|
|
2093
|
-
*
|
|
2094
|
-
* Converts outputs to MessageContent items:
|
|
2095
|
-
* - string → { type: "text", text }
|
|
2096
|
-
* - list → { type: "text", text: joined }
|
|
2097
|
-
* - dict with type "image"/"video"/"audio" → media content
|
|
2098
|
-
* - other → { type: "text", text: stringified }
|
|
2099
|
-
*/
|
|
2100
|
-
createWorkflowResponseContent(result) {
|
|
2101
|
-
const content = [];
|
|
2102
|
-
for (const [, value] of Object.entries(result)) {
|
|
2103
|
-
if (value === null || value === undefined)
|
|
2104
|
-
continue;
|
|
2105
|
-
if (typeof value === "string") {
|
|
2106
|
-
content.push({ type: "text", text: value });
|
|
2107
|
-
}
|
|
2108
|
-
else if (Array.isArray(value)) {
|
|
2109
|
-
content.push({ type: "text", text: value.map(String).join(" ") });
|
|
2110
|
-
}
|
|
2111
|
-
else if (typeof value === "object") {
|
|
2112
|
-
const obj = value;
|
|
2113
|
-
const assetType = typeof obj.type === "string" ? obj.type : "";
|
|
2114
|
-
if (assetType === "image") {
|
|
2115
|
-
content.push({
|
|
2116
|
-
type: "image",
|
|
2117
|
-
image: { uri: obj.uri, asset_id: obj.asset_id, data: obj.data }
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
else if (assetType === "video") {
|
|
2121
|
-
content.push({
|
|
2122
|
-
type: "video",
|
|
2123
|
-
video: { uri: obj.uri, asset_id: obj.asset_id, data: obj.data }
|
|
2124
|
-
});
|
|
2125
|
-
}
|
|
2126
|
-
else if (assetType === "audio") {
|
|
2127
|
-
content.push({
|
|
2128
|
-
type: "audio",
|
|
2129
|
-
audio: { uri: obj.uri, asset_id: obj.asset_id, data: obj.data }
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
else {
|
|
2133
|
-
content.push({ type: "text", text: JSON.stringify(obj) });
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
else {
|
|
2137
|
-
content.push({ type: "text", text: String(value) });
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
if (content.length === 0) {
|
|
2141
|
-
content.push({ type: "text", text: "Workflow completed successfully." });
|
|
2142
|
-
}
|
|
2143
|
-
return content;
|
|
2144
|
-
}
|
|
2145
|
-
/**
|
|
2146
|
-
* Handle a chat message that targets a workflow.
|
|
2147
|
-
*
|
|
2148
|
-
* Mirrors Python's process_messages_for_workflow → WorkflowMessageProcessor/
|
|
2149
|
-
* ChatWorkflowMessageProcessor flow:
|
|
2150
|
-
* 1. Load workflow from DB
|
|
2151
|
-
* 2. Detect message input node names from graph
|
|
2152
|
-
* 3. Prepare params (serialized message + history)
|
|
2153
|
-
* 4. Run workflow via WorkflowRunner
|
|
2154
|
-
* 5. Stream events (job_update, node_update, output_update)
|
|
2155
|
-
* 6. Collect output_update results
|
|
2156
|
-
* 7. Send done chunk + response message with typed content
|
|
2157
|
-
*/
|
|
2158
|
-
/**
|
|
2159
|
-
* Handle a chat_message with a `media_generation` payload by invoking the
|
|
2160
|
-
* selected provider's textToImage / textToVideo API, storing the resulting
|
|
2161
|
-
* asset(s), and returning them to the client as an assistant `Message`
|
|
2162
|
-
* whose `content` is an array of `MessageImageContent` / `MessageVideoContent`
|
|
2163
|
-
* blocks.
|
|
2164
|
-
*
|
|
2165
|
-
* The generated bytes are persisted via `ctx.storage.store()` so each
|
|
2166
|
-
* output receives a stable URI the client can resolve as a server asset.
|
|
2167
|
-
* The `media_generation` echo on the assistant message lets the UI render
|
|
2168
|
-
* the generation header (model, variation count, resolution, etc.) in the
|
|
2169
|
-
* conversation stream.
|
|
2170
|
-
*/
|
|
2171
|
-
async handleMediaGenerationMessage(data, mediaGeneration, requestSeq) {
|
|
2172
|
-
const threadId = typeof data.thread_id === "string" ? data.thread_id : "";
|
|
2173
|
-
const workflowId = typeof data.workflow_id === "string" ? data.workflow_id : null;
|
|
2174
|
-
const userId = this.userId ?? "1";
|
|
2175
|
-
const mode = String(mediaGeneration.mode ?? "");
|
|
2176
|
-
const providerId = String(mediaGeneration.provider ?? data.provider ?? this.defaultProvider);
|
|
2177
|
-
const modelId = String(mediaGeneration.model ?? data.model ?? this.defaultModel);
|
|
2178
|
-
const prompt = this.extractTextContent(data.content);
|
|
2179
|
-
log.info("Media generation", {
|
|
2180
|
-
threadId,
|
|
2181
|
-
mode,
|
|
2182
|
-
provider: providerId,
|
|
2183
|
-
model: modelId,
|
|
2184
|
-
promptLen: prompt.length
|
|
2185
|
-
});
|
|
2186
|
-
if (!this.resolveProvider) {
|
|
2187
|
-
await this.sendMessage({
|
|
2188
|
-
type: "error",
|
|
2189
|
-
message: "No provider resolver configured",
|
|
2190
|
-
thread_id: threadId
|
|
2191
|
-
});
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
|
-
if (!modelId || modelId === "undefined") {
|
|
2195
|
-
await this.sendMessage({
|
|
2196
|
-
type: "error",
|
|
2197
|
-
message: `Please select a ${mode} model before generating`,
|
|
2198
|
-
thread_id: threadId
|
|
2199
|
-
});
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
if (!prompt) {
|
|
2203
|
-
await this.sendMessage({
|
|
2204
|
-
type: "error",
|
|
2205
|
-
message: "Please enter a prompt",
|
|
2206
|
-
thread_id: threadId
|
|
2207
|
-
});
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
2211
|
-
return;
|
|
2212
|
-
const provider = await this.resolveProvider(providerId, userId);
|
|
2213
|
-
// Wire up progress forwarding so provider.emitMessage() reaches the client.
|
|
2214
|
-
provider.setMessageEmitter((msg) => void this.sendMessage(msg));
|
|
2215
|
-
// Store generated media as a proper Asset record and return the
|
|
2216
|
-
// asset ID. The DB message stores only `asset_id` — URLs are
|
|
2217
|
-
// resolved at serve time by resolveContentUrls / sendMessage.
|
|
2218
|
-
const storagePath = getAssetStoragePath();
|
|
2219
|
-
const storeMediaAsset = async (bytes, contentType, ext) => {
|
|
2220
|
-
const { join } = await import("node:path");
|
|
2221
|
-
const { writeFile: fsWrite, mkdir: fsMkdir } = await import("node:fs/promises");
|
|
2222
|
-
const asset = new Asset({
|
|
2223
|
-
user_id: userId,
|
|
2224
|
-
workflow_id: workflowId ?? null,
|
|
2225
|
-
name: `${mode}_${Date.now()}`,
|
|
2226
|
-
content_type: contentType,
|
|
2227
|
-
parent_id: null
|
|
2228
|
-
});
|
|
2229
|
-
const fileName = `${asset.id}.${ext}`;
|
|
2230
|
-
await fsMkdir(storagePath, { recursive: true });
|
|
2231
|
-
await fsWrite(join(storagePath, fileName), bytes);
|
|
2232
|
-
asset.size = bytes.length;
|
|
2233
|
-
await asset.save();
|
|
2234
|
-
return asset.id;
|
|
2235
|
-
};
|
|
2236
|
-
try {
|
|
2237
|
-
if (mode === "image") {
|
|
2238
|
-
const variations = Math.max(1, Math.min(Number(mediaGeneration.variations ?? 1), 8));
|
|
2239
|
-
const width = typeof mediaGeneration.width === "number"
|
|
2240
|
-
? mediaGeneration.width
|
|
2241
|
-
: undefined;
|
|
2242
|
-
const height = typeof mediaGeneration.height === "number"
|
|
2243
|
-
? mediaGeneration.height
|
|
2244
|
-
: undefined;
|
|
2245
|
-
const imageModel = {
|
|
2246
|
-
id: modelId,
|
|
2247
|
-
name: modelId,
|
|
2248
|
-
provider: providerId
|
|
2249
|
-
};
|
|
2250
|
-
const params = {
|
|
2251
|
-
model: imageModel,
|
|
2252
|
-
prompt,
|
|
2253
|
-
width,
|
|
2254
|
-
height
|
|
2255
|
-
};
|
|
2256
|
-
// Surface a progress chunk so the UI can show the request flight
|
|
2257
|
-
await this.sendMessage({
|
|
2258
|
-
type: "chunk",
|
|
2259
|
-
thread_id: threadId,
|
|
2260
|
-
content: "",
|
|
2261
|
-
content_type: "text",
|
|
2262
|
-
content_metadata: { media_generation: mediaGeneration },
|
|
2263
|
-
done: false
|
|
2264
|
-
});
|
|
2265
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
2266
|
-
return;
|
|
2267
|
-
const imageBytesList = await provider.textToImages(params, variations);
|
|
2268
|
-
const imageContents = [];
|
|
2269
|
-
for (const bytes of imageBytesList) {
|
|
2270
|
-
const assetId = await storeMediaAsset(bytes, "image/png", "png");
|
|
2271
|
-
imageContents.push({
|
|
2272
|
-
type: "image_url",
|
|
2273
|
-
image: { type: "image", asset_id: assetId, mimeType: "image/png" }
|
|
2274
|
-
});
|
|
2275
|
-
}
|
|
2276
|
-
await this.sendMessage({
|
|
2277
|
-
type: "chunk",
|
|
2278
|
-
thread_id: threadId,
|
|
2279
|
-
content: "",
|
|
2280
|
-
done: true
|
|
2281
|
-
});
|
|
2282
|
-
const assistantMsgData = {
|
|
2283
|
-
type: "message",
|
|
2284
|
-
role: "assistant",
|
|
2285
|
-
content: imageContents,
|
|
2286
|
-
thread_id: threadId,
|
|
2287
|
-
workflow_id: workflowId,
|
|
2288
|
-
provider: providerId,
|
|
2289
|
-
model: modelId,
|
|
2290
|
-
media_generation: mediaGeneration
|
|
2291
|
-
};
|
|
2292
|
-
await this.saveMessageToDb(assistantMsgData);
|
|
2293
|
-
await this.sendMessage(assistantMsgData);
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
if (mode === "video") {
|
|
2297
|
-
const aspectRatio = typeof mediaGeneration.aspect_ratio === "string"
|
|
2298
|
-
? mediaGeneration.aspect_ratio
|
|
2299
|
-
: null;
|
|
2300
|
-
const resolution = typeof mediaGeneration.resolution === "string"
|
|
2301
|
-
? mediaGeneration.resolution
|
|
2302
|
-
: null;
|
|
2303
|
-
const duration = typeof mediaGeneration.duration === "number"
|
|
2304
|
-
? mediaGeneration.duration
|
|
2305
|
-
: null;
|
|
2306
|
-
const videoModel = {
|
|
2307
|
-
id: modelId,
|
|
2308
|
-
name: modelId,
|
|
2309
|
-
provider: providerId
|
|
2310
|
-
};
|
|
2311
|
-
const params = {
|
|
2312
|
-
model: videoModel,
|
|
2313
|
-
prompt,
|
|
2314
|
-
aspectRatio,
|
|
2315
|
-
resolution,
|
|
2316
|
-
durationSeconds: duration
|
|
2317
|
-
};
|
|
2318
|
-
await this.sendMessage({
|
|
2319
|
-
type: "chunk",
|
|
2320
|
-
thread_id: threadId,
|
|
2321
|
-
content: "",
|
|
2322
|
-
content_type: "text",
|
|
2323
|
-
content_metadata: { media_generation: mediaGeneration },
|
|
2324
|
-
done: false
|
|
2325
|
-
});
|
|
2326
|
-
const bytes = await provider.textToVideo(params);
|
|
2327
|
-
const assetId = await storeMediaAsset(bytes, "video/mp4", "mp4");
|
|
2328
|
-
await this.sendMessage({
|
|
2329
|
-
type: "chunk",
|
|
2330
|
-
thread_id: threadId,
|
|
2331
|
-
content: "",
|
|
2332
|
-
done: true
|
|
2333
|
-
});
|
|
2334
|
-
const assistantMsgData = {
|
|
2335
|
-
type: "message",
|
|
2336
|
-
role: "assistant",
|
|
2337
|
-
content: [
|
|
2338
|
-
{
|
|
2339
|
-
type: "video",
|
|
2340
|
-
video: {
|
|
2341
|
-
type: "video",
|
|
2342
|
-
asset_id: assetId,
|
|
2343
|
-
format: "mp4",
|
|
2344
|
-
duration: duration
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
],
|
|
2348
|
-
thread_id: threadId,
|
|
2349
|
-
workflow_id: workflowId,
|
|
2350
|
-
provider: providerId,
|
|
2351
|
-
model: modelId,
|
|
2352
|
-
media_generation: mediaGeneration
|
|
2353
|
-
};
|
|
2354
|
-
await this.saveMessageToDb(assistantMsgData);
|
|
2355
|
-
await this.sendMessage(assistantMsgData);
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
if (mode === "audio") {
|
|
2359
|
-
const voice = typeof mediaGeneration.voice === "string"
|
|
2360
|
-
? mediaGeneration.voice
|
|
2361
|
-
: undefined;
|
|
2362
|
-
const speed = typeof mediaGeneration.speed === "number"
|
|
2363
|
-
? mediaGeneration.speed
|
|
2364
|
-
: 1.0;
|
|
2365
|
-
const requestedFormatRaw = typeof mediaGeneration.audio_format === "string"
|
|
2366
|
-
? mediaGeneration.audio_format.toLowerCase()
|
|
2367
|
-
: null;
|
|
2368
|
-
const supportedFormats = new Set([
|
|
2369
|
-
"mp3",
|
|
2370
|
-
"wav",
|
|
2371
|
-
"pcm",
|
|
2372
|
-
"opus",
|
|
2373
|
-
"flac",
|
|
2374
|
-
"aac"
|
|
2375
|
-
]);
|
|
2376
|
-
const requestedFormat = requestedFormatRaw && supportedFormats.has(requestedFormatRaw)
|
|
2377
|
-
? requestedFormatRaw
|
|
2378
|
-
: null;
|
|
2379
|
-
await this.sendMessage({
|
|
2380
|
-
type: "chunk",
|
|
2381
|
-
thread_id: threadId,
|
|
2382
|
-
content: "",
|
|
2383
|
-
content_type: "text",
|
|
2384
|
-
content_metadata: { media_generation: mediaGeneration },
|
|
2385
|
-
done: false
|
|
2386
|
-
});
|
|
2387
|
-
let assetId;
|
|
2388
|
-
let audioMimeType;
|
|
2389
|
-
// Some providers (e.g. HuggingFace, OpenAI) can return fully-encoded
|
|
2390
|
-
// audio. Prefer that path when available and honor the requested
|
|
2391
|
-
// container when the provider supports it.
|
|
2392
|
-
const encoded = await provider.textToSpeechEncoded({
|
|
2393
|
-
text: prompt,
|
|
2394
|
-
model: modelId,
|
|
2395
|
-
voice,
|
|
2396
|
-
speed,
|
|
2397
|
-
audioFormat: requestedFormat ?? undefined
|
|
2398
|
-
});
|
|
2399
|
-
if (encoded) {
|
|
2400
|
-
const mimeToExt = {
|
|
2401
|
-
"audio/mpeg": "mp3",
|
|
2402
|
-
"audio/wav": "wav",
|
|
2403
|
-
"audio/ogg": "ogg",
|
|
2404
|
-
"audio/flac": "flac",
|
|
2405
|
-
"audio/aac": "aac"
|
|
2406
|
-
};
|
|
2407
|
-
const ext = mimeToExt[encoded.mimeType] ?? "flac";
|
|
2408
|
-
if (requestedFormat &&
|
|
2409
|
-
requestedFormat !== ext &&
|
|
2410
|
-
requestedFormat !== "pcm") {
|
|
2411
|
-
log.warn("Requested audio_format not supported by provider; returning native format", { providerId, modelId, requestedFormat, returnedMime: encoded.mimeType });
|
|
2412
|
-
}
|
|
2413
|
-
assetId = await storeMediaAsset(encoded.data, encoded.mimeType, ext);
|
|
2414
|
-
audioMimeType = encoded.mimeType;
|
|
2415
|
-
}
|
|
2416
|
-
else {
|
|
2417
|
-
// Streaming PCM path (OpenAI, Gemini, etc.)
|
|
2418
|
-
const pcmChunks = [];
|
|
2419
|
-
let totalBytes = 0;
|
|
2420
|
-
let chunkSampleRate = 24000;
|
|
2421
|
-
for await (const chunk of provider.textToSpeech({
|
|
2422
|
-
text: prompt,
|
|
2423
|
-
model: modelId,
|
|
2424
|
-
voice,
|
|
2425
|
-
speed,
|
|
2426
|
-
audioFormat: requestedFormat ?? undefined
|
|
2427
|
-
})) {
|
|
2428
|
-
if (requestSeq !== undefined &&
|
|
2429
|
-
requestSeq !== this.chatRequestSeq)
|
|
2430
|
-
return;
|
|
2431
|
-
if (chunk?.samples) {
|
|
2432
|
-
if (chunk.sampleRate)
|
|
2433
|
-
chunkSampleRate = chunk.sampleRate;
|
|
2434
|
-
const view = new Uint8Array(chunk.samples.buffer, chunk.samples.byteOffset, chunk.samples.byteLength);
|
|
2435
|
-
const copy = new Uint8Array(view);
|
|
2436
|
-
pcmChunks.push(copy);
|
|
2437
|
-
totalBytes += copy.byteLength;
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
const merged = new Uint8Array(totalBytes);
|
|
2441
|
-
let off = 0;
|
|
2442
|
-
for (const c of pcmChunks) {
|
|
2443
|
-
merged.set(c, off);
|
|
2444
|
-
off += c.byteLength;
|
|
2445
|
-
}
|
|
2446
|
-
if (requestedFormat === "pcm") {
|
|
2447
|
-
// Return raw PCM Int16 bytes (no container).
|
|
2448
|
-
assetId = await storeMediaAsset(merged, "audio/pcm", "pcm");
|
|
2449
|
-
audioMimeType = "audio/pcm";
|
|
2450
|
-
}
|
|
2451
|
-
else {
|
|
2452
|
-
if (requestedFormat &&
|
|
2453
|
-
requestedFormat !== "wav" &&
|
|
2454
|
-
requestedFormat !== "pcm") {
|
|
2455
|
-
log.warn("Requested audio_format cannot be produced from streaming PCM; falling back to WAV", { providerId, modelId, requestedFormat });
|
|
2456
|
-
}
|
|
2457
|
-
// Wrap raw PCM Int16 in a WAV container so browsers can play it.
|
|
2458
|
-
const sampleRate = chunkSampleRate;
|
|
2459
|
-
const numChannels = 1;
|
|
2460
|
-
const bitsPerSample = 16;
|
|
2461
|
-
const byteRate = sampleRate * numChannels * (bitsPerSample / 8);
|
|
2462
|
-
const blockAlign = numChannels * (bitsPerSample / 8);
|
|
2463
|
-
const wavHeader = new ArrayBuffer(44);
|
|
2464
|
-
const dv = new DataView(wavHeader);
|
|
2465
|
-
const writeStr = (pos, str) => {
|
|
2466
|
-
for (let i = 0; i < str.length; i++)
|
|
2467
|
-
dv.setUint8(pos + i, str.charCodeAt(i));
|
|
2468
|
-
};
|
|
2469
|
-
writeStr(0, "RIFF");
|
|
2470
|
-
dv.setUint32(4, 36 + merged.byteLength, true);
|
|
2471
|
-
writeStr(8, "WAVE");
|
|
2472
|
-
writeStr(12, "fmt ");
|
|
2473
|
-
dv.setUint32(16, 16, true);
|
|
2474
|
-
dv.setUint16(20, 1, true);
|
|
2475
|
-
dv.setUint16(22, numChannels, true);
|
|
2476
|
-
dv.setUint32(24, sampleRate, true);
|
|
2477
|
-
dv.setUint32(28, byteRate, true);
|
|
2478
|
-
dv.setUint16(32, blockAlign, true);
|
|
2479
|
-
dv.setUint16(34, bitsPerSample, true);
|
|
2480
|
-
writeStr(36, "data");
|
|
2481
|
-
dv.setUint32(40, merged.byteLength, true);
|
|
2482
|
-
const wav = new Uint8Array(44 + merged.byteLength);
|
|
2483
|
-
wav.set(new Uint8Array(wavHeader), 0);
|
|
2484
|
-
wav.set(merged, 44);
|
|
2485
|
-
assetId = await storeMediaAsset(wav, "audio/wav", "wav");
|
|
2486
|
-
audioMimeType = "audio/wav";
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
await this.sendMessage({
|
|
2490
|
-
type: "chunk",
|
|
2491
|
-
thread_id: threadId,
|
|
2492
|
-
content: "",
|
|
2493
|
-
done: true
|
|
2494
|
-
});
|
|
2495
|
-
const assistantMsgData = {
|
|
2496
|
-
type: "message",
|
|
2497
|
-
role: "assistant",
|
|
2498
|
-
content: [
|
|
2499
|
-
{
|
|
2500
|
-
type: "audio",
|
|
2501
|
-
audio: {
|
|
2502
|
-
type: "audio",
|
|
2503
|
-
asset_id: assetId,
|
|
2504
|
-
mimeType: audioMimeType
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
],
|
|
2508
|
-
thread_id: threadId,
|
|
2509
|
-
workflow_id: workflowId,
|
|
2510
|
-
provider: providerId,
|
|
2511
|
-
model: modelId,
|
|
2512
|
-
media_generation: mediaGeneration
|
|
2513
|
-
};
|
|
2514
|
-
await this.saveMessageToDb(assistantMsgData);
|
|
2515
|
-
await this.sendMessage(assistantMsgData);
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
|
-
if (mode === "image_edit" || mode === "image_to_video") {
|
|
2519
|
-
// Resolve the source image from either the message content (most
|
|
2520
|
-
// common path: user dropped an image into the composer) or from the
|
|
2521
|
-
// explicit `source_asset_id` echo on the media_generation payload.
|
|
2522
|
-
const sourceBytes = await this.resolveSourceImageBytes(data, mediaGeneration, userId);
|
|
2523
|
-
if (!sourceBytes) {
|
|
2524
|
-
await this.sendMessage({
|
|
2525
|
-
type: "error",
|
|
2526
|
-
message: "A source image is required — drop or attach an image first",
|
|
2527
|
-
thread_id: threadId
|
|
2528
|
-
});
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
await this.sendMessage({
|
|
2532
|
-
type: "chunk",
|
|
2533
|
-
thread_id: threadId,
|
|
2534
|
-
content: "",
|
|
2535
|
-
content_type: "text",
|
|
2536
|
-
content_metadata: { media_generation: mediaGeneration },
|
|
2537
|
-
done: false
|
|
2538
|
-
});
|
|
2539
|
-
if (mode === "image_edit") {
|
|
2540
|
-
const variations = Math.max(1, Math.min(Number(mediaGeneration.variations ?? 1), 8));
|
|
2541
|
-
const targetWidth = typeof mediaGeneration.width === "number"
|
|
2542
|
-
? mediaGeneration.width
|
|
2543
|
-
: undefined;
|
|
2544
|
-
const targetHeight = typeof mediaGeneration.height === "number"
|
|
2545
|
-
? mediaGeneration.height
|
|
2546
|
-
: undefined;
|
|
2547
|
-
const strength = typeof mediaGeneration.strength === "number"
|
|
2548
|
-
? mediaGeneration.strength
|
|
2549
|
-
: undefined;
|
|
2550
|
-
const numInferenceSteps = typeof mediaGeneration.num_inference_steps === "number"
|
|
2551
|
-
? mediaGeneration.num_inference_steps
|
|
2552
|
-
: undefined;
|
|
2553
|
-
const editModel = {
|
|
2554
|
-
id: modelId,
|
|
2555
|
-
name: modelId,
|
|
2556
|
-
provider: providerId
|
|
2557
|
-
};
|
|
2558
|
-
const params = {
|
|
2559
|
-
model: editModel,
|
|
2560
|
-
prompt,
|
|
2561
|
-
targetWidth: targetWidth ?? null,
|
|
2562
|
-
targetHeight: targetHeight ?? null,
|
|
2563
|
-
strength: strength ?? null,
|
|
2564
|
-
numInferenceSteps: numInferenceSteps ?? null
|
|
2565
|
-
};
|
|
2566
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
2567
|
-
return;
|
|
2568
|
-
const imageBytesList = await provider.imageToImages(sourceBytes, params, variations);
|
|
2569
|
-
const imageContents = [];
|
|
2570
|
-
for (const bytes of imageBytesList) {
|
|
2571
|
-
const assetId = await storeMediaAsset(bytes, "image/png", "png");
|
|
2572
|
-
imageContents.push({
|
|
2573
|
-
type: "image_url",
|
|
2574
|
-
image: {
|
|
2575
|
-
type: "image",
|
|
2576
|
-
asset_id: assetId,
|
|
2577
|
-
mimeType: "image/png"
|
|
2578
|
-
}
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
await this.sendMessage({
|
|
2582
|
-
type: "chunk",
|
|
2583
|
-
thread_id: threadId,
|
|
2584
|
-
content: "",
|
|
2585
|
-
done: true
|
|
2586
|
-
});
|
|
2587
|
-
const assistantMsgData = {
|
|
2588
|
-
type: "message",
|
|
2589
|
-
role: "assistant",
|
|
2590
|
-
content: imageContents,
|
|
2591
|
-
thread_id: threadId,
|
|
2592
|
-
workflow_id: workflowId,
|
|
2593
|
-
provider: providerId,
|
|
2594
|
-
model: modelId,
|
|
2595
|
-
media_generation: mediaGeneration
|
|
2596
|
-
};
|
|
2597
|
-
await this.saveMessageToDb(assistantMsgData);
|
|
2598
|
-
await this.sendMessage(assistantMsgData);
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
// image_to_video
|
|
2602
|
-
const aspectRatio = typeof mediaGeneration.aspect_ratio === "string"
|
|
2603
|
-
? mediaGeneration.aspect_ratio
|
|
2604
|
-
: null;
|
|
2605
|
-
const resolution = typeof mediaGeneration.resolution === "string"
|
|
2606
|
-
? mediaGeneration.resolution
|
|
2607
|
-
: null;
|
|
2608
|
-
const duration = typeof mediaGeneration.duration === "number"
|
|
2609
|
-
? mediaGeneration.duration
|
|
2610
|
-
: null;
|
|
2611
|
-
const numInferenceSteps = typeof mediaGeneration.num_inference_steps === "number"
|
|
2612
|
-
? mediaGeneration.num_inference_steps
|
|
2613
|
-
: null;
|
|
2614
|
-
const i2vModel = {
|
|
2615
|
-
id: modelId,
|
|
2616
|
-
name: modelId,
|
|
2617
|
-
provider: providerId
|
|
2618
|
-
};
|
|
2619
|
-
const params = {
|
|
2620
|
-
model: i2vModel,
|
|
2621
|
-
prompt,
|
|
2622
|
-
aspectRatio,
|
|
2623
|
-
resolution,
|
|
2624
|
-
durationSeconds: duration,
|
|
2625
|
-
numInferenceSteps
|
|
2626
|
-
};
|
|
2627
|
-
const bytes = await provider.imageToVideo(sourceBytes, params);
|
|
2628
|
-
const assetId = await storeMediaAsset(bytes, "video/mp4", "mp4");
|
|
2629
|
-
await this.sendMessage({
|
|
2630
|
-
type: "chunk",
|
|
2631
|
-
thread_id: threadId,
|
|
2632
|
-
content: "",
|
|
2633
|
-
done: true
|
|
2634
|
-
});
|
|
2635
|
-
const assistantMsgData = {
|
|
2636
|
-
type: "message",
|
|
2637
|
-
role: "assistant",
|
|
2638
|
-
content: [
|
|
2639
|
-
{
|
|
2640
|
-
type: "video",
|
|
2641
|
-
video: {
|
|
2642
|
-
type: "video",
|
|
2643
|
-
asset_id: assetId,
|
|
2644
|
-
format: "mp4",
|
|
2645
|
-
duration
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
],
|
|
2649
|
-
thread_id: threadId,
|
|
2650
|
-
workflow_id: workflowId,
|
|
2651
|
-
provider: providerId,
|
|
2652
|
-
model: modelId,
|
|
2653
|
-
media_generation: mediaGeneration
|
|
2654
|
-
};
|
|
2655
|
-
await this.saveMessageToDb(assistantMsgData);
|
|
2656
|
-
await this.sendMessage(assistantMsgData);
|
|
2657
|
-
return;
|
|
2658
|
-
}
|
|
2659
|
-
// Modes not yet implemented on the backend — fall back to an informative
|
|
2660
|
-
// error so the client can render the unsupported state cleanly.
|
|
2661
|
-
await this.sendMessage({
|
|
2662
|
-
type: "error",
|
|
2663
|
-
message: `Media generation mode "${mode}" is not yet supported`,
|
|
2664
|
-
thread_id: threadId
|
|
2665
|
-
});
|
|
2666
|
-
}
|
|
2667
|
-
catch (err) {
|
|
2668
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2669
|
-
log.error("Media generation error", { threadId, mode, error: errMsg });
|
|
2670
|
-
await this.sendMessage({
|
|
2671
|
-
type: "error",
|
|
2672
|
-
message: `Generation failed: ${errMsg}`,
|
|
2673
|
-
thread_id: threadId
|
|
2674
|
-
});
|
|
2675
|
-
}
|
|
2676
|
-
}
|
|
2677
|
-
/**
|
|
2678
|
-
* Resolve the source image bytes for image-edit / image-to-video calls.
|
|
2679
|
-
* Searches in priority order:
|
|
2680
|
-
* 1. `media_generation.source_asset_id` → load from Asset storage
|
|
2681
|
-
* 2. The first `image_url` content block on the user message
|
|
2682
|
-
* (supports asset_id, http(s) uri, and inline data:base64 payloads)
|
|
2683
|
-
* Returns `null` when no usable source image can be found.
|
|
2684
|
-
*/
|
|
2685
|
-
async resolveSourceImageBytes(data, mediaGeneration, userId) {
|
|
2686
|
-
const tryLoadAsset = async (assetId) => {
|
|
2687
|
-
if (!assetId)
|
|
2688
|
-
return null;
|
|
2689
|
-
try {
|
|
2690
|
-
const asset = await Asset.find(userId, assetId);
|
|
2691
|
-
if (!asset)
|
|
2692
|
-
return null;
|
|
2693
|
-
const ext = (asset.content_type ?? "image/png").split("/")[1] ?? "png";
|
|
2694
|
-
const { join } = await import("node:path");
|
|
2695
|
-
const { readFile } = await import("node:fs/promises");
|
|
2696
|
-
return new Uint8Array(await readFile(join(getAssetStoragePath(), `${assetId}.${ext}`)));
|
|
2697
|
-
}
|
|
2698
|
-
catch (err) {
|
|
2699
|
-
log.warn("resolveSourceImageBytes: asset load failed", {
|
|
2700
|
-
assetId,
|
|
2701
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2702
|
-
});
|
|
2703
|
-
return null;
|
|
2704
|
-
}
|
|
2705
|
-
};
|
|
2706
|
-
const explicitId = typeof mediaGeneration.source_asset_id === "string"
|
|
2707
|
-
? mediaGeneration.source_asset_id
|
|
2708
|
-
: null;
|
|
2709
|
-
if (explicitId) {
|
|
2710
|
-
const fromAsset = await tryLoadAsset(explicitId);
|
|
2711
|
-
if (fromAsset)
|
|
2712
|
-
return fromAsset;
|
|
2713
|
-
}
|
|
2714
|
-
const content = data.content;
|
|
2715
|
-
if (Array.isArray(content)) {
|
|
2716
|
-
for (const c of content) {
|
|
2717
|
-
if (!c || typeof c !== "object")
|
|
2718
|
-
continue;
|
|
2719
|
-
const block = c;
|
|
2720
|
-
if (block.type !== "image_url")
|
|
2721
|
-
continue;
|
|
2722
|
-
const image = (block.image ?? {});
|
|
2723
|
-
const assetId = typeof image.asset_id === "string"
|
|
2724
|
-
? image.asset_id
|
|
2725
|
-
: null;
|
|
2726
|
-
if (assetId) {
|
|
2727
|
-
const bytes = await tryLoadAsset(assetId);
|
|
2728
|
-
if (bytes)
|
|
2729
|
-
return bytes;
|
|
2730
|
-
}
|
|
2731
|
-
const uri = typeof image.uri === "string" ? image.uri : null;
|
|
2732
|
-
if (uri) {
|
|
2733
|
-
if (uri.startsWith("data:")) {
|
|
2734
|
-
const commaIdx = uri.indexOf(",");
|
|
2735
|
-
if (commaIdx > -1) {
|
|
2736
|
-
const b64 = uri.slice(commaIdx + 1);
|
|
2737
|
-
try {
|
|
2738
|
-
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
2739
|
-
}
|
|
2740
|
-
catch {
|
|
2741
|
-
/* fall through */
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
else if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
2746
|
-
if (!isSafeExternalUrl(uri)) {
|
|
2747
|
-
log.warn("resolveSourceImageBytes: refusing to fetch non-public URL", { uri });
|
|
2748
|
-
}
|
|
2749
|
-
else {
|
|
2750
|
-
try {
|
|
2751
|
-
const resp = await fetch(uri);
|
|
2752
|
-
if (resp.ok) {
|
|
2753
|
-
return new Uint8Array(await resp.arrayBuffer());
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
catch (err) {
|
|
2757
|
-
log.warn("resolveSourceImageBytes: fetch failed", {
|
|
2758
|
-
uri,
|
|
2759
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2760
|
-
});
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
const data64 = typeof image.data === "string" ? image.data : null;
|
|
2766
|
-
if (data64) {
|
|
2767
|
-
try {
|
|
2768
|
-
return new Uint8Array(Buffer.from(data64, "base64"));
|
|
2769
|
-
}
|
|
2770
|
-
catch {
|
|
2771
|
-
/* ignore */
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
return null;
|
|
2777
|
-
}
|
|
2778
|
-
async handleWorkflowMessage(data, _requestSeq) {
|
|
2779
|
-
const threadId = typeof data.thread_id === "string" ? data.thread_id : "";
|
|
2780
|
-
const workflowId = typeof data.workflow_id === "string" ? data.workflow_id : null;
|
|
2781
|
-
const providerId = typeof data.provider === "string" ? data.provider : this.defaultProvider;
|
|
2782
|
-
const model = typeof data.model === "string" ? data.model : this.defaultModel;
|
|
2783
|
-
const userId = this.userId ?? "1";
|
|
2784
|
-
const jobId = randomUUID();
|
|
2785
|
-
log.info("Workflow message", { threadId, workflowId, jobId });
|
|
2786
|
-
try {
|
|
2787
|
-
if (!workflowId) {
|
|
2788
|
-
throw new Error("workflow_id is required for workflow processing");
|
|
2789
|
-
}
|
|
2790
|
-
// Load workflow from DB
|
|
2791
|
-
const workflow = await Workflow.find(userId, workflowId);
|
|
2792
|
-
if (!workflow) {
|
|
2793
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
2794
|
-
}
|
|
2795
|
-
const rawGraph = workflow.graph;
|
|
2796
|
-
if (this.beforeRunJob) {
|
|
2797
|
-
await this.beforeRunJob(rawGraph);
|
|
2798
|
-
}
|
|
2799
|
-
// Detect message input names from raw graph (reads node.data) — matches Python
|
|
2800
|
-
const { messageName, messagesName } = this.detectMessageInputNames(rawGraph);
|
|
2801
|
-
const graph = await this.hydrateGraph(rawGraph);
|
|
2802
|
-
const messageInputName = (typeof data.workflow_message_input_name === "string"
|
|
2803
|
-
? data.workflow_message_input_name
|
|
2804
|
-
: null) ??
|
|
2805
|
-
messageName ??
|
|
2806
|
-
"message";
|
|
2807
|
-
const messagesInputName = (typeof data.workflow_messages_input_name === "string"
|
|
2808
|
-
? data.workflow_messages_input_name
|
|
2809
|
-
: null) ??
|
|
2810
|
-
messagesName ??
|
|
2811
|
-
"messages";
|
|
2812
|
-
// Build chat history for params — matches Python
|
|
2813
|
-
const [dbMessages] = await Message.paginate(threadId, { limit: 1000 });
|
|
2814
|
-
const chatHistorySerialized = dbMessages.map((m) => ({
|
|
2815
|
-
role: m.role,
|
|
2816
|
-
content: m.content,
|
|
2817
|
-
created_at: m.created_at,
|
|
2818
|
-
thread_id: m.thread_id
|
|
2819
|
-
}));
|
|
2820
|
-
// Serialize current message
|
|
2821
|
-
const currentMessage = {
|
|
2822
|
-
role: typeof data.role === "string" ? data.role : "user",
|
|
2823
|
-
content: data.content,
|
|
2824
|
-
thread_id: threadId,
|
|
2825
|
-
workflow_id: workflowId,
|
|
2826
|
-
provider: providerId,
|
|
2827
|
-
model
|
|
2828
|
-
};
|
|
2829
|
-
// Prepare params — matches Python's WorkflowMessageProcessor
|
|
2830
|
-
const params = {
|
|
2831
|
-
[messageInputName]: currentMessage,
|
|
2832
|
-
[messagesInputName]: [...chatHistorySerialized, currentMessage],
|
|
2833
|
-
...(typeof data.params === "object" && data.params !== null
|
|
2834
|
-
? data.params
|
|
2835
|
-
: {})
|
|
2836
|
-
};
|
|
2837
|
-
// If chat workflow, add legacy params — matches Python's ChatWorkflowMessageProcessor
|
|
2838
|
-
if (workflow.run_mode === "chat") {
|
|
2839
|
-
const legacyChatInput = chatHistorySerialized.map((m) => ({
|
|
2840
|
-
role: m.role,
|
|
2841
|
-
content: this.extractTextContent(m.content),
|
|
2842
|
-
created_at: m.created_at
|
|
2843
|
-
}));
|
|
2844
|
-
params["chat_input"] = legacyChatInput;
|
|
2845
|
-
if (messagesInputName !== "messages") {
|
|
2846
|
-
params["messages"] = legacyChatInput;
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
// Create processing context
|
|
2850
|
-
const workspaceDir = this.workspaceResolver
|
|
2851
|
-
? await this.workspaceResolver(workflowId, userId)
|
|
2852
|
-
: null;
|
|
2853
|
-
const context = createRuntimeContext({
|
|
2854
|
-
jobId,
|
|
2855
|
-
workflowId,
|
|
2856
|
-
userId,
|
|
2857
|
-
workspaceDir,
|
|
2858
|
-
assetOutputMode: this.mode === "text" ? "data_uri" : "temp_url"
|
|
2859
|
-
});
|
|
2860
|
-
// Expose executor/node-type resolution for sub-workflow nodes
|
|
2861
|
-
context.setResolveExecutor((node) => this.resolveExecutor(node));
|
|
2862
|
-
if (this.resolveNodeType) {
|
|
2863
|
-
const resolverObj = typeof this.resolveNodeType === "function"
|
|
2864
|
-
? { resolveNodeType: this.resolveNodeType }
|
|
2865
|
-
: this.resolveNodeType;
|
|
2866
|
-
context.setResolveNodeType((nodeType) => resolverObj.resolveNodeType(nodeType));
|
|
2867
|
-
}
|
|
2868
|
-
// Create and run workflow
|
|
2869
|
-
const runner = new WorkflowRunner(jobId, {
|
|
2870
|
-
resolveExecutor: (node) => this.resolveExecutor(node),
|
|
2871
|
-
executionContext: context,
|
|
2872
|
-
validateNode: this.validateNode
|
|
2873
|
-
});
|
|
2874
|
-
const active = {
|
|
2875
|
-
jobId,
|
|
2876
|
-
workflowId,
|
|
2877
|
-
context,
|
|
2878
|
-
runner,
|
|
2879
|
-
graph,
|
|
2880
|
-
finished: false,
|
|
2881
|
-
status: "running"
|
|
2882
|
-
};
|
|
2883
|
-
this.activeJobs.set(jobId, active);
|
|
2884
|
-
// Persist job to DB (best-effort)
|
|
2885
|
-
try {
|
|
2886
|
-
await Job.create({
|
|
2887
|
-
id: jobId,
|
|
2888
|
-
workflow_id: workflowId,
|
|
2889
|
-
user_id: userId,
|
|
2890
|
-
status: "running",
|
|
2891
|
-
params,
|
|
2892
|
-
graph
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
catch (error) {
|
|
2896
|
-
this.logError("workflow job persistence failed", error);
|
|
2897
|
-
}
|
|
2898
|
-
// Execute workflow and stream messages
|
|
2899
|
-
const executePromise = runner.run({ job_id: jobId, workflow_id: workflowId, params }, graph);
|
|
2900
|
-
// Stream events, collect output_update results
|
|
2901
|
-
const result = {};
|
|
2902
|
-
await this.sendMessage({
|
|
2903
|
-
type: "job_update",
|
|
2904
|
-
status: "running",
|
|
2905
|
-
job_id: jobId,
|
|
2906
|
-
workflow_id: workflowId
|
|
2907
|
-
});
|
|
2908
|
-
let finalOutputs = {};
|
|
2909
|
-
void executePromise
|
|
2910
|
-
.then((r) => {
|
|
2911
|
-
active.status = r.status;
|
|
2912
|
-
active.error = r.error;
|
|
2913
|
-
finalOutputs = r.outputs ?? {};
|
|
2914
|
-
})
|
|
2915
|
-
.catch((err) => {
|
|
2916
|
-
active.status = "failed";
|
|
2917
|
-
active.error = err instanceof Error ? err.message : String(err);
|
|
2918
|
-
})
|
|
2919
|
-
.finally(() => {
|
|
2920
|
-
active.finished = true;
|
|
2921
|
-
});
|
|
2922
|
-
while (!active.finished || active.context.hasMessages()) {
|
|
2923
|
-
while (active.context.hasMessages()) {
|
|
2924
|
-
const msg = active.context.popMessage();
|
|
2925
|
-
if (!msg)
|
|
2926
|
-
break;
|
|
2927
|
-
const outbound = {
|
|
2928
|
-
...msg,
|
|
2929
|
-
job_id: msg.job_id ?? jobId,
|
|
2930
|
-
workflow_id: msg.workflow_id ??
|
|
2931
|
-
workflowId
|
|
2932
|
-
};
|
|
2933
|
-
// Capture output_update values for the response message
|
|
2934
|
-
if (outbound.type === "output_update") {
|
|
2935
|
-
const nodeId = String(outbound.node_id ?? "");
|
|
2936
|
-
const graphNodes = graph.nodes ?? [];
|
|
2937
|
-
const node = graphNodes.find((n) => n.id === nodeId);
|
|
2938
|
-
const nodeType = typeof node?.type === "string" ? node.type : "";
|
|
2939
|
-
if (nodeType.includes("Output")) {
|
|
2940
|
-
const nodeName = typeof outbound.node_name === "string"
|
|
2941
|
-
? outbound.node_name
|
|
2942
|
-
: nodeType;
|
|
2943
|
-
result[nodeName] = outbound.value;
|
|
2944
|
-
}
|
|
2945
|
-
else {
|
|
2946
|
-
continue; // Skip non-output node output_updates
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
await this.sendMessage(outbound);
|
|
2950
|
-
}
|
|
2951
|
-
if (!active.finished) {
|
|
2952
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
// Collect any outputs from the runner result — only Output-type nodes
|
|
2956
|
-
// The kernel considers all leaf nodes as "output nodes", but for the
|
|
2957
|
-
// response message we only want nodes whose type includes "Output"
|
|
2958
|
-
// (matching Python's WorkflowMessageProcessor behavior).
|
|
2959
|
-
for (const [nodeType, values] of Object.entries(finalOutputs)) {
|
|
2960
|
-
if (!nodeType.includes("Output"))
|
|
2961
|
-
continue;
|
|
2962
|
-
if (!result[nodeType] && Array.isArray(values) && values.length > 0) {
|
|
2963
|
-
result[nodeType] = values.length === 1 ? values[0] : values;
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
// Send terminal job_update if not already sent
|
|
2967
|
-
await this.sendMessage({
|
|
2968
|
-
type: "job_update",
|
|
2969
|
-
status: active.status,
|
|
2970
|
-
job_id: jobId,
|
|
2971
|
-
workflow_id: workflowId,
|
|
2972
|
-
error: active.error,
|
|
2973
|
-
result: { outputs: finalOutputs }
|
|
2974
|
-
});
|
|
2975
|
-
// Persist final job status
|
|
2976
|
-
try {
|
|
2977
|
-
const job = (await Job.get(jobId));
|
|
2978
|
-
if (job) {
|
|
2979
|
-
if (active.status === "completed")
|
|
2980
|
-
job.markCompleted();
|
|
2981
|
-
else if (active.status === "failed")
|
|
2982
|
-
job.markFailed(active.error ?? "Unknown error");
|
|
2983
|
-
else if (active.status === "cancelled")
|
|
2984
|
-
job.markCancelled();
|
|
2985
|
-
await job.save();
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
catch (error) {
|
|
2989
|
-
this.logError("workflow job persistence (final) failed", error);
|
|
2990
|
-
}
|
|
2991
|
-
this.activeJobs.delete(jobId);
|
|
2992
|
-
// Signal completion — done chunk with job_id + workflow_id
|
|
2993
|
-
await this.sendMessage({
|
|
2994
|
-
type: "chunk",
|
|
2995
|
-
content: "",
|
|
2996
|
-
done: true,
|
|
2997
|
-
job_id: jobId,
|
|
2998
|
-
workflow_id: workflowId,
|
|
2999
|
-
thread_id: threadId
|
|
3000
|
-
});
|
|
3001
|
-
// Create response message from workflow outputs — matches Python's _create_response_message
|
|
3002
|
-
const responseContent = this.createWorkflowResponseContent(result);
|
|
3003
|
-
const responseMsg = {
|
|
3004
|
-
type: "message",
|
|
3005
|
-
role: "assistant",
|
|
3006
|
-
content: responseContent,
|
|
3007
|
-
thread_id: threadId,
|
|
3008
|
-
workflow_id: workflowId,
|
|
3009
|
-
provider: providerId,
|
|
3010
|
-
model,
|
|
3011
|
-
job_id: jobId
|
|
3012
|
-
};
|
|
3013
|
-
await this.saveMessageToDb(responseMsg);
|
|
3014
|
-
await this.sendMessage(responseMsg);
|
|
3015
|
-
log.debug("Workflow message complete", { threadId, workflowId, jobId });
|
|
3016
|
-
}
|
|
3017
|
-
catch (err) {
|
|
3018
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3019
|
-
log.error("Workflow message error", {
|
|
3020
|
-
threadId,
|
|
3021
|
-
workflowId,
|
|
3022
|
-
error: errMsg
|
|
3023
|
-
});
|
|
3024
|
-
await this.sendMessage({
|
|
3025
|
-
type: "error",
|
|
3026
|
-
message: `Error processing workflow: ${errMsg}`,
|
|
3027
|
-
job_id: jobId,
|
|
3028
|
-
workflow_id: workflowId,
|
|
3029
|
-
thread_id: threadId
|
|
3030
|
-
});
|
|
3031
|
-
// Send done chunk even on error — matches Python
|
|
3032
|
-
await this.sendMessage({
|
|
3033
|
-
type: "chunk",
|
|
3034
|
-
content: "",
|
|
3035
|
-
done: true,
|
|
3036
|
-
job_id: jobId,
|
|
3037
|
-
workflow_id: workflowId,
|
|
3038
|
-
thread_id: threadId
|
|
3039
|
-
});
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
/**
|
|
3043
|
-
* Build the map of configured BaseProvider instances for the given user.
|
|
3044
|
-
* Cached per user — invalidate by clearing `configuredProvidersCache`.
|
|
3045
|
-
* Used to expose `find_model` to the GraphPlanner agent so it can pick a
|
|
3046
|
-
* real model+provider for generic AI nodes.
|
|
3047
|
-
*/
|
|
3048
|
-
async getConfiguredProviders(userId) {
|
|
3049
|
-
const cached = this.configuredProvidersCache.get(userId);
|
|
3050
|
-
if (cached)
|
|
3051
|
-
return cached;
|
|
3052
|
-
const providersMod = await import("@nodetool-ai/runtime");
|
|
3053
|
-
const ids = providersMod.listRegisteredProviderIds();
|
|
3054
|
-
const result = {};
|
|
3055
|
-
await Promise.all(ids.map(async (id) => {
|
|
3056
|
-
try {
|
|
3057
|
-
if (await providersMod.isProviderConfigured(id, userId)) {
|
|
3058
|
-
result[id] = await providersMod.getProvider(id, userId);
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
catch (err) {
|
|
3062
|
-
log.debug("Skipping provider for find_model", {
|
|
3063
|
-
provider: id,
|
|
3064
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3065
|
-
});
|
|
3066
|
-
}
|
|
3067
|
-
}));
|
|
3068
|
-
this.configuredProvidersCache.set(userId, result);
|
|
3069
|
-
return result;
|
|
3070
|
-
}
|
|
3071
|
-
/**
|
|
3072
|
-
* Handle agent mode messages.
|
|
3073
|
-
* Mirrors Python's AgentMessageProcessor.process().
|
|
3074
|
-
*
|
|
3075
|
-
* Creates an Agent with the user's objective and streams execution events
|
|
3076
|
-
* (Chunk, ToolCallUpdate, TaskUpdate, PlanningUpdate, LogUpdate, StepResult)
|
|
3077
|
-
* to the client. Messages with type "message" are persisted to DB.
|
|
3078
|
-
*/
|
|
3079
|
-
async handleAgentMessage(data, requestSeq) {
|
|
3080
|
-
const threadId = typeof data.thread_id === "string" ? data.thread_id : "";
|
|
3081
|
-
const providerId = data.provider;
|
|
3082
|
-
const model = data.model;
|
|
3083
|
-
const workflowId = typeof data.workflow_id === "string" ? data.workflow_id : null;
|
|
3084
|
-
const userId = this.userId ?? "1";
|
|
3085
|
-
const provider = await this.resolveProvider(providerId, userId);
|
|
3086
|
-
// Extract objective from content
|
|
3087
|
-
const objective = this.extractTextContent(data.content, "Complete the requested task");
|
|
3088
|
-
// Generate unique execution ID — matches Python
|
|
3089
|
-
const agentExecutionId = randomUUID();
|
|
3090
|
-
// Resolve tools — matches Python's tool resolution
|
|
3091
|
-
const { MultiModeAgent, ReadFileTool, WriteFileTool, BrowserTool, GoogleSearchTool, getAllMcpTools, resolveTool } = await import("@nodetool-ai/agents");
|
|
3092
|
-
let selectedTools = [];
|
|
3093
|
-
const rawToolNames = Array.isArray(data.tools)
|
|
3094
|
-
? data.tools.filter((t) => typeof t === "string")
|
|
3095
|
-
: [];
|
|
3096
|
-
// Build configured providers up-front so both `getAllMcpTools` and the
|
|
3097
|
-
// MultiModeAgent below share the same map. Cached per-user in
|
|
3098
|
-
// `getConfiguredProviders`. Independent of the NodeRegistry — the
|
|
3099
|
-
// multi-task planner needs `find_model` and the media-generation tools
|
|
3100
|
-
// even when no registry is wired (i.e. graph mode is unavailable).
|
|
3101
|
-
const agentProviders = await this.getConfiguredProviders(userId);
|
|
3102
|
-
if (rawToolNames.length > 0) {
|
|
3103
|
-
// User explicitly specified tools — resolve by name
|
|
3104
|
-
for (const name of rawToolNames) {
|
|
3105
|
-
const tool = resolveTool(name);
|
|
3106
|
-
if (tool)
|
|
3107
|
-
selectedTools.push(tool);
|
|
3108
|
-
}
|
|
3109
|
-
log.debug("Selected tools for agent", {
|
|
3110
|
-
tools: selectedTools.map((t) => t.name)
|
|
3111
|
-
});
|
|
3112
|
-
}
|
|
3113
|
-
else {
|
|
3114
|
-
// No tools specified — use the current built-in defaults plus NodeTool's
|
|
3115
|
-
// MCP-style backend tools. When a NodeRegistry is wired up, the MCP
|
|
3116
|
-
// helper swaps the REST search/list/get tools for the in-process biased
|
|
3117
|
-
// versions and adds `find_model`, so any agent loop (not just the
|
|
3118
|
-
// GraphPlanner) gets the same node-selection bias.
|
|
3119
|
-
selectedTools = [
|
|
3120
|
-
new ReadFileTool(),
|
|
3121
|
-
new WriteFileTool(),
|
|
3122
|
-
new BrowserTool(),
|
|
3123
|
-
new GoogleSearchTool(),
|
|
3124
|
-
...getAllMcpTools({
|
|
3125
|
-
registry: this.nodeRegistry,
|
|
3126
|
-
providers: agentProviders
|
|
3127
|
-
})
|
|
3128
|
-
];
|
|
3129
|
-
log.debug("Using default + MCP tools for agent", {
|
|
3130
|
-
count: selectedTools.length
|
|
3131
|
-
});
|
|
3132
|
-
}
|
|
3133
|
-
// Server-side tools from resolveTools option
|
|
3134
|
-
if (rawToolNames.length > 0 && this.resolveTools) {
|
|
3135
|
-
const serverTools = await this.resolveTools(rawToolNames, userId);
|
|
3136
|
-
for (const st of serverTools) {
|
|
3137
|
-
if (!selectedTools.find((t) => t.name === st.name)) {
|
|
3138
|
-
selectedTools.push(st);
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
// Include UI proxy tools if client provided a manifest via tool bridge — matches Python
|
|
3143
|
-
if (Object.keys(this.clientToolsManifest).length > 0) {
|
|
3144
|
-
const sendMsg = this.sendMessage.bind(this);
|
|
3145
|
-
for (const [, manifest] of Object.entries(this.clientToolsManifest)) {
|
|
3146
|
-
try {
|
|
3147
|
-
selectedTools.push(new UIToolProxy(manifest, this.toolBridge, sendMsg));
|
|
3148
|
-
}
|
|
3149
|
-
catch (err) {
|
|
3150
|
-
log.warn("Failed to register UI tool proxy", {
|
|
3151
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3152
|
-
});
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
log.debug("Added UI tool proxies to agent", {
|
|
3156
|
-
count: Object.keys(this.clientToolsManifest).length
|
|
3157
|
-
});
|
|
3158
|
-
}
|
|
3159
|
-
// Create ProcessingContext for agent execution
|
|
3160
|
-
const agentWorkspaceDir = workflowId && this.workspaceResolver
|
|
3161
|
-
? await this.workspaceResolver(workflowId, userId)
|
|
3162
|
-
: tmpdir();
|
|
3163
|
-
const ctx = createRuntimeContext({
|
|
3164
|
-
jobId: randomUUID(),
|
|
3165
|
-
userId,
|
|
3166
|
-
workspaceDir: agentWorkspaceDir
|
|
3167
|
-
});
|
|
3168
|
-
const maxStepIterations = typeof data.max_step_iterations === "number" &&
|
|
3169
|
-
data.max_step_iterations > 0
|
|
3170
|
-
? Math.floor(data.max_step_iterations)
|
|
3171
|
-
: 20;
|
|
3172
|
-
const maxSteps = typeof data.max_steps === "number" && data.max_steps > 0
|
|
3173
|
-
? Math.floor(data.max_steps)
|
|
3174
|
-
: undefined;
|
|
3175
|
-
// Pick the planner. Explicit `agent_planner` from the client wins; if
|
|
3176
|
-
// omitted, default to "graph" when a NodeRegistry is wired (the
|
|
3177
|
-
// workflow-builder path) and "multi" otherwise (TaskPlanner →
|
|
3178
|
-
// ParallelTaskExecutor). Either way the request only reaches the
|
|
3179
|
-
// graph path when a registry is actually available.
|
|
3180
|
-
const requestedPlanner = data.agent_planner === "graph" || data.agent_planner === "multi"
|
|
3181
|
-
? data.agent_planner
|
|
3182
|
-
: null;
|
|
3183
|
-
const defaultPlanner = this.nodeRegistry
|
|
3184
|
-
? "graph"
|
|
3185
|
-
: "multi";
|
|
3186
|
-
const plannerType = requestedPlanner ?? defaultPlanner;
|
|
3187
|
-
const useGraphPlanner = plannerType === "graph" && !!this.nodeRegistry;
|
|
3188
|
-
if (requestedPlanner === "graph" && !this.nodeRegistry) {
|
|
3189
|
-
log.warn("Client requested graph planner but no NodeRegistry is wired — falling back to multi-task planner.");
|
|
3190
|
-
}
|
|
3191
|
-
const markdownOutputSchema = {
|
|
3192
|
-
type: "object",
|
|
3193
|
-
properties: {
|
|
3194
|
-
markdown: {
|
|
3195
|
-
type: "string",
|
|
3196
|
-
description: "The markdown content of the response"
|
|
3197
|
-
}
|
|
3198
|
-
},
|
|
3199
|
-
required: ["markdown"]
|
|
3200
|
-
};
|
|
3201
|
-
try {
|
|
3202
|
-
// Both planner types route through MultiModeAgent in plan mode; the
|
|
3203
|
-
// `useGraphPlanner` flag picks GraphPlanner vs TaskPlanner under the
|
|
3204
|
-
// hood. The legacy plain `Agent` is no longer used for agent_mode.
|
|
3205
|
-
const agent = new MultiModeAgent({
|
|
3206
|
-
name: "Assistant",
|
|
3207
|
-
objective,
|
|
3208
|
-
provider,
|
|
3209
|
-
model,
|
|
3210
|
-
mode: "plan",
|
|
3211
|
-
tools: selectedTools,
|
|
3212
|
-
useGraphPlanner,
|
|
3213
|
-
registry: useGraphPlanner ? this.nodeRegistry : undefined,
|
|
3214
|
-
providers: useGraphPlanner ? agentProviders : undefined,
|
|
3215
|
-
maxStepIterations,
|
|
3216
|
-
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
3217
|
-
outputSchema: markdownOutputSchema
|
|
3218
|
-
});
|
|
3219
|
-
for await (const item of agent.execute(ctx)) {
|
|
3220
|
-
if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
|
|
3221
|
-
return;
|
|
3222
|
-
const msgType = item.type;
|
|
3223
|
-
if (msgType === "chunk") {
|
|
3224
|
-
// Stream text chunks to client
|
|
3225
|
-
const chunk = item;
|
|
3226
|
-
await this.sendMessage({
|
|
3227
|
-
type: "chunk",
|
|
3228
|
-
content: chunk.content ?? "",
|
|
3229
|
-
done: chunk.done ?? false,
|
|
3230
|
-
thinking: chunk.thinking ?? false,
|
|
3231
|
-
thread_id: chunk.thread_id ?? threadId
|
|
3232
|
-
});
|
|
3233
|
-
}
|
|
3234
|
-
else if (msgType === "tool_call_update") {
|
|
3235
|
-
// Forward tool call updates live AND persist them as an
|
|
3236
|
-
// agent_execution message so that reopening a thread preserves the
|
|
3237
|
-
// full per-step debug trace (which tools were called, with what args).
|
|
3238
|
-
const tc = item;
|
|
3239
|
-
await this.sendMessage({
|
|
3240
|
-
type: "tool_call_update",
|
|
3241
|
-
thread_id: threadId,
|
|
3242
|
-
workflow_id: workflowId,
|
|
3243
|
-
tool_call_id: tc.tool_call_id ?? null,
|
|
3244
|
-
name: tc.name,
|
|
3245
|
-
message: tc.message ?? `Calling ${tc.name}...`,
|
|
3246
|
-
args: tc.args,
|
|
3247
|
-
step_id: tc.step_id ?? null,
|
|
3248
|
-
node_id: tc.node_id ?? null,
|
|
3249
|
-
agent_execution_id: agentExecutionId
|
|
3250
|
-
});
|
|
3251
|
-
const persisted = {
|
|
3252
|
-
type: "message",
|
|
3253
|
-
role: "agent_execution",
|
|
3254
|
-
execution_event_type: "tool_call_update",
|
|
3255
|
-
agent_execution_id: agentExecutionId,
|
|
3256
|
-
content: {
|
|
3257
|
-
type: "tool_call_update",
|
|
3258
|
-
tool_call_id: tc.tool_call_id ?? null,
|
|
3259
|
-
name: tc.name,
|
|
3260
|
-
message: tc.message ?? `Calling ${tc.name}...`,
|
|
3261
|
-
args: tc.args ?? {},
|
|
3262
|
-
step_id: tc.step_id ?? null,
|
|
3263
|
-
node_id: tc.node_id ?? null
|
|
3264
|
-
},
|
|
3265
|
-
thread_id: threadId,
|
|
3266
|
-
workflow_id: workflowId,
|
|
3267
|
-
provider: providerId,
|
|
3268
|
-
model,
|
|
3269
|
-
agent_mode: true
|
|
3270
|
-
};
|
|
3271
|
-
await this.saveMessageToDb(persisted);
|
|
3272
|
-
}
|
|
3273
|
-
else if (msgType === "task_update") {
|
|
3274
|
-
// Send task update as agent_execution message — persisted by _run_processor pattern
|
|
3275
|
-
const tu = item;
|
|
3276
|
-
const contentDict = {
|
|
3277
|
-
type: "task_update",
|
|
3278
|
-
event: tu.event,
|
|
3279
|
-
task: tu.task ?? null,
|
|
3280
|
-
step: tu.step ?? null
|
|
3281
|
-
};
|
|
3282
|
-
const msg = {
|
|
3283
|
-
type: "message",
|
|
3284
|
-
role: "agent_execution",
|
|
3285
|
-
execution_event_type: "task_update",
|
|
3286
|
-
agent_execution_id: agentExecutionId,
|
|
3287
|
-
content: contentDict,
|
|
3288
|
-
thread_id: threadId,
|
|
3289
|
-
workflow_id: workflowId,
|
|
3290
|
-
provider: providerId,
|
|
3291
|
-
model,
|
|
3292
|
-
agent_mode: true
|
|
3293
|
-
};
|
|
3294
|
-
await this.saveMessageToDb(msg);
|
|
3295
|
-
await this.sendMessage(msg);
|
|
3296
|
-
}
|
|
3297
|
-
else if (msgType === "planning_update") {
|
|
3298
|
-
// Send planning update as agent_execution message
|
|
3299
|
-
const pu = item;
|
|
3300
|
-
const contentDict = {
|
|
3301
|
-
type: "planning_update",
|
|
3302
|
-
phase: pu.phase,
|
|
3303
|
-
status: pu.status,
|
|
3304
|
-
content: pu.content,
|
|
3305
|
-
node_id: pu.node_id
|
|
3306
|
-
};
|
|
3307
|
-
const msg = {
|
|
3308
|
-
type: "message",
|
|
3309
|
-
role: "agent_execution",
|
|
3310
|
-
execution_event_type: "planning_update",
|
|
3311
|
-
agent_execution_id: agentExecutionId,
|
|
3312
|
-
content: contentDict,
|
|
3313
|
-
thread_id: threadId,
|
|
3314
|
-
workflow_id: workflowId,
|
|
3315
|
-
provider: providerId,
|
|
3316
|
-
model,
|
|
3317
|
-
agent_mode: true
|
|
3318
|
-
};
|
|
3319
|
-
await this.saveMessageToDb(msg);
|
|
3320
|
-
await this.sendMessage(msg);
|
|
3321
|
-
// Also send persistent LogUpdate for completed phases — matches Python
|
|
3322
|
-
if (pu.status === "Success" || pu.status === "Failed") {
|
|
3323
|
-
const logMsg = {
|
|
3324
|
-
type: "message",
|
|
3325
|
-
role: "agent_execution",
|
|
3326
|
-
execution_event_type: "log_update",
|
|
3327
|
-
agent_execution_id: agentExecutionId,
|
|
3328
|
-
content: {
|
|
3329
|
-
type: "log_update",
|
|
3330
|
-
node_id: pu.node_id ?? "agent",
|
|
3331
|
-
node_name: "Agent",
|
|
3332
|
-
content: `${pu.phase}: ${pu.content ?? ""}`,
|
|
3333
|
-
severity: pu.status === "Failed" ? "error" : "info"
|
|
3334
|
-
},
|
|
3335
|
-
thread_id: threadId,
|
|
3336
|
-
workflow_id: workflowId,
|
|
3337
|
-
provider: providerId,
|
|
3338
|
-
model,
|
|
3339
|
-
agent_mode: true
|
|
3340
|
-
};
|
|
3341
|
-
await this.saveMessageToDb(logMsg);
|
|
3342
|
-
await this.sendMessage(logMsg);
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
else if (msgType === "log_update") {
|
|
3346
|
-
// Forward log updates as agent_execution messages
|
|
3347
|
-
const lu = item;
|
|
3348
|
-
const msg = {
|
|
3349
|
-
type: "message",
|
|
3350
|
-
role: "agent_execution",
|
|
3351
|
-
execution_event_type: "log_update",
|
|
3352
|
-
agent_execution_id: agentExecutionId,
|
|
3353
|
-
content: {
|
|
3354
|
-
type: "log_update",
|
|
3355
|
-
node_id: lu.node_id,
|
|
3356
|
-
node_name: lu.node_name,
|
|
3357
|
-
content: lu.content,
|
|
3358
|
-
severity: lu.severity
|
|
3359
|
-
},
|
|
3360
|
-
thread_id: threadId,
|
|
3361
|
-
workflow_id: workflowId,
|
|
3362
|
-
provider: providerId,
|
|
3363
|
-
model,
|
|
3364
|
-
agent_mode: true
|
|
3365
|
-
};
|
|
3366
|
-
await this.saveMessageToDb(msg);
|
|
3367
|
-
await this.sendMessage(msg);
|
|
3368
|
-
}
|
|
3369
|
-
else if (msgType === "step_result") {
|
|
3370
|
-
const sr = item;
|
|
3371
|
-
// Only forward non-task step results — task result handled via agent.results
|
|
3372
|
-
if (!sr.is_task_result) {
|
|
3373
|
-
const contentDict = {
|
|
3374
|
-
type: "step_result",
|
|
3375
|
-
result: sr.result,
|
|
3376
|
-
step: sr.step ?? null,
|
|
3377
|
-
error: sr.error,
|
|
3378
|
-
is_task_result: sr.is_task_result
|
|
3379
|
-
};
|
|
3380
|
-
const msg = {
|
|
3381
|
-
type: "message",
|
|
3382
|
-
role: "agent_execution",
|
|
3383
|
-
execution_event_type: "step_result",
|
|
3384
|
-
agent_execution_id: agentExecutionId,
|
|
3385
|
-
content: contentDict,
|
|
3386
|
-
thread_id: threadId,
|
|
3387
|
-
workflow_id: workflowId,
|
|
3388
|
-
provider: providerId,
|
|
3389
|
-
model,
|
|
3390
|
-
agent_mode: true
|
|
3391
|
-
};
|
|
3392
|
-
await this.saveMessageToDb(msg);
|
|
3393
|
-
await this.sendMessage(msg);
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
// Normalize final agent output — matches Python
|
|
3398
|
-
const results = agent.getResults();
|
|
3399
|
-
let content;
|
|
3400
|
-
if (typeof results === "string") {
|
|
3401
|
-
content = results;
|
|
3402
|
-
}
|
|
3403
|
-
else if (results &&
|
|
3404
|
-
typeof results === "object" &&
|
|
3405
|
-
"markdown" in results) {
|
|
3406
|
-
const md = results.markdown;
|
|
3407
|
-
content = typeof md === "string" ? md : String(results);
|
|
3408
|
-
}
|
|
3409
|
-
else if (results != null && typeof results === "object") {
|
|
3410
|
-
// Structured result — run one more LLM call to present it
|
|
3411
|
-
const resultsJson = JSON.stringify(results, null, 2);
|
|
3412
|
-
content = await this.presentStructuredResults(provider, model, objective, resultsJson, threadId);
|
|
3413
|
-
}
|
|
3414
|
-
else {
|
|
3415
|
-
content = results != null ? String(results) : "";
|
|
3416
|
-
}
|
|
3417
|
-
// Send final assistant message — persisted
|
|
3418
|
-
const finalMsg = {
|
|
3419
|
-
type: "message",
|
|
3420
|
-
role: "assistant",
|
|
3421
|
-
content,
|
|
3422
|
-
thread_id: threadId,
|
|
3423
|
-
workflow_id: workflowId,
|
|
3424
|
-
provider: providerId,
|
|
3425
|
-
model,
|
|
3426
|
-
agent_mode: true
|
|
3427
|
-
};
|
|
3428
|
-
await this.saveMessageToDb(finalMsg);
|
|
3429
|
-
await this.sendMessage(finalMsg);
|
|
3430
|
-
// Signal completion
|
|
3431
|
-
await this.sendMessage({
|
|
3432
|
-
type: "chunk",
|
|
3433
|
-
content: "",
|
|
3434
|
-
done: true,
|
|
3435
|
-
thread_id: threadId,
|
|
3436
|
-
workflow_id: workflowId
|
|
3437
|
-
});
|
|
3438
|
-
log.debug("Agent execution complete", { threadId });
|
|
3439
|
-
}
|
|
3440
|
-
catch (err) {
|
|
3441
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3442
|
-
log.error("Agent execution error", { threadId, error: errMsg });
|
|
3443
|
-
await this.sendMessage({
|
|
3444
|
-
type: "error",
|
|
3445
|
-
message: `Agent execution error: ${errMsg}`,
|
|
3446
|
-
error_type: "agent_error",
|
|
3447
|
-
thread_id: threadId,
|
|
3448
|
-
workflow_id: workflowId
|
|
3449
|
-
});
|
|
3450
|
-
// Signal completion even on error
|
|
3451
|
-
await this.sendMessage({
|
|
3452
|
-
type: "chunk",
|
|
3453
|
-
content: "",
|
|
3454
|
-
done: true,
|
|
3455
|
-
thread_id: threadId,
|
|
3456
|
-
workflow_id: workflowId
|
|
3457
|
-
});
|
|
3458
|
-
// Return error assistant message
|
|
3459
|
-
const errorFinalMsg = {
|
|
3460
|
-
type: "message",
|
|
3461
|
-
role: "assistant",
|
|
3462
|
-
content: `Agent execution error: ${errMsg}`,
|
|
3463
|
-
thread_id: threadId,
|
|
3464
|
-
workflow_id: workflowId,
|
|
3465
|
-
provider: providerId,
|
|
3466
|
-
model,
|
|
3467
|
-
agent_mode: true
|
|
3468
|
-
};
|
|
3469
|
-
await this.saveMessageToDb(errorFinalMsg);
|
|
3470
|
-
await this.sendMessage(errorFinalMsg);
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
async handleInference(data, requestSeq) {
|
|
3474
|
-
const providerId = typeof data.provider === "string" ? data.provider : this.defaultProvider;
|
|
3475
|
-
const model = typeof data.model === "string" ? data.model : this.defaultModel;
|
|
3476
|
-
const rawMessages = Array.isArray(data.messages) ? data.messages : [];
|
|
3477
|
-
log.debug("Inference request", {
|
|
3478
|
-
model,
|
|
3479
|
-
provider: providerId,
|
|
3480
|
-
messages: rawMessages.length
|
|
3481
|
-
});
|
|
3482
|
-
const messages = rawMessages.map((m) => {
|
|
3483
|
-
const msg = m;
|
|
3484
|
-
return {
|
|
3485
|
-
role: (typeof msg.role === "string"
|
|
3486
|
-
? msg.role
|
|
3487
|
-
: "user"),
|
|
3488
|
-
content: typeof msg.content === "string"
|
|
3489
|
-
? msg.content
|
|
3490
|
-
: Array.isArray(msg.content)
|
|
3491
|
-
? msg.content
|
|
3492
|
-
: "",
|
|
3493
|
-
toolCallId: typeof msg.toolCallId === "string" ? msg.toolCallId : null,
|
|
3494
|
-
toolCalls: Array.isArray(msg.toolCalls)
|
|
3495
|
-
? msg.toolCalls
|
|
3496
|
-
: null,
|
|
3497
|
-
threadId: null
|
|
3498
|
-
};
|
|
3499
|
-
});
|
|
3500
|
-
if (!this.resolveProvider) {
|
|
3501
|
-
await this.sendMessage({
|
|
3502
|
-
type: "error",
|
|
3503
|
-
message: "No provider resolver configured"
|
|
3504
|
-
});
|
|
3505
|
-
return;
|
|
3506
|
-
}
|
|
3507
|
-
const rawTools = Array.isArray(data.tools) ? data.tools : [];
|
|
3508
|
-
const tools = rawTools
|
|
3509
|
-
.map((t) => {
|
|
3510
|
-
const tool = t;
|
|
3511
|
-
return {
|
|
3512
|
-
name: typeof tool.name === "string" ? tool.name : "",
|
|
3513
|
-
description: typeof tool.description === "string" ? tool.description : undefined,
|
|
3514
|
-
inputSchema: typeof tool.inputSchema === "object"
|
|
3515
|
-
? tool.inputSchema
|
|
3516
|
-
: undefined
|
|
3517
|
-
};
|
|
3518
|
-
})
|
|
3519
|
-
.filter((t) => t.name.length > 0);
|
|
3520
|
-
const provider = await this.resolveProvider(providerId, this.userId ?? "1");
|
|
3521
|
-
for await (const item of provider.generateMessagesTraced({
|
|
3522
|
-
messages,
|
|
3523
|
-
model,
|
|
3524
|
-
tools: tools.length > 0 ? tools : undefined
|
|
3525
|
-
})) {
|
|
3526
|
-
if (requestSeq !== this.chatRequestSeq)
|
|
3527
|
-
break; // cancelled
|
|
3528
|
-
if ("type" in item && item.type === "chunk") {
|
|
3529
|
-
await this.sendMessage({
|
|
3530
|
-
...item,
|
|
3531
|
-
seq: requestSeq
|
|
3532
|
-
});
|
|
3533
|
-
}
|
|
3534
|
-
else if ("name" in item) {
|
|
3535
|
-
const toolItem = item;
|
|
3536
|
-
log.info("Tool call", { tool: toolItem.name, args: toolItem.args });
|
|
3537
|
-
await this.sendMessage({
|
|
3538
|
-
type: "tool_call",
|
|
3539
|
-
id: toolItem.id,
|
|
3540
|
-
name: toolItem.name,
|
|
3541
|
-
args: toolItem.args,
|
|
3542
|
-
seq: requestSeq
|
|
3543
|
-
});
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
if (requestSeq === this.chatRequestSeq) {
|
|
3547
|
-
log.debug("Inference complete");
|
|
3548
|
-
await this.sendMessage({ type: "inference_done", seq: requestSeq });
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
async handleCommand(command) {
|
|
3552
|
-
const data = command.data ?? {};
|
|
3553
|
-
const jobId = typeof data.job_id === "string" ? data.job_id : undefined;
|
|
3554
|
-
const workflowId = typeof data.workflow_id === "string" ? data.workflow_id : undefined;
|
|
3555
|
-
log.debug("Command", { command: command.command });
|
|
3556
|
-
switch (command.command) {
|
|
3557
|
-
case "clear_models":
|
|
3558
|
-
return this.clearModels();
|
|
3559
|
-
case "run_job":
|
|
3560
|
-
await this.runJob(data);
|
|
3561
|
-
return { message: "Job started", workflow_id: workflowId ?? null };
|
|
3562
|
-
case "reconnect_job":
|
|
3563
|
-
if (!jobId)
|
|
3564
|
-
return { error: "job_id is required" };
|
|
3565
|
-
void this.reconnectJob(jobId, workflowId);
|
|
3566
|
-
return {
|
|
3567
|
-
message: `Reconnecting to job ${jobId}`,
|
|
3568
|
-
job_id: jobId,
|
|
3569
|
-
workflow_id: workflowId ?? null
|
|
3570
|
-
};
|
|
3571
|
-
case "resume_job":
|
|
3572
|
-
if (!jobId)
|
|
3573
|
-
return { error: "job_id is required" };
|
|
3574
|
-
void this.resumeJob(jobId, workflowId);
|
|
3575
|
-
return {
|
|
3576
|
-
message: `Resumption initiated for job ${jobId}`,
|
|
3577
|
-
job_id: jobId,
|
|
3578
|
-
workflow_id: workflowId ?? null
|
|
3579
|
-
};
|
|
3580
|
-
case "stream_input":
|
|
3581
|
-
if (!jobId)
|
|
3582
|
-
return { error: "job_id is required" };
|
|
3583
|
-
{
|
|
3584
|
-
const active = this.activeJobs.get(jobId);
|
|
3585
|
-
log.info("stream_input command", {
|
|
3586
|
-
jobId,
|
|
3587
|
-
hasActive: !!active,
|
|
3588
|
-
inputName: data.input,
|
|
3589
|
-
handle: data.handle,
|
|
3590
|
-
hasValue: data.value !== undefined,
|
|
3591
|
-
activeJobIds: [...this.activeJobs.keys()]
|
|
3592
|
-
});
|
|
3593
|
-
if (!active)
|
|
3594
|
-
return { error: "No active job/context" };
|
|
3595
|
-
const inputName = typeof data.input === "string" ? data.input : "";
|
|
3596
|
-
if (!inputName.trim())
|
|
3597
|
-
return { error: "Invalid input name" };
|
|
3598
|
-
const value = data.value;
|
|
3599
|
-
const handle = typeof data.handle === "string" ? data.handle : undefined;
|
|
3600
|
-
try {
|
|
3601
|
-
await active.runner.pushInputValue(inputName, value, handle);
|
|
3602
|
-
return {
|
|
3603
|
-
message: "Input item streamed",
|
|
3604
|
-
job_id: jobId,
|
|
3605
|
-
workflow_id: workflowId ?? active.workflowId
|
|
3606
|
-
};
|
|
3607
|
-
}
|
|
3608
|
-
catch (err) {
|
|
3609
|
-
log.error("stream_input failed", {
|
|
3610
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3611
|
-
});
|
|
3612
|
-
return {
|
|
3613
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3614
|
-
job_id: jobId,
|
|
3615
|
-
workflow_id: workflowId ?? active.workflowId
|
|
3616
|
-
};
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
case "end_input_stream":
|
|
3620
|
-
if (!jobId)
|
|
3621
|
-
return { error: "job_id is required" };
|
|
3622
|
-
{
|
|
3623
|
-
const active = this.activeJobs.get(jobId);
|
|
3624
|
-
if (!active)
|
|
3625
|
-
return { error: "No active job/context" };
|
|
3626
|
-
const inputName = typeof data.input === "string" ? data.input : "";
|
|
3627
|
-
if (!inputName.trim())
|
|
3628
|
-
return { error: "Invalid input name" };
|
|
3629
|
-
const handle = typeof data.handle === "string" ? data.handle : undefined;
|
|
3630
|
-
try {
|
|
3631
|
-
active.runner.finishInputStream(inputName, handle);
|
|
3632
|
-
return {
|
|
3633
|
-
message: "Input stream ended",
|
|
3634
|
-
job_id: jobId,
|
|
3635
|
-
workflow_id: workflowId ?? active.workflowId
|
|
3636
|
-
};
|
|
3637
|
-
}
|
|
3638
|
-
catch (err) {
|
|
3639
|
-
return {
|
|
3640
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3641
|
-
job_id: jobId,
|
|
3642
|
-
workflow_id: workflowId ?? active.workflowId
|
|
3643
|
-
};
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3646
|
-
case "cancel_job":
|
|
3647
|
-
if (!jobId)
|
|
3648
|
-
return { error: "job_id is required" };
|
|
3649
|
-
return this.cancelJob(jobId, workflowId);
|
|
3650
|
-
case "get_status":
|
|
3651
|
-
return this.getStatus(jobId);
|
|
3652
|
-
case "set_mode": {
|
|
3653
|
-
const mode = data.mode;
|
|
3654
|
-
if (mode !== "binary" && mode !== "text") {
|
|
3655
|
-
return { error: "mode must be binary or text" };
|
|
3656
|
-
}
|
|
3657
|
-
this.mode = mode;
|
|
3658
|
-
return { message: `Mode set to ${mode}` };
|
|
3659
|
-
}
|
|
3660
|
-
case "chat_message": {
|
|
3661
|
-
const threadId = data.thread_id;
|
|
3662
|
-
if (typeof threadId !== "string" || threadId.length === 0) {
|
|
3663
|
-
return { error: "thread_id is required for chat_message command" };
|
|
3664
|
-
}
|
|
3665
|
-
this.chatRequestSeq += 1;
|
|
3666
|
-
const seq = this.chatRequestSeq;
|
|
3667
|
-
this.currentTask = this.handleChatMessage(data, seq);
|
|
3668
|
-
void this.currentTask.catch(async (err) => {
|
|
3669
|
-
this.logError("chat_message processing failed", err);
|
|
3670
|
-
await this.sendMessage({
|
|
3671
|
-
type: "error",
|
|
3672
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3673
|
-
});
|
|
3674
|
-
});
|
|
3675
|
-
return {
|
|
3676
|
-
message: "Chat message processing started",
|
|
3677
|
-
thread_id: threadId
|
|
3678
|
-
};
|
|
3679
|
-
}
|
|
3680
|
-
case "inference": {
|
|
3681
|
-
this.chatRequestSeq += 1;
|
|
3682
|
-
const seq = this.chatRequestSeq;
|
|
3683
|
-
this.currentTask = this.handleInference(data, seq);
|
|
3684
|
-
void this.currentTask.catch(async (err) => {
|
|
3685
|
-
this.logError("inference processing failed", err);
|
|
3686
|
-
await this.sendMessage({
|
|
3687
|
-
type: "error",
|
|
3688
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3689
|
-
});
|
|
3690
|
-
});
|
|
3691
|
-
return { message: "Inference started" };
|
|
3692
|
-
}
|
|
3693
|
-
case "stop": {
|
|
3694
|
-
const threadId = typeof data.thread_id === "string" ? data.thread_id : undefined;
|
|
3695
|
-
// Always increment seq to cancel any in-progress chat or inference
|
|
3696
|
-
this.chatRequestSeq += 1;
|
|
3697
|
-
this.currentTask = null;
|
|
3698
|
-
if (jobId) {
|
|
3699
|
-
const active = this.activeJobs.get(jobId);
|
|
3700
|
-
if (active) {
|
|
3701
|
-
active.runner.cancel();
|
|
3702
|
-
active.finished = true;
|
|
3703
|
-
active.status = "cancelled";
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
this.toolBridge.cancelAll();
|
|
3707
|
-
await this.sendMessage({
|
|
3708
|
-
type: "generation_stopped",
|
|
3709
|
-
message: "Generation stopped by user",
|
|
3710
|
-
job_id: jobId ?? null,
|
|
3711
|
-
thread_id: threadId ?? null
|
|
3712
|
-
});
|
|
3713
|
-
return {
|
|
3714
|
-
message: "Stop command processed",
|
|
3715
|
-
job_id: jobId ?? null,
|
|
3716
|
-
thread_id: threadId ?? null
|
|
3717
|
-
};
|
|
3718
|
-
}
|
|
3719
|
-
default:
|
|
3720
|
-
return { error: "Unknown command" };
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
startHeartbeat() {
|
|
3724
|
-
this.stopHeartbeat();
|
|
3725
|
-
this.heartbeatTimer = setInterval(() => {
|
|
3726
|
-
this.sendMessage({ type: "ping", ts: Date.now() / 1000 }).catch((err) => {
|
|
3727
|
-
log.warn("Failed to send heartbeat ping", { error: String(err) });
|
|
3728
|
-
});
|
|
3729
|
-
}, 25_000);
|
|
3730
|
-
}
|
|
3731
|
-
stopHeartbeat() {
|
|
3732
|
-
if (this.heartbeatTimer) {
|
|
3733
|
-
clearInterval(this.heartbeatTimer);
|
|
3734
|
-
this.heartbeatTimer = null;
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
startStatsBroadcast() {
|
|
3738
|
-
this.stopStatsBroadcast();
|
|
3739
|
-
this.statsTimer = setInterval(() => {
|
|
3740
|
-
this.sendMessage({
|
|
3741
|
-
type: "system_stats",
|
|
3742
|
-
stats: this.getSystemStats()
|
|
3743
|
-
}).catch((err) => {
|
|
3744
|
-
log.warn("Failed to send system stats", { error: String(err) });
|
|
3745
|
-
});
|
|
3746
|
-
}, 30_000);
|
|
3747
|
-
}
|
|
3748
|
-
stopStatsBroadcast() {
|
|
3749
|
-
if (this.statsTimer) {
|
|
3750
|
-
clearInterval(this.statsTimer);
|
|
3751
|
-
this.statsTimer = null;
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
registerObserver() {
|
|
3755
|
-
if (this.observerRegistered)
|
|
3756
|
-
return;
|
|
3757
|
-
ModelObserver.subscribe(this.onModelChange);
|
|
3758
|
-
this.observerRegistered = true;
|
|
3759
|
-
}
|
|
3760
|
-
unregisterObserver() {
|
|
3761
|
-
if (!this.observerRegistered)
|
|
3762
|
-
return;
|
|
3763
|
-
ModelObserver.unsubscribe(this.onModelChange);
|
|
3764
|
-
this.observerRegistered = false;
|
|
3765
|
-
}
|
|
3766
|
-
onModelChange = (instance, event) => {
|
|
3767
|
-
if (!this.websocket)
|
|
3768
|
-
return;
|
|
3769
|
-
void this.sendMessage({
|
|
3770
|
-
type: "resource_change",
|
|
3771
|
-
event,
|
|
3772
|
-
resource_type: instance.constructor.name.toLowerCase(),
|
|
3773
|
-
resource: {
|
|
3774
|
-
id: instance.partitionValue(),
|
|
3775
|
-
etag: instance.getEtag()
|
|
3776
|
-
}
|
|
3777
|
-
});
|
|
3778
|
-
};
|
|
3779
|
-
async run(websocket) {
|
|
3780
|
-
await this.connect(websocket, this.userId ?? undefined, this.authToken ?? undefined);
|
|
3781
|
-
try {
|
|
3782
|
-
await this.receiveMessages();
|
|
3783
|
-
}
|
|
3784
|
-
finally {
|
|
3785
|
-
await this.disconnect();
|
|
3786
|
-
}
|
|
3787
|
-
}
|
|
3788
|
-
async receiveMessages() {
|
|
3789
|
-
while (true) {
|
|
3790
|
-
const data = await this.receiveMessage();
|
|
3791
|
-
if (data === null)
|
|
3792
|
-
break;
|
|
3793
|
-
const msgType = typeof data.type === "string" ? data.type : null;
|
|
3794
|
-
if (msgType === "client_tools_manifest") {
|
|
3795
|
-
const tools = Array.isArray(data.tools) ? data.tools : [];
|
|
3796
|
-
this.clientToolsManifest = {};
|
|
3797
|
-
for (const tool of tools) {
|
|
3798
|
-
if (tool &&
|
|
3799
|
-
typeof tool === "object" &&
|
|
3800
|
-
typeof tool.name === "string") {
|
|
3801
|
-
const name = tool.name;
|
|
3802
|
-
this.clientToolsManifest[name] = tool;
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
continue;
|
|
3806
|
-
}
|
|
3807
|
-
if (msgType === "tool_result") {
|
|
3808
|
-
const toolCallId = typeof data.tool_call_id === "string" ? data.tool_call_id : null;
|
|
3809
|
-
if (toolCallId) {
|
|
3810
|
-
this.toolBridge.resolveResult(toolCallId, data);
|
|
3811
|
-
}
|
|
3812
|
-
continue;
|
|
3813
|
-
}
|
|
3814
|
-
if (msgType === "ping") {
|
|
3815
|
-
await this.sendMessage({ type: "pong", ts: Date.now() / 1000 });
|
|
3816
|
-
continue;
|
|
3817
|
-
}
|
|
3818
|
-
if (typeof data.command === "string") {
|
|
3819
|
-
try {
|
|
3820
|
-
const command = data;
|
|
3821
|
-
const response = await this.handleCommand(command);
|
|
3822
|
-
await this.sendMessage(response);
|
|
3823
|
-
}
|
|
3824
|
-
catch (err) {
|
|
3825
|
-
this.logError("invalid_command handling failed", err);
|
|
3826
|
-
await this.sendMessage({
|
|
3827
|
-
error: "invalid_command",
|
|
3828
|
-
details: err instanceof Error ? err.message : String(err)
|
|
3829
|
-
});
|
|
3830
|
-
}
|
|
3831
|
-
continue;
|
|
3832
|
-
}
|
|
3833
|
-
await this.sendMessage({
|
|
3834
|
-
error: "invalid_message",
|
|
3835
|
-
message: "All messages must include a 'command' field. Use 'chat_message' command for chat."
|
|
3836
|
-
});
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
}
|
|
3840
|
-
//# sourceMappingURL=unified-websocket-runner.js.map
|