@trigger.dev/sdk 0.0.0-python-preview-20250226140121 → 0.0.0-rc-4-8-0-20261021151928
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -0
- package/dist/commonjs/imports/ai-runtime.d.ts +1 -0
- package/dist/commonjs/imports/ai-runtime.js +31 -0
- package/dist/commonjs/imports/uncrypto.js +4 -4
- package/dist/commonjs/package.json +2 -1
- package/dist/commonjs/v3/agentSkillsRuntime.d.ts +28 -0
- package/dist/commonjs/v3/agentSkillsRuntime.js +173 -0
- package/dist/commonjs/v3/agentSkillsRuntime.js.map +1 -0
- package/dist/commonjs/v3/ai-shared.d.ts +156 -0
- package/dist/commonjs/v3/ai-shared.js +215 -0
- package/dist/commonjs/v3/ai-shared.js.map +1 -0
- package/dist/commonjs/v3/ai.d.ts +4225 -0
- package/dist/commonjs/v3/ai.js +11569 -0
- package/dist/commonjs/v3/ai.js.map +1 -0
- package/dist/commonjs/v3/aiAutoTelemetry.d.ts +2 -0
- package/dist/commonjs/v3/aiAutoTelemetry.js +81 -0
- package/dist/commonjs/v3/aiAutoTelemetry.js.map +1 -0
- package/dist/commonjs/v3/auth.d.ts +71 -10
- package/dist/commonjs/v3/auth.js +85 -19
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/batch.d.ts +3 -3
- package/dist/commonjs/v3/batch.js.map +1 -1
- package/dist/commonjs/v3/cache.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +324 -0
- package/dist/commonjs/v3/chat-client.js +774 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -0
- package/dist/commonjs/v3/chat-react.d.ts +244 -0
- package/dist/commonjs/v3/chat-react.js +463 -0
- package/dist/commonjs/v3/chat-react.js.map +1 -0
- package/dist/commonjs/v3/chat-server.d.ts +350 -0
- package/dist/commonjs/v3/chat-server.js +965 -0
- package/dist/commonjs/v3/chat-server.js.map +1 -0
- package/dist/commonjs/v3/chat-tab-coordinator.d.ts +65 -0
- package/dist/commonjs/v3/chat-tab-coordinator.js +235 -0
- package/dist/commonjs/v3/chat-tab-coordinator.js.map +1 -0
- package/dist/commonjs/v3/chat.d.ts +792 -0
- package/dist/commonjs/v3/chat.js +1662 -0
- package/dist/commonjs/v3/chat.js.map +1 -0
- package/dist/commonjs/v3/chatRouteWait.d.ts +21 -0
- package/dist/commonjs/v3/chatRouteWait.js +43 -0
- package/dist/commonjs/v3/chatRouteWait.js.map +1 -0
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
- package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
- package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
- package/dist/commonjs/v3/chatVersionSkew.js +30 -0
- package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
- package/dist/commonjs/v3/compactionResponse.d.ts +9 -0
- package/dist/commonjs/v3/compactionResponse.js +37 -0
- package/dist/commonjs/v3/compactionResponse.js.map +1 -0
- package/dist/commonjs/v3/concurrency-shared.d.ts +13 -0
- package/dist/commonjs/v3/concurrency-shared.js +35 -0
- package/dist/commonjs/v3/concurrency-shared.js.map +1 -0
- package/dist/commonjs/v3/concurrencyLimits.d.ts +103 -0
- package/dist/commonjs/v3/concurrencyLimits.js +134 -0
- package/dist/commonjs/v3/concurrencyLimits.js.map +1 -0
- package/dist/commonjs/v3/config.js.map +1 -1
- package/dist/commonjs/v3/deployments.d.ts +25 -0
- package/dist/commonjs/v3/deployments.js +37 -0
- package/dist/commonjs/v3/deployments.js.map +1 -0
- package/dist/commonjs/v3/envvars.d.ts +5 -5
- package/dist/commonjs/v3/envvars.js.map +1 -1
- package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
- package/dist/commonjs/v3/externalDeploymentId.js +43 -0
- package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
- package/dist/commonjs/v3/heartbeats.d.ts +37 -0
- package/dist/commonjs/v3/heartbeats.js +45 -0
- package/dist/commonjs/v3/heartbeats.js.map +1 -0
- package/dist/commonjs/v3/hooks.d.ts +26 -0
- package/dist/commonjs/v3/hooks.js +79 -0
- package/dist/commonjs/v3/hooks.js.map +1 -0
- package/dist/commonjs/v3/idempotencyKeys.d.ts +2 -1
- package/dist/commonjs/v3/idempotencyKeys.js +1 -0
- package/dist/commonjs/v3/idempotencyKeys.js.map +1 -1
- package/dist/commonjs/v3/index.d.ts +20 -5
- package/dist/commonjs/v3/index.js +56 -20
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/locals.d.ts +3 -0
- package/dist/commonjs/v3/locals.js +6 -0
- package/dist/commonjs/v3/locals.js.map +1 -0
- package/dist/commonjs/v3/managedChatResponse.d.ts +44 -0
- package/dist/commonjs/v3/managedChatResponse.js +221 -0
- package/dist/commonjs/v3/managedChatResponse.js.map +1 -0
- package/dist/commonjs/v3/metadata.d.ts +5 -2
- package/dist/commonjs/v3/metadata.js +8 -1
- package/dist/commonjs/v3/metadata.js.map +1 -1
- package/dist/commonjs/v3/modelFileDownload.d.ts +10 -0
- package/dist/commonjs/v3/modelFileDownload.js +164 -0
- package/dist/commonjs/v3/modelFileDownload.js.map +1 -0
- package/dist/commonjs/v3/otel.d.ts +4 -0
- package/dist/commonjs/v3/otel.js +12 -0
- package/dist/commonjs/v3/otel.js.map +1 -0
- package/dist/commonjs/v3/prompt.d.ts +38 -0
- package/dist/commonjs/v3/prompt.js +146 -0
- package/dist/commonjs/v3/prompt.js.map +1 -0
- package/dist/commonjs/v3/promptManagement.d.ts +26 -0
- package/dist/commonjs/v3/promptManagement.js +180 -0
- package/dist/commonjs/v3/promptManagement.js.map +1 -0
- package/dist/commonjs/v3/prompts.d.ts +3 -0
- package/dist/commonjs/v3/prompts.js +15 -0
- package/dist/commonjs/v3/prompts.js.map +1 -0
- package/dist/commonjs/v3/query.d.ts +545 -0
- package/dist/commonjs/v3/query.js +163 -0
- package/dist/commonjs/v3/query.js.map +1 -0
- package/dist/commonjs/v3/queues.d.ts +107 -0
- package/dist/commonjs/v3/queues.js +225 -0
- package/dist/commonjs/v3/queues.js.map +1 -0
- package/dist/commonjs/v3/retry.d.ts +6 -2
- package/dist/commonjs/v3/retry.js +34 -23
- package/dist/commonjs/v3/retry.js.map +1 -1
- package/dist/commonjs/v3/runs.d.ts +182 -101
- package/dist/commonjs/v3/runs.js +99 -5
- package/dist/commonjs/v3/runs.js.map +1 -1
- package/dist/commonjs/v3/schedules/api.d.ts +8 -1
- package/dist/commonjs/v3/schedules/index.d.ts +29 -9
- package/dist/commonjs/v3/schedules/index.js +5 -1
- package/dist/commonjs/v3/schedules/index.js.map +1 -1
- package/dist/commonjs/v3/schemas.d.ts +1 -0
- package/dist/commonjs/{integrations.js → v3/schemas.js} +1 -1
- package/dist/commonjs/v3/schemas.js.map +1 -0
- package/dist/commonjs/v3/sessionTracing.d.ts +7 -0
- package/dist/commonjs/v3/sessionTracing.js +44 -0
- package/dist/commonjs/v3/sessionTracing.js.map +1 -0
- package/dist/commonjs/v3/sessions.d.ts +268 -0
- package/dist/commonjs/v3/sessions.js +776 -0
- package/dist/commonjs/v3/sessions.js.map +1 -0
- package/dist/commonjs/v3/shared.d.ts +129 -24
- package/dist/commonjs/v3/shared.js +1527 -689
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/skill.d.ts +99 -0
- package/dist/commonjs/v3/skill.js +165 -0
- package/dist/commonjs/v3/skill.js.map +1 -0
- package/dist/commonjs/v3/skills.d.ts +2 -0
- package/dist/commonjs/v3/skills.js +6 -0
- package/dist/commonjs/v3/skills.js.map +1 -0
- package/dist/commonjs/v3/steeringContext.d.ts +41 -0
- package/dist/commonjs/v3/steeringContext.js +118 -0
- package/dist/commonjs/v3/steeringContext.js.map +1 -0
- package/dist/commonjs/v3/streams.d.ts +298 -0
- package/dist/commonjs/v3/streams.js +584 -0
- package/dist/commonjs/v3/streams.js.map +1 -0
- package/dist/commonjs/v3/tasks.d.ts +19 -4
- package/dist/commonjs/v3/tasks.js +15 -1
- package/dist/commonjs/v3/tasks.js.map +1 -1
- package/dist/commonjs/v3/test/index.d.ts +5 -0
- package/dist/commonjs/v3/test/index.js +23 -0
- package/dist/commonjs/v3/test/index.js.map +1 -0
- package/dist/commonjs/v3/test/mock-channel-connector.d.ts +172 -0
- package/dist/commonjs/v3/test/mock-channel-connector.js +173 -0
- package/dist/commonjs/v3/test/mock-channel-connector.js.map +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +388 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js +689 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -0
- package/dist/commonjs/v3/test/setup-catalog.d.ts +1 -0
- package/dist/commonjs/v3/test/setup-catalog.js +18 -0
- package/dist/commonjs/v3/test/setup-catalog.js.map +1 -0
- package/dist/commonjs/v3/test/test-session-handle.d.ts +24 -0
- package/dist/commonjs/v3/test/test-session-handle.js +271 -0
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/commonjs/v3/toolResultOrder.d.ts +15 -0
- package/dist/commonjs/v3/toolResultOrder.js +58 -0
- package/dist/commonjs/v3/toolResultOrder.js.map +1 -0
- package/dist/commonjs/v3/transcriptStorage.d.ts +307 -0
- package/dist/commonjs/v3/transcriptStorage.js +445 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
- package/dist/commonjs/v3/triggerClient.d.ts +45 -0
- package/dist/commonjs/v3/triggerClient.js +114 -0
- package/dist/commonjs/v3/triggerClient.js.map +1 -0
- package/dist/commonjs/v3/uiMessageChunks.d.ts +1 -0
- package/dist/commonjs/v3/uiMessageChunks.js +119 -0
- package/dist/commonjs/v3/uiMessageChunks.js.map +1 -0
- package/dist/commonjs/v3/usage.js +7 -6
- package/dist/commonjs/v3/usage.js.map +1 -1
- package/dist/commonjs/v3/wait.d.ts +218 -3
- package/dist/commonjs/v3/wait.js +433 -7
- package/dist/commonjs/v3/wait.js.map +1 -1
- package/dist/commonjs/v3/waitUntil.js.map +1 -1
- package/dist/commonjs/v3/webhookWaiters.d.ts +80 -0
- package/dist/commonjs/v3/webhookWaiters.js +156 -0
- package/dist/commonjs/v3/webhookWaiters.js.map +1 -0
- package/dist/commonjs/v3/webhooks.d.ts +192 -2
- package/dist/commonjs/v3/webhooks.js +340 -2
- package/dist/commonjs/v3/webhooks.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/imports/ai-runtime.d.ts +2 -0
- package/dist/esm/imports/ai-runtime.js +16 -0
- package/dist/esm/imports/ai-runtime.js.map +1 -0
- package/dist/esm/package.json +2 -1
- package/dist/esm/v3/agentSkillsRuntime.d.ts +28 -0
- package/dist/esm/v3/agentSkillsRuntime.js +136 -0
- package/dist/esm/v3/agentSkillsRuntime.js.map +1 -0
- package/dist/esm/v3/ai-shared.d.ts +156 -0
- package/dist/esm/v3/ai-shared.js +208 -0
- package/dist/esm/v3/ai-shared.js.map +1 -0
- package/dist/esm/v3/ai.d.ts +4225 -0
- package/dist/esm/v3/ai.js +11550 -0
- package/dist/esm/v3/ai.js.map +1 -0
- package/dist/esm/v3/aiAutoTelemetry.d.ts +2 -0
- package/dist/esm/v3/aiAutoTelemetry.js +78 -0
- package/dist/esm/v3/aiAutoTelemetry.js.map +1 -0
- package/dist/esm/v3/auth.d.ts +71 -10
- package/dist/esm/v3/auth.js +83 -17
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/batch.d.ts +3 -3
- package/dist/esm/v3/batch.js +2 -2
- package/dist/esm/v3/batch.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +324 -0
- package/dist/esm/v3/chat-client.js +769 -0
- package/dist/esm/v3/chat-client.js.map +1 -0
- package/dist/esm/v3/chat-react.d.ts +244 -0
- package/dist/esm/v3/chat-react.js +455 -0
- package/dist/esm/v3/chat-react.js.map +1 -0
- package/dist/esm/v3/chat-server.d.ts +350 -0
- package/dist/esm/v3/chat-server.js +962 -0
- package/dist/esm/v3/chat-server.js.map +1 -0
- package/dist/esm/v3/chat-tab-coordinator.d.ts +65 -0
- package/dist/esm/v3/chat-tab-coordinator.js +231 -0
- package/dist/esm/v3/chat-tab-coordinator.js.map +1 -0
- package/dist/esm/v3/chat.d.ts +792 -0
- package/dist/esm/v3/chat.js +1655 -0
- package/dist/esm/v3/chat.js.map +1 -0
- package/dist/esm/v3/chatRouteWait.d.ts +21 -0
- package/dist/esm/v3/chatRouteWait.js +40 -0
- package/dist/esm/v3/chatRouteWait.js.map +1 -0
- package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/esm/v3/chatSnapshotIo.js +165 -0
- package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
- package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
- package/dist/esm/v3/chatVersionSkew.js +27 -0
- package/dist/esm/v3/chatVersionSkew.js.map +1 -0
- package/dist/esm/v3/compactionResponse.d.ts +9 -0
- package/dist/esm/v3/compactionResponse.js +34 -0
- package/dist/esm/v3/compactionResponse.js.map +1 -0
- package/dist/esm/v3/concurrency-shared.d.ts +13 -0
- package/dist/esm/v3/concurrency-shared.js +31 -0
- package/dist/esm/v3/concurrency-shared.js.map +1 -0
- package/dist/esm/v3/concurrencyLimits.d.ts +103 -0
- package/dist/esm/v3/concurrencyLimits.js +126 -0
- package/dist/esm/v3/concurrencyLimits.js.map +1 -0
- package/dist/esm/v3/deployments.d.ts +25 -0
- package/dist/esm/v3/deployments.js +34 -0
- package/dist/esm/v3/deployments.js.map +1 -0
- package/dist/esm/v3/envvars.d.ts +5 -5
- package/dist/esm/v3/envvars.js.map +1 -1
- package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
- package/dist/esm/v3/externalDeploymentId.js +38 -0
- package/dist/esm/v3/externalDeploymentId.js.map +1 -0
- package/dist/esm/v3/heartbeats.d.ts +37 -0
- package/dist/esm/v3/heartbeats.js +42 -0
- package/dist/esm/v3/heartbeats.js.map +1 -0
- package/dist/esm/v3/hooks.d.ts +26 -0
- package/dist/esm/v3/hooks.js +66 -0
- package/dist/esm/v3/hooks.js.map +1 -0
- package/dist/esm/v3/idempotencyKeys.d.ts +2 -1
- package/dist/esm/v3/idempotencyKeys.js +2 -1
- package/dist/esm/v3/idempotencyKeys.js.map +1 -1
- package/dist/esm/v3/index.d.ts +20 -5
- package/dist/esm/v3/index.js +16 -2
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/esm/v3/locals.d.ts +3 -0
- package/dist/esm/v3/locals.js +3 -0
- package/dist/esm/v3/locals.js.map +1 -0
- package/dist/esm/v3/managedChatResponse.d.ts +44 -0
- package/dist/esm/v3/managedChatResponse.js +216 -0
- package/dist/esm/v3/managedChatResponse.js.map +1 -0
- package/dist/esm/v3/metadata.d.ts +5 -2
- package/dist/esm/v3/metadata.js +8 -1
- package/dist/esm/v3/metadata.js.map +1 -1
- package/dist/esm/v3/modelFileDownload.d.ts +10 -0
- package/dist/esm/v3/modelFileDownload.js +161 -0
- package/dist/esm/v3/modelFileDownload.js.map +1 -0
- package/dist/esm/v3/otel.d.ts +4 -0
- package/dist/esm/v3/otel.js +9 -0
- package/dist/esm/v3/otel.js.map +1 -0
- package/dist/esm/v3/prompt.d.ts +38 -0
- package/dist/esm/v3/prompt.js +143 -0
- package/dist/esm/v3/prompt.js.map +1 -0
- package/dist/esm/v3/promptManagement.d.ts +26 -0
- package/dist/esm/v3/promptManagement.js +170 -0
- package/dist/esm/v3/promptManagement.js.map +1 -0
- package/dist/esm/v3/prompts.d.ts +3 -0
- package/dist/esm/v3/prompts.js +3 -0
- package/dist/esm/v3/prompts.js.map +1 -0
- package/dist/esm/v3/query.d.ts +545 -0
- package/dist/esm/v3/query.js +160 -0
- package/dist/esm/v3/query.js.map +1 -0
- package/dist/esm/v3/queues.d.ts +107 -0
- package/dist/esm/v3/queues.js +217 -0
- package/dist/esm/v3/queues.js.map +1 -0
- package/dist/esm/v3/retry.d.ts +6 -2
- package/dist/esm/v3/retry.js +27 -19
- package/dist/esm/v3/retry.js.map +1 -1
- package/dist/esm/v3/runs.d.ts +179 -98
- package/dist/esm/v3/runs.js +99 -5
- package/dist/esm/v3/runs.js.map +1 -1
- package/dist/esm/v3/schedules/api.d.ts +8 -1
- package/dist/esm/v3/schedules/index.d.ts +29 -9
- package/dist/esm/v3/schedules/index.js +6 -2
- package/dist/esm/v3/schedules/index.js.map +1 -1
- package/dist/esm/v3/schemas.d.ts +1 -0
- package/dist/esm/v3/schemas.js +2 -0
- package/dist/esm/v3/schemas.js.map +1 -0
- package/dist/esm/v3/sessionTracing.d.ts +7 -0
- package/dist/esm/v3/sessionTracing.js +40 -0
- package/dist/esm/v3/sessionTracing.js.map +1 -0
- package/dist/esm/v3/sessions.d.ts +268 -0
- package/dist/esm/v3/sessions.js +767 -0
- package/dist/esm/v3/sessions.js.map +1 -0
- package/dist/esm/v3/shared.d.ts +129 -24
- package/dist/esm/v3/shared.js +1521 -688
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/skill.d.ts +99 -0
- package/dist/esm/v3/skill.js +128 -0
- package/dist/esm/v3/skill.js.map +1 -0
- package/dist/esm/v3/skills.d.ts +2 -0
- package/dist/esm/v3/skills.js +2 -0
- package/dist/esm/v3/skills.js.map +1 -0
- package/dist/esm/v3/steeringContext.d.ts +41 -0
- package/dist/esm/v3/steeringContext.js +113 -0
- package/dist/esm/v3/steeringContext.js.map +1 -0
- package/dist/esm/v3/streams.d.ts +298 -0
- package/dist/esm/v3/streams.js +580 -0
- package/dist/esm/v3/streams.js.map +1 -0
- package/dist/esm/v3/tasks.d.ts +19 -4
- package/dist/esm/v3/tasks.js +16 -2
- package/dist/esm/v3/tasks.js.map +1 -1
- package/dist/esm/v3/test/index.d.ts +5 -0
- package/dist/esm/v3/test/index.js +15 -0
- package/dist/esm/v3/test/index.js.map +1 -0
- package/dist/esm/v3/test/mock-channel-connector.d.ts +172 -0
- package/dist/esm/v3/test/mock-channel-connector.js +170 -0
- package/dist/esm/v3/test/mock-channel-connector.js.map +1 -0
- package/dist/esm/v3/test/mock-chat-agent.d.ts +388 -0
- package/dist/esm/v3/test/mock-chat-agent.js +685 -0
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -0
- package/dist/esm/v3/test/setup-catalog.d.ts +1 -0
- package/dist/esm/v3/test/setup-catalog.js +16 -0
- package/dist/esm/v3/test/setup-catalog.js.map +1 -0
- package/dist/esm/v3/test/test-session-handle.d.ts +24 -0
- package/dist/esm/v3/test/test-session-handle.js +268 -0
- package/dist/esm/v3/test/test-session-handle.js.map +1 -0
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/esm/v3/toolResultOrder.d.ts +15 -0
- package/dist/esm/v3/toolResultOrder.js +55 -0
- package/dist/esm/v3/toolResultOrder.js.map +1 -0
- package/dist/esm/v3/transcriptStorage.d.ts +307 -0
- package/dist/esm/v3/transcriptStorage.js +431 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -0
- package/dist/esm/v3/triggerClient.d.ts +45 -0
- package/dist/esm/v3/triggerClient.js +77 -0
- package/dist/esm/v3/triggerClient.js.map +1 -0
- package/dist/esm/v3/uiMessageChunks.d.ts +1 -0
- package/dist/esm/v3/uiMessageChunks.js +116 -0
- package/dist/esm/v3/uiMessageChunks.js.map +1 -0
- package/dist/esm/v3/usage.js +7 -6
- package/dist/esm/v3/usage.js.map +1 -1
- package/dist/esm/v3/wait.d.ts +218 -3
- package/dist/esm/v3/wait.js +431 -7
- package/dist/esm/v3/wait.js.map +1 -1
- package/dist/esm/v3/webhookWaiters.d.ts +80 -0
- package/dist/esm/v3/webhookWaiters.js +149 -0
- package/dist/esm/v3/webhookWaiters.js.map +1 -0
- package/dist/esm/v3/webhooks.d.ts +192 -2
- package/dist/esm/v3/webhooks.js +336 -2
- package/dist/esm/v3/webhooks.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/observability.mdx +174 -0
- package/docs/ai/prompts.mdx +426 -0
- package/docs/ai-chat/actions.mdx +148 -0
- package/docs/ai-chat/anatomy.mdx +87 -0
- package/docs/ai-chat/backend.mdx +933 -0
- package/docs/ai-chat/background-injection.mdx +320 -0
- package/docs/ai-chat/changelog.mdx +958 -0
- package/docs/ai-chat/chat-local.mdx +170 -0
- package/docs/ai-chat/client-protocol.mdx +1091 -0
- package/docs/ai-chat/compaction.mdx +413 -0
- package/docs/ai-chat/custom-agents.mdx +545 -0
- package/docs/ai-chat/error-handling.mdx +379 -0
- package/docs/ai-chat/fast-starts.mdx +764 -0
- package/docs/ai-chat/frontend.mdx +634 -0
- package/docs/ai-chat/how-it-works.mdx +226 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +532 -0
- package/docs/ai-chat/mcp.mdx +97 -0
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +594 -0
- package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
- package/docs/ai-chat/overview.mdx +86 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +145 -0
- package/docs/ai-chat/patterns/code-sandbox.mdx +122 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +425 -0
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +281 -0
- package/docs/ai-chat/patterns/large-payloads.mdx +165 -0
- package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
- package/docs/ai-chat/patterns/oom-resilience.mdx +116 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +218 -0
- package/docs/ai-chat/patterns/recovery-boot.mdx +233 -0
- package/docs/ai-chat/patterns/redacting-telemetry.mdx +117 -0
- package/docs/ai-chat/patterns/skills.mdx +214 -0
- package/docs/ai-chat/patterns/sub-agents.mdx +412 -0
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +145 -0
- package/docs/ai-chat/patterns/trusted-edge-signals.mdx +333 -0
- package/docs/ai-chat/patterns/version-upgrades.mdx +261 -0
- package/docs/ai-chat/pending-messages.mdx +363 -0
- package/docs/ai-chat/prompt-caching.mdx +204 -0
- package/docs/ai-chat/quick-start.mdx +163 -0
- package/docs/ai-chat/reference.mdx +1071 -0
- package/docs/ai-chat/server-chat.mdx +259 -0
- package/docs/ai-chat/sessions.mdx +334 -0
- package/docs/ai-chat/side-channels.mdx +172 -0
- package/docs/ai-chat/testing.mdx +691 -0
- package/docs/ai-chat/tools.mdx +192 -0
- package/docs/ai-chat/transcript-storage.mdx +247 -0
- package/docs/ai-chat/types.mdx +242 -0
- package/docs/ai-chat/upgrade-guide.mdx +511 -0
- package/docs/apikeys.mdx +170 -0
- package/docs/billing-limits.mdx +73 -0
- package/docs/building-with-ai.mdx +261 -0
- package/docs/bulk-actions.mdx +77 -0
- package/docs/changelog.mdx +6 -0
- package/docs/cli-deploy-commands.mdx +9 -0
- package/docs/cli-dev-commands.mdx +9 -0
- package/docs/cli-env-commands.mdx +115 -0
- package/docs/cli-init-commands.mdx +84 -0
- package/docs/cli-introduction.mdx +25 -0
- package/docs/cli-list-profiles-commands.mdx +42 -0
- package/docs/cli-login-commands.mdx +53 -0
- package/docs/cli-logout-commands.mdx +33 -0
- package/docs/cli-preview-archive.mdx +59 -0
- package/docs/cli-projects-commands.mdx +74 -0
- package/docs/cli-promote-commands.mdx +9 -0
- package/docs/cli-runs-commands.mdx +95 -0
- package/docs/cli-switch.mdx +43 -0
- package/docs/cli-tasks-commands.mdx +95 -0
- package/docs/cli-update-commands.mdx +48 -0
- package/docs/cli-whoami-commands.mdx +45 -0
- package/docs/community.mdx +6 -0
- package/docs/concurrency.mdx +416 -0
- package/docs/config/config-file.mdx +662 -0
- package/docs/config/extensions/additionalFiles.mdx +38 -0
- package/docs/config/extensions/additionalPackages.mdx +40 -0
- package/docs/config/extensions/aptGet.mdx +34 -0
- package/docs/config/extensions/audioWaveform.mdx +23 -0
- package/docs/config/extensions/custom.mdx +393 -0
- package/docs/config/extensions/emitDecoratorMetadata.mdx +52 -0
- package/docs/config/extensions/esbuildPlugin.mdx +31 -0
- package/docs/config/extensions/ffmpeg.mdx +45 -0
- package/docs/config/extensions/lightpanda.mdx +56 -0
- package/docs/config/extensions/overview.mdx +69 -0
- package/docs/config/extensions/playwright.mdx +195 -0
- package/docs/config/extensions/prismaExtension.mdx +1013 -0
- package/docs/config/extensions/puppeteer.mdx +28 -0
- package/docs/config/extensions/pythonExtension.mdx +190 -0
- package/docs/config/extensions/syncEnvVars.mdx +330 -0
- package/docs/context.mdx +235 -0
- package/docs/database-connections.mdx +213 -0
- package/docs/deploy-environment-variables.mdx +466 -0
- package/docs/deployment/atomic-deployment.mdx +470 -0
- package/docs/deployment/dev-branches.mdx +92 -0
- package/docs/deployment/overview.mdx +260 -0
- package/docs/deployment/preview-branches.mdx +227 -0
- package/docs/errors-retrying.mdx +383 -0
- package/docs/github-actions.mdx +260 -0
- package/docs/github-integration.mdx +148 -0
- package/docs/github-repo.mdx +8 -0
- package/docs/help-email.mdx +6 -0
- package/docs/help-slack.mdx +12 -0
- package/docs/hidden-tasks.mdx +56 -0
- package/docs/how-it-works.mdx +454 -0
- package/docs/how-to-reduce-your-spend.mdx +211 -0
- package/docs/idempotency.mdx +542 -0
- package/docs/introduction.mdx +135 -0
- package/docs/limits.mdx +284 -0
- package/docs/logging.mdx +195 -0
- package/docs/machines.mdx +952 -0
- package/docs/manual-setup.mdx +632 -0
- package/docs/mcp-agent-rules.mdx +41 -0
- package/docs/mcp-introduction.mdx +386 -0
- package/docs/mcp-tools.mdx +425 -0
- package/docs/migrating-from-v3.mdx +332 -0
- package/docs/observability/dashboards.mdx +166 -0
- package/docs/observability/query.mdx +614 -0
- package/docs/open-source-contributing.mdx +16 -0
- package/docs/private-networking/aws-console-setup.mdx +304 -0
- package/docs/private-networking/overview.mdx +144 -0
- package/docs/private-networking/troubleshooting.mdx +78 -0
- package/docs/queues.mdx +271 -0
- package/docs/quick-start.mdx +60 -0
- package/docs/realtime/auth.mdx +228 -0
- package/docs/realtime/backend/input-streams.mdx +159 -0
- package/docs/realtime/backend/overview.mdx +45 -0
- package/docs/realtime/backend/streams.mdx +433 -0
- package/docs/realtime/backend/subscribe.mdx +225 -0
- package/docs/realtime/how-it-works.mdx +94 -0
- package/docs/realtime/overview.mdx +63 -0
- package/docs/realtime/react-hooks/overview.mdx +73 -0
- package/docs/realtime/react-hooks/session-stream.mdx +126 -0
- package/docs/realtime/react-hooks/streams.mdx +517 -0
- package/docs/realtime/react-hooks/subscribe.mdx +690 -0
- package/docs/realtime/react-hooks/swr.mdx +87 -0
- package/docs/realtime/react-hooks/triggering.mdx +194 -0
- package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
- package/docs/realtime/run-object.mdx +197 -0
- package/docs/replaying.mdx +97 -0
- package/docs/reports.mdx +159 -0
- package/docs/request-feature.mdx +6 -0
- package/docs/roadmap.mdx +6 -0
- package/docs/run-tests.mdx +21 -0
- package/docs/run-usage.mdx +113 -0
- package/docs/runs/bulk-actions.mdx +183 -0
- package/docs/runs/heartbeats.mdx +38 -0
- package/docs/runs/list-best-practices.mdx +234 -0
- package/docs/runs/max-duration.mdx +143 -0
- package/docs/runs/metadata.mdx +731 -0
- package/docs/runs/priority.mdx +8 -0
- package/docs/runs.mdx +406 -0
- package/docs/self-hosting/docker.mdx +512 -0
- package/docs/self-hosting/env/supervisor.mdx +78 -0
- package/docs/self-hosting/env/webapp.mdx +305 -0
- package/docs/self-hosting/kubernetes.mdx +725 -0
- package/docs/self-hosting/overview.mdx +145 -0
- package/docs/self-hosting/security.mdx +67 -0
- package/docs/skills.mdx +85 -0
- package/docs/tags.mdx +126 -0
- package/docs/tasks/overview.mdx +695 -0
- package/docs/tasks/scheduled.mdx +479 -0
- package/docs/tasks/schemaTask.mdx +413 -0
- package/docs/tasks/streams.mdx +887 -0
- package/docs/triggering.mdx +1340 -0
- package/docs/troubleshooting-alerts.mdx +607 -0
- package/docs/troubleshooting-debugging-in-vscode.mdx +9 -0
- package/docs/troubleshooting-github-issues.mdx +6 -0
- package/docs/troubleshooting-uptime-status.mdx +7 -0
- package/docs/troubleshooting-zod.mdx +158 -0
- package/docs/troubleshooting.mdx +414 -0
- package/docs/upgrading-packages.mdx +80 -0
- package/docs/vercel-integration.mdx +250 -0
- package/docs/versioning.mdx +58 -0
- package/docs/video-walkthrough.mdx +23 -0
- package/docs/wait-for-token.mdx +540 -0
- package/docs/wait-for.mdx +42 -0
- package/docs/wait-until.mdx +53 -0
- package/docs/wait.mdx +18 -0
- package/docs/webhooks/channels.mdx +154 -0
- package/docs/webhooks/connect.mdx +91 -0
- package/docs/webhooks/deliveries.mdx +99 -0
- package/docs/webhooks/filters.mdx +146 -0
- package/docs/webhooks/human-in-the-loop.mdx +158 -0
- package/docs/webhooks/linear.mdx +186 -0
- package/docs/webhooks/overview.mdx +115 -0
- package/docs/webhooks/session-routing.mdx +141 -0
- package/docs/webhooks/slack.mdx +393 -0
- package/docs/webhooks/sources.mdx +250 -0
- package/docs/webhooks/waiters.mdx +190 -0
- package/docs/writing-tasks-introduction.mdx +34 -0
- package/package.json +143 -42
- package/skills/trigger-authoring-chat-agent/SKILL.md +308 -0
- package/skills/trigger-authoring-tasks/SKILL.md +254 -0
- package/skills/trigger-chat-agent-advanced/SKILL.md +403 -0
- package/skills/trigger-cost-savings/SKILL.md +116 -0
- package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
- package/dist/commonjs/apiClient.d.ts +0 -727
- package/dist/commonjs/apiClient.js +0 -692
- package/dist/commonjs/apiClient.js.map +0 -1
- package/dist/commonjs/concurrencyLimit.d.ts +0 -10
- package/dist/commonjs/concurrencyLimit.js +0 -17
- package/dist/commonjs/concurrencyLimit.js.map +0 -1
- package/dist/commonjs/errors.d.ts +0 -66
- package/dist/commonjs/errors.js +0 -109
- package/dist/commonjs/errors.js.map +0 -1
- package/dist/commonjs/httpEndpoint.d.ts +0 -87
- package/dist/commonjs/httpEndpoint.js +0 -130
- package/dist/commonjs/httpEndpoint.js.map +0 -1
- package/dist/commonjs/index.d.ts +0 -23
- package/dist/commonjs/index.js +0 -55
- package/dist/commonjs/index.js.map +0 -1
- package/dist/commonjs/integrations.d.ts +0 -11
- package/dist/commonjs/integrations.js.map +0 -1
- package/dist/commonjs/io.d.ts +0 -441
- package/dist/commonjs/io.js +0 -1165
- package/dist/commonjs/io.js.map +0 -1
- package/dist/commonjs/ioWithIntegrations.d.ts +0 -4
- package/dist/commonjs/ioWithIntegrations.js +0 -32
- package/dist/commonjs/ioWithIntegrations.js.map +0 -1
- package/dist/commonjs/job.d.ts +0 -96
- package/dist/commonjs/job.js +0 -210
- package/dist/commonjs/job.js.map +0 -1
- package/dist/commonjs/retry.d.ts +0 -20
- package/dist/commonjs/retry.js +0 -22
- package/dist/commonjs/retry.js.map +0 -1
- package/dist/commonjs/runLocalStorage.d.ts +0 -8
- package/dist/commonjs/runLocalStorage.js +0 -6
- package/dist/commonjs/runLocalStorage.js.map +0 -1
- package/dist/commonjs/security.d.ts +0 -18
- package/dist/commonjs/security.js +0 -41
- package/dist/commonjs/security.js.map +0 -1
- package/dist/commonjs/status.d.ts +0 -19
- package/dist/commonjs/status.js +0 -38
- package/dist/commonjs/status.js.map +0 -1
- package/dist/commonjs/store/keyValueStore.d.ts +0 -17
- package/dist/commonjs/store/keyValueStore.js +0 -138
- package/dist/commonjs/store/keyValueStore.js.map +0 -1
- package/dist/commonjs/store/keyValueStoreClient.d.ts +0 -19
- package/dist/commonjs/store/keyValueStoreClient.js +0 -65
- package/dist/commonjs/store/keyValueStoreClient.js.map +0 -1
- package/dist/commonjs/triggerClient.d.ts +0 -290
- package/dist/commonjs/triggerClient.js +0 -1366
- package/dist/commonjs/triggerClient.js.map +0 -1
- package/dist/commonjs/triggers/dynamic.d.ts +0 -56
- package/dist/commonjs/triggers/dynamic.js +0 -97
- package/dist/commonjs/triggers/dynamic.js.map +0 -1
- package/dist/commonjs/triggers/eventTrigger.d.ts +0 -58
- package/dist/commonjs/triggers/eventTrigger.js +0 -69
- package/dist/commonjs/triggers/eventTrigger.js.map +0 -1
- package/dist/commonjs/triggers/externalSource.d.ts +0 -148
- package/dist/commonjs/triggers/externalSource.js +0 -106
- package/dist/commonjs/triggers/externalSource.js.map +0 -1
- package/dist/commonjs/triggers/invokeTrigger.d.ts +0 -39
- package/dist/commonjs/triggers/invokeTrigger.js +0 -58
- package/dist/commonjs/triggers/invokeTrigger.js.map +0 -1
- package/dist/commonjs/triggers/notifications.d.ts +0 -111
- package/dist/commonjs/triggers/notifications.js +0 -101
- package/dist/commonjs/triggers/notifications.js.map +0 -1
- package/dist/commonjs/triggers/scheduled.d.ts +0 -145
- package/dist/commonjs/triggers/scheduled.js +0 -208
- package/dist/commonjs/triggers/scheduled.js.map +0 -1
- package/dist/commonjs/triggers/webhook.d.ts +0 -143
- package/dist/commonjs/triggers/webhook.js +0 -133
- package/dist/commonjs/triggers/webhook.js.map +0 -1
- package/dist/commonjs/typed-emitter.d.ts +0 -37
- package/dist/commonjs/typed-emitter.js +0 -3
- package/dist/commonjs/typed-emitter.js.map +0 -1
- package/dist/commonjs/types.d.ts +0 -204
- package/dist/commonjs/types.js +0 -23
- package/dist/commonjs/types.js.map +0 -1
- package/dist/commonjs/utils/formatSchemaErrors.d.ts +0 -3
- package/dist/commonjs/utils/formatSchemaErrors.js +0 -10
- package/dist/commonjs/utils/formatSchemaErrors.js.map +0 -1
- package/dist/commonjs/utils/typedAsyncLocalStorage.d.ts +0 -6
- package/dist/commonjs/utils/typedAsyncLocalStorage.js +0 -18
- package/dist/commonjs/utils/typedAsyncLocalStorage.js.map +0 -1
- package/dist/commonjs/utils.d.ts +0 -1
- package/dist/commonjs/utils.js +0 -11
- package/dist/commonjs/utils.js.map +0 -1
- package/dist/esm/apiClient.d.ts +0 -727
- package/dist/esm/apiClient.js +0 -687
- package/dist/esm/apiClient.js.map +0 -1
- package/dist/esm/concurrencyLimit.d.ts +0 -10
- package/dist/esm/concurrencyLimit.js +0 -13
- package/dist/esm/concurrencyLimit.js.map +0 -1
- package/dist/esm/errors.d.ts +0 -66
- package/dist/esm/errors.js +0 -95
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/httpEndpoint.d.ts +0 -87
- package/dist/esm/httpEndpoint.js +0 -125
- package/dist/esm/httpEndpoint.js.map +0 -1
- package/dist/esm/index.d.ts +0 -23
- package/dist/esm/index.js +0 -35
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/integrations.d.ts +0 -11
- package/dist/esm/integrations.js +0 -2
- package/dist/esm/integrations.js.map +0 -1
- package/dist/esm/io.d.ts +0 -441
- package/dist/esm/io.js +0 -1159
- package/dist/esm/io.js.map +0 -1
- package/dist/esm/ioWithIntegrations.d.ts +0 -4
- package/dist/esm/ioWithIntegrations.js +0 -29
- package/dist/esm/ioWithIntegrations.js.map +0 -1
- package/dist/esm/job.d.ts +0 -96
- package/dist/esm/job.js +0 -206
- package/dist/esm/job.js.map +0 -1
- package/dist/esm/retry.d.ts +0 -20
- package/dist/esm/retry.js +0 -19
- package/dist/esm/retry.js.map +0 -1
- package/dist/esm/runLocalStorage.d.ts +0 -8
- package/dist/esm/runLocalStorage.js +0 -3
- package/dist/esm/runLocalStorage.js.map +0 -1
- package/dist/esm/security.d.ts +0 -18
- package/dist/esm/security.js +0 -34
- package/dist/esm/security.js.map +0 -1
- package/dist/esm/status.d.ts +0 -19
- package/dist/esm/status.js +0 -34
- package/dist/esm/status.js.map +0 -1
- package/dist/esm/store/keyValueStore.d.ts +0 -17
- package/dist/esm/store/keyValueStore.js +0 -134
- package/dist/esm/store/keyValueStore.js.map +0 -1
- package/dist/esm/store/keyValueStoreClient.d.ts +0 -19
- package/dist/esm/store/keyValueStoreClient.js +0 -61
- package/dist/esm/store/keyValueStoreClient.js.map +0 -1
- package/dist/esm/triggerClient.d.ts +0 -288
- package/dist/esm/triggerClient.js +0 -1359
- package/dist/esm/triggerClient.js.map +0 -1
- package/dist/esm/triggers/dynamic.d.ts +0 -56
- package/dist/esm/triggers/dynamic.js +0 -93
- package/dist/esm/triggers/dynamic.js.map +0 -1
- package/dist/esm/triggers/eventTrigger.d.ts +0 -58
- package/dist/esm/triggers/eventTrigger.js +0 -64
- package/dist/esm/triggers/eventTrigger.js.map +0 -1
- package/dist/esm/triggers/externalSource.d.ts +0 -148
- package/dist/esm/triggers/externalSource.js +0 -100
- package/dist/esm/triggers/externalSource.js.map +0 -1
- package/dist/esm/triggers/invokeTrigger.d.ts +0 -39
- package/dist/esm/triggers/invokeTrigger.js +0 -53
- package/dist/esm/triggers/invokeTrigger.js.map +0 -1
- package/dist/esm/triggers/notifications.d.ts +0 -111
- package/dist/esm/triggers/notifications.js +0 -94
- package/dist/esm/triggers/notifications.js.map +0 -1
- package/dist/esm/triggers/scheduled.d.ts +0 -145
- package/dist/esm/triggers/scheduled.js +0 -197
- package/dist/esm/triggers/scheduled.js.map +0 -1
- package/dist/esm/triggers/webhook.d.ts +0 -143
- package/dist/esm/triggers/webhook.js +0 -128
- package/dist/esm/triggers/webhook.js.map +0 -1
- package/dist/esm/typed-emitter.d.ts +0 -37
- package/dist/esm/typed-emitter.js +0 -2
- package/dist/esm/typed-emitter.js.map +0 -1
- package/dist/esm/types.d.ts +0 -204
- package/dist/esm/types.js +0 -19
- package/dist/esm/types.js.map +0 -1
- package/dist/esm/utils/formatSchemaErrors.d.ts +0 -3
- package/dist/esm/utils/formatSchemaErrors.js +0 -7
- package/dist/esm/utils/formatSchemaErrors.js.map +0 -1
- package/dist/esm/utils/typedAsyncLocalStorage.d.ts +0 -6
- package/dist/esm/utils/typedAsyncLocalStorage.js +0 -14
- package/dist/esm/utils/typedAsyncLocalStorage.js.map +0 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -8
- package/dist/esm/utils.js.map +0 -1
|
@@ -0,0 +1,1655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @trigger.dev/sdk/chat
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe module for AI SDK chat transport integration.
|
|
5
|
+
* Use this on the frontend with the AI SDK's `useChat` hook.
|
|
6
|
+
*
|
|
7
|
+
* For backend helpers (`chatAgent`, `pipeChat`), use `@trigger.dev/sdk/ai` instead.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { useChat } from "@ai-sdk/react";
|
|
12
|
+
* import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
|
|
13
|
+
*
|
|
14
|
+
* function Chat() {
|
|
15
|
+
* const { messages, sendMessage, status } = useChat({
|
|
16
|
+
* transport: new TriggerChatTransport({
|
|
17
|
+
* task: "my-chat-task",
|
|
18
|
+
* accessToken: async ({ chatId }) => fetchSessionToken(chatId),
|
|
19
|
+
* startSession: async ({ chatId, taskId }) => createChatSession({ chatId, taskId }),
|
|
20
|
+
* }),
|
|
21
|
+
* });
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { controlSubtype, headerValue, SESSION_CLOSED_HEADER, SESSION_CLOSED_REASON_HEADER, PUBLIC_ACCESS_TOKEN_HEADER, SESSION_IN_EVENT_ID_HEADER, SSEStreamSubscription, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
|
|
26
|
+
function byteLength(body) {
|
|
27
|
+
return new TextEncoder().encode(body).byteLength;
|
|
28
|
+
}
|
|
29
|
+
import { ChatTabCoordinator } from "./chat-tab-coordinator.js";
|
|
30
|
+
import { MAX_EOF_RESUBSCRIBES, slimSubmitMessageForWire, waitBeforeEofResubscribe, } from "./ai-shared.js";
|
|
31
|
+
const DEFAULT_BASE_URL = "https://api.trigger.dev";
|
|
32
|
+
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
|
|
33
|
+
/**
|
|
34
|
+
* Detect 401/403 from realtime/input-stream calls without relying on `instanceof`
|
|
35
|
+
* (Vitest can load duplicate `@trigger.dev/core` copies, which breaks subclass checks).
|
|
36
|
+
*/
|
|
37
|
+
function isAuthError(error) {
|
|
38
|
+
if (error === null || typeof error !== "object")
|
|
39
|
+
return false;
|
|
40
|
+
const e = error;
|
|
41
|
+
return e.name === "TriggerApiError" && (e.status === 401 || e.status === 403);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Detect the closed-session refusal from `.in/append`: HTTP 409 with the
|
|
45
|
+
* stable `session_closed` code. Terminal — never retried.
|
|
46
|
+
*/
|
|
47
|
+
function isSessionClosedError(error) {
|
|
48
|
+
if (error === null || typeof error !== "object")
|
|
49
|
+
return false;
|
|
50
|
+
const e = error;
|
|
51
|
+
return e.name === "TriggerApiError" && e.status === 409 && e.code === "session_closed";
|
|
52
|
+
}
|
|
53
|
+
function sessionClosedReasonOf(error) {
|
|
54
|
+
if (error === null || typeof error !== "object")
|
|
55
|
+
return undefined;
|
|
56
|
+
const reason = error.closedReason;
|
|
57
|
+
return typeof reason === "string" ? reason : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The local form of the refusal, for sends the transport rejects itself. */
|
|
60
|
+
function sessionClosedError(chatId, reason) {
|
|
61
|
+
const err = new Error(reason ? `Chat session ${chatId} is closed: ${reason}` : `Chat session ${chatId} is closed`);
|
|
62
|
+
err.name = "TriggerApiError";
|
|
63
|
+
err.status = 409;
|
|
64
|
+
err.code = "session_closed";
|
|
65
|
+
if (reason)
|
|
66
|
+
err.closedReason = reason;
|
|
67
|
+
return err;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Detect a 404 from a session-PAT-authed call — i.e. the session doesn't
|
|
71
|
+
* exist in the current environment. Happens when hydrated session state
|
|
72
|
+
* (the `sessions` option / a customer's persisted record) was created in a
|
|
73
|
+
* different trigger environment, or predates the upgrade to the sessions
|
|
74
|
+
* model, so there's no real Session row behind the cached PAT.
|
|
75
|
+
*/
|
|
76
|
+
function isSessionNotFoundError(error) {
|
|
77
|
+
if (error === null || typeof error !== "object")
|
|
78
|
+
return false;
|
|
79
|
+
const e = error;
|
|
80
|
+
return e.name === "TriggerApiError" && e.status === 404;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parses an SSE byte/text stream of `data: <UIMessageChunk JSON>\n\n`
|
|
84
|
+
* frames back into `UIMessageChunk` objects. Used by the handover
|
|
85
|
+
* first-turn path to convert the customer's route handler response
|
|
86
|
+
* (which is AI-SDK-shaped SSE text) into the chunk form the AI SDK's
|
|
87
|
+
* `useChat` consumes from a transport.
|
|
88
|
+
*
|
|
89
|
+
* Spec-light parser — assumes well-formed `data:` events from our own
|
|
90
|
+
* `chat.handover` SSE writer. Lines starting with `:` (comments) and
|
|
91
|
+
* other event types are ignored.
|
|
92
|
+
*/
|
|
93
|
+
function parseUIMessageSseTransform() {
|
|
94
|
+
let buffer = "";
|
|
95
|
+
return new TransformStream({
|
|
96
|
+
transform(chunk, controller) {
|
|
97
|
+
buffer += chunk;
|
|
98
|
+
// Frames are separated by blank lines.
|
|
99
|
+
let idx = buffer.indexOf("\n\n");
|
|
100
|
+
while (idx !== -1) {
|
|
101
|
+
const frame = buffer.slice(0, idx);
|
|
102
|
+
buffer = buffer.slice(idx + 2);
|
|
103
|
+
for (const line of frame.split("\n")) {
|
|
104
|
+
if (line.startsWith("data: ")) {
|
|
105
|
+
const data = line.slice(6).trim();
|
|
106
|
+
if (!data)
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
controller.enqueue(JSON.parse(data));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* drop malformed chunk; the response source is our own writer */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
idx = buffer.indexOf("\n\n");
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
flush(controller) {
|
|
120
|
+
// Trailing data without a closing blank line — treat as a final frame.
|
|
121
|
+
if (buffer.trim().length === 0)
|
|
122
|
+
return;
|
|
123
|
+
for (const line of buffer.split("\n")) {
|
|
124
|
+
if (line.startsWith("data: ")) {
|
|
125
|
+
const data = line.slice(6).trim();
|
|
126
|
+
if (!data)
|
|
127
|
+
continue;
|
|
128
|
+
try {
|
|
129
|
+
controller.enqueue(JSON.parse(data));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* drop */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
buffer = "";
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A custom AI SDK `ChatTransport` that runs chat completions as durable
|
|
142
|
+
* Trigger.dev tasks via the Sessions primitive.
|
|
143
|
+
*
|
|
144
|
+
* Lifecycle:
|
|
145
|
+
* 1. Customer pre-creates the session server-side OR calls
|
|
146
|
+
* `transport.start(chatId)` to mint a one-shot start token and
|
|
147
|
+
* `POST /api/v1/sessions` from the browser.
|
|
148
|
+
* 2. The server triggers the first run as part of session create and
|
|
149
|
+
* returns a session-scoped PAT.
|
|
150
|
+
* 3. `sendMessages` appends to `.in` and subscribes to `.out`. When a
|
|
151
|
+
* run dies (idle, cancel, end-and-continue), the server's
|
|
152
|
+
* append-time probe triggers a fresh run for the same session —
|
|
153
|
+
* transport keeps streaming.
|
|
154
|
+
* 4. `stop()` posts a `{kind:"stop"}` chunk; the agent's turn aborts
|
|
155
|
+
* but the run keeps reading `.in` for the next message.
|
|
156
|
+
* 5. PAT expiry: transport invokes `accessToken` to refresh and
|
|
157
|
+
* retries the failing request once.
|
|
158
|
+
*/
|
|
159
|
+
export class TriggerChatTransport {
|
|
160
|
+
taskId;
|
|
161
|
+
resolveAccessToken;
|
|
162
|
+
resolveStartSession;
|
|
163
|
+
resolveBaseURLFn;
|
|
164
|
+
fetchOverride;
|
|
165
|
+
extraHeaders;
|
|
166
|
+
streamTimeoutSeconds;
|
|
167
|
+
defaultMetadata;
|
|
168
|
+
watchMode;
|
|
169
|
+
headStart;
|
|
170
|
+
coordinator = null;
|
|
171
|
+
_onSessionChange;
|
|
172
|
+
_onEvent;
|
|
173
|
+
sessions = new Map();
|
|
174
|
+
pendingResumeCursors = new Map();
|
|
175
|
+
activeStreams = new Map();
|
|
176
|
+
pendingStarts = new Map();
|
|
177
|
+
// Last turn-producing send per chat — attribution source for the
|
|
178
|
+
// response-side events' `messageId` / `sinceSendMs`.
|
|
179
|
+
lastTurnSends = new Map();
|
|
180
|
+
constructor(options) {
|
|
181
|
+
this.taskId = options.task;
|
|
182
|
+
this.resolveAccessToken = options.accessToken;
|
|
183
|
+
this.resolveStartSession = options.startSession;
|
|
184
|
+
const baseURLOption = options.baseURL ?? DEFAULT_BASE_URL;
|
|
185
|
+
const streamOverride = options.streamBaseURL;
|
|
186
|
+
this.resolveBaseURLFn =
|
|
187
|
+
typeof baseURLOption === "function"
|
|
188
|
+
? (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption(ctx))
|
|
189
|
+
: (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption);
|
|
190
|
+
this.fetchOverride = options.fetch;
|
|
191
|
+
this.extraHeaders = options.headers ?? {};
|
|
192
|
+
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
|
|
193
|
+
this.defaultMetadata = options.clientData;
|
|
194
|
+
this._onSessionChange = options.onSessionChange;
|
|
195
|
+
this._onEvent = options.onEvent;
|
|
196
|
+
this.watchMode = options.watch ?? false;
|
|
197
|
+
this.headStart = options.headStart;
|
|
198
|
+
if (options.multiTab && !this.watchMode) {
|
|
199
|
+
this.coordinator = new ChatTabCoordinator();
|
|
200
|
+
this.coordinator.addSessionListener((chatId, sessionUpdate) => {
|
|
201
|
+
const session = this.sessions.get(chatId);
|
|
202
|
+
if (session && sessionUpdate.lastEventId) {
|
|
203
|
+
session.lastEventId = sessionUpdate.lastEventId;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (options.sessions) {
|
|
208
|
+
for (const [chatId, session] of Object.entries(options.sessions)) {
|
|
209
|
+
this.sessions.set(chatId, {
|
|
210
|
+
publicAccessToken: session.publicAccessToken,
|
|
211
|
+
lastEventId: session.lastEventId,
|
|
212
|
+
activeInputSeq: session.activeInputSeq,
|
|
213
|
+
isStreaming: session.isStreaming,
|
|
214
|
+
closed: session.closed,
|
|
215
|
+
closedReason: session.closedReason,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
221
|
+
// Public lifecycle
|
|
222
|
+
// -------------------------------------------------------------------------
|
|
223
|
+
/**
|
|
224
|
+
* Eagerly create a Session and trigger its first run. Useful as a
|
|
225
|
+
* "the user might be about to send a message — boot the agent now"
|
|
226
|
+
* preload, or to take ownership of the session before any sendMessage.
|
|
227
|
+
*
|
|
228
|
+
* Idempotent: calling `start(chatId)` twice converges to the same
|
|
229
|
+
* session via the `(env, externalId)` upsert. Concurrent calls
|
|
230
|
+
* deduplicate via an in-flight promise.
|
|
231
|
+
*
|
|
232
|
+
* Requires `getStartToken` to be configured. Customers who pre-create
|
|
233
|
+
* sessions server-side don't need to call this.
|
|
234
|
+
*/
|
|
235
|
+
async start(chatId) {
|
|
236
|
+
const existing = this.sessions.get(chatId);
|
|
237
|
+
if (existing?.publicAccessToken) {
|
|
238
|
+
return this.toPersisted(existing);
|
|
239
|
+
}
|
|
240
|
+
const inflight = this.pendingStarts.get(chatId);
|
|
241
|
+
if (inflight)
|
|
242
|
+
return inflight.then(this.toPersisted);
|
|
243
|
+
const promise = this.doStart(chatId).finally(() => {
|
|
244
|
+
this.pendingStarts.delete(chatId);
|
|
245
|
+
});
|
|
246
|
+
this.pendingStarts.set(chatId, promise);
|
|
247
|
+
return promise.then(this.toPersisted);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Eagerly create the session before the user types. Same semantics as
|
|
251
|
+
* {@link start} — kept as a separate name for the AI SDK Chat hook,
|
|
252
|
+
* which calls `preload` rather than `start`.
|
|
253
|
+
*/
|
|
254
|
+
async preload(chatId) {
|
|
255
|
+
await this.start(chatId);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Send a user message via the session's `.in` channel. The server
|
|
259
|
+
* probes `currentRunId`; if terminal/null it triggers a fresh run on
|
|
260
|
+
* the same session before the append lands. The returned
|
|
261
|
+
* `ReadableStream` carries the agent's response chunks via `.out` SSE.
|
|
262
|
+
*/
|
|
263
|
+
sendMessages = async (options) => {
|
|
264
|
+
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
|
|
265
|
+
const closedState = this.sessions.get(chatId);
|
|
266
|
+
if (closedState?.closed) {
|
|
267
|
+
throw sessionClosedError(chatId, closedState.closedReason);
|
|
268
|
+
}
|
|
269
|
+
if (this.coordinator) {
|
|
270
|
+
if (this.coordinator.isReadOnly(chatId)) {
|
|
271
|
+
throw new Error("This chat is active in another tab");
|
|
272
|
+
}
|
|
273
|
+
this.coordinator.claim(chatId);
|
|
274
|
+
}
|
|
275
|
+
const mergedMetadata = this.defaultMetadata || metadata
|
|
276
|
+
? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
|
|
277
|
+
: undefined;
|
|
278
|
+
// An action sent through `useChat`. `useChatActions` (or any caller) puts
|
|
279
|
+
// it in `body.action`; sending it here rather than through
|
|
280
|
+
// `transport.sendAction` means `useChat` owns the response stream, so an
|
|
281
|
+
// action that becomes a turn renders the way a message turn does.
|
|
282
|
+
const actionInBody = body?.action;
|
|
283
|
+
if (actionInBody !== undefined) {
|
|
284
|
+
return this.sendAction(chatId, actionInBody, { abortSignal, metadata: mergedMetadata });
|
|
285
|
+
}
|
|
286
|
+
// First-turn handover routing — when `headStart` is set AND no
|
|
287
|
+
// session state exists yet for this chatId, POST the wire payload
|
|
288
|
+
// to the customer's `chat.handover` route handler. The handler
|
|
289
|
+
// creates the session, triggers the agent run with
|
|
290
|
+
// `handover-prepare`, runs `streamText` step 1 in its warm
|
|
291
|
+
// process, and tees the output back as the SSE response. We
|
|
292
|
+
// hydrate session state from the response headers so subsequent
|
|
293
|
+
// turns bypass the handler and use direct `session.in` writes.
|
|
294
|
+
if (this.headStart && !this.sessions.has(chatId)) {
|
|
295
|
+
return this.sendMessagesViaHandover({
|
|
296
|
+
trigger,
|
|
297
|
+
chatId,
|
|
298
|
+
messageId,
|
|
299
|
+
messages,
|
|
300
|
+
abortSignal,
|
|
301
|
+
body,
|
|
302
|
+
metadata: mergedMetadata,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// Slim wire — at most ONE message per record. The agent rebuilds prior
|
|
306
|
+
// history from its durable S3 snapshot + session.out replay at run boot
|
|
307
|
+
// (or `hydrateMessages`, if registered).
|
|
308
|
+
//
|
|
309
|
+
// - "submit-message": ship the latest message (new user message OR a
|
|
310
|
+
// tool-approval-responded assistant message). Throw if absent.
|
|
311
|
+
// Assistant messages with already-resolved tool parts are slimmed
|
|
312
|
+
// to just their resolution payload — reasoning blobs, prior text,
|
|
313
|
+
// and tool `input` stay on the agent side (rebuilt from snapshot
|
|
314
|
+
// or `hydrateMessages`). Keeps continuation payloads under the
|
|
315
|
+
// `.in/append` cap on reasoning-heavy turns.
|
|
316
|
+
// - "regenerate-message": omit `message`; the agent slices its own
|
|
317
|
+
// history (drops the trailing assistant) and re-runs.
|
|
318
|
+
if (trigger === "submit-message" && messages.length === 0) {
|
|
319
|
+
throw new Error("TriggerChatTransport.sendMessages: 'submit-message' trigger requires at least one message");
|
|
320
|
+
}
|
|
321
|
+
const wirePayload = {
|
|
322
|
+
...(body ?? {}),
|
|
323
|
+
...(trigger === "submit-message"
|
|
324
|
+
? { message: slimSubmitMessageForWire(messages.at(-1)) }
|
|
325
|
+
: {}),
|
|
326
|
+
chatId,
|
|
327
|
+
trigger,
|
|
328
|
+
messageId,
|
|
329
|
+
metadata: mergedMetadata,
|
|
330
|
+
};
|
|
331
|
+
const state = await this.ensureSessionState(chatId);
|
|
332
|
+
// Generated outside the closure so auth-retries reuse the same part id
|
|
333
|
+
// and the server-side dedupe sees one logical append.
|
|
334
|
+
const partId = crypto.randomUUID();
|
|
335
|
+
const serializedBody = this.serializeInputChunk({ kind: "message", payload: wirePayload });
|
|
336
|
+
const sendChatMessage = (token) => this.appendInputChunk(chatId, token, serializedBody, partId);
|
|
337
|
+
const inSeq = await this.sendWithEvents(chatId, trigger, {
|
|
338
|
+
messageId: messageId ?? messages.at(-1)?.id,
|
|
339
|
+
partId,
|
|
340
|
+
bodyBytes: byteLength(serializedBody),
|
|
341
|
+
}, () => this.callWithAuthRetry(chatId, state, sendChatMessage));
|
|
342
|
+
// Cancel any in-flight stream for this chat — the new turn supersedes it.
|
|
343
|
+
const activeStream = this.activeStreams.get(chatId);
|
|
344
|
+
if (activeStream) {
|
|
345
|
+
activeStream.abort();
|
|
346
|
+
this.activeStreams.delete(chatId);
|
|
347
|
+
}
|
|
348
|
+
state.activeInputSeq = inSeq;
|
|
349
|
+
state.isStreaming = true;
|
|
350
|
+
state.outstandingTurnAbandoned = false;
|
|
351
|
+
this.notifySessionChange(chatId, state);
|
|
352
|
+
// Owning turn: aborting this live send stops the turn the user drives.
|
|
353
|
+
return this.subscribeToSessionStream(state, abortSignal, chatId, {
|
|
354
|
+
sinceInSeq: inSeq,
|
|
355
|
+
sendStopOnAbort: true,
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* First-turn-only path used when `headStart` is configured. POSTs the
|
|
360
|
+
* wire payload to the customer's `chat.handover` route handler and
|
|
361
|
+
* pipes its SSE response back as a UIMessageChunk stream. Hydrates
|
|
362
|
+
* session state from response headers so subsequent turns bypass
|
|
363
|
+
* the endpoint and use the direct `session.in` path.
|
|
364
|
+
*/
|
|
365
|
+
async sendMessagesViaHandover(args) {
|
|
366
|
+
if (!this.headStart) {
|
|
367
|
+
throw new Error("sendMessagesViaHandover called without headStart configured");
|
|
368
|
+
}
|
|
369
|
+
// Head-start ships full UIMessage history via `headStartMessages`. The
|
|
370
|
+
// route handler runs on the customer's own HTTP endpoint (NOT
|
|
371
|
+
// `/realtime/v1/sessions/{id}/in/append`), so the 512 KiB body cap
|
|
372
|
+
// doesn't apply. The agent's run boot consumes `headStartMessages` ONLY
|
|
373
|
+
// when no snapshot exists yet (very first turn) — see plan section B.3.
|
|
374
|
+
const wirePayload = {
|
|
375
|
+
...(args.body ?? {}),
|
|
376
|
+
headStartMessages: args.messages,
|
|
377
|
+
chatId: args.chatId,
|
|
378
|
+
trigger: args.trigger,
|
|
379
|
+
messageId: args.messageId,
|
|
380
|
+
metadata: args.metadata,
|
|
381
|
+
};
|
|
382
|
+
let response;
|
|
383
|
+
const serializedBody = JSON.stringify(wirePayload);
|
|
384
|
+
await this.sendWithEvents(args.chatId, "head-start", {
|
|
385
|
+
messageId: args.messageId ?? args.messages.at(-1)?.id,
|
|
386
|
+
bodyBytes: byteLength(serializedBody),
|
|
387
|
+
}, async () => {
|
|
388
|
+
response = await fetch(this.headStart, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: {
|
|
391
|
+
"Content-Type": "application/json",
|
|
392
|
+
...this.extraHeaders,
|
|
393
|
+
},
|
|
394
|
+
body: serializedBody,
|
|
395
|
+
signal: args.abortSignal,
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
const err = new Error(`chat.handover endpoint returned ${response.status} ${response.statusText}`);
|
|
399
|
+
err.status = response.status;
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
if (!response.body) {
|
|
404
|
+
throw new Error("chat.handover endpoint returned no response body");
|
|
405
|
+
}
|
|
406
|
+
// Hydrate session state from response headers so subsequent turns
|
|
407
|
+
// skip the endpoint and write directly to session.in. Failing fast
|
|
408
|
+
// when the header is missing avoids a quiet degraded state where
|
|
409
|
+
// every later turn re-runs the handover route instead of taking
|
|
410
|
+
// the slim-wire path.
|
|
411
|
+
const accessToken = response.headers.get("X-Trigger-Chat-Access-Token");
|
|
412
|
+
const chatId = args.chatId;
|
|
413
|
+
if (!accessToken) {
|
|
414
|
+
throw new Error("chat.handover response is missing the X-Trigger-Chat-Access-Token header. chat.agent's handover endpoint must echo the session PAT so the transport can hydrate.");
|
|
415
|
+
}
|
|
416
|
+
const state = {
|
|
417
|
+
publicAccessToken: accessToken,
|
|
418
|
+
isStreaming: true,
|
|
419
|
+
};
|
|
420
|
+
this.sessions.set(chatId, state);
|
|
421
|
+
this.notifySessionChange(chatId, state);
|
|
422
|
+
// Step 1 streams from the warm server either way; this says the agent run that owes step 2
|
|
423
|
+
// is parked on an undeployed external deployment id.
|
|
424
|
+
if (response.headers.get("X-Trigger-Chat-Pending-Version") === "1") {
|
|
425
|
+
this.emitEvent({
|
|
426
|
+
type: "run-pending-version",
|
|
427
|
+
chatId,
|
|
428
|
+
timestamp: Date.now(),
|
|
429
|
+
source: "head-start",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
// Filter the parsed UIMessage stream:
|
|
433
|
+
// - Drop control chunks (`trigger:turn-complete`,
|
|
434
|
+
// `trigger:session-state`) before they reach AI SDK — they
|
|
435
|
+
// aren't valid UIMessageChunks and the AI SDK chunk parser
|
|
436
|
+
// would reject them.
|
|
437
|
+
// - On `trigger:turn-complete`, clear `isStreaming` so the
|
|
438
|
+
// useChat resume / reconnectToStream path doesn't open a
|
|
439
|
+
// second `session.out` subscription on top of our stitched
|
|
440
|
+
// response.
|
|
441
|
+
// - On `trigger:session-state`, hydrate `state.lastEventId`
|
|
442
|
+
// with the agent's final S2 event id. Without this, turn 2's
|
|
443
|
+
// `session.out` subscribe reads from the start and replays
|
|
444
|
+
// turn 1's chunks back into the UI.
|
|
445
|
+
// - On stream end (handover-skip case — no
|
|
446
|
+
// `trigger:turn-complete` arrives, customer's stream just
|
|
447
|
+
// ends), also clear `isStreaming` for the same reason.
|
|
448
|
+
const sessions = this.sessions;
|
|
449
|
+
const notifyChange = (id, state) => this.notifySessionChange(id, state);
|
|
450
|
+
const TRIGGER_TURN_COMPLETE = "trigger:turn-complete";
|
|
451
|
+
const TRIGGER_SESSION_STATE = "trigger:session-state";
|
|
452
|
+
const clearStreaming = () => {
|
|
453
|
+
const state = sessions.get(chatId);
|
|
454
|
+
if (state && state.isStreaming) {
|
|
455
|
+
state.isStreaming = false;
|
|
456
|
+
notifyChange(chatId, state);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const setLastEventId = (lastEventId) => {
|
|
460
|
+
const state = sessions.get(chatId);
|
|
461
|
+
if (state) {
|
|
462
|
+
state.lastEventId = lastEventId;
|
|
463
|
+
notifyChange(chatId, state);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const emit = (event) => this.emitEvent(event);
|
|
467
|
+
const attribution = () => this.turnAttribution(chatId);
|
|
468
|
+
let sawFirstChunk = false;
|
|
469
|
+
emit({
|
|
470
|
+
type: "stream-connected",
|
|
471
|
+
chatId,
|
|
472
|
+
timestamp: Date.now(),
|
|
473
|
+
resumed: false,
|
|
474
|
+
messageId: this.lastTurnSends.get(chatId)?.messageId,
|
|
475
|
+
});
|
|
476
|
+
const piped = response.body
|
|
477
|
+
.pipeThrough(new TextDecoderStream())
|
|
478
|
+
.pipeThrough(parseUIMessageSseTransform())
|
|
479
|
+
.pipeThrough(new TransformStream({
|
|
480
|
+
transform(chunk, controller) {
|
|
481
|
+
if (chunk && typeof chunk === "object") {
|
|
482
|
+
const type = chunk.type;
|
|
483
|
+
if (type === TRIGGER_TURN_COMPLETE) {
|
|
484
|
+
clearStreaming();
|
|
485
|
+
emit({
|
|
486
|
+
type: "turn-completed",
|
|
487
|
+
chatId,
|
|
488
|
+
timestamp: Date.now(),
|
|
489
|
+
lastEventId: sessions.get(chatId)?.lastEventId,
|
|
490
|
+
...attribution(),
|
|
491
|
+
});
|
|
492
|
+
return; // drop — not a real UIMessageChunk
|
|
493
|
+
}
|
|
494
|
+
if (type === TRIGGER_SESSION_STATE) {
|
|
495
|
+
const lastEventId = chunk.lastEventId;
|
|
496
|
+
if (typeof lastEventId === "string") {
|
|
497
|
+
setLastEventId(lastEventId);
|
|
498
|
+
}
|
|
499
|
+
return; // drop
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!sawFirstChunk) {
|
|
503
|
+
sawFirstChunk = true;
|
|
504
|
+
emit({
|
|
505
|
+
type: "first-chunk",
|
|
506
|
+
chatId,
|
|
507
|
+
timestamp: Date.now(),
|
|
508
|
+
chunkType: chunk?.type,
|
|
509
|
+
...attribution(),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
controller.enqueue(chunk);
|
|
513
|
+
},
|
|
514
|
+
flush() {
|
|
515
|
+
clearStreaming();
|
|
516
|
+
},
|
|
517
|
+
}));
|
|
518
|
+
// Pump wrapper: a TransformStream can't observe upstream errors, so
|
|
519
|
+
// read here to emit stream-error (aborts close cleanly, matching the
|
|
520
|
+
// session subscribe path).
|
|
521
|
+
const pipedReader = piped.getReader();
|
|
522
|
+
return new ReadableStream({
|
|
523
|
+
async pull(controller) {
|
|
524
|
+
try {
|
|
525
|
+
const next = await pipedReader.read();
|
|
526
|
+
if (next.done) {
|
|
527
|
+
controller.close();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
controller.enqueue(next.value);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
534
|
+
try {
|
|
535
|
+
controller.close();
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
/* already closed */
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const errorStatus = error.status;
|
|
543
|
+
emit({
|
|
544
|
+
type: "stream-error",
|
|
545
|
+
chatId,
|
|
546
|
+
timestamp: Date.now(),
|
|
547
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
548
|
+
status: typeof errorStatus === "number" ? errorStatus : undefined,
|
|
549
|
+
});
|
|
550
|
+
controller.error(error);
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
cancel(reason) {
|
|
554
|
+
return pipedReader.cancel(reason);
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Send a steering message during an active stream without disrupting
|
|
560
|
+
* it. The agent's `pendingMessages` config decides whether to inject
|
|
561
|
+
* between tool-call steps or buffer for the next turn.
|
|
562
|
+
*/
|
|
563
|
+
sendPendingMessage = async (chatId, message, metadata) => {
|
|
564
|
+
const state = this.sessions.get(chatId);
|
|
565
|
+
if (!state)
|
|
566
|
+
return false;
|
|
567
|
+
const mergedMetadata = this.defaultMetadata || metadata
|
|
568
|
+
? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
|
|
569
|
+
: undefined;
|
|
570
|
+
const wirePayload = {
|
|
571
|
+
message,
|
|
572
|
+
chatId,
|
|
573
|
+
trigger: "submit-message",
|
|
574
|
+
metadata: mergedMetadata,
|
|
575
|
+
};
|
|
576
|
+
const partId = crypto.randomUUID();
|
|
577
|
+
const serializedBody = this.serializeInputChunk({ kind: "message", payload: wirePayload });
|
|
578
|
+
const send = async (token) => {
|
|
579
|
+
await this.appendInputChunk(chatId, token, serializedBody, partId);
|
|
580
|
+
};
|
|
581
|
+
try {
|
|
582
|
+
await this.sendWithEvents(chatId, "steer", { messageId: message.id, partId, bodyBytes: byteLength(serializedBody) }, () => this.callWithAuthRetry(chatId, state, send));
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
/**
|
|
590
|
+
* Re-establish an SSE subscription to a known session. Used after a
|
|
591
|
+
* page refresh: the customer hydrates `sessions` in the constructor,
|
|
592
|
+
* the AI SDK calls `reconnectToStream` to resume the stream.
|
|
593
|
+
*/
|
|
594
|
+
reconnectToStream = async (options) => {
|
|
595
|
+
const state = this.sessions.get(options.chatId);
|
|
596
|
+
if (!state)
|
|
597
|
+
return null;
|
|
598
|
+
// A closed session has no further turns to resume.
|
|
599
|
+
if (state.closed)
|
|
600
|
+
return null;
|
|
601
|
+
// Watch is a standing subscription: a settled session is exactly the
|
|
602
|
+
// state it waits in, so a completed last turn must not block the resume.
|
|
603
|
+
if (state.isStreaming === false && !this.watchMode)
|
|
604
|
+
return null;
|
|
605
|
+
if (this.activeStreams.has(options.chatId))
|
|
606
|
+
return null;
|
|
607
|
+
const abortController = new AbortController();
|
|
608
|
+
this.activeStreams.set(options.chatId, abortController);
|
|
609
|
+
const abortSignal = options.abortSignal
|
|
610
|
+
? AbortSignal.any([options.abortSignal, abortController.signal])
|
|
611
|
+
: abortController.signal;
|
|
612
|
+
return this.subscribeToSessionStream(state, abortSignal, options.chatId, {
|
|
613
|
+
resumed: true,
|
|
614
|
+
sendStopOnAbort: options.stopOnAbort ?? false,
|
|
615
|
+
sinceInSeq: state.activeInputSeq,
|
|
616
|
+
// Reconnect-on-reload opts into the server's settled-peek shortcut
|
|
617
|
+
// so the SSE doesn't hang for 60s when no turn is in flight. A known
|
|
618
|
+
// active input must not peek because the previous turn's completion
|
|
619
|
+
// can remain at the tail until the current turn writes its first chunk.
|
|
620
|
+
// Watch mode must NOT peek: a settled peek between turns closes the
|
|
621
|
+
// standing subscription, so the viewer never sees the next turn.
|
|
622
|
+
peekSettled: !this.watchMode && state.activeInputSeq === undefined,
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* Stop the current generation. Sends `{kind:"stop"}` on `.in`; the
|
|
627
|
+
* agent aborts its `streamText` call but stays alive for the next
|
|
628
|
+
* message.
|
|
629
|
+
*/
|
|
630
|
+
stopGeneration = async (chatId) => {
|
|
631
|
+
const state = this.sessions.get(chatId);
|
|
632
|
+
if (!state)
|
|
633
|
+
return false;
|
|
634
|
+
const partId = crypto.randomUUID();
|
|
635
|
+
const serializedBody = this.serializeInputChunk({ kind: "stop" });
|
|
636
|
+
const send = async (token) => {
|
|
637
|
+
await this.appendInputChunk(chatId, token, serializedBody, partId);
|
|
638
|
+
};
|
|
639
|
+
try {
|
|
640
|
+
await this.sendWithEvents(chatId, "stop", { partId, bodyBytes: byteLength(serializedBody) }, () => this.callWithAuthRetry(chatId, state, send));
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
// Only gate when a sent turn is still outstanding. A stop at a boundary has
|
|
646
|
+
// nothing to supersede, and gating it would swallow the next turn.
|
|
647
|
+
if (!state.outstandingTurnAbandoned &&
|
|
648
|
+
(state.isStreaming || state.activeInputSeq !== undefined)) {
|
|
649
|
+
state.skipToTurnComplete = true;
|
|
650
|
+
state.supersededInputSeq = state.activeInputSeq;
|
|
651
|
+
}
|
|
652
|
+
const activeStream = this.activeStreams.get(chatId);
|
|
653
|
+
if (activeStream) {
|
|
654
|
+
activeStream.abort();
|
|
655
|
+
this.activeStreams.delete(chatId);
|
|
656
|
+
}
|
|
657
|
+
// Release here, not in the stream teardown: that only releases while it
|
|
658
|
+
// still owns the map entry, and we just deleted it. Unlike a supersede,
|
|
659
|
+
// no successor stream follows a stop, so the claim would never be freed
|
|
660
|
+
// and other tabs would stay read-only until this one closes.
|
|
661
|
+
this.coordinator?.release(chatId);
|
|
662
|
+
// The turn won't reach its turn-complete on this client (we just
|
|
663
|
+
// aborted the reader), so clear the streaming flag here and persist —
|
|
664
|
+
// otherwise a reload resumes mid-turn and replays the chunks the user
|
|
665
|
+
// explicitly stopped.
|
|
666
|
+
state.isStreaming = false;
|
|
667
|
+
this.notifySessionChange(chatId, state);
|
|
668
|
+
return true;
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* Clear the supersede gate armed by a stop. Call this when the stopped
|
|
672
|
+
* turn died without ever writing its `trigger:turn-complete` boundary,
|
|
673
|
+
* otherwise the next turn stays gated forever.
|
|
674
|
+
*/
|
|
675
|
+
clearSupersedeGate = (chatId) => {
|
|
676
|
+
const state = this.sessions.get(chatId);
|
|
677
|
+
if (!state)
|
|
678
|
+
return;
|
|
679
|
+
state.skipToTurnComplete = false;
|
|
680
|
+
state.supersededInputSeq = undefined;
|
|
681
|
+
state.outstandingTurnAbandoned = true;
|
|
682
|
+
};
|
|
683
|
+
/**
|
|
684
|
+
* Send a custom action chunk (for `chat.agent`'s `actionSchema` /
|
|
685
|
+
* `onAction` hook). An action is an edit: only `hydrateMessages` and
|
|
686
|
+
* `onAction` fire on the agent side, and the returned stream completes
|
|
687
|
+
* with `trigger:turn-complete` once the edit is persisted. When `onAction`
|
|
688
|
+
* returns `chat.turn()` the turn's response follows on the same stream.
|
|
689
|
+
* Per-action `metadata` is merged over the transport's `clientData`.
|
|
690
|
+
*/
|
|
691
|
+
sendAction = async (chatId, action, options) => {
|
|
692
|
+
if (this.coordinator) {
|
|
693
|
+
if (this.coordinator.isReadOnly(chatId)) {
|
|
694
|
+
throw new Error("This chat is active in another tab");
|
|
695
|
+
}
|
|
696
|
+
this.coordinator.claim(chatId);
|
|
697
|
+
}
|
|
698
|
+
const state = await this.ensureSessionState(chatId);
|
|
699
|
+
const wirePayload = {
|
|
700
|
+
chatId,
|
|
701
|
+
trigger: "action",
|
|
702
|
+
action,
|
|
703
|
+
metadata: this.defaultMetadata || options?.metadata
|
|
704
|
+
? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
|
|
705
|
+
: undefined,
|
|
706
|
+
};
|
|
707
|
+
const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
|
|
708
|
+
const partId = crypto.randomUUID();
|
|
709
|
+
const send = (token) => this.appendInputChunk(chatId, token, body, partId);
|
|
710
|
+
const inSeq = await this.sendWithEvents(chatId, "action", { partId, bodyBytes: byteLength(body) }, () => this.callWithAuthRetry(chatId, state, send));
|
|
711
|
+
// Supersede any in-flight reader before subscribing — same as
|
|
712
|
+
// `sendMessages`. Two concurrent readers both write `state.lastEventId`
|
|
713
|
+
// and the slower one can regress the cursor, replaying records on the
|
|
714
|
+
// next reconnect.
|
|
715
|
+
const activeStream = this.activeStreams.get(chatId);
|
|
716
|
+
if (activeStream) {
|
|
717
|
+
activeStream.abort();
|
|
718
|
+
this.activeStreams.delete(chatId);
|
|
719
|
+
}
|
|
720
|
+
// Mark streaming + persist so a reload mid-action resumes (reconnectToStream
|
|
721
|
+
// no-ops when the persisted session says isStreaming: false).
|
|
722
|
+
state.activeInputSeq = inSeq;
|
|
723
|
+
state.isStreaming = true;
|
|
724
|
+
state.outstandingTurnAbandoned = false;
|
|
725
|
+
this.notifySessionChange(chatId, state);
|
|
726
|
+
// Owning action: aborting this send stops the turn the user drives.
|
|
727
|
+
return this.subscribeToSessionStream(state, options?.abortSignal, chatId, {
|
|
728
|
+
sinceInSeq: inSeq,
|
|
729
|
+
sendStopOnAbort: true,
|
|
730
|
+
onSettled: options?.onSettled,
|
|
731
|
+
});
|
|
732
|
+
};
|
|
733
|
+
// -------------------------------------------------------------------------
|
|
734
|
+
// External-state surface
|
|
735
|
+
// -------------------------------------------------------------------------
|
|
736
|
+
getSession = (chatId) => {
|
|
737
|
+
const state = this.sessions.get(chatId);
|
|
738
|
+
if (!state)
|
|
739
|
+
return undefined;
|
|
740
|
+
return this.toPersisted(state);
|
|
741
|
+
};
|
|
742
|
+
setSession(chatId, session) {
|
|
743
|
+
this.sessions.set(chatId, this.applyPendingResumeCursor(chatId, {
|
|
744
|
+
publicAccessToken: session.publicAccessToken,
|
|
745
|
+
lastEventId: session.lastEventId,
|
|
746
|
+
activeInputSeq: session.activeInputSeq,
|
|
747
|
+
isStreaming: session.isStreaming,
|
|
748
|
+
}));
|
|
749
|
+
this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Seed the `.out` resume cursor from a loaded transcript. Applied to the
|
|
753
|
+
* session now if it exists, otherwise held until the session is created so
|
|
754
|
+
* the first live subscription opens past the persisted history instead of
|
|
755
|
+
* replaying it. Never moves an existing cursor backward: the transcript load
|
|
756
|
+
* is async, so a live `.out` record can already have advanced the session
|
|
757
|
+
* past the snapshot, and overwriting it would replay those records.
|
|
758
|
+
*/
|
|
759
|
+
seedResumeCursor = (chatId, lastEventId) => {
|
|
760
|
+
const existing = this.sessions.get(chatId);
|
|
761
|
+
if (existing?.publicAccessToken) {
|
|
762
|
+
if (existing.lastEventId === undefined) {
|
|
763
|
+
existing.lastEventId = lastEventId;
|
|
764
|
+
this.notifySessionChange(chatId, this.toPersisted(existing));
|
|
765
|
+
}
|
|
766
|
+
this.pendingResumeCursors.delete(chatId);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
this.pendingResumeCursors.set(chatId, lastEventId);
|
|
770
|
+
};
|
|
771
|
+
applyPendingResumeCursor(chatId, state) {
|
|
772
|
+
const pending = this.pendingResumeCursors.get(chatId);
|
|
773
|
+
if (pending !== undefined && state.lastEventId === undefined) {
|
|
774
|
+
state.lastEventId = pending;
|
|
775
|
+
}
|
|
776
|
+
this.pendingResumeCursors.delete(chatId);
|
|
777
|
+
return state;
|
|
778
|
+
}
|
|
779
|
+
setOnSessionChange(callback) {
|
|
780
|
+
this._onSessionChange = callback;
|
|
781
|
+
}
|
|
782
|
+
/** Update the `onEvent` callback without recreating the transport. */
|
|
783
|
+
setOnEvent(callback) {
|
|
784
|
+
this._onEvent = callback;
|
|
785
|
+
}
|
|
786
|
+
emitEvent(event) {
|
|
787
|
+
const cb = this._onEvent;
|
|
788
|
+
if (!cb)
|
|
789
|
+
return;
|
|
790
|
+
try {
|
|
791
|
+
cb(event);
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// observability must never break the chat
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/** Run a send op, emitting the terminal message-sent / message-send-failed event. */
|
|
798
|
+
async sendWithEvents(chatId, source, extras, op) {
|
|
799
|
+
const startedAt = Date.now();
|
|
800
|
+
try {
|
|
801
|
+
const result = await op();
|
|
802
|
+
const turnProducing = source === "submit-message" || source === "regenerate-message" || source === "head-start";
|
|
803
|
+
if (turnProducing) {
|
|
804
|
+
this.lastTurnSends.set(chatId, { messageId: extras.messageId, at: Date.now() });
|
|
805
|
+
}
|
|
806
|
+
this.emitEvent({
|
|
807
|
+
type: "message-sent",
|
|
808
|
+
chatId,
|
|
809
|
+
timestamp: Date.now(),
|
|
810
|
+
source,
|
|
811
|
+
durationMs: Date.now() - startedAt,
|
|
812
|
+
...extras,
|
|
813
|
+
});
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
const status = error.status;
|
|
818
|
+
this.emitEvent({
|
|
819
|
+
type: "message-send-failed",
|
|
820
|
+
chatId,
|
|
821
|
+
timestamp: Date.now(),
|
|
822
|
+
source,
|
|
823
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
824
|
+
status: typeof status === "number" ? status : undefined,
|
|
825
|
+
durationMs: Date.now() - startedAt,
|
|
826
|
+
...extras,
|
|
827
|
+
});
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/** Attribution fields for response-side events. */
|
|
832
|
+
turnAttribution(chatId) {
|
|
833
|
+
const lastSend = this.lastTurnSends.get(chatId);
|
|
834
|
+
if (!lastSend)
|
|
835
|
+
return {};
|
|
836
|
+
return { messageId: lastSend.messageId, sinceSendMs: Date.now() - lastSend.at };
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Update the transport's `clientData`. Used by `useTriggerChatTransport`
|
|
840
|
+
* to keep the latest value reachable from inside `startSession` and
|
|
841
|
+
* the per-turn `metadata` merge without recreating the transport.
|
|
842
|
+
*
|
|
843
|
+
* Reads always go through the live field — closures around the
|
|
844
|
+
* transport see the latest value the next time they fire.
|
|
845
|
+
*/
|
|
846
|
+
setClientData(clientData) {
|
|
847
|
+
this.defaultMetadata = clientData;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Update the request-time callbacks. Same reason as `setClientData`:
|
|
851
|
+
* a host that rebuilds these per render (a dashboard navigating between
|
|
852
|
+
* projects) would otherwise keep hitting the endpoint captured at
|
|
853
|
+
* construction. Every read goes through the live field, so the next
|
|
854
|
+
* request uses the latest callback.
|
|
855
|
+
*/
|
|
856
|
+
setAccessToken(accessToken) {
|
|
857
|
+
this.resolveAccessToken = accessToken;
|
|
858
|
+
}
|
|
859
|
+
setStartSession(startSession) {
|
|
860
|
+
this.resolveStartSession = startSession;
|
|
861
|
+
}
|
|
862
|
+
setFetch(fetchOverride) {
|
|
863
|
+
this.fetchOverride = fetchOverride;
|
|
864
|
+
}
|
|
865
|
+
// -------------------------------------------------------------------------
|
|
866
|
+
// Multi-tab coordination passthrough
|
|
867
|
+
// -------------------------------------------------------------------------
|
|
868
|
+
isReadOnly(chatId) {
|
|
869
|
+
return this.coordinator?.isReadOnly(chatId) ?? false;
|
|
870
|
+
}
|
|
871
|
+
hasClaim(chatId) {
|
|
872
|
+
return this.coordinator?.hasClaim(chatId) ?? false;
|
|
873
|
+
}
|
|
874
|
+
addReadOnlyListener(fn) {
|
|
875
|
+
this.coordinator?.addListener(fn);
|
|
876
|
+
}
|
|
877
|
+
removeReadOnlyListener(fn) {
|
|
878
|
+
this.coordinator?.removeListener(fn);
|
|
879
|
+
}
|
|
880
|
+
broadcastMessages(chatId, messages) {
|
|
881
|
+
this.coordinator?.broadcastMessages(chatId, messages);
|
|
882
|
+
}
|
|
883
|
+
addMessagesListener(fn) {
|
|
884
|
+
this.coordinator?.addMessagesListener(fn);
|
|
885
|
+
}
|
|
886
|
+
removeMessagesListener(fn) {
|
|
887
|
+
this.coordinator?.removeMessagesListener(fn);
|
|
888
|
+
}
|
|
889
|
+
dispose() {
|
|
890
|
+
// Tear down any open session.out subscriptions before the coordinator
|
|
891
|
+
// goes away. Otherwise controllers in `activeStreams` keep reading
|
|
892
|
+
// until they time out, leaking network and memory on every
|
|
893
|
+
// unmount/navigation.
|
|
894
|
+
for (const controller of this.activeStreams.values()) {
|
|
895
|
+
controller.abort();
|
|
896
|
+
}
|
|
897
|
+
this.activeStreams.clear();
|
|
898
|
+
this.coordinator?.dispose();
|
|
899
|
+
this.coordinator = null;
|
|
900
|
+
}
|
|
901
|
+
// -------------------------------------------------------------------------
|
|
902
|
+
// Internal helpers
|
|
903
|
+
// -------------------------------------------------------------------------
|
|
904
|
+
serializeInputChunk(chunk) {
|
|
905
|
+
return JSON.stringify(chunk);
|
|
906
|
+
}
|
|
907
|
+
toPersisted = (state) => ({
|
|
908
|
+
publicAccessToken: state.publicAccessToken,
|
|
909
|
+
lastEventId: state.lastEventId,
|
|
910
|
+
activeInputSeq: state.activeInputSeq,
|
|
911
|
+
isStreaming: state.isStreaming,
|
|
912
|
+
closed: state.closed,
|
|
913
|
+
closedReason: state.closedReason,
|
|
914
|
+
});
|
|
915
|
+
/**
|
|
916
|
+
* Flip a session to closed and tell listeners once. Terminal: sends and
|
|
917
|
+
* reconnects stop from here on, and there is no reopening.
|
|
918
|
+
*/
|
|
919
|
+
markSessionClosed(chatId, state, reason, source) {
|
|
920
|
+
if (state.closed)
|
|
921
|
+
return;
|
|
922
|
+
state.closed = true;
|
|
923
|
+
if (reason)
|
|
924
|
+
state.closedReason = reason;
|
|
925
|
+
state.isStreaming = false;
|
|
926
|
+
state.activeInputSeq = undefined;
|
|
927
|
+
state.skipToTurnComplete = false;
|
|
928
|
+
state.supersededInputSeq = undefined;
|
|
929
|
+
this.emitEvent({
|
|
930
|
+
type: "session-closed",
|
|
931
|
+
chatId,
|
|
932
|
+
timestamp: Date.now(),
|
|
933
|
+
...(reason ? { reason } : {}),
|
|
934
|
+
source,
|
|
935
|
+
});
|
|
936
|
+
this.notifySessionChange(chatId, state);
|
|
937
|
+
this.coordinator?.release(chatId);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Whether the conversation has been closed — by the agent via
|
|
941
|
+
* `chat.close()`, or from outside via `sessions.close()`. Closed is
|
|
942
|
+
* one-way: the transport refuses further sends and reconnects, and the
|
|
943
|
+
* transcript stays readable.
|
|
944
|
+
*/
|
|
945
|
+
sessionStatus = (chatId) => this.sessions.get(chatId)?.closed ? "closed" : "open";
|
|
946
|
+
/** The reason the session was closed, when one was given. */
|
|
947
|
+
sessionClosedReason = (chatId) => this.sessions.get(chatId)?.closedReason;
|
|
948
|
+
notifySessionChange(chatId, session) {
|
|
949
|
+
if (!this._onSessionChange)
|
|
950
|
+
return;
|
|
951
|
+
this._onSessionChange(chatId, session ? this.toPersisted(session) : null);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Resolves the session state for a chatId, starting the session if
|
|
955
|
+
* needed (and `getStartToken` is configured). Customers who provide
|
|
956
|
+
* `accessToken` but no `getStartToken` are expected to have created
|
|
957
|
+
* the session server-side; in that case the first `accessToken` call
|
|
958
|
+
* returns a fresh session PAT.
|
|
959
|
+
*/
|
|
960
|
+
async ensureSessionState(chatId) {
|
|
961
|
+
const existing = this.sessions.get(chatId);
|
|
962
|
+
if (existing?.publicAccessToken)
|
|
963
|
+
return existing;
|
|
964
|
+
if (this.resolveStartSession) {
|
|
965
|
+
// Lazily start: customer's server action creates the session and
|
|
966
|
+
// returns a PAT. Idempotent on `(env, externalId)` so concurrent
|
|
967
|
+
// tabs / repeat calls converge to the same session.
|
|
968
|
+
const inflight = this.pendingStarts.get(chatId);
|
|
969
|
+
if (inflight)
|
|
970
|
+
return inflight;
|
|
971
|
+
const promise = this.doStart(chatId).finally(() => {
|
|
972
|
+
this.pendingStarts.delete(chatId);
|
|
973
|
+
});
|
|
974
|
+
this.pendingStarts.set(chatId, promise);
|
|
975
|
+
return promise;
|
|
976
|
+
}
|
|
977
|
+
// No `startSession` configured. Customer fully manages session
|
|
978
|
+
// lifecycle externally — they're expected to have hydrated
|
|
979
|
+
// `sessions: { ... }` already, or the very first `accessToken` call
|
|
980
|
+
// returns a PAT for an out-of-band-created session.
|
|
981
|
+
const token = await this.resolveAccessToken({ chatId });
|
|
982
|
+
const state = this.applyPendingResumeCursor(chatId, { publicAccessToken: token });
|
|
983
|
+
this.sessions.set(chatId, state);
|
|
984
|
+
this.notifySessionChange(chatId, state);
|
|
985
|
+
return state;
|
|
986
|
+
}
|
|
987
|
+
async doStart(chatId) {
|
|
988
|
+
if (!this.resolveStartSession) {
|
|
989
|
+
throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
|
|
990
|
+
}
|
|
991
|
+
const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
|
|
992
|
+
taskId: this.taskId,
|
|
993
|
+
chatId,
|
|
994
|
+
clientData: (this.defaultMetadata ?? {}),
|
|
995
|
+
});
|
|
996
|
+
if (pendingVersion) {
|
|
997
|
+
this.emitEvent({
|
|
998
|
+
type: "run-pending-version",
|
|
999
|
+
chatId,
|
|
1000
|
+
timestamp: Date.now(),
|
|
1001
|
+
source: "start",
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
const state = this.applyPendingResumeCursor(chatId, {
|
|
1005
|
+
publicAccessToken,
|
|
1006
|
+
isStreaming: false,
|
|
1007
|
+
});
|
|
1008
|
+
this.sessions.set(chatId, state);
|
|
1009
|
+
this.notifySessionChange(chatId, state);
|
|
1010
|
+
return state;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Run `op` with the session's stored PAT. On 401/403, refresh the PAT
|
|
1014
|
+
* via `accessToken` and retry once. Surfaces non-auth errors as-is.
|
|
1015
|
+
*/
|
|
1016
|
+
resolveBaseURL(ctx) {
|
|
1017
|
+
const raw = this.resolveBaseURLFn(ctx);
|
|
1018
|
+
return raw.replace(/\/$/, "");
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* `fetchClient` for an `out` subscription. Reads `this.fetchOverride` on
|
|
1022
|
+
* every call, not once per subscription: a watch-mode subscription
|
|
1023
|
+
* reconnects for the lifetime of the page, and `setFetch` may have swapped
|
|
1024
|
+
* the override in between.
|
|
1025
|
+
*/
|
|
1026
|
+
sseFetchClient(ctx) {
|
|
1027
|
+
return ((input, init) => {
|
|
1028
|
+
const override = this.fetchOverride;
|
|
1029
|
+
if (!override)
|
|
1030
|
+
return fetch(input, init);
|
|
1031
|
+
if (typeof input === "string") {
|
|
1032
|
+
return override(input, init ?? {}, ctx);
|
|
1033
|
+
}
|
|
1034
|
+
if (input instanceof URL) {
|
|
1035
|
+
return override(input.toString(), init ?? {}, ctx);
|
|
1036
|
+
}
|
|
1037
|
+
// Request — preserve its url + intrinsic init, let any provided init
|
|
1038
|
+
// override on top (matches fetch(Request, init) semantics).
|
|
1039
|
+
return override(input.url, {
|
|
1040
|
+
method: input.method,
|
|
1041
|
+
headers: input.headers,
|
|
1042
|
+
signal: input.signal,
|
|
1043
|
+
...(init ?? {}),
|
|
1044
|
+
}, ctx);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
async doFetch(ctx, url, init) {
|
|
1048
|
+
return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
|
|
1049
|
+
}
|
|
1050
|
+
async appendInputChunk(chatId, token, body, partId) {
|
|
1051
|
+
const ctx = { endpoint: "in", chatId };
|
|
1052
|
+
const url = `${this.resolveBaseURL(ctx)}/realtime/v1/sessions/${encodeURIComponent(chatId)}/in/append`;
|
|
1053
|
+
// extraHeaders first so the fixed headers below win — a transport-wide
|
|
1054
|
+
// X-Part-Id must not override the per-append idempotency key.
|
|
1055
|
+
const headers = {
|
|
1056
|
+
...this.extraHeaders,
|
|
1057
|
+
"Content-Type": "application/json",
|
|
1058
|
+
Authorization: `Bearer ${token}`,
|
|
1059
|
+
"x-trigger-source": "sdk",
|
|
1060
|
+
"X-Part-Id": partId ?? crypto.randomUUID(),
|
|
1061
|
+
};
|
|
1062
|
+
const response = await this.doFetch(ctx, url, { method: "POST", headers, body });
|
|
1063
|
+
if (!response.ok) {
|
|
1064
|
+
const text = await response.text().catch(() => "");
|
|
1065
|
+
const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
|
|
1066
|
+
err.name = "TriggerApiError";
|
|
1067
|
+
err.status = response.status;
|
|
1068
|
+
let parsed;
|
|
1069
|
+
try {
|
|
1070
|
+
parsed = JSON.parse(text);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
parsed = undefined;
|
|
1074
|
+
}
|
|
1075
|
+
if (typeof parsed?.code === "string")
|
|
1076
|
+
err.code = parsed.code;
|
|
1077
|
+
if (typeof parsed?.closedReason === "string")
|
|
1078
|
+
err.closedReason = parsed.closedReason;
|
|
1079
|
+
throw err;
|
|
1080
|
+
}
|
|
1081
|
+
// The appended record's `.in` seq, for correlating the response stream to
|
|
1082
|
+
// this send. Omitted by older webapps / a lost idempotency claim.
|
|
1083
|
+
const data = (await response.json().catch(() => undefined));
|
|
1084
|
+
if (data?.pendingVersion === true) {
|
|
1085
|
+
this.emitEvent({
|
|
1086
|
+
type: "run-pending-version",
|
|
1087
|
+
chatId,
|
|
1088
|
+
timestamp: Date.now(),
|
|
1089
|
+
source: "send",
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
return typeof data?.seq === "number" ? data.seq : undefined;
|
|
1093
|
+
}
|
|
1094
|
+
async callWithAuthRetry(chatId, state, op) {
|
|
1095
|
+
if (state.closed) {
|
|
1096
|
+
throw sessionClosedError(chatId, state.closedReason);
|
|
1097
|
+
}
|
|
1098
|
+
// Every attempt goes through here, so a closed-session refusal is terminal
|
|
1099
|
+
// on whichever one hits it. Reaching a 409 from a later attempt and not
|
|
1100
|
+
// marking it would leave local status open on a conversation the server
|
|
1101
|
+
// has already ended.
|
|
1102
|
+
const attempt = async (token) => {
|
|
1103
|
+
try {
|
|
1104
|
+
return await op(token);
|
|
1105
|
+
}
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
if (isSessionClosedError(err)) {
|
|
1108
|
+
// Terminal. Retrying, refreshing the PAT, or recreating the session
|
|
1109
|
+
// would all be wrong: the conversation is over.
|
|
1110
|
+
this.markSessionClosed(chatId, state, sessionClosedReasonOf(err), "append");
|
|
1111
|
+
}
|
|
1112
|
+
throw err;
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
// 1) Try with the current PAT.
|
|
1116
|
+
try {
|
|
1117
|
+
return await attempt(state.publicAccessToken);
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
if (isSessionClosedError(err))
|
|
1121
|
+
throw err;
|
|
1122
|
+
if (isSessionNotFoundError(err)) {
|
|
1123
|
+
// The cached PAT authenticated but the session doesn't exist here —
|
|
1124
|
+
// recreate it and retry.
|
|
1125
|
+
await this.recreateSession(chatId, state);
|
|
1126
|
+
return await attempt(state.publicAccessToken);
|
|
1127
|
+
}
|
|
1128
|
+
if (!isAuthError(err))
|
|
1129
|
+
throw err;
|
|
1130
|
+
}
|
|
1131
|
+
// 2) Auth error — refresh the PAT and retry. (A cross-environment cached
|
|
1132
|
+
// PAT fails auth here because it was signed by another env's keys.)
|
|
1133
|
+
const fresh = await this.resolveAccessToken({ chatId });
|
|
1134
|
+
state.publicAccessToken = fresh;
|
|
1135
|
+
this.notifySessionChange(chatId, state);
|
|
1136
|
+
try {
|
|
1137
|
+
return await attempt(fresh);
|
|
1138
|
+
}
|
|
1139
|
+
catch (err) {
|
|
1140
|
+
if (!isSessionNotFoundError(err))
|
|
1141
|
+
throw err;
|
|
1142
|
+
}
|
|
1143
|
+
// 3) PAT is now valid but the session still 404s — the hydrated session
|
|
1144
|
+
// state is stale (created in a different environment, or before the
|
|
1145
|
+
// sessions upgrade). Recreate the session and retry once.
|
|
1146
|
+
await this.recreateSession(chatId, state);
|
|
1147
|
+
return await attempt(state.publicAccessToken);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Repair a stale/phantom hydrated session. The cached state pointed at a
|
|
1151
|
+
* session that doesn't exist in the current environment (404) — typically
|
|
1152
|
+
* because it was persisted against a different trigger environment, or
|
|
1153
|
+
* predates the upgrade to the sessions model. Recreate the session via
|
|
1154
|
+
* `startSession` and overwrite the cached state **in place** so callers
|
|
1155
|
+
* holding a reference (e.g. `sendMessages` before it opens the SSE) pick
|
|
1156
|
+
* up the new PAT. The stale `lastEventId` resume cursor is dropped — it
|
|
1157
|
+
* pointed at a different environment's stream.
|
|
1158
|
+
*/
|
|
1159
|
+
async recreateSession(chatId, state) {
|
|
1160
|
+
if (!this.resolveStartSession) {
|
|
1161
|
+
throw new Error("TriggerChatTransport: session not found and no `startSession` configured to recreate it. The stored session state for this chat may be stale (e.g. created in a different environment) — provide `startSession` or clear the stored session so a fresh one can be created.");
|
|
1162
|
+
}
|
|
1163
|
+
const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
|
|
1164
|
+
taskId: this.taskId,
|
|
1165
|
+
chatId,
|
|
1166
|
+
clientData: (this.defaultMetadata ?? {}),
|
|
1167
|
+
});
|
|
1168
|
+
if (pendingVersion) {
|
|
1169
|
+
this.emitEvent({
|
|
1170
|
+
type: "run-pending-version",
|
|
1171
|
+
chatId,
|
|
1172
|
+
timestamp: Date.now(),
|
|
1173
|
+
source: "start",
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
state.publicAccessToken = publicAccessToken;
|
|
1177
|
+
state.lastEventId = undefined;
|
|
1178
|
+
state.isStreaming = false;
|
|
1179
|
+
this.sessions.set(chatId, state);
|
|
1180
|
+
this.notifySessionChange(chatId, state);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Open an SSE subscription to the session's `.out` stream and pipe
|
|
1184
|
+
* UIMessageChunks through to the AI SDK. Trigger control records
|
|
1185
|
+
* (`turn-complete`, `upgrade-required` — see `trigger-control` header
|
|
1186
|
+
* on `client-protocol.mdx#records-on-session-out`) are routed by
|
|
1187
|
+
* header and never reach the consumer. `upgrade-required` is purely
|
|
1188
|
+
* telemetry now since the server handles the run swap inline (see
|
|
1189
|
+
* `end-and-continue`).
|
|
1190
|
+
*/
|
|
1191
|
+
subscribeToSessionStream(state, abortSignal, chatId, options) {
|
|
1192
|
+
const internalAbort = new AbortController();
|
|
1193
|
+
let didSettle = false;
|
|
1194
|
+
this.activeStreams.set(chatId, internalAbort);
|
|
1195
|
+
const combinedSignal = abortSignal
|
|
1196
|
+
? AbortSignal.any([abortSignal, internalAbort.signal])
|
|
1197
|
+
: internalAbort.signal;
|
|
1198
|
+
if (abortSignal) {
|
|
1199
|
+
abortSignal.addEventListener("abort", () => {
|
|
1200
|
+
// A late abort (unmount, or the consumer dropping a drained stream)
|
|
1201
|
+
// has no turn to stop: don't gate the next one, don't write a stop.
|
|
1202
|
+
const outstanding = !state.outstandingTurnAbandoned &&
|
|
1203
|
+
(state.isStreaming || state.activeInputSeq !== undefined);
|
|
1204
|
+
if (options?.sendStopOnAbort !== false && outstanding && !internalAbort.signal.aborted) {
|
|
1205
|
+
state.skipToTurnComplete = true;
|
|
1206
|
+
state.supersededInputSeq = state.activeInputSeq;
|
|
1207
|
+
this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
|
|
1208
|
+
}
|
|
1209
|
+
internalAbort.abort();
|
|
1210
|
+
}, { once: true });
|
|
1211
|
+
}
|
|
1212
|
+
const streamUrl = `${this.resolveBaseURL({ endpoint: "out", chatId })}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`;
|
|
1213
|
+
return new ReadableStream({
|
|
1214
|
+
start: async (controller) => {
|
|
1215
|
+
// Track the live subscription so browser wake events can act
|
|
1216
|
+
// on it. Three classes of wake:
|
|
1217
|
+
// - `online`: network came back. Existing connection might
|
|
1218
|
+
// be silently dead; force a fresh one.
|
|
1219
|
+
// - `visibilitychange` → visible after long hidden: tab
|
|
1220
|
+
// was backgrounded long enough that the OS likely killed
|
|
1221
|
+
// the TCP socket. Force reconnect.
|
|
1222
|
+
// - `visibilitychange` → visible after short hidden: cheap
|
|
1223
|
+
// wake of any in-flight backoff.
|
|
1224
|
+
// - `pageshow` with `event.persisted`: bfcache restore
|
|
1225
|
+
// (mobile Safari back/forward, app-switcher resume). The
|
|
1226
|
+
// socket is definitely dead. Force reconnect.
|
|
1227
|
+
let currentSubscription = null;
|
|
1228
|
+
let hiddenSince = null;
|
|
1229
|
+
const FORCE_RECONNECT_AFTER_HIDDEN_MS = 30_000;
|
|
1230
|
+
const onVisibilityChange = () => {
|
|
1231
|
+
if (typeof document === "undefined")
|
|
1232
|
+
return;
|
|
1233
|
+
if (document.visibilityState === "hidden") {
|
|
1234
|
+
hiddenSince = Date.now();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const wasHiddenForMs = hiddenSince ? Date.now() - hiddenSince : 0;
|
|
1238
|
+
hiddenSince = null;
|
|
1239
|
+
if (wasHiddenForMs >= FORCE_RECONNECT_AFTER_HIDDEN_MS) {
|
|
1240
|
+
currentSubscription?.forceReconnect();
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
currentSubscription?.retryNow();
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
const onPageShow = (event) => {
|
|
1247
|
+
// PageTransitionEvent in browsers; type guard via `persisted`.
|
|
1248
|
+
if (event.persisted) {
|
|
1249
|
+
currentSubscription?.forceReconnect();
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
const onOnline = () => currentSubscription?.forceReconnect();
|
|
1253
|
+
const teardownWakeListeners = typeof document !== "undefined" && typeof window !== "undefined"
|
|
1254
|
+
? (() => {
|
|
1255
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1256
|
+
window.addEventListener("online", onOnline);
|
|
1257
|
+
window.addEventListener("pageshow", onPageShow);
|
|
1258
|
+
return () => {
|
|
1259
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1260
|
+
window.removeEventListener("online", onOnline);
|
|
1261
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
1262
|
+
};
|
|
1263
|
+
})()
|
|
1264
|
+
: () => { };
|
|
1265
|
+
const sseCtx = { endpoint: "out", chatId };
|
|
1266
|
+
const sseFetchClient = this.sseFetchClient(sseCtx);
|
|
1267
|
+
let sawFirstChunk = false;
|
|
1268
|
+
let sinceInSeq = options?.sinceInSeq;
|
|
1269
|
+
const connectSseOnce = async (token) => {
|
|
1270
|
+
const subscription = new SSEStreamSubscription(streamUrl, {
|
|
1271
|
+
headers: {
|
|
1272
|
+
Authorization: `Bearer ${token}`,
|
|
1273
|
+
...this.extraHeaders,
|
|
1274
|
+
...(options?.peekSettled ? { "X-Peek-Settled": "1" } : {}),
|
|
1275
|
+
},
|
|
1276
|
+
signal: combinedSignal,
|
|
1277
|
+
timeoutInSeconds: this.streamTimeoutSeconds,
|
|
1278
|
+
lastEventId: state.lastEventId,
|
|
1279
|
+
// Reconnect if no decoded record arrives for 60 seconds.
|
|
1280
|
+
stallTimeoutMs: 60_000,
|
|
1281
|
+
// Bound connected silence while preserving recovery from network failures.
|
|
1282
|
+
...(!this.watchMode && {
|
|
1283
|
+
maxStallRetries: 5,
|
|
1284
|
+
retryDelayMs: 1_000,
|
|
1285
|
+
maxRetryDelayMs: 5_000,
|
|
1286
|
+
}),
|
|
1287
|
+
fetchClient: sseFetchClient,
|
|
1288
|
+
});
|
|
1289
|
+
currentSubscription = subscription;
|
|
1290
|
+
const sseStream = await subscription.subscribe();
|
|
1291
|
+
const reader = sseStream.getReader();
|
|
1292
|
+
try {
|
|
1293
|
+
const first = await reader.read();
|
|
1294
|
+
if (first.done) {
|
|
1295
|
+
reader.releaseLock();
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
return { reader, primed: first.value, subscription };
|
|
1299
|
+
}
|
|
1300
|
+
catch (readErr) {
|
|
1301
|
+
reader.releaseLock();
|
|
1302
|
+
throw readErr;
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
const openWithAuthRetry = async () => {
|
|
1306
|
+
try {
|
|
1307
|
+
return await connectSseOnce(state.publicAccessToken);
|
|
1308
|
+
}
|
|
1309
|
+
catch (e) {
|
|
1310
|
+
if (!isAuthError(e))
|
|
1311
|
+
throw e;
|
|
1312
|
+
const fresh = await this.resolveAccessToken({ chatId });
|
|
1313
|
+
state.publicAccessToken = fresh;
|
|
1314
|
+
this.notifySessionChange(chatId, state);
|
|
1315
|
+
return await connectSseOnce(fresh);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
let eofResubscribes = 0;
|
|
1319
|
+
const resumeAfterEof = async () => {
|
|
1320
|
+
// Watch mode is a standing subscription: it outlives turn-complete
|
|
1321
|
+
// (which clears `isStreaming`) and idle windows EOF by design, so the
|
|
1322
|
+
// give-up budget doesn't apply. Only abort or a settled session ends it.
|
|
1323
|
+
while ((this.watchMode || (state.isStreaming && eofResubscribes < MAX_EOF_RESUBSCRIBES)) &&
|
|
1324
|
+
!currentSubscription?.sessionSettled &&
|
|
1325
|
+
!combinedSignal.aborted) {
|
|
1326
|
+
eofResubscribes++;
|
|
1327
|
+
await waitBeforeEofResubscribe(eofResubscribes, combinedSignal);
|
|
1328
|
+
if (combinedSignal.aborted)
|
|
1329
|
+
break;
|
|
1330
|
+
const opened = await openWithAuthRetry();
|
|
1331
|
+
if (opened)
|
|
1332
|
+
return opened;
|
|
1333
|
+
}
|
|
1334
|
+
// A settled session or an abort ends the turn cleanly. Exhausting the
|
|
1335
|
+
// resubscribe budget while the turn is still streaming means it was cut
|
|
1336
|
+
// off — surface an error so the UI doesn't read a truncated reply as
|
|
1337
|
+
// complete. The caller's catch emits stream-error and errors the stream.
|
|
1338
|
+
if (state.isStreaming &&
|
|
1339
|
+
!currentSubscription?.sessionSettled &&
|
|
1340
|
+
!combinedSignal.aborted) {
|
|
1341
|
+
throw new Error("Chat stream ended before the turn completed (reconnect budget exhausted).");
|
|
1342
|
+
}
|
|
1343
|
+
// Settled close, or the turn is gone — tell the UI instead of
|
|
1344
|
+
// leaving it spinning on a stream nobody will finish.
|
|
1345
|
+
if (state.isStreaming && this.activeStreams.get(chatId) === internalAbort) {
|
|
1346
|
+
state.isStreaming = false;
|
|
1347
|
+
this.notifySessionChange(chatId, state);
|
|
1348
|
+
}
|
|
1349
|
+
return null;
|
|
1350
|
+
};
|
|
1351
|
+
try {
|
|
1352
|
+
let reader;
|
|
1353
|
+
let primed;
|
|
1354
|
+
const opened = (await openWithAuthRetry()) ?? (await resumeAfterEof());
|
|
1355
|
+
if (opened === null) {
|
|
1356
|
+
try {
|
|
1357
|
+
controller.close();
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
/* already closed by a consumer cancel */
|
|
1361
|
+
}
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
reader = opened.reader;
|
|
1365
|
+
primed = opened.primed;
|
|
1366
|
+
this.emitEvent({
|
|
1367
|
+
type: "stream-connected",
|
|
1368
|
+
chatId,
|
|
1369
|
+
timestamp: Date.now(),
|
|
1370
|
+
resumed: options?.resumed ?? false,
|
|
1371
|
+
lastEventId: state.lastEventId,
|
|
1372
|
+
messageId: this.lastTurnSends.get(chatId)?.messageId,
|
|
1373
|
+
});
|
|
1374
|
+
while (true) {
|
|
1375
|
+
let value;
|
|
1376
|
+
if (primed !== undefined) {
|
|
1377
|
+
value = primed;
|
|
1378
|
+
primed = undefined;
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
const next = await reader.read();
|
|
1382
|
+
if (next.done) {
|
|
1383
|
+
const resumed = await resumeAfterEof();
|
|
1384
|
+
if (resumed === null) {
|
|
1385
|
+
try {
|
|
1386
|
+
controller.close();
|
|
1387
|
+
}
|
|
1388
|
+
catch {
|
|
1389
|
+
/* already closed by a consumer cancel */
|
|
1390
|
+
}
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
reader = resumed.reader;
|
|
1394
|
+
primed = resumed.primed;
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
value = next.value;
|
|
1398
|
+
}
|
|
1399
|
+
if (combinedSignal.aborted) {
|
|
1400
|
+
internalAbort.abort();
|
|
1401
|
+
await reader.cancel();
|
|
1402
|
+
try {
|
|
1403
|
+
controller.close();
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
/* already closed by a consumer cancel */
|
|
1407
|
+
}
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (value.id)
|
|
1411
|
+
state.lastEventId = value.id;
|
|
1412
|
+
// Any record — including the first of a resumed connection, which
|
|
1413
|
+
// arrives via `primed` — re-earns the resubscribe budget, so a long
|
|
1414
|
+
// turn spanning many long-poll windows keeps streaming.
|
|
1415
|
+
eofResubscribes = 0;
|
|
1416
|
+
// Trigger control record (turn-complete, upgrade-required) —
|
|
1417
|
+
// routed by header, body is empty. Detect via the
|
|
1418
|
+
// `trigger-control` header on the SSE record. Data records
|
|
1419
|
+
// (UIMessageChunks) fall through to the chunk path below.
|
|
1420
|
+
//
|
|
1421
|
+
// Cross-version bridge: a customer who redeploys their
|
|
1422
|
+
// Next.js app (new browser SDK) before their next
|
|
1423
|
+
// `trigger deploy` (old agent SDK still writing turn-complete
|
|
1424
|
+
// / upgrade-required as `chunk.type` data records) would
|
|
1425
|
+
// otherwise hang. Fall back to the legacy chunk-type form
|
|
1426
|
+
// when no header is present so the deploy-skew window
|
|
1427
|
+
// closes turns correctly.
|
|
1428
|
+
let controlValue = controlSubtype(value.headers);
|
|
1429
|
+
let legacyChunk;
|
|
1430
|
+
if (!controlValue && value.chunk && typeof value.chunk === "object") {
|
|
1431
|
+
const chunk = value.chunk;
|
|
1432
|
+
if (chunk.type === "trigger:turn-complete") {
|
|
1433
|
+
controlValue = TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE;
|
|
1434
|
+
legacyChunk = chunk;
|
|
1435
|
+
}
|
|
1436
|
+
else if (chunk.type === "trigger:upgrade-required") {
|
|
1437
|
+
controlValue = TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED;
|
|
1438
|
+
}
|
|
1439
|
+
else if (typeof chunk.type === "string" && chunk.type.startsWith("trigger:")) {
|
|
1440
|
+
// Future / unknown `trigger:*` legacy control type from
|
|
1441
|
+
// a pre-upgrade agent — drop so it doesn't reach the AI
|
|
1442
|
+
// SDK as an unrecognised UIMessageChunk.
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (state.skipToTurnComplete &&
|
|
1447
|
+
controlValue !== TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED &&
|
|
1448
|
+
controlValue !== TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1449
|
+
// Fall through on a boundary so the `sinceInSeq` check and the
|
|
1450
|
+
// turn-completed emit below still run; content records are skipped.
|
|
1451
|
+
// A close is terminal and a pending version is the gated turn's own
|
|
1452
|
+
// handover status, so neither is ever gated away.
|
|
1453
|
+
if (controlValue !== TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
1454
|
+
continue;
|
|
1455
|
+
// A boundary older than the superseded turn is not its boundary —
|
|
1456
|
+
// keep gating. A header-less boundary (legacy wire) still clears.
|
|
1457
|
+
const gateCursorRaw = headerValue(value.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
1458
|
+
const gateCursor = gateCursorRaw !== undefined ? Number.parseInt(gateCursorRaw, 10) : NaN;
|
|
1459
|
+
if (state.supersededInputSeq !== undefined &&
|
|
1460
|
+
!Number.isNaN(gateCursor) &&
|
|
1461
|
+
gateCursor < state.supersededInputSeq) {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
state.skipToTurnComplete = false;
|
|
1465
|
+
state.supersededInputSeq = undefined;
|
|
1466
|
+
// This boundary is the new turn's own, so the gate swallowed its
|
|
1467
|
+
// output: fail the turn instead of completing an empty answer, and
|
|
1468
|
+
// leave nothing armed for the retry.
|
|
1469
|
+
if (sinceInSeq !== undefined &&
|
|
1470
|
+
!Number.isNaN(gateCursor) &&
|
|
1471
|
+
gateCursor >= sinceInSeq) {
|
|
1472
|
+
// The lost boundary may also carry the session close.
|
|
1473
|
+
if (headerValue(value.headers, SESSION_CLOSED_HEADER) === "true") {
|
|
1474
|
+
this.markSessionClosed(chatId, state, headerValue(value.headers, SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1475
|
+
}
|
|
1476
|
+
state.activeInputSeq = undefined;
|
|
1477
|
+
state.isStreaming = false;
|
|
1478
|
+
this.notifySessionChange(chatId, state);
|
|
1479
|
+
throw new Error("The previous turn's output was lost. Try again.");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (controlValue === TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
|
|
1483
|
+
// Server has already triggered the new run via
|
|
1484
|
+
// `end-and-continue`; the next chunks on this same `.out`
|
|
1485
|
+
// stream come from v2. Filter the marker for cleanliness
|
|
1486
|
+
// and keep reading.
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (controlValue === TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED) {
|
|
1490
|
+
this.markSessionClosed(chatId, state, headerValue(value.headers, SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1491
|
+
internalAbort.abort();
|
|
1492
|
+
try {
|
|
1493
|
+
controller.close();
|
|
1494
|
+
}
|
|
1495
|
+
catch {
|
|
1496
|
+
/* already closed */
|
|
1497
|
+
}
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (controlValue === TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1501
|
+
this.emitEvent({
|
|
1502
|
+
type: "run-pending-version",
|
|
1503
|
+
chatId,
|
|
1504
|
+
timestamp: Date.now(),
|
|
1505
|
+
source: "upgrade",
|
|
1506
|
+
});
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
|
|
1510
|
+
const sessionInEventId = headerValue(value.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
1511
|
+
const lastEventId = value.id || undefined;
|
|
1512
|
+
// Skip a turn-complete from an earlier turn (committed `.in` cursor
|
|
1513
|
+
// below this send's seq), e.g. an undo action that raced this send.
|
|
1514
|
+
if (sinceInSeq !== undefined) {
|
|
1515
|
+
const cursorRaw = headerValue(value.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
1516
|
+
const cursor = cursorRaw !== undefined ? Number.parseInt(cursorRaw, 10) : NaN;
|
|
1517
|
+
if (!Number.isNaN(cursor) && cursor < sinceInSeq) {
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// A close decided during this turn rides out on turn-complete —
|
|
1522
|
+
// the last record a reader sees before it ends the stream.
|
|
1523
|
+
if (headerValue(value.headers, SESSION_CLOSED_HEADER) === "true") {
|
|
1524
|
+
this.markSessionClosed(chatId, state, headerValue(value.headers, SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1525
|
+
}
|
|
1526
|
+
const refreshedToken = headerValue(value.headers, PUBLIC_ACCESS_TOKEN_HEADER) ??
|
|
1527
|
+
legacyChunk?.publicAccessToken;
|
|
1528
|
+
if (refreshedToken) {
|
|
1529
|
+
state.publicAccessToken = refreshedToken;
|
|
1530
|
+
}
|
|
1531
|
+
this.emitEvent({
|
|
1532
|
+
type: "turn-completed",
|
|
1533
|
+
chatId,
|
|
1534
|
+
timestamp: Date.now(),
|
|
1535
|
+
lastEventId: state.lastEventId,
|
|
1536
|
+
sessionInEventId: headerValue(value.headers, SESSION_IN_EVENT_ID_HEADER),
|
|
1537
|
+
...this.turnAttribution(chatId),
|
|
1538
|
+
});
|
|
1539
|
+
state.activeInputSeq = undefined;
|
|
1540
|
+
sinceInSeq = undefined;
|
|
1541
|
+
state.isStreaming = false;
|
|
1542
|
+
this.notifySessionChange(chatId, state);
|
|
1543
|
+
this.coordinator?.release(chatId);
|
|
1544
|
+
this.coordinator?.broadcastSession(chatId, {
|
|
1545
|
+
lastEventId: state.lastEventId,
|
|
1546
|
+
});
|
|
1547
|
+
// Use this subscription's original append, never mutable session state.
|
|
1548
|
+
const inputSeq = options?.sinceInSeq;
|
|
1549
|
+
const settledSeq = sessionInEventId !== undefined &&
|
|
1550
|
+
sessionInEventId.length > 0 &&
|
|
1551
|
+
!/\D/.test(sessionInEventId)
|
|
1552
|
+
? Number(sessionInEventId)
|
|
1553
|
+
: NaN;
|
|
1554
|
+
if (!didSettle &&
|
|
1555
|
+
!combinedSignal.aborted &&
|
|
1556
|
+
this.activeStreams.get(chatId) === internalAbort &&
|
|
1557
|
+
inputSeq !== undefined &&
|
|
1558
|
+
Number.isSafeInteger(inputSeq) &&
|
|
1559
|
+
inputSeq >= 0 &&
|
|
1560
|
+
sessionInEventId !== undefined &&
|
|
1561
|
+
Number.isSafeInteger(settledSeq) &&
|
|
1562
|
+
settledSeq >= inputSeq) {
|
|
1563
|
+
didSettle = true;
|
|
1564
|
+
try {
|
|
1565
|
+
options?.onSettled?.({ inputSeq, sessionInEventId, lastEventId });
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
// An observer must not turn a completed action into a stream error.
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
// Re-arm per-turn events for watch mode's next turn.
|
|
1572
|
+
sawFirstChunk = false;
|
|
1573
|
+
if (this.watchMode)
|
|
1574
|
+
continue;
|
|
1575
|
+
internalAbort.abort();
|
|
1576
|
+
try {
|
|
1577
|
+
controller.close();
|
|
1578
|
+
}
|
|
1579
|
+
catch {
|
|
1580
|
+
/* already closed */
|
|
1581
|
+
}
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
// Data record — `value.chunk` is the parsed UIMessageChunk
|
|
1585
|
+
// unwrapped from the S2 record envelope (the parser does the
|
|
1586
|
+
// JSON unwrap). Drop empty/malformed payloads defensively.
|
|
1587
|
+
if (value.chunk == null)
|
|
1588
|
+
continue;
|
|
1589
|
+
if (!sawFirstChunk) {
|
|
1590
|
+
sawFirstChunk = true;
|
|
1591
|
+
this.emitEvent({
|
|
1592
|
+
type: "first-chunk",
|
|
1593
|
+
chatId,
|
|
1594
|
+
timestamp: Date.now(),
|
|
1595
|
+
chunkType: value.chunk.type,
|
|
1596
|
+
lastEventId: value.id || undefined,
|
|
1597
|
+
...this.turnAttribution(chatId),
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
controller.enqueue(value.chunk);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
catch (error) {
|
|
1604
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1605
|
+
try {
|
|
1606
|
+
controller.close();
|
|
1607
|
+
}
|
|
1608
|
+
catch {
|
|
1609
|
+
/* already closed */
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const errorStatus = error.status;
|
|
1614
|
+
// A superseded stream cannot settle the replacement stream.
|
|
1615
|
+
if (this.activeStreams.get(chatId) === internalAbort) {
|
|
1616
|
+
state.isStreaming = false;
|
|
1617
|
+
this.notifySessionChange(chatId, state);
|
|
1618
|
+
}
|
|
1619
|
+
this.emitEvent({
|
|
1620
|
+
type: "stream-error",
|
|
1621
|
+
chatId,
|
|
1622
|
+
timestamp: Date.now(),
|
|
1623
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1624
|
+
status: typeof errorStatus === "number" ? errorStatus : undefined,
|
|
1625
|
+
});
|
|
1626
|
+
controller.error(error);
|
|
1627
|
+
}
|
|
1628
|
+
finally {
|
|
1629
|
+
teardownWakeListeners();
|
|
1630
|
+
// Only clear the entry (and drop the tab claim) if it is still
|
|
1631
|
+
// ours — a superseding send registers its controller before this
|
|
1632
|
+
// teardown runs, and owns the claim from then on.
|
|
1633
|
+
if (this.activeStreams.get(chatId) === internalAbort) {
|
|
1634
|
+
this.activeStreams.delete(chatId);
|
|
1635
|
+
this.coordinator?.release(chatId);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
// A consumer that stops reading without aborting (drops the reader)
|
|
1640
|
+
// would otherwise leave the resubscribe loop running forever.
|
|
1641
|
+
cancel() {
|
|
1642
|
+
internalAbort.abort();
|
|
1643
|
+
},
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Convenience constructor matching {@link TriggerChatTransport}.
|
|
1649
|
+
*/
|
|
1650
|
+
export function createChatTransport(options) {
|
|
1651
|
+
return new TriggerChatTransport(options);
|
|
1652
|
+
}
|
|
1653
|
+
// Server-side agent chat re-exports.
|
|
1654
|
+
export { AgentChat, ChatStream, } from "./chat-client.js";
|
|
1655
|
+
//# sourceMappingURL=chat.js.map
|