diff --git a/.changeset/agent-parity.md b/.changeset/agent-parity.md deleted file mode 100644 index 339bbaf..0000000 --- a/.changeset/agent-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Agents gain structured output through a `submit_output` tool, one compaction-and-retry when the model reports a context overflow, `resolveTools` for tools that depend on the agent input, an `assertCredits` gate before each model call, and `trackUsage` beside a new `agent.turn.usage` event. Team members spawned with `asAgentTool` stop at `TEAM_MEMBER_MAX_DEPTH` (3); the parent sends the child its depth with a new `agent.lineage` event. diff --git a/.changeset/blob-offload.md b/.changeset/blob-offload.md deleted file mode 100644 index 8a46da6..0000000 --- a/.changeset/blob-offload.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@swirls/looms': minor ---- - -`createLooms({ blobStore })` and `LoomsDurableObject` `blobs` store strings over 64 KiB by SHA-256 and keep a reference in the event or snapshot. A payload that is still over the 1 MiB event limit is stored as one blob. Reads, subscribers, folds, projections, and snapshot loads resolve the reference, so a run can complete with a payload larger than that limit. Trim removes a blob body once nothing retained still points at it. `memoryBlobStore`, `filesystemBlobStore`, and `r2BlobStore` are the included stores. Without a blob store, events over 1 MiB and snapshots over 8 MiB are still rejected. diff --git a/.changeset/child-runs.md b/.changeset/child-runs.md deleted file mode 100644 index 3be0a62..0000000 --- a/.changeset/child-runs.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@swirls/looms': minor ---- - -feat(runtime)!: launch workflow children as their own runs - -A workflow can set `childExecution: 'run'` so spawn, map, fanout, and loop start a child run instead of a thread. The child run id is stable across retries. When the child finishes it signals `runtime.child.settled`, which completes the parent's wait. `childClose` is `terminate` or `wait`. The default remains in-process threads. - -BREAKING CHANGE: `RunLauncher` implementations must provide `signal` and `cancel`. diff --git a/.changeset/definition-resolver.md b/.changeset/definition-resolver.md deleted file mode 100644 index 9314e40..0000000 --- a/.changeset/definition-resolver.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Definitions can be loaded at start, spawn, and recovery through `definitionResolver`, with registered definitions checked first and an LRU cache in front. A missing definition fails the start before any event is written, and the accepted version stays pinned on the run. Evaluator definitions now include `version`, defaulting to `v1`, and lookup takes the name and version. - -BREAKING CHANGE: `EvaluatorDefinition` requires `version`, and evaluator lookup is `(name, version)` instead of `(name)`. `defineEvaluator` still defaults the version to `v1`. diff --git a/.changeset/effect-reliability.md b/.changeset/effect-reliability.md deleted file mode 100644 index 5e278e9..0000000 --- a/.changeset/effect-reliability.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Worker callbacks now answer `{ status: 'accepted' | 'stale' | 'cancelled', cancelRequested }` instead of the run state. `createLooms().workerCallback` and `createLoomsClient().workerCallback` return that result. `cancelRequested` tells an attempt that is already running to stop, because a dispatch that has left the host cannot be recalled. A stale report records nothing. - -`failEffect({ error, nonRetryable, retryAfterMs })` fails the current attempt. `nonRetryable` ends the effect even when the retry policy has attempts left. `retryAfterMs` waits at least that long before the next attempt and never shortens the policy backoff. Worker `fail` callbacks take the same two fields. - -A heartbeat may include `details`. The latest details are kept across retries and included on the next `EffectWorkerTask`, so a resumed attempt can continue from the recorded progress. diff --git a/.changeset/hibernating-websockets.md b/.changeset/hibernating-websockets.md deleted file mode 100644 index 152ff72..0000000 --- a/.changeset/hibernating-websockets.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Durable Objects accept a hibernating WebSocket on `GET /runs/:id/events`. The socket replays the retained log, then receives later events without a subscription held in memory. SSE sends the same `hello` and `resync` frames. A cursor below the retained head is told to resume from `head` instead of silently skipping trimmed events. `subscribeEvents` and the React store take `transport: "websocket" | "sse" | "auto"`; the default remains SSE, and a numeric reconnect delay still works. diff --git a/.changeset/launch-provenance.md b/.changeset/launch-provenance.md deleted file mode 100644 index 07a7449..0000000 --- a/.changeset/launch-provenance.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@swirls/looms': minor ---- - -A run started from an effect through `RunLauncher` now records its source on `runtime.run.started`. The payload's `launchedBy` holds the launching run, thread, and effect, plus the id and seq of the event that requested the effect. The event's origin is `{ type: 'run', runId }`, and its `correlationId` names the root run of the launch chain. The runtime sets these fields itself and replaces any value a handler passes. `RunState.startIdentity` exposes both fields, and a later start of the same run id that does not match them rejects with `StartRunConflictError`. - -The launching run's log gets a new `runtime.run.launched` event for each start the target accepts. It carries the started run's id and definition plus the effect id, so listing what a run launched reads that run's log alone. - -The index projectors store `launchedByRunId`, `launchedByEffectId`, and `correlationId` on run rows, and `listLaunched(runId)` returns the runs a run launched. Their version is now 3, so delivery replays history to fill the new columns, and `init` adds the columns to existing SQLite and Postgres tables. - -`useRunSummary` returns `launchedBy`. The debugger shows a Launched by link above the thread tree that opens the launching run at the requesting event, the event inspector links `runtime.run.launched` and `runtime.run.started` to the run on the other side, and `openRun` accepts a `{ seq }` target. diff --git a/.changeset/otel-trace.md b/.changeset/otel-trace.md deleted file mode 100644 index 3580c53..0000000 --- a/.changeset/otel-trace.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Runs carry the caller's W3C trace context. `POST /runs` reads the `traceparent` and `tracestate` headers, and `start`, `startRun`, and `client.startRun` accept `trace`. A valid context is recorded on `runtime.run.started` and `startIdentity.trace`, then passed unchanged to effect handlers (`ctx.trace`), worker tasks (`task.trace`), and every launched or child run unless the launch sets its own. diff --git a/.changeset/postgres-event-log.md b/.changeset/postgres-event-log.md deleted file mode 100644 index 8261e3d..0000000 --- a/.changeset/postgres-event-log.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@swirls/looms': minor ---- - -feat(projectors): project runs into a Postgres event log without blocking append - -Durable Object projectors are scheduled with `waitUntil` after the log commits. `postgresEventLog` stores each non-ephemeral envelope and a run index with status, launch links, and tenant attributes, and can rebuild that sink from the log. diff --git a/.changeset/queue-worker.md b/.changeset/queue-worker.md deleted file mode 100644 index d3b2f78..0000000 --- a/.changeset/queue-worker.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@swirls/looms': minor ---- - -feat(cloudflare): run worker effects over Cloudflare Queues - -`cloudflareQueueWorker` sends each attempt to a queue as JSON, with the run's trace context and the attempt's deadlines; inputs over 100 KB travel as a blob reference and secrets stay off the queue. `queueConsumer` (push) and `createQueuePullConsumer` with `cloudflareQueuePullSource` (HTTP pull) run handlers that heartbeat, read secrets through the worker callback API, and stop when the run is cancelled. The consumer acks after `started`, so Looms owns retries; duplicate and stale deliveries are dropped, and dead-lettered messages are recovered by the schedule-to-start timeout. The pull consumer batches acks, backs off when idle, reports the backlog, and drains on `stop()`. `createMemoryQueue` gives tests the same lease and dead-letter behavior. - -In the runtime, `EffectWorker.dispatchSemantics: 'acknowledged'` fails a rejected dispatch under the retry policy instead of parking it as ambiguous, and creating a runtime fails if such a worker claims an effect without `retry.scheduleToStartTimeoutMs`. Worker tasks carry `deadlines`, and `maxInFlightWorkerEffects` caps the worker attempts one run has in flight. diff --git a/.changeset/run-context.md b/.changeset/run-context.md deleted file mode 100644 index d38fe7a..0000000 --- a/.changeset/run-context.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Runs accept immutable `attributes` and a `secrets` context. Attributes are recorded on `runtime.run.started`, the run state, and the run summary, and are passed to `authorize` when present. Secrets stay in a side store: they are available to effect handlers and in-process worker dispatch, and they are not written to events, snapshots, or run responses. A worker whose transport persists tasks reads them with `POST /runs/:runId/effects/:effectId/:attempt/secrets` (`client.workerSecrets`), which only answers the attempt that is currently running. diff --git a/.changeset/run-grants.md b/.changeset/run-grants.md deleted file mode 100644 index 4e1f007..0000000 --- a/.changeset/run-grants.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@swirls/looms': minor ---- - -feat(runtime): authorize browser event streams with a signed run grant - -`grantAuth` checks a short-lived HMAC grant from the `grant` query parameter or a `v1.` WebSocket subprotocol. The grant allows one principal to read one run, which is the credential a browser can send on EventSource or WebSocket. The Durable Object socket echoes a subprotocol grant on the upgrade, and `grantSubprotocol` gives other hosts the value to echo. diff --git a/.changeset/schedule-module.md b/.changeset/schedule-module.md deleted file mode 100644 index 6997ca0..0000000 --- a/.changeset/schedule-module.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@swirls/looms': minor ---- - -Add a schedule module so a cron, interval, or one-shot time can start a run, and so a deploy can update, pause, or retire that schedule without a new run id. diff --git a/.changeset/upgrade-safety.md b/.changeset/upgrade-safety.md deleted file mode 100644 index b96c11d..0000000 --- a/.changeset/upgrade-safety.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@swirls/looms': minor ---- - -feat(testing): replay recorded run logs in CI and document effect contracts - -Adds a production-shaped event-log corpus under `packages/testing/corpus`, folded -deterministically in CI with `assertCorpusEntry`. Documents versioning effect -types (`payments.charge@v2`) and worker `handles` so fleets can roll out beside -each other without Temporal-style patches. diff --git a/packages/looms/CHANGELOG.md b/packages/looms/CHANGELOG.md index 5035d4a..ef79efb 100644 --- a/packages/looms/CHANGELOG.md +++ b/packages/looms/CHANGELOG.md @@ -1,5 +1,60 @@ # @swirls/looms +## 0.5.0 + +### Minor Changes + +- edd3c5c: Agents gain structured output through a `submit_output` tool, one compaction-and-retry when the model reports a context overflow, `resolveTools` for tools that depend on the agent input, an `assertCredits` gate before each model call, and `trackUsage` beside a new `agent.turn.usage` event. Team members spawned with `asAgentTool` stop at `TEAM_MEMBER_MAX_DEPTH` (3); the parent sends the child its depth with a new `agent.lineage` event. +- 0ac83ab: `createLooms({ blobStore })` and `LoomsDurableObject` `blobs` store strings over 64 KiB by SHA-256 and keep a reference in the event or snapshot. A payload that is still over the 1 MiB event limit is stored as one blob. Reads, subscribers, folds, projections, and snapshot loads resolve the reference, so a run can complete with a payload larger than that limit. Trim removes a blob body once nothing retained still points at it. `memoryBlobStore`, `filesystemBlobStore`, and `r2BlobStore` are the included stores. Without a blob store, events over 1 MiB and snapshots over 8 MiB are still rejected. +- 0ac83ab: feat(runtime)!: launch workflow children as their own runs + + A workflow can set `childExecution: 'run'` so spawn, map, fanout, and loop start a child run instead of a thread. The child run id is stable across retries. When the child finishes it signals `runtime.child.settled`, which completes the parent's wait. `childClose` is `terminate` or `wait`. The default remains in-process threads. + + BREAKING CHANGE: `RunLauncher` implementations must provide `signal` and `cancel`. + +- 0ac83ab: Definitions can be loaded at start, spawn, and recovery through `definitionResolver`, with registered definitions checked first and an LRU cache in front. A missing definition fails the start before any event is written, and the accepted version stays pinned on the run. Evaluator definitions now include `version`, defaulting to `v1`, and lookup takes the name and version. + + BREAKING CHANGE: `EvaluatorDefinition` requires `version`, and evaluator lookup is `(name, version)` instead of `(name)`. `defineEvaluator` still defaults the version to `v1`. + +- 2436516: Worker callbacks now answer `{ status: 'accepted' | 'stale' | 'cancelled', cancelRequested }` instead of the run state. `createLooms().workerCallback` and `createLoomsClient().workerCallback` return that result. `cancelRequested` tells an attempt that is already running to stop, because a dispatch that has left the host cannot be recalled. A stale report records nothing. + + `failEffect({ error, nonRetryable, retryAfterMs })` fails the current attempt. `nonRetryable` ends the effect even when the retry policy has attempts left. `retryAfterMs` waits at least that long before the next attempt and never shortens the policy backoff. Worker `fail` callbacks take the same two fields. + + A heartbeat may include `details`. The latest details are kept across retries and included on the next `EffectWorkerTask`, so a resumed attempt can continue from the recorded progress. + +- 0ac83ab: Durable Objects accept a hibernating WebSocket on `GET /runs/:id/events`. The socket replays the retained log, then receives later events without a subscription held in memory. SSE sends the same `hello` and `resync` frames. A cursor below the retained head is told to resume from `head` instead of silently skipping trimmed events. `subscribeEvents` and the React store take `transport: "websocket" | "sse" | "auto"`; the default remains SSE, and a numeric reconnect delay still works. +- c01d046: A run started from an effect through `RunLauncher` now records its source on `runtime.run.started`. The payload's `launchedBy` holds the launching run, thread, and effect, plus the id and seq of the event that requested the effect. The event's origin is `{ type: 'run', runId }`, and its `correlationId` names the root run of the launch chain. The runtime sets these fields itself and replaces any value a handler passes. `RunState.startIdentity` exposes both fields, and a later start of the same run id that does not match them rejects with `StartRunConflictError`. + + The launching run's log gets a new `runtime.run.launched` event for each start the target accepts. It carries the started run's id and definition plus the effect id, so listing what a run launched reads that run's log alone. + + The index projectors store `launchedByRunId`, `launchedByEffectId`, and `correlationId` on run rows, and `listLaunched(runId)` returns the runs a run launched. Their version is now 3, so delivery replays history to fill the new columns, and `init` adds the columns to existing SQLite and Postgres tables. + + `useRunSummary` returns `launchedBy`. The debugger shows a Launched by link above the thread tree that opens the launching run at the requesting event, the event inspector links `runtime.run.launched` and `runtime.run.started` to the run on the other side, and `openRun` accepts a `{ seq }` target. + +- 0ac83ab: Runs carry the caller's W3C trace context. `POST /runs` reads the `traceparent` and `tracestate` headers, and `start`, `startRun`, and `client.startRun` accept `trace`. A valid context is recorded on `runtime.run.started` and `startIdentity.trace`, then passed unchanged to effect handlers (`ctx.trace`), worker tasks (`task.trace`), and every launched or child run unless the launch sets its own. +- a1fa2c3: feat(projectors): project runs into a Postgres event log without blocking append + + Durable Object projectors are scheduled with `waitUntil` after the log commits. `postgresEventLog` stores each non-ephemeral envelope and a run index with status, launch links, and tenant attributes, and can rebuild that sink from the log. + +- 0ac83ab: feat(cloudflare): run worker effects over Cloudflare Queues + + `cloudflareQueueWorker` sends each attempt to a queue as JSON, with the run's trace context and the attempt's deadlines; inputs over 100 KB travel as a blob reference and secrets stay off the queue. `queueConsumer` (push) and `createQueuePullConsumer` with `cloudflareQueuePullSource` (HTTP pull) run handlers that heartbeat, read secrets through the worker callback API, and stop when the run is cancelled. The consumer acks after `started`, so Looms owns retries; duplicate and stale deliveries are dropped, and dead-lettered messages are recovered by the schedule-to-start timeout. The pull consumer batches acks, backs off when idle, reports the backlog, and drains on `stop()`. `createMemoryQueue` gives tests the same lease and dead-letter behavior. + + In the runtime, `EffectWorker.dispatchSemantics: 'acknowledged'` fails a rejected dispatch under the retry policy instead of parking it as ambiguous, and creating a runtime fails if such a worker claims an effect without `retry.scheduleToStartTimeoutMs`. Worker tasks carry `deadlines`, and `maxInFlightWorkerEffects` caps the worker attempts one run has in flight. + +- 0ac83ab: Runs accept immutable `attributes` and a `secrets` context. Attributes are recorded on `runtime.run.started`, the run state, and the run summary, and are passed to `authorize` when present. Secrets stay in a side store: they are available to effect handlers and in-process worker dispatch, and they are not written to events, snapshots, or run responses. A worker whose transport persists tasks reads them with `POST /runs/:runId/effects/:effectId/:attempt/secrets` (`client.workerSecrets`), which only answers the attempt that is currently running. +- 0ac83ab: feat(runtime): authorize browser event streams with a signed run grant + + `grantAuth` checks a short-lived HMAC grant from the `grant` query parameter or a `v1.` WebSocket subprotocol. The grant allows one principal to read one run, which is the credential a browser can send on EventSource or WebSocket. The Durable Object socket echoes a subprotocol grant on the upgrade, and `grantSubprotocol` gives other hosts the value to echo. + +- c01d046: Add a schedule module so a cron, interval, or one-shot time can start a run, and so a deploy can update, pause, or retire that schedule without a new run id. +- 0ac83ab: feat(testing): replay recorded run logs in CI and document effect contracts + + Adds a production-shaped event-log corpus under `packages/testing/corpus`, folded + deterministically in CI with `assertCorpusEntry`. Documents versioning effect + types (`payments.charge@v2`) and worker `handles` so fleets can roll out beside + each other without Temporal-style patches. + ## 0.4.0 ### Minor Changes diff --git a/packages/looms/package.json b/packages/looms/package.json index 1591fab..b7db917 100644 --- a/packages/looms/package.json +++ b/packages/looms/package.json @@ -1,6 +1,6 @@ { "name": "@swirls/looms", - "version": "0.4.0", + "version": "0.5.0", "description": "Pluggable event-sourced runtime for durable agents and workflows", "homepage": "https://github.com/ByteSliceHQ/looms#readme", "bugs": {