@retinue/agentkit 0.1.0
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/LICENSE +22 -0
- package/README.md +310 -0
- package/dist/adapters/bullmq/consumer.d.ts +33 -0
- package/dist/adapters/bullmq/consumer.js +41 -0
- package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
- package/dist/adapters/bullmq/dispatcher.js +160 -0
- package/dist/adapters/bullmq/export.d.ts +31 -0
- package/dist/adapters/bullmq/export.js +53 -0
- package/dist/adapters/bullmq/extraction.d.ts +42 -0
- package/dist/adapters/bullmq/extraction.js +63 -0
- package/dist/adapters/bullmq/index.d.ts +13 -0
- package/dist/adapters/bullmq/index.js +13 -0
- package/dist/adapters/bullmq/lock.d.ts +77 -0
- package/dist/adapters/bullmq/lock.js +126 -0
- package/dist/adapters/bullmq/queue.d.ts +50 -0
- package/dist/adapters/bullmq/queue.js +81 -0
- package/dist/adapters/memory/artifact-exports.d.ts +11 -0
- package/dist/adapters/memory/artifact-exports.js +102 -0
- package/dist/adapters/memory/artifacts.d.ts +15 -0
- package/dist/adapters/memory/artifacts.js +134 -0
- package/dist/adapters/memory/blobs.d.ts +7 -0
- package/dist/adapters/memory/blobs.js +27 -0
- package/dist/adapters/memory/evaluation.d.ts +18 -0
- package/dist/adapters/memory/evaluation.js +148 -0
- package/dist/adapters/memory/files.d.ts +27 -0
- package/dist/adapters/memory/files.js +0 -0
- package/dist/adapters/memory/flows.d.ts +16 -0
- package/dist/adapters/memory/flows.js +117 -0
- package/dist/adapters/memory/hitl.d.ts +9 -0
- package/dist/adapters/memory/hitl.js +130 -0
- package/dist/adapters/memory/idempotency.d.ts +13 -0
- package/dist/adapters/memory/idempotency.js +32 -0
- package/dist/adapters/memory/index.d.ts +39 -0
- package/dist/adapters/memory/index.js +107 -0
- package/dist/adapters/memory/knowledge.d.ts +43 -0
- package/dist/adapters/memory/knowledge.js +248 -0
- package/dist/adapters/memory/mcp.d.ts +9 -0
- package/dist/adapters/memory/mcp.js +37 -0
- package/dist/adapters/memory/message-store.d.ts +17 -0
- package/dist/adapters/memory/message-store.js +70 -0
- package/dist/adapters/memory/principal-memory.d.ts +7 -0
- package/dist/adapters/memory/principal-memory.js +83 -0
- package/dist/adapters/memory/runtime.d.ts +29 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/memory/sessions.d.ts +29 -0
- package/dist/adapters/memory/sessions.js +0 -0
- package/dist/adapters/memory/skills.d.ts +10 -0
- package/dist/adapters/memory/skills.js +41 -0
- package/dist/adapters/memory/thread-summary.d.ts +7 -0
- package/dist/adapters/memory/thread-summary.js +29 -0
- package/dist/adapters/memory/usage-limits.d.ts +13 -0
- package/dist/adapters/memory/usage-limits.js +72 -0
- package/dist/adapters/memory/usage.d.ts +16 -0
- package/dist/adapters/memory/usage.js +279 -0
- package/dist/adapters/otel/index.d.ts +111 -0
- package/dist/adapters/otel/index.js +133 -0
- package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
- package/dist/adapters/postgres/artifact-exports.js +117 -0
- package/dist/adapters/postgres/artifacts.d.ts +16 -0
- package/dist/adapters/postgres/artifacts.js +172 -0
- package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
- package/dist/adapters/postgres/checkpoint-store.js +34 -0
- package/dist/adapters/postgres/config.d.ts +15 -0
- package/dist/adapters/postgres/config.js +187 -0
- package/dist/adapters/postgres/conversation-store.d.ts +4 -0
- package/dist/adapters/postgres/conversation-store.js +82 -0
- package/dist/adapters/postgres/evaluation.d.ts +17 -0
- package/dist/adapters/postgres/evaluation.js +193 -0
- package/dist/adapters/postgres/file-content.d.ts +30 -0
- package/dist/adapters/postgres/file-content.js +111 -0
- package/dist/adapters/postgres/files.d.ts +19 -0
- package/dist/adapters/postgres/files.js +209 -0
- package/dist/adapters/postgres/flows.d.ts +20 -0
- package/dist/adapters/postgres/flows.js +206 -0
- package/dist/adapters/postgres/hitl.d.ts +5 -0
- package/dist/adapters/postgres/hitl.js +247 -0
- package/dist/adapters/postgres/index.d.ts +35 -0
- package/dist/adapters/postgres/index.js +35 -0
- package/dist/adapters/postgres/knowledge.d.ts +48 -0
- package/dist/adapters/postgres/knowledge.js +255 -0
- package/dist/adapters/postgres/memory.d.ts +14 -0
- package/dist/adapters/postgres/memory.js +194 -0
- package/dist/adapters/postgres/message-store.d.ts +11 -0
- package/dist/adapters/postgres/message-store.js +145 -0
- package/dist/adapters/postgres/migrations.d.ts +69 -0
- package/dist/adapters/postgres/migrations.js +1594 -0
- package/dist/adapters/postgres/pg-executor.d.ts +19 -0
- package/dist/adapters/postgres/pg-executor.js +32 -0
- package/dist/adapters/postgres/retention.d.ts +26 -0
- package/dist/adapters/postgres/retention.js +59 -0
- package/dist/adapters/postgres/rollups.d.ts +17 -0
- package/dist/adapters/postgres/rollups.js +267 -0
- package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
- package/dist/adapters/postgres/run-coordinator.js +98 -0
- package/dist/adapters/postgres/run-event-log.d.ts +26 -0
- package/dist/adapters/postgres/run-event-log.js +30 -0
- package/dist/adapters/postgres/run-store.d.ts +4 -0
- package/dist/adapters/postgres/run-store.js +199 -0
- package/dist/adapters/postgres/schema.d.ts +39 -0
- package/dist/adapters/postgres/schema.js +70 -0
- package/dist/adapters/postgres/session-state.d.ts +7 -0
- package/dist/adapters/postgres/session-state.js +99 -0
- package/dist/adapters/postgres/sql.d.ts +8 -0
- package/dist/adapters/postgres/sql.js +2 -0
- package/dist/adapters/postgres/transaction.d.ts +37 -0
- package/dist/adapters/postgres/transaction.js +93 -0
- package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
- package/dist/adapters/postgres/unit-of-work.js +8 -0
- package/dist/adapters/postgres/usage-limits.d.ts +15 -0
- package/dist/adapters/postgres/usage-limits.js +136 -0
- package/dist/adapters/postgres/usage.d.ts +15 -0
- package/dist/adapters/postgres/usage.js +226 -0
- package/dist/adapters/redis/index.d.ts +9 -0
- package/dist/adapters/redis/index.js +9 -0
- package/dist/adapters/redis/realtime.d.ts +74 -0
- package/dist/adapters/redis/realtime.js +112 -0
- package/dist/adapters/supabase/index.d.ts +88 -0
- package/dist/adapters/supabase/index.js +84 -0
- package/dist/adapters/supabase/realtime.d.ts +30 -0
- package/dist/adapters/supabase/realtime.js +53 -0
- package/dist/adapters/supabase/rls.d.ts +99 -0
- package/dist/adapters/supabase/rls.js +216 -0
- package/dist/adapters/supabase/storage.d.ts +50 -0
- package/dist/adapters/supabase/storage.js +207 -0
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +209 -0
- package/dist/agents/define.d.ts +21 -0
- package/dist/agents/define.js +63 -0
- package/dist/agents/engine.d.ts +98 -0
- package/dist/agents/engine.js +462 -0
- package/dist/agents/index.d.ts +50 -0
- package/dist/agents/index.js +17 -0
- package/dist/artifacts/index.d.ts +114 -0
- package/dist/artifacts/index.js +219 -0
- package/dist/authorization/index.d.ts +76 -0
- package/dist/authorization/index.js +76 -0
- package/dist/capabilities/index.d.ts +120 -0
- package/dist/capabilities/index.js +167 -0
- package/dist/capabilities/runtime.d.ts +89 -0
- package/dist/capabilities/runtime.js +84 -0
- package/dist/citations/index.d.ts +161 -0
- package/dist/citations/index.js +182 -0
- package/dist/context/assembler.d.ts +82 -0
- package/dist/context/assembler.js +129 -0
- package/dist/context/compaction.d.ts +45 -0
- package/dist/context/compaction.js +55 -0
- package/dist/context/index.d.ts +75 -0
- package/dist/context/index.js +17 -0
- package/dist/core/content-parts.d.ts +194 -0
- package/dist/core/content-parts.js +23 -0
- package/dist/core/context.d.ts +51 -0
- package/dist/core/context.js +9 -0
- package/dist/core/env.d.ts +25 -0
- package/dist/core/env.js +41 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/events.d.ts +139 -0
- package/dist/core/events.js +99 -0
- package/dist/core/ids.d.ts +52 -0
- package/dist/core/ids.js +13 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/tokens.d.ts +22 -0
- package/dist/core/tokens.js +22 -0
- package/dist/core/validation.d.ts +34 -0
- package/dist/core/validation.js +176 -0
- package/dist/documents/extraction.d.ts +121 -0
- package/dist/documents/extraction.js +293 -0
- package/dist/documents/index.d.ts +199 -0
- package/dist/documents/index.js +65 -0
- package/dist/documents/parsers/pdf.d.ts +47 -0
- package/dist/documents/parsers/pdf.js +508 -0
- package/dist/documents/parsers/text.d.ts +59 -0
- package/dist/documents/parsers/text.js +325 -0
- package/dist/documents/read-tool.d.ts +52 -0
- package/dist/documents/read-tool.js +109 -0
- package/dist/documents/render.d.ts +29 -0
- package/dist/documents/render.js +59 -0
- package/dist/documents/vision.d.ts +159 -0
- package/dist/documents/vision.js +214 -0
- package/dist/entries/adapters-bullmq.d.ts +8 -0
- package/dist/entries/adapters-bullmq.js +8 -0
- package/dist/entries/adapters-otel.d.ts +13 -0
- package/dist/entries/adapters-otel.js +13 -0
- package/dist/entries/adapters-postgres.d.ts +10 -0
- package/dist/entries/adapters-postgres.js +10 -0
- package/dist/entries/adapters-redis.d.ts +3 -0
- package/dist/entries/adapters-redis.js +3 -0
- package/dist/entries/context.d.ts +20 -0
- package/dist/entries/context.js +20 -0
- package/dist/entries/flows.d.ts +15 -0
- package/dist/entries/flows.js +15 -0
- package/dist/entries/hitl.d.ts +10 -0
- package/dist/entries/hitl.js +10 -0
- package/dist/entries/knowledge.d.ts +18 -0
- package/dist/entries/knowledge.js +19 -0
- package/dist/entries/mcp.d.ts +10 -0
- package/dist/entries/mcp.js +10 -0
- package/dist/entries/observability.d.ts +14 -0
- package/dist/entries/observability.js +16 -0
- package/dist/entries/persistence.d.ts +11 -0
- package/dist/entries/persistence.js +11 -0
- package/dist/entries/providers.d.ts +14 -0
- package/dist/entries/providers.js +14 -0
- package/dist/entries/runtime.d.ts +13 -0
- package/dist/entries/runtime.js +13 -0
- package/dist/entries/server.d.ts +24 -0
- package/dist/entries/server.js +24 -0
- package/dist/entries/tools.d.ts +21 -0
- package/dist/entries/tools.js +21 -0
- package/dist/entries/usage.d.ts +10 -0
- package/dist/entries/usage.js +10 -0
- package/dist/evaluation/gate.d.ts +168 -0
- package/dist/evaluation/gate.js +180 -0
- package/dist/evaluation/graders.d.ts +125 -0
- package/dist/evaluation/graders.js +203 -0
- package/dist/evaluation/index.d.ts +120 -0
- package/dist/evaluation/index.js +183 -0
- package/dist/evaluation/judge.d.ts +75 -0
- package/dist/evaluation/judge.js +111 -0
- package/dist/export/index.d.ts +162 -0
- package/dist/export/index.js +363 -0
- package/dist/export/markdown.d.ts +19 -0
- package/dist/export/markdown.js +29 -0
- package/dist/export/pdf.d.ts +73 -0
- package/dist/export/pdf.js +407 -0
- package/dist/files/context.d.ts +97 -0
- package/dist/files/context.js +185 -0
- package/dist/files/index.d.ts +210 -0
- package/dist/files/index.js +338 -0
- package/dist/files/read-tool.d.ts +81 -0
- package/dist/files/read-tool.js +163 -0
- package/dist/files/turn-parts.d.ts +96 -0
- package/dist/files/turn-parts.js +171 -0
- package/dist/flows/index.d.ts +270 -0
- package/dist/flows/index.js +62 -0
- package/dist/flows/interpreter.d.ts +146 -0
- package/dist/flows/interpreter.js +426 -0
- package/dist/flows/runner.d.ts +145 -0
- package/dist/flows/runner.js +270 -0
- package/dist/graphql/index.d.ts +8 -0
- package/dist/graphql/index.js +8 -0
- package/dist/graphql/resolvers.d.ts +237 -0
- package/dist/graphql/resolvers.js +253 -0
- package/dist/graphql/schema.d.ts +11 -0
- package/dist/graphql/schema.js +258 -0
- package/dist/graphql/sse.d.ts +77 -0
- package/dist/graphql/sse.js +100 -0
- package/dist/hitl/approved-execution.d.ts +127 -0
- package/dist/hitl/approved-execution.js +177 -0
- package/dist/hitl/index.d.ts +79 -0
- package/dist/hitl/index.js +12 -0
- package/dist/hitl/service.d.ts +221 -0
- package/dist/hitl/service.js +268 -0
- package/dist/idempotency/index.d.ts +70 -0
- package/dist/idempotency/index.js +59 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +60 -0
- package/dist/knowledge/chunking.d.ts +57 -0
- package/dist/knowledge/chunking.js +158 -0
- package/dist/knowledge/index.d.ts +119 -0
- package/dist/knowledge/index.js +166 -0
- package/dist/knowledge/retrieval.d.ts +146 -0
- package/dist/knowledge/retrieval.js +170 -0
- package/dist/loadtest/harness.d.ts +168 -0
- package/dist/loadtest/harness.js +507 -0
- package/dist/loadtest/index.d.ts +13 -0
- package/dist/loadtest/index.js +13 -0
- package/dist/loadtest/injection.d.ts +89 -0
- package/dist/loadtest/injection.js +147 -0
- package/dist/loadtest/metrics.d.ts +197 -0
- package/dist/loadtest/metrics.js +160 -0
- package/dist/loadtest/runbooks.d.ts +28 -0
- package/dist/loadtest/runbooks.js +159 -0
- package/dist/loadtest/scenario.d.ts +104 -0
- package/dist/loadtest/scenario.js +208 -0
- package/dist/mcp/egress.d.ts +53 -0
- package/dist/mcp/egress.js +115 -0
- package/dist/mcp/index.d.ts +93 -0
- package/dist/mcp/index.js +33 -0
- package/dist/mcp/provider.d.ts +62 -0
- package/dist/mcp/provider.js +0 -0
- package/dist/models/index.d.ts +98 -0
- package/dist/models/index.js +74 -0
- package/dist/models/pricing.d.ts +24 -0
- package/dist/models/pricing.js +37 -0
- package/dist/models/provider-factory.d.ts +31 -0
- package/dist/models/provider-factory.js +67 -0
- package/dist/models/streaming.d.ts +145 -0
- package/dist/models/streaming.js +272 -0
- package/dist/models/vision.d.ts +38 -0
- package/dist/models/vision.js +62 -0
- package/dist/persistence/index.d.ts +1654 -0
- package/dist/persistence/index.js +226 -0
- package/dist/principal-memory/index.d.ts +106 -0
- package/dist/principal-memory/index.js +89 -0
- package/dist/retention/index.d.ts +89 -0
- package/dist/retention/index.js +70 -0
- package/dist/runtime/checkpoint.d.ts +37 -0
- package/dist/runtime/checkpoint.js +22 -0
- package/dist/runtime/index.d.ts +118 -0
- package/dist/runtime/index.js +69 -0
- package/dist/runtime/retry.d.ts +95 -0
- package/dist/runtime/retry.js +126 -0
- package/dist/runtime/serialization.d.ts +85 -0
- package/dist/runtime/serialization.js +95 -0
- package/dist/runtime/streaming.d.ts +54 -0
- package/dist/runtime/streaming.js +115 -0
- package/dist/runtime/worker.d.ts +130 -0
- package/dist/runtime/worker.js +405 -0
- package/dist/security/checklist.d.ts +53 -0
- package/dist/security/checklist.js +204 -0
- package/dist/security/findings.d.ts +56 -0
- package/dist/security/findings.js +168 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.js +14 -0
- package/dist/security/prompt-safety.d.ts +100 -0
- package/dist/security/prompt-safety.js +133 -0
- package/dist/server/boot.d.ts +32 -0
- package/dist/server/boot.js +36 -0
- package/dist/server/cli-worker.d.ts +37 -0
- package/dist/server/cli-worker.js +151 -0
- package/dist/server/cli.d.ts +27 -0
- package/dist/server/cli.js +74 -0
- package/dist/server/config.d.ts +42 -0
- package/dist/server/config.js +127 -0
- package/dist/server/health.d.ts +59 -0
- package/dist/server/health.js +90 -0
- package/dist/server/host.d.ts +39 -0
- package/dist/server/host.js +124 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +15 -0
- package/dist/server/main.d.ts +16 -0
- package/dist/server/main.js +31 -0
- package/dist/server/sse-route.d.ts +21 -0
- package/dist/server/sse-route.js +282 -0
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +31 -0
- package/dist/skills/resolver.d.ts +54 -0
- package/dist/skills/resolver.js +121 -0
- package/dist/teams/index.d.ts +93 -0
- package/dist/teams/index.js +207 -0
- package/dist/telemetry/index.d.ts +157 -0
- package/dist/telemetry/index.js +71 -0
- package/dist/telemetry/instrument.d.ts +108 -0
- package/dist/telemetry/instrument.js +232 -0
- package/dist/telemetry/log-events.d.ts +17 -0
- package/dist/telemetry/log-events.js +58 -0
- package/dist/telemetry/metrics.d.ts +123 -0
- package/dist/telemetry/metrics.js +135 -0
- package/dist/telemetry/noop.d.ts +39 -0
- package/dist/telemetry/noop.js +143 -0
- package/dist/telemetry/redaction.d.ts +64 -0
- package/dist/telemetry/redaction.js +153 -0
- package/dist/telemetry/spans.d.ts +56 -0
- package/dist/telemetry/spans.js +78 -0
- package/dist/telemetry/trace-context.d.ts +55 -0
- package/dist/telemetry/trace-context.js +60 -0
- package/dist/toolkit/compute.d.ts +53 -0
- package/dist/toolkit/compute.js +152 -0
- package/dist/toolkit/data.d.ts +98 -0
- package/dist/toolkit/data.js +235 -0
- package/dist/toolkit/http.d.ts +113 -0
- package/dist/toolkit/http.js +205 -0
- package/dist/toolkit/index.d.ts +21 -0
- package/dist/toolkit/index.js +17 -0
- package/dist/toolkit/web.d.ts +107 -0
- package/dist/toolkit/web.js +147 -0
- package/dist/tools/define.d.ts +25 -0
- package/dist/tools/define.js +45 -0
- package/dist/tools/delegating.d.ts +132 -0
- package/dist/tools/delegating.js +211 -0
- package/dist/tools/index.d.ts +129 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/library/compute.d.ts +11 -0
- package/dist/tools/library/compute.js +46 -0
- package/dist/tools/library/data.d.ts +16 -0
- package/dist/tools/library/data.js +92 -0
- package/dist/tools/library/http.d.ts +28 -0
- package/dist/tools/library/http.js +71 -0
- package/dist/tools/library/index.d.ts +97 -0
- package/dist/tools/library/index.js +134 -0
- package/dist/tools/library/knowledge.d.ts +39 -0
- package/dist/tools/library/knowledge.js +58 -0
- package/dist/tools/library/web.d.ts +19 -0
- package/dist/tools/library/web.js +65 -0
- package/dist/tools/meta-tools.d.ts +19 -0
- package/dist/tools/meta-tools.js +36 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +291 -0
- package/dist/usage/index.d.ts +105 -0
- package/dist/usage/index.js +20 -0
- package/dist/usage/quota.d.ts +258 -0
- package/dist/usage/quota.js +510 -0
- package/dist/usage/recorder.d.ts +29 -0
- package/dist/usage/recorder.js +96 -0
- package/dist/usage/rollups.d.ts +121 -0
- package/dist/usage/rollups.js +157 -0
- package/dist/worker/export.d.ts +57 -0
- package/dist/worker/export.js +81 -0
- package/dist/worker/extraction.d.ts +57 -0
- package/dist/worker/extraction.js +84 -0
- package/dist/worker/main.d.ts +103 -0
- package/dist/worker/main.js +159 -0
- package/package.json +187 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral streaming — `docs/04-durable-runtime-and-hitl.md` → Transport events.
|
|
3
|
+
*
|
|
4
|
+
* The reconnect layer that sits under every transport (GraphQL subscription, SSE, …): a client
|
|
5
|
+
* reconnects, reads everything after its cursor from the durable `RunEventLog`, then follows the
|
|
6
|
+
* live feed — with catch-up and live de-duplicated by `sequence` so no part is missed or repeated.
|
|
7
|
+
* Nothing here knows about a wire format, which is what keeps the layer transport-agnostic.
|
|
8
|
+
*/
|
|
9
|
+
import { reduceRunEvents, } from "../core/events.js";
|
|
10
|
+
const TERMINAL_EVENT_TYPES = new Set([
|
|
11
|
+
"run.completed",
|
|
12
|
+
"run.failed",
|
|
13
|
+
"run.cancelled",
|
|
14
|
+
]);
|
|
15
|
+
export const isTerminalEventType = (type) => TERMINAL_EVENT_TYPES.has(type);
|
|
16
|
+
/**
|
|
17
|
+
* The persisted snapshot a transport sends on connect: the projected state plus the cursor to
|
|
18
|
+
* resume streaming from. Built purely from the durable log, so it never disagrees with live deltas.
|
|
19
|
+
*/
|
|
20
|
+
export const reconnectSnapshot = async (input) => {
|
|
21
|
+
const events = await input.log.listAfter({ tenantId: input.tenantId, runId: input.runId, after: 0 });
|
|
22
|
+
const state = reduceRunEvents(events);
|
|
23
|
+
return { state, after: state.sequence };
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Ordered, gap-free, duplicate-free event stream from `after`. Subscribes first (so no live event
|
|
27
|
+
* is lost during catch-up), replays the durable log, then follows live — skipping anything whose
|
|
28
|
+
* sequence was already delivered. Ends on the run's terminal event or when `signal` aborts.
|
|
29
|
+
*/
|
|
30
|
+
export async function* openRunEventStream(input) {
|
|
31
|
+
const iterator = input.live.subscribe(input.channel)[Symbol.asyncIterator]();
|
|
32
|
+
try {
|
|
33
|
+
let cursor = input.after;
|
|
34
|
+
let terminalSeen = false;
|
|
35
|
+
const catchup = await input.log.listAfter({ tenantId: input.tenantId, runId: input.runId, after: input.after });
|
|
36
|
+
for (const event of catchup) {
|
|
37
|
+
if (event.sequence <= cursor)
|
|
38
|
+
continue;
|
|
39
|
+
cursor = event.sequence;
|
|
40
|
+
yield event;
|
|
41
|
+
if (isTerminalEventType(event.type))
|
|
42
|
+
terminalSeen = true;
|
|
43
|
+
}
|
|
44
|
+
if (terminalSeen)
|
|
45
|
+
return; // the run finished before we reconnected; no live events will come.
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (input.signal?.aborted)
|
|
48
|
+
return;
|
|
49
|
+
const next = await iterator.next();
|
|
50
|
+
if (next.done)
|
|
51
|
+
return;
|
|
52
|
+
const event = next.value;
|
|
53
|
+
if (event.sequence <= cursor)
|
|
54
|
+
continue; // duplicate already delivered via catch-up
|
|
55
|
+
cursor = event.sequence;
|
|
56
|
+
yield event;
|
|
57
|
+
if (isTerminalEventType(event.type))
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await iterator.return?.(undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* In-memory event bus: a `RealtimePublisher` paired with a `LiveEventSource`. Each subscriber gets
|
|
67
|
+
* its own queue that starts filling the moment it subscribes, so `openRunEventStream` never races.
|
|
68
|
+
* Serves tests, single-process deployments and the SSE embedded profile (#37).
|
|
69
|
+
*/
|
|
70
|
+
export const createMemoryEventBus = () => {
|
|
71
|
+
const channels = new Map();
|
|
72
|
+
const publisher = {
|
|
73
|
+
async publish(channel, event) {
|
|
74
|
+
const subs = channels.get(channel);
|
|
75
|
+
if (!subs)
|
|
76
|
+
return;
|
|
77
|
+
for (const sub of subs) {
|
|
78
|
+
sub.queue.push(event);
|
|
79
|
+
sub.wake?.();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const live = {
|
|
84
|
+
subscribe(channel) {
|
|
85
|
+
const sub = { queue: [], closed: false };
|
|
86
|
+
let subs = channels.get(channel);
|
|
87
|
+
if (!subs)
|
|
88
|
+
channels.set(channel, (subs = new Set()));
|
|
89
|
+
subs.add(sub);
|
|
90
|
+
return {
|
|
91
|
+
async *[Symbol.asyncIterator]() {
|
|
92
|
+
try {
|
|
93
|
+
while (!sub.closed) {
|
|
94
|
+
const event = sub.queue.shift();
|
|
95
|
+
if (event === undefined) {
|
|
96
|
+
await new Promise((resolve) => {
|
|
97
|
+
sub.wake = resolve;
|
|
98
|
+
});
|
|
99
|
+
sub.wake = undefined;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
yield event;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
sub.closed = true;
|
|
107
|
+
channels.get(channel)?.delete(sub);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
return { publisher, live };
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=streaming.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable run worker — `docs/04-durable-runtime-and-hitl.md` → Durable execution.
|
|
3
|
+
*
|
|
4
|
+
* The worker owns a run's lifecycle: claim it under a lease, drive the injected `AgentEngine`,
|
|
5
|
+
* checkpoint every streamed event, keep the lease alive, and finish inside a terminal transition.
|
|
6
|
+
* It is provider-neutral — the engine produces the events; the worker makes them durable and
|
|
7
|
+
* recoverable. The guarantees it enforces (per the acceptance criteria):
|
|
8
|
+
*
|
|
9
|
+
* - **Atomic claim** — a lease-based `RunStore.claim` means two workers never process one run.
|
|
10
|
+
* - **Refresh loses nothing** — parts are checkpointed and appended to the durable `RunEventLog`
|
|
11
|
+
* as they stream, so a reconnecting client catches up from its cursor with no gap.
|
|
12
|
+
* - **Safe crash recovery** — a re-claim reloads the checkpoint and *finalizes* (never re-runs)
|
|
13
|
+
* tool calls that were mid-flight, so no external action fires twice.
|
|
14
|
+
* - **Cooperative cancellation** — a durable cancel request stops the engine and finalizes cleanly.
|
|
15
|
+
*
|
|
16
|
+
* Streaming state is projected through the one canonical reducer (`reduceRunEvent`), the same fold a
|
|
17
|
+
* client uses, so the checkpoint and any client rebuild identical state. Retrying transient provider
|
|
18
|
+
* failures is the engine's job (`runWithRetry`); the worker just relays the `run.retry-pending`
|
|
19
|
+
* notifications it emits. Retried *external* writes are made safe by idempotency keys.
|
|
20
|
+
*/
|
|
21
|
+
import type { ExecutionContext } from "../core/context.js";
|
|
22
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
23
|
+
import { type RealtimePublisher, type RunEvent, type RunEventLog } from "../core/events.js";
|
|
24
|
+
import type { RunId } from "../core/ids.js";
|
|
25
|
+
import type { CheckpointStore, MessageStore, RunStore } from "../persistence/index.js";
|
|
26
|
+
import type { UsageRecorder } from "../usage/index.js";
|
|
27
|
+
import type { RunCheckpoint } from "./checkpoint.js";
|
|
28
|
+
import { type DistributedLockStore, type Run } from "./index.js";
|
|
29
|
+
/** Stable assistant-message id for a run, so a resumed/recovered run upserts one row, not many. */
|
|
30
|
+
export declare const deriveRunMessageId: (runId: RunId) => string;
|
|
31
|
+
/** Cooperative cancellation the engine polls between steps and tool calls. */
|
|
32
|
+
export interface CancellationSignal {
|
|
33
|
+
readonly isCancelled: () => boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Distributive `Omit` — plain `Omit` over a union keeps only common keys, collapsing the union. */
|
|
36
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
37
|
+
/**
|
|
38
|
+
* An event the engine yields. The worker stamps `runId`, a monotonic `sequence` and `occurredAt`,
|
|
39
|
+
* so engines never have to track sequencing. Distributes over the union, keeping `type` discriminating.
|
|
40
|
+
*/
|
|
41
|
+
export type EngineEvent = DistributiveOmit<RunEvent, "sequence" | "occurredAt" | "runId">;
|
|
42
|
+
export type EngineRunInput = {
|
|
43
|
+
readonly run: Run;
|
|
44
|
+
readonly context: ExecutionContext;
|
|
45
|
+
/** Non-null on recovery: parts already persisted, step reached, dangling calls already finalized. */
|
|
46
|
+
readonly resume: RunCheckpoint | null;
|
|
47
|
+
readonly signal: CancellationSignal;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The pluggable agent loop (model + tools). Yields typed events; MUST be resumable from `resume`
|
|
51
|
+
* (never re-run a tool already present in the checkpoint), MUST wrap provider calls in
|
|
52
|
+
* `runWithRetry` and yield `run.retry-pending`, and SHOULD abort provider/tools when the async
|
|
53
|
+
* iterator is `return()`-ed or `signal.isCancelled()` flips.
|
|
54
|
+
*/
|
|
55
|
+
export interface AgentEngine {
|
|
56
|
+
run(input: EngineRunInput): AsyncIterable<EngineEvent>;
|
|
57
|
+
}
|
|
58
|
+
export type DurableWorkerDeps = {
|
|
59
|
+
readonly runs: RunStore;
|
|
60
|
+
readonly checkpoints: CheckpointStore;
|
|
61
|
+
readonly publisher: RealtimePublisher;
|
|
62
|
+
readonly engine: AgentEngine;
|
|
63
|
+
/** Durable per-run event log for reconnect catch-up. Optional but required for gap-free reconnect. */
|
|
64
|
+
readonly eventLog?: RunEventLog;
|
|
65
|
+
/** Records durable usage per realized step (doc 12). Recorded as usage is realized, so a later
|
|
66
|
+
* failure never loses the usage already consumed — and idempotently, so recovery never double-counts. */
|
|
67
|
+
readonly usage?: UsageRecorder;
|
|
68
|
+
/** Host builds the execution context; identity never comes from model output. */
|
|
69
|
+
readonly buildContext: (run: Run) => ExecutionContext | Promise<ExecutionContext>;
|
|
70
|
+
readonly workerId: string;
|
|
71
|
+
/** Optional belt-and-suspenders mutual exclusion around the atomic claim. */
|
|
72
|
+
readonly locks?: DistributedLockStore;
|
|
73
|
+
readonly clock?: () => string;
|
|
74
|
+
readonly now?: () => number;
|
|
75
|
+
readonly leaseMs?: number;
|
|
76
|
+
readonly keepaliveEveryMs?: number;
|
|
77
|
+
/** Realtime channel for a run's events. Defaults to `conversation:<id>`. */
|
|
78
|
+
readonly channelFor?: (run: Run) => string;
|
|
79
|
+
/**
|
|
80
|
+
* Records the assistant's turn when a run reaches a terminal state — #157.
|
|
81
|
+
*
|
|
82
|
+
* Optional, because a host that reads history from the event log instead does not need it. Supply it and the
|
|
83
|
+
* next run's `loadHistory` sees what the assistant said; leave it out and the agent has amnesia between runs
|
|
84
|
+
* unless the host reconstructs the turn itself. That reconstruction was the gap: `MessageStore` was
|
|
85
|
+
* read-only, so the user's turn was persisted and the assistant's never was, and every host had to fold the
|
|
86
|
+
* event log to paper over the asymmetry.
|
|
87
|
+
*
|
|
88
|
+
* Writes are idempotent on a message id derived from the run id, so a resumed run that completes after a
|
|
89
|
+
* restart records one turn, not two.
|
|
90
|
+
*/
|
|
91
|
+
readonly messages?: MessageStore;
|
|
92
|
+
/**
|
|
93
|
+
* Called once, after a run reaches a terminal state and its state is durable — #202.
|
|
94
|
+
*
|
|
95
|
+
* Added for a flow parked on a child run, but deliberately general: anything that wants to know a run finished
|
|
96
|
+
* — an audit, a webhook, a parent flow — wires this rather than polling.
|
|
97
|
+
*
|
|
98
|
+
* **Best effort, and that is a decision.** A failure here is reported and swallowed, because the run genuinely
|
|
99
|
+
* did complete and failing it because a listener threw would be recording a lie. What makes that safe is that
|
|
100
|
+
* no listener may *depend* on the notification: the flow runner polls its child's state on every resume, so a
|
|
101
|
+
* lost message costs latency rather than a stuck flow. A hook whose delivery was load-bearing would need a
|
|
102
|
+
* queue, not a callback.
|
|
103
|
+
*/
|
|
104
|
+
readonly onRunSettled?: (input: {
|
|
105
|
+
readonly context: ExecutionContext;
|
|
106
|
+
readonly run: Run;
|
|
107
|
+
readonly outcome: "completed" | "failed" | "cancelled";
|
|
108
|
+
}) => Promise<void> | void;
|
|
109
|
+
};
|
|
110
|
+
export type ProcessOutcome = "completed" | "failed" | "cancelled" | "skipped" | "lost" | "paused";
|
|
111
|
+
export type ProcessResult = {
|
|
112
|
+
readonly run: Run | null;
|
|
113
|
+
readonly outcome: ProcessOutcome;
|
|
114
|
+
};
|
|
115
|
+
export declare const createDurableWorker: (deps: DurableWorkerDeps) => {
|
|
116
|
+
/** Claim (under an optional lock) and drive a run. Idempotent: a claimed/terminal run is skipped. */
|
|
117
|
+
process(input: {
|
|
118
|
+
tenantId: Run["tenantId"];
|
|
119
|
+
runId: RunId;
|
|
120
|
+
}): Promise<ProcessResult>;
|
|
121
|
+
/**
|
|
122
|
+
* Recover runs whose lease expired (crashed workers). Returns the reclaimed candidates; a caller
|
|
123
|
+
* re-enqueues each via its `JobDispatcher`. Kept separate so recovery cadence is the host's call.
|
|
124
|
+
*/
|
|
125
|
+
reapExpired(limit?: number): Promise<readonly Run[]>;
|
|
126
|
+
};
|
|
127
|
+
export type DurableWorker = ReturnType<typeof createDurableWorker>;
|
|
128
|
+
/** Re-export for adapters that construct terminal errors from thrown values. */
|
|
129
|
+
export { AgentPlatformError };
|
|
130
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable run worker — `docs/04-durable-runtime-and-hitl.md` → Durable execution.
|
|
3
|
+
*
|
|
4
|
+
* The worker owns a run's lifecycle: claim it under a lease, drive the injected `AgentEngine`,
|
|
5
|
+
* checkpoint every streamed event, keep the lease alive, and finish inside a terminal transition.
|
|
6
|
+
* It is provider-neutral — the engine produces the events; the worker makes them durable and
|
|
7
|
+
* recoverable. The guarantees it enforces (per the acceptance criteria):
|
|
8
|
+
*
|
|
9
|
+
* - **Atomic claim** — a lease-based `RunStore.claim` means two workers never process one run.
|
|
10
|
+
* - **Refresh loses nothing** — parts are checkpointed and appended to the durable `RunEventLog`
|
|
11
|
+
* as they stream, so a reconnecting client catches up from its cursor with no gap.
|
|
12
|
+
* - **Safe crash recovery** — a re-claim reloads the checkpoint and *finalizes* (never re-runs)
|
|
13
|
+
* tool calls that were mid-flight, so no external action fires twice.
|
|
14
|
+
* - **Cooperative cancellation** — a durable cancel request stops the engine and finalizes cleanly.
|
|
15
|
+
*
|
|
16
|
+
* Streaming state is projected through the one canonical reducer (`reduceRunEvent`), the same fold a
|
|
17
|
+
* client uses, so the checkpoint and any client rebuild identical state. Retrying transient provider
|
|
18
|
+
* failures is the engine's job (`runWithRetry`); the worker just relays the `run.retry-pending`
|
|
19
|
+
* notifications it emits. Retried *external* writes are made safe by idempotency keys.
|
|
20
|
+
*/
|
|
21
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
22
|
+
import { EMPTY_RUN_STREAM_STATE, reduceRunEvent, } from "../core/events.js";
|
|
23
|
+
import {} from "./index.js";
|
|
24
|
+
import { isTerminal } from "./index.js";
|
|
25
|
+
import { toPlatformError } from "./retry.js";
|
|
26
|
+
/** Stable assistant-message id for a run, so a resumed/recovered run upserts one row, not many. */
|
|
27
|
+
export const deriveRunMessageId = (runId) => `run:${runId}:assistant`;
|
|
28
|
+
/** Thrown internally when a keepalive reveals the lease was lost; never marks the run failed. */
|
|
29
|
+
class ClaimLostError extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("run claim lost");
|
|
32
|
+
this.name = "ClaimLostError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const createDurableWorker = (deps) => {
|
|
36
|
+
const now = deps.now ?? Date.now;
|
|
37
|
+
const clock = deps.clock ?? (() => new Date(now()).toISOString());
|
|
38
|
+
const leaseMs = deps.leaseMs ?? 30_000;
|
|
39
|
+
const keepaliveEveryMs = deps.keepaliveEveryMs ?? Math.max(1, Math.floor(leaseMs / 3));
|
|
40
|
+
const channelFor = deps.channelFor ?? ((r) => `conversation:${r.conversationId}`);
|
|
41
|
+
const { runs, checkpoints, publisher, engine, workerId } = deps;
|
|
42
|
+
/** Drive one already-claimed run to a terminal state. */
|
|
43
|
+
const drive = async (run) => {
|
|
44
|
+
const tenantId = run.tenantId;
|
|
45
|
+
/**
|
|
46
|
+
* A run whose context cannot be built is **failed**, not retried — #172.
|
|
47
|
+
*
|
|
48
|
+
* `buildContext` throwing escaped `drive` entirely: the try that marks a run failed starts after the engine
|
|
49
|
+
* begins, so the run stayed `running`, its lease expired, the reaper re-claimed it, and it threw again.
|
|
50
|
+
* Forever. Seen in the wild the moment `buildContext` started refusing runs with no recorded principal
|
|
51
|
+
* (#164): every pre-#164 run became an unkillable reap loop, reported once per sweep and never resolved.
|
|
52
|
+
*
|
|
53
|
+
* Failed rather than retried because this class of error is **deterministic by construction**. The host is
|
|
54
|
+
* saying it cannot construct an identity for this run; the next attempt has exactly the same run row and will
|
|
55
|
+
* say the same thing. A transient failure fetching a checkpoint is different, and stays inside the retry
|
|
56
|
+
* path below — this covers only the host's own refusal.
|
|
57
|
+
*
|
|
58
|
+
* A failed run is visible: it has a status, an error, and a place in the UI. An eternally-reaped one is a log
|
|
59
|
+
* line nobody reads.
|
|
60
|
+
*/
|
|
61
|
+
let context;
|
|
62
|
+
try {
|
|
63
|
+
context = await deps.buildContext(run);
|
|
64
|
+
}
|
|
65
|
+
catch (thrown) {
|
|
66
|
+
const error = toPlatformError(thrown);
|
|
67
|
+
const failed = await runs.transition({ tenantId, id: run.id, workerId, to: "failed", now: clock(), error });
|
|
68
|
+
// The event too, so a client watching this run learns why rather than waiting on a stream that never ends.
|
|
69
|
+
if (deps.eventLog) {
|
|
70
|
+
await deps.eventLog
|
|
71
|
+
.append({
|
|
72
|
+
tenantId,
|
|
73
|
+
event: { type: "run.failed", runId: run.id, sequence: 1, occurredAt: clock(), error },
|
|
74
|
+
})
|
|
75
|
+
.catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
return { run: failed, outcome: "failed" };
|
|
78
|
+
}
|
|
79
|
+
const channel = channelFor(run);
|
|
80
|
+
const initial = await checkpoints.latest({ tenantId, runId: run.id });
|
|
81
|
+
let state = initial
|
|
82
|
+
? {
|
|
83
|
+
...EMPTY_RUN_STREAM_STATE,
|
|
84
|
+
parts: initial.parts,
|
|
85
|
+
pendingToolCalls: initial.pendingToolCalls,
|
|
86
|
+
usage: initial.usage,
|
|
87
|
+
sequence: initial.sequence,
|
|
88
|
+
}
|
|
89
|
+
: EMPTY_RUN_STREAM_STATE;
|
|
90
|
+
let cancelRequested = run.cancelRequestedAt !== undefined;
|
|
91
|
+
let lastKeepalive = now();
|
|
92
|
+
const toCheckpoint = () => ({
|
|
93
|
+
runId: run.id,
|
|
94
|
+
sequence: state.sequence,
|
|
95
|
+
parts: state.parts,
|
|
96
|
+
// Step = tool-call rounds reached, so a resumed engine trusting `resume.step` never re-drives.
|
|
97
|
+
step: state.parts.filter((p) => p.type === "tool-call").length,
|
|
98
|
+
pendingToolCalls: state.pendingToolCalls,
|
|
99
|
+
usage: state.usage,
|
|
100
|
+
updatedAt: clock(),
|
|
101
|
+
});
|
|
102
|
+
const emit = async (body) => {
|
|
103
|
+
const event = { ...body, runId: run.id, sequence: state.sequence + 1, occurredAt: clock() };
|
|
104
|
+
state = reduceRunEvent(state, event);
|
|
105
|
+
await publisher.publish(channel, event);
|
|
106
|
+
if (deps.eventLog)
|
|
107
|
+
await deps.eventLog.append({ tenantId, event });
|
|
108
|
+
// Record durable usage for a realized step. Keyed by sequence so recovery never double-counts.
|
|
109
|
+
if (deps.usage && event.type === "usage.updated" && event.modelId !== undefined) {
|
|
110
|
+
await deps.usage.record(context, {
|
|
111
|
+
runId: run.id,
|
|
112
|
+
conversationId: run.conversationId,
|
|
113
|
+
modelId: event.modelId,
|
|
114
|
+
inputTokens: event.inputTokens,
|
|
115
|
+
outputTokens: event.outputTokens,
|
|
116
|
+
cachedInputTokens: event.cachedInputTokens ?? 0,
|
|
117
|
+
...(event.reasoningTokens === undefined ? {} : { reasoningTokens: event.reasoningTokens }),
|
|
118
|
+
...(event.imageCount === undefined ? {} : { imageCount: event.imageCount }),
|
|
119
|
+
...(event.audioSeconds === undefined ? {} : { audioSeconds: event.audioSeconds }),
|
|
120
|
+
costMinorUnits: event.costMinorUnits ?? 0,
|
|
121
|
+
currency: event.currency ?? "USD",
|
|
122
|
+
stepId: event.stepId ?? String(event.sequence),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const persist = () => checkpoints.save({ tenantId, checkpoint: toCheckpoint() });
|
|
127
|
+
/**
|
|
128
|
+
* Tell whoever asked that this run settled.
|
|
129
|
+
*
|
|
130
|
+
* Swallowed on failure, with a report. The run completed; failing it because a listener threw would record a
|
|
131
|
+
* lie about what happened. Safe only because nothing may depend on delivery — the flow runner polls its
|
|
132
|
+
* child's state on every resume, so a lost notification costs latency rather than a stuck flow.
|
|
133
|
+
*/
|
|
134
|
+
const notifySettled = async (settled, outcome) => {
|
|
135
|
+
if (deps.onRunSettled === undefined || settled === null)
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
await deps.onRunSettled({ context, run: settled, outcome });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
/**
|
|
142
|
+
* To stderr, because this layer has no telemetry port and inventing a dependency for one log line would
|
|
143
|
+
* be worse than the line. A listener that throws is a wiring bug in the *host*, and the host is who reads
|
|
144
|
+
* this — the alternative is silence, which is how a parent flow that never woke becomes unexplainable.
|
|
145
|
+
*/
|
|
146
|
+
console.error(`[worker] onRunSettled threw for run ${String(settled.id)} (${outcome}); the run is unaffected: ${toPlatformError(error).message}`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Record the assistant's turn — #157. Called on every terminal exit, and only there.
|
|
151
|
+
*
|
|
152
|
+
* `isTerminal` is the gate rather than a check for "completed", because a run that failed or was cancelled
|
|
153
|
+
* still streamed text the user read; dropping it would show them a reply that vanishes on reload. The
|
|
154
|
+
* paused states are excluded by construction: `waiting-for-approval` and `waiting-for-question` transition
|
|
155
|
+
* back to `queued`, so they are not terminal, and writing there would be worse than not writing at all —
|
|
156
|
+
* the id is derived from the run, so the partial turn would win and the completed one would be discarded
|
|
157
|
+
* as a duplicate.
|
|
158
|
+
*/
|
|
159
|
+
const persistAssistantTurn = async (status) => {
|
|
160
|
+
if (deps.messages === undefined || !isTerminal(status))
|
|
161
|
+
return;
|
|
162
|
+
if (state.parts.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
/**
|
|
165
|
+
* No conversation, nothing to persist the turn to — #198.
|
|
166
|
+
*
|
|
167
|
+
* A no-op rather than a throw, and the distinction matters: one runtime serves both a chat assistant and a
|
|
168
|
+
* headless automation, so a run without a conversation is a *fact about that run*, not a wiring mistake.
|
|
169
|
+
* Throwing here would make an automation fail at the end of successful work.
|
|
170
|
+
*
|
|
171
|
+
* What must still fail loudly is a caller *asking* for conversation-scoped data on such a run — that is a
|
|
172
|
+
* programming error, and `conversationScoped` below is where it is refused.
|
|
173
|
+
*/
|
|
174
|
+
if (run.conversationId === undefined)
|
|
175
|
+
return;
|
|
176
|
+
const message = {
|
|
177
|
+
// The same id the client already saw on every `part.added`. A second convention here would mean the
|
|
178
|
+
// streamed message and the persisted row disagreed about their own identity, so a client that kept the
|
|
179
|
+
// streamed id could never match it to the row it later loads from history.
|
|
180
|
+
id: deriveRunMessageId(run.id),
|
|
181
|
+
conversationId: run.conversationId,
|
|
182
|
+
runId: run.id,
|
|
183
|
+
role: "assistant",
|
|
184
|
+
parts: state.parts,
|
|
185
|
+
createdAt: clock(),
|
|
186
|
+
};
|
|
187
|
+
await deps.messages.append({ tenantId, message });
|
|
188
|
+
};
|
|
189
|
+
/** Finalize any tool calls started but never completed — as interrupted errors, never re-run. */
|
|
190
|
+
const finalizePending = async () => {
|
|
191
|
+
for (const pending of state.pendingToolCalls) {
|
|
192
|
+
const error = {
|
|
193
|
+
code: "cancelled",
|
|
194
|
+
message: `Tool call '${pending.toolName}' was interrupted before it completed`,
|
|
195
|
+
retryable: false,
|
|
196
|
+
};
|
|
197
|
+
const part = {
|
|
198
|
+
id: `${pending.toolCallId}:interrupted`,
|
|
199
|
+
type: "error",
|
|
200
|
+
schemaVersion: 1,
|
|
201
|
+
createdAt: clock(),
|
|
202
|
+
error,
|
|
203
|
+
};
|
|
204
|
+
await emit({ type: "tool.failed", toolCallId: pending.toolCallId, toolName: pending.toolName });
|
|
205
|
+
await emit({ type: "part.added", messageId: deriveRunMessageId(run.id), part });
|
|
206
|
+
}
|
|
207
|
+
await persist();
|
|
208
|
+
};
|
|
209
|
+
/** True once a keepalive reported the claim gone; the loop below turns this into a ClaimLostError. */
|
|
210
|
+
let claimLost = false;
|
|
211
|
+
const beat = async () => {
|
|
212
|
+
lastKeepalive = now();
|
|
213
|
+
const alive = await runs.keepalive({ tenantId, id: run.id, workerId, leaseMs, now: clock() });
|
|
214
|
+
if (!alive) {
|
|
215
|
+
claimLost = true;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const fresh = await runs.findById({ tenantId, id: run.id });
|
|
219
|
+
if (fresh?.cancelRequestedAt !== undefined)
|
|
220
|
+
cancelRequested = true;
|
|
221
|
+
};
|
|
222
|
+
const heartbeat = async () => {
|
|
223
|
+
if (claimLost)
|
|
224
|
+
throw new ClaimLostError();
|
|
225
|
+
if (now() - lastKeepalive < keepaliveEveryMs)
|
|
226
|
+
return;
|
|
227
|
+
await beat();
|
|
228
|
+
if (claimLost)
|
|
229
|
+
throw new ClaimLostError();
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* A timer-based heartbeat for the duration of the run, independent of engine events (#107 AC-5).
|
|
233
|
+
*
|
|
234
|
+
* `heartbeat()` above runs after each event, which keeps a tool-*heavy* run alive — many short
|
|
235
|
+
* tools, an event between each. It does nothing for a *single* long tool call: while the engine
|
|
236
|
+
* awaits one tool it yields nothing, so nothing calls keepalive, and a tool slower than the lease
|
|
237
|
+
* loses its claim. The run is then reaped and re-executed while the first call is still in flight,
|
|
238
|
+
* which for a slow external write is exactly the duplicate the lease exists to prevent.
|
|
239
|
+
*
|
|
240
|
+
* This is the one place #107 adds behaviour rather than wiring what exists. AC-5 cannot be
|
|
241
|
+
* satisfied by composition, because nothing outside the run's own loop knows the run is alive.
|
|
242
|
+
*
|
|
243
|
+
* A failed keepalive sets `claimLost` rather than throwing: an unhandled rejection from a timer
|
|
244
|
+
* would take the process down, and the loop below is where losing a claim is already handled.
|
|
245
|
+
*/
|
|
246
|
+
const startTimerHeartbeat = () => {
|
|
247
|
+
const timer = setInterval(() => {
|
|
248
|
+
if (claimLost)
|
|
249
|
+
return;
|
|
250
|
+
// Skipped when an event-driven beat happened recently, so a busy run does not double its
|
|
251
|
+
// keepalive traffic.
|
|
252
|
+
if (now() - lastKeepalive < keepaliveEveryMs)
|
|
253
|
+
return;
|
|
254
|
+
void beat().catch(() => {
|
|
255
|
+
// A failed round trip is not proof the claim is gone, so it is not treated as loss here;
|
|
256
|
+
// the next beat, or the event-driven one, will find out.
|
|
257
|
+
});
|
|
258
|
+
}, Math.max(1, keepaliveEveryMs));
|
|
259
|
+
timer.unref?.();
|
|
260
|
+
return () => clearInterval(timer);
|
|
261
|
+
};
|
|
262
|
+
// Recovery reconciliation (C1). `emit` makes an event durable in the log *before* the checkpoint
|
|
263
|
+
// is written, so after a crash the log can lead the checkpoint. Fold the events the log has beyond
|
|
264
|
+
// the checkpoint back into state, so: (a) new sequences continue past the true durable max instead
|
|
265
|
+
// of colliding, and (b) a tool.started that was logged but not yet checkpointed is still seen as
|
|
266
|
+
// pending — and therefore finalized below — so it is never silently re-run.
|
|
267
|
+
if (deps.eventLog) {
|
|
268
|
+
const missed = await deps.eventLog.listAfter({ tenantId, runId: run.id, after: state.sequence });
|
|
269
|
+
for (const event of missed)
|
|
270
|
+
state = reduceRunEvent(state, event);
|
|
271
|
+
if (missed.length > 0)
|
|
272
|
+
await persist();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Whether there is prior *work* to resume from — not merely a prior event (#170).
|
|
276
|
+
*
|
|
277
|
+
* This read `state.sequence > 0`, which was true exactly when the log held something. That worked while the
|
|
278
|
+
* only things in the log were the worker's own events, and became wrong the moment admission started
|
|
279
|
+
* emitting `run.queued`: every fresh run would arrive with sequence 1 and be handed a "resume checkpoint"
|
|
280
|
+
* containing nothing but the fact that it was queued.
|
|
281
|
+
*
|
|
282
|
+
* Parts and pending tool calls are the honest predicate, because they are what a resume *uses*: the engine
|
|
283
|
+
* replays from the checkpoint's parts and finalizes its dangling calls. A run that was claimed and died
|
|
284
|
+
* before producing either has nothing to resume from, and passing null says so.
|
|
285
|
+
*/
|
|
286
|
+
const recovered = state.parts.length > 0 || state.pendingToolCalls.length > 0;
|
|
287
|
+
// A re-claimed run may carry dangling tool calls (from the checkpoint or the reconciled log).
|
|
288
|
+
// Finalize them once, before the engine resumes, so it observes them as failed and never re-fires.
|
|
289
|
+
if (state.pendingToolCalls.length > 0)
|
|
290
|
+
await finalizePending();
|
|
291
|
+
// Started before the engine runs and stopped in the `finally` below, so the lease is held for the
|
|
292
|
+
// whole of the run rather than only across event boundaries.
|
|
293
|
+
const stopTimerHeartbeat = startTimerHeartbeat();
|
|
294
|
+
try {
|
|
295
|
+
const started = await runs.transition({ tenantId, id: run.id, workerId, to: "running", now: clock() });
|
|
296
|
+
await emit({ type: "run.started" });
|
|
297
|
+
await persist();
|
|
298
|
+
const signal = { isCancelled: () => cancelRequested };
|
|
299
|
+
const iterable = engine.run({ run: started, context, resume: recovered ? toCheckpoint() : null, signal });
|
|
300
|
+
// A run pauses (rather than completes) when the engine requests a question/approval and ends.
|
|
301
|
+
let pause = null;
|
|
302
|
+
for await (const body of iterable) {
|
|
303
|
+
await emit(body);
|
|
304
|
+
await persist(); // durable before the engine proceeds (tool.started) / between retry attempts
|
|
305
|
+
/**
|
|
306
|
+
* `run.checkpointed`, at tool boundaries — #170.
|
|
307
|
+
*
|
|
308
|
+
* The event type was specified in `docs/04` and in `RUN_EVENT_TYPES`, and nothing emitted it. What
|
|
309
|
+
* "checkpointed" should *mean* was the open question, since `persist()` runs on every engine event and an
|
|
310
|
+
* event per text delta would double the stream to say nothing.
|
|
311
|
+
*
|
|
312
|
+
* Tool boundaries, because that is what the checkpoint above exists for — its own comment says
|
|
313
|
+
* "durable before the engine proceeds (tool.started)". A crash mid-tool is the case where durability is
|
|
314
|
+
* load-bearing, so "your progress through this tool is saved" is the signal a client can act on. The
|
|
315
|
+
* cadence is a judgement; the alternatives were one per event (noise) or none (what we had).
|
|
316
|
+
*/
|
|
317
|
+
if (body.type === "tool.started" || body.type === "tool.completed" || body.type === "tool.failed") {
|
|
318
|
+
// The event carries its own sequence, stamped by `emit` — no payload needed.
|
|
319
|
+
await emit({ type: "run.checkpointed" });
|
|
320
|
+
}
|
|
321
|
+
if (body.type === "question.requested")
|
|
322
|
+
pause = "waiting-for-question";
|
|
323
|
+
else if (body.type === "approval.requested")
|
|
324
|
+
pause = "waiting-for-approval";
|
|
325
|
+
else if (body.type === "question.answered" || body.type === "approval.decided")
|
|
326
|
+
pause = null;
|
|
327
|
+
await heartbeat(); // throttled; runs on every event so a tool-heavy run keeps its lease alive
|
|
328
|
+
if (cancelRequested)
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
if (cancelRequested) {
|
|
332
|
+
await finalizePending();
|
|
333
|
+
const cancelled = await runs.transition({ tenantId, id: run.id, workerId, to: "cancelled", now: clock() });
|
|
334
|
+
await emit({ type: "run.cancelled" });
|
|
335
|
+
await persist();
|
|
336
|
+
await persistAssistantTurn("cancelled");
|
|
337
|
+
await notifySettled(cancelled, "cancelled");
|
|
338
|
+
return { run: cancelled, outcome: "cancelled" };
|
|
339
|
+
}
|
|
340
|
+
if (pause) {
|
|
341
|
+
// Paused for human input: persist and release the claim so a continuation can re-claim it.
|
|
342
|
+
// Pending tool calls are NOT finalized — nothing was interrupted; the run is waiting.
|
|
343
|
+
await persist();
|
|
344
|
+
const paused = await runs.transition({ tenantId, id: run.id, workerId, to: pause, now: clock() });
|
|
345
|
+
return { run: paused, outcome: "paused" };
|
|
346
|
+
}
|
|
347
|
+
await finalizePending();
|
|
348
|
+
const completed = await runs.transition({ tenantId, id: run.id, workerId, to: "completed", now: clock() });
|
|
349
|
+
await emit({ type: "run.completed" });
|
|
350
|
+
await persist();
|
|
351
|
+
await persistAssistantTurn("completed");
|
|
352
|
+
await notifySettled(completed, "completed");
|
|
353
|
+
return { run: completed, outcome: "completed" };
|
|
354
|
+
}
|
|
355
|
+
catch (thrown) {
|
|
356
|
+
if (thrown instanceof ClaimLostError) {
|
|
357
|
+
return { run: await runs.findById({ tenantId, id: run.id }), outcome: "lost" };
|
|
358
|
+
}
|
|
359
|
+
const error = toPlatformError(thrown);
|
|
360
|
+
await finalizePending();
|
|
361
|
+
const failed = await runs.transition({ tenantId, id: run.id, workerId, to: "failed", now: clock(), error });
|
|
362
|
+
await emit({ type: "run.failed", error });
|
|
363
|
+
await persist();
|
|
364
|
+
await persistAssistantTurn("failed");
|
|
365
|
+
await notifySettled(failed, "failed");
|
|
366
|
+
return { run: failed, outcome: "failed" };
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
// Every exit path: completed, paused, failed, claim lost, or a throw from the engine. A timer
|
|
370
|
+
// left running would keep renewing the lease on a run this worker is no longer driving.
|
|
371
|
+
stopTimerHeartbeat();
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
return {
|
|
375
|
+
/** Claim (under an optional lock) and drive a run. Idempotent: a claimed/terminal run is skipped. */
|
|
376
|
+
async process(input) {
|
|
377
|
+
const { tenantId, runId } = input;
|
|
378
|
+
// Best-effort mutual exclusion. The authoritative guard is the RunStore lease (kept alive by
|
|
379
|
+
// heartbeat); this lock is not renewed, so on a run longer than leaseMs it simply expires —
|
|
380
|
+
// a harmless degradation, since claim/keepalive still prevent two workers driving one run.
|
|
381
|
+
const lock = deps.locks ? await deps.locks.acquire(`run:${runId}`, leaseMs) : { released: async () => { } };
|
|
382
|
+
if (!lock)
|
|
383
|
+
return { run: await runs.findById({ tenantId, id: runId }), outcome: "skipped" };
|
|
384
|
+
try {
|
|
385
|
+
const claimed = await runs.claim({ tenantId, id: runId, workerId, leaseMs, now: clock() });
|
|
386
|
+
if (!claimed)
|
|
387
|
+
return { run: await runs.findById({ tenantId, id: runId }), outcome: "skipped" };
|
|
388
|
+
return await drive(claimed);
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
await lock.released();
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
/**
|
|
395
|
+
* Recover runs whose lease expired (crashed workers). Returns the reclaimed candidates; a caller
|
|
396
|
+
* re-enqueues each via its `JobDispatcher`. Kept separate so recovery cadence is the host's call.
|
|
397
|
+
*/
|
|
398
|
+
async reapExpired(limit = 20) {
|
|
399
|
+
return runs.reapExpired({ now: clock(), limit });
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
/** Re-export for adapters that construct terminal errors from thrown values. */
|
|
404
|
+
export { AgentPlatformError };
|
|
405
|
+
//# sourceMappingURL=worker.js.map
|