diff --git a/AGENTS.md b/AGENTS.md index d05b786..431e8f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Corollary — **extend, never duplicate.** Before writing anything that complete 3. **Structural over hard-dep where possible.** `/tangle` and `/billing` take the tcloud client as a structural contract (no tcloud dep). Prefer that to a dep when the surface is small. 4. **Substrate-free is a feature.** `/runtime`, `/web`, `/crypto`, `/redact` import nothing — they're pure mechanism behind callback seams. Keep them that way. 5. **Additive subpaths.** New capability = new `./subpath` (entry in `tsup.config.ts` + `exports` in `package.json` + root barrel). Never a breaking change to an existing export. -6. **Reuse the engine primitive; never hand-roll one the engine already gives you.** Before adding ANY streaming / turn / session / durability primitive, grep the engines (`@tangle-network/sandbox`, `agent-runtime`, `agent-eval`) AND existing agent-app exports for one that already does it. A new primitive requires a written "why every existing one doesn't fit" — no exceptions. The recurring smell is the shell reinventing an engine capability and paying latency + drift for it. **Canonical case: streaming a sandbox agent run to a browser is the sandbox SDK's read-only JWT session gateway (`streamPrompt` + `lastEventId` replay) — durable replay + reconnect, browser-direct, lowest latency.** Do NOT hand-roll a Durable Object or the `/stream` turn-buffer to re-broadcast a sandbox session's turn events; the `/stream` turn-buffer + `/turn-stream` DO are the **sandbox-FREE path ONLY** (a copilot streaming its own runtime, where no sandbox session exists to attach to). Using them for a sandbox product is the smell (a caught instance: a fleet app streaming a sandbox turn through a hand-rolled `TurnStreamDO` instead of the gateway). +6. **Reuse the engine primitive; never hand-roll one the engine already gives you.** Before adding ANY streaming / turn / session / durability primitive, grep the engines (`@tangle-network/sandbox`, `agent-runtime`, `agent-eval`) AND existing agent-app exports for one that already does it. A new primitive requires a written "why every existing one doesn't fit" — no exceptions. The recurring smell is the shell reinventing an engine capability and paying latency + drift for it. **Canonical case: streaming a sandbox agent run to a browser is the sandbox SDK's session gateway — `box.mintScopedToken()` (`@tangle-network/sandbox`) mints the short-lived read-only JWT, and the browser connects with `SessionGatewayClient` (`@tangle-network/sandbox/session-gateway`, WebSocket, replay from `lastEventId`) — durable replay + reconnect, browser-direct, lowest latency.** The server-side leg is a *different* method and belongs to the same package: `box.streamPrompt()` is an SSE `AsyncGenerator` a worker consumes, and its `lastEventId` / `executionId` options are forwarded as `Last-Event-ID` / `X-Execution-ID` headers so the worker can reconnect. A browser never calls `streamPrompt`. Do NOT hand-roll a Durable Object or the `/stream` turn-buffer to re-broadcast a sandbox session's turn events; the `/stream` turn-buffer + `/turn-stream` DO are the **sandbox-FREE path ONLY** (a copilot streaming its own runtime, where no sandbox session exists to attach to). Using them for a sandbox product is the smell (a caught instance: a fleet app streaming a sandbox turn through a hand-rolled `TurnStreamDO` instead of the gateway). ## Module map @@ -26,7 +26,7 @@ Corollary — **extend, never duplicate.** Before writing anything that complete | Subpath | Owns (app-shell) | Composes (peer/structural) | |---|---|---| | `/tools` | structured agent→app side channel (`submit_proposal`/`schedule_followup`/`render_ui`/`add_citation`): OpenAI defs, MCP-server builder (`buildHttpMcpServer`/`buildAppToolMcpServer`), HTTP route handler, runtime executor, capability auth, `ToolInputError` | — | -| `/runtime` | `streamAppToolLoop`/`runAppToolLoop` (bounded turn tool-loop) + `resolveTangleModelConfig` + `toLoopEvents`/`createOpenAICompatStreamTurn` (sandbox-free browser/edge copilot adapter — OpenAI-compat stream → LoopEvents, fragmented tool-calls assembled) | ⚠️ turn-loop is a contribute-down candidate to agent-runtime; the OpenAI-compat adapter only maps stream shape (no HTTP client — see `examples/browser-copilot.md`) | +| `/runtime` | `streamAppToolLoop`/`runAppToolLoop` (bounded in-turn tool loop, **1:1 aliases** — see the collision note below) + `resolveTangleModelConfig` + `toLoopEvents`/`createOpenAICompatStreamTurn` (sandbox-free browser/edge copilot adapter — OpenAI-compat stream → LoopEvents, fragmented tool-calls assembled) | peer `@tangle-network/agent-runtime` — the contribute-down already happened: `src/runtime/loop.ts` is `export { runToolLoop as runAppToolLoop, streamToolLoop as streamAppToolLoop }`, so agent-app holds **zero** loop logic. The OpenAI-compat adapter only maps stream shape (no HTTP client — see `examples/browser-copilot.md`) | | `/eval` | `producedFromToolEvents` (side-channel→`RuntimeEventLike` bridge) + `createTokenRecallChecker` | **re-exports** agent-eval's `verifyCompletion`/`extractProducedState`/`weightedComposite`/`createLlmCorrectnessChecker` | | `/integrations` | hub `/exec` client + `resolveIntegrationAction` + `invokeIntegrationHub` (wiring) | peer-dep `@tangle-network/agent-integrations` (the engine/catalog) | | `/interactions` | human-in-the-loop ask channel, both halves: the shared wire/persisted-part contract (`ChatInteraction`, part codecs, composer-as-answer routing, content-signature dedupe) + the server side — structural sidecar `/agents/sessions/{id}/interactions` client and `createInteractionAnswerRoute()` (list/answer endpoint factory: body validation, gone→410 mapping, duplicate-answer safety net, unblock verification, `resolveConnection` product seam) | peer `@tangle-network/agent-interface` (schema types only); connection is structural — no sandbox-SDK import. Server half must never reach client bundles (`tests/interactions-browser-safe.test.ts`) | @@ -52,7 +52,7 @@ The sandbox runs full agent harnesses — skills, tools, sub-agents, MCP, bash, 1. **Intelligence and tooling live in the agent; durability and money live in the platform.** Reasoning, tool selection, installation, evidence gathering, content production → a prompt to an agent session. Surviving restarts, gating spend, pausing for approval → platform code (product or this shell). 2. **Prompts state intents, never implementations.** No shell commands, CLI flags, or install scripts inside system prompts, plan steps, or directives. Name the outcome and the evidence path; the executing agent chooses tools at execution time. 3. **No domain logic in execution infrastructure.** Engines, dispatchers, and schedulers must not pattern-match intents or embed per-vertical scripts. Vertical knowledge belongs in prompt directives and product content, the layers the agent reads. (Shell corollary of the engine/shell rule: domain is a parameter, never baked.) -4. **Don't rebuild harness or platform primitives.** The sandbox SDK already provides durable *session* execution: `dispatchPrompt({ detach: true })` runs the turn server-side after the caller disconnects, `findCompletedTurn(turnId)` is the idempotent completion check, `_sessionStatus`/`_sessionResult` poll lifecycle, and the session gateway mints read-only JWTs so browsers attach to live streams without the product worker. Autonomous/queue work must dispatch detached and poll — never hold an SSE stream open in a worker to learn that a session finished. What the SDK does NOT provide is multi-step *orchestration* (sequencing, gates, budgets, schedules) — that is the legitimate product/shell layer. +4. **Don't rebuild harness or platform primitives.** Every primitive named here belongs to `@tangle-network/sandbox` — the SDK already provides durable *session* execution. `box.driveTurn(message, opts)` (SDK ≥ 0.10.5) is the one an autonomous driver should tick: exactly one settle → poll → dispatch pass, returning `running` / `completed` / `failed`, never looping, never sleeping, never holding a connection open past the pass — agent-app wraps it as `driveSandboxTurn` (`/sandbox`). Underneath it: `box.dispatchPrompt({ detach: true })` runs the turn server-side after the caller disconnects; `box.findCompletedTurn(turnId, { sessionId })` is the idempotent completion check (`sessionId` is **required**); `box.session(id).status()` / `.result()` poll lifecycle — `_sessionStatus` / `_sessionResult` are `@internal` SDK plumbing, never call them from a product; `box.mintScopedToken()` + `SessionGatewayClient` let a browser attach to a live session with no product worker in the path. Autonomous/queue work must drive-or-dispatch detached and tick — never hold an SSE stream open in a worker to learn that a session finished. What the SDK does NOT provide is multi-step *orchestration* (sequencing, gates, budgets, schedules) — that is the legitimate product/shell layer. 5. **No text-block data channels.** Agent writes (proposals, tasks, records, plans) go through schema-validated tools that fail loud back to the model — never through `:::block` text conventions scraped from output after the fact (regex parsing drops malformed data silently). `:::` blocks may exist only as SYSTEM-authored render vocabulary: the platform writes them into persisted messages as UI card anchors; no prompt teaches an agent to author one. (The canonical pattern is tool + fail-loud validation + byte-compatible rows + system-side anchor.) 6. **Gate actions, not mechanics.** Approvals attach to what an action does (spend, publish, vault writes) classified from intent — not to literal commands. @@ -60,14 +60,51 @@ The sandbox runs full agent harnesses — skills, tools, sub-agents, MCP, bash, Three callers, three transports — picking wrong is how durability bugs and overbuilt workers happen: +Every primitive below is written `package → symbol`, because three packages ship similarly-named turn functions. + | Caller | Transport | Why | | --- | --- | --- | -| **Interactive product turn** (chat, copilot) | `streamPrompt` held open for the turn; session-gateway read JWT for the browser to attach directly | A user is watching; worker lifetime ≈ turn length. The gateway replays buffered events on reconnect, so a dropped tab or worker restart loses nothing. | -| **Autonomous product work** (missions, queues, crons, scheduled jobs) | `dispatchPrompt({ detach: true })` + poll (`findCompletedTurn` / `_sessionStatus`) from a durable driver (CF Workflows, DO alarm, queue consumer) | No consumer exists and workers die in minutes. The platform executes the turn server-side; deterministic session/turn ids make crash re-dispatch a lookup, not a second agent run. Never hold an SSE stream open in a worker to learn that a session finished. | -| **Eval agent** (agent-eval loops, self-improve, CI) | `streamPrompt` / `runLoop` in a long-lived process | The harness IS the consumer and outlives the run; durability machinery adds nothing — reproducibility comes from scenarios and seeds, and a failed run is re-run, not resumed. | +| **Interactive product turn** (chat, copilot) | sandbox → `box.streamPrompt()` held open for the turn (agent-app wraps it as `streamSandboxPrompt`, `/sandbox`); for the browser leg, sandbox → `box.mintScopedToken()` + `SessionGatewayClient` (`@tangle-network/sandbox/session-gateway`) so the tab attaches directly | A user is watching; worker lifetime ≈ turn length. The gateway replays buffered events on reconnect, so a dropped tab or worker restart loses nothing. | +| **Autonomous product work** (missions, queues, crons, scheduled jobs) | sandbox → `box.driveTurn()` (agent-app wraps it as `driveSandboxTurn`, `/sandbox`) ticked from a durable driver (CF Workflows step, DO alarm, queue consumer). Drop to raw `box.dispatchPrompt({ detach: true })` + `box.findCompletedTurn(turnId, { sessionId })` / `box.session(id).status()` only when one pass is too coarse | No consumer exists and workers die in minutes. The platform executes the turn server-side; deterministic session/turn ids make crash re-dispatch a lookup, not a second agent run. Never hold an SSE stream open in a worker to learn that a session finished. | +| **Eval agent** (agent-eval loops, self-improve, CI) | For a sandboxed harness: sandbox → `box.streamPrompt()`. For an in-process model turn: agent-runtime (root) → `runToolLoop` / `streamToolLoop` (same functions as agent-app `/runtime`'s `runAppToolLoop` / `streamAppToolLoop`). Only for a multi-round, multi-candidate search: agent-runtime → `runLoop` from the **`/loops`** subpath — a different kernel, not a turn loop | The harness IS the consumer and outlives the run; durability machinery adds nothing — reproducibility comes from scenarios and seeds, and a failed run is re-run, not resumed. | `agent-runtime` stays durability-free on purpose: it must run identically in a local eval process, CI, and a sandbox. Durable *session* execution is the sandbox platform's job; durable *orchestration* (sequencing, gates, budgets, schedules) is the product/shell layer above it. +### Turn execution primitives — which one, when + +Fifteen entry points across three packages can plausibly "run a turn", and several of the names look interchangeable but are not. +Pick from this table before writing anything new; invariant 6 makes a *new* primitive require a written argument for why every row below fails. + +| Primitive | Owner | What it does | USE WHEN | +| --- | --- | --- | --- | +| `box.streamPrompt(msg, opts)` | `@tangle-network/sandbox` | SSE `AsyncGenerator` over one sandbox session's events; `lastEventId`/`executionId` reconnect from a worker, `detach: true` keeps the run alive past the stream | A server holds a sandbox turn open and someone is watching it. Prefer `streamSandboxPrompt` — don't call this bare from a product. | +| `box.driveTurn(msg, opts)` | `@tangle-network/sandbox` (≥ 0.10.5) | ONE settle → poll → dispatch pass over a detached turn; returns `running` / `completed` / `failed` and returns immediately | A tick-based durable driver (Workflow step, DO alarm, queue consumer) advances an unattended turn. | +| `box.mintScopedToken()` + `SessionGatewayClient` | `@tangle-network/sandbox` + `/session-gateway` | Short-lived read-only JWT, then a WebSocket client that subscribes and replays from `lastEventId` | The browser should tail a sandbox session directly, with no product worker in the path. This is the invariant-6 canonical case. | +| `streamSandboxPrompt(shell, box, msg, opts)` | agent-app `/sandbox` | `box.streamPrompt` + the shell's profile / model / MCP / effort resolution, severed-stream fail-loud, `disallowQuestions` for unattended runs | Any product streaming a sandbox turn. | +| `runSandboxPrompt(shell, box, msg, opts)` | agent-app `/sandbox` | The same stream, aggregated down to one final `string` | An unattended non-chat turn whose only output is text (cron brief, judge run). Import it — three products still carry a local fork of this function. | +| `driveSandboxTurn(shell, box, msg, opts)` | agent-app `/sandbox` | `box.driveTurn` + profile/model resolution; returns `Outcome` so a retryable transport `fail` is distinguishable from a settled `.state` | A mission step / cron / queue tick advances a detached turn. This is the autonomous default. | +| `createBufferedTurnTap(opts)` | agent-app `/stream` | Sequence-numbers, coalesces, and persists a turn's events to a `TurnEventStore` while writing live — for a producer that owns iteration and hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`) | Your route's own NDJSON turn stream must survive a client disconnect and replay by seq. `createChatTurnRoutes` wires this by default. Not a substitute for the sandbox gateway when the browser should attach to the sandbox session itself. | +| `pumpBufferedTurn(opts)` | agent-app `/stream` | The same tap driven over an `AsyncIterable` you own; the returned promise goes to `ctx.waitUntil` so a disconnect can't kill the turn | Same as above, but you own the producer as an iterable rather than a callback. | +| `createSandboxChatProducer(opts)` | agent-app `/chat-routes` | Raw sandbox events → flattened client vocabulary + persisted parts, authoritative usage, composed error text, interaction finalization | You are filling `createChatTurnRoutes`' producer seam with a sandbox session. | +| `createChatTurnRoutes(opts)` | agent-app `/chat-routes` | The assembled server chat vertical: parse → `authorize` → producer → buffered tap → NDJSON response + replay + interaction answer endpoints + persistence | You are building a server chat turn at all. Reach for the individual modules only for what it doesn't cover. | +| `runDetachedTurn(opts)` | agent-app `/chat-routes` | Streams an already-detached run into the SAME turn-event buffer the interactive lane uses; idempotent (a `complete` turn returns its cached result) and crash-safe (a stale `running` turn is reconciled, not double-streamed) | An autonomous run must still be watchable live by a browser that opens the session mid-run. An unattended run MUST wire `declineInteraction` or a non-renderable ask deadlocks. | +| `runAppToolLoop` / `streamAppToolLoop` | agent-app `/runtime` | **Aliases, not an implementation** — `src/runtime/loop.ts` re-exports agent-runtime's `runToolLoop` / `streamToolLoop` verbatim | Your product drives an OpenAI-compatible model turn itself (browser/edge copilot) and wants the app-side vocabulary. Identical at runtime to the row below. | +| `runToolLoop` / `streamToolLoop` | `@tangle-network/agent-runtime` (**root** entry) | The real in-turn tool loop: one model turn → collect tool calls → dispatch → fold results back in OpenAI function-calling shape → re-run, bounded by `maxToolTurns` / `deadlineMs` / `maxCostUsd`. No rounds, no sandboxes | Same as above, importing the engine directly. Both spellings are fine; they are one function. | +| `handleChatTurn(input)` | `@tangle-network/agent-runtime` (**root** entry) | The turn engine `createChatTurnRoutes` composes — agent-app contributes zero loop logic on this path | You need the engine turn without agent-app's route assembly. | +| `runLoop(options)` | `@tangle-network/agent-runtime` **`/loops`** subpath | **Not a turn loop.** A multi-round, multi-candidate kernel: per round a driver plans N tasks, they run in N sandbox boxes at bounded concurrency, each output is parsed + validated, and results fold through `driver.decide`. Owns box lifecycle, lineage/fork, cost ledger, winner selection. Tagged `@experimental` | An eval or search harness explores candidates across rounds. Never for a chat turn. | + +#### The `runLoop` name collision — read this before importing anything with "Loop" in it + +Four different functions, and the names do not disambiguate them: + +1. **`runLoop`** — `@tangle-network/agent-runtime/loops`. The multi-agent *round* kernel described above. It is **not** on the package root; the only `runLoop`-ish name on the root entry is `runLoopRunnerCli`. +2. **`runToolLoop` / `streamToolLoop`** — `@tangle-network/agent-runtime` **root**. The *in-turn* tool loop. Different file, different subpath, different job from (1). +3. **`runAppToolLoop` / `streamAppToolLoop`** — agent-app `/runtime`. A pure alias of (2); agent-app owns no loop code. The fleet is currently split — some products import the alias, others import (2) directly. Both resolve to the same function, so neither is a bug; do not "fix" one into the other opportunistically. +4. **`routerToolLoop`** — a *third* tool loop, shipped on the **same `/loops` subpath as (1)**. So the collision exists inside one subpath, not just across packages. + +Quick test when reading code or docs: a loop described with **rounds / candidates / a driver** is (1); a loop described with **tool calls folded back into one turn** is (2)/(3)/(4). +If you are writing a chat turn, you want (2)/(3) — or better, no loop at all: let `createChatTurnRoutes` + `handleChatTurn` run the turn. + The test for new code: *"Could the agent in the sandbox do this itself if we told it the intent?"* If yes, write the prompt, not the wrapper. ## Develop diff --git a/README.md b/README.md index 5ebb1ee..9cc57e0 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ The substrate packages — `@tangle-network/agent-runtime`, `agent-eval`, `agent ## Highlights - **Structured tool side channel** — `submit_proposal` (approval-gated), `schedule_followup`, `render_ui`, `add_citation`, exposed as validated tool calls over three surfaces (HTTP route, per-turn MCP server, agent-runtime executor). No fenced-text parsing. -- **Bounded tool loop** — `runAppToolLoop` / `streamAppToolLoop`: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. Substrate-free behind a `streamTurn` seam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged. +- **Bounded tool loop** — `runAppToolLoop` / `streamAppToolLoop`: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. These are 1:1 aliases of `@tangle-network/agent-runtime`'s `runToolLoop` / `streamToolLoop` (the engine owns the loop; this package adds no logic on that path). Substrate-free behind a `streamTurn` seam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged. - **Assembled chat vertical** — `createChatTurnRoutes` wires auth → thread/message store → streaming turn with buffered replay → uploads → sidecar question answering into one route factory, over `authorize` / `produce` / `store` / `interactions` seams. No hand-rolled orchestration. See [`examples/chat-app.md`](./examples/chat-app.md). - **Sandbox-optional** — the same tools, billing, eval, and loop work without a container. A `fetch`-only adapter maps any OpenAI-compatible stream (Tangle Router, tcloud) into the loop. See [`examples/browser-copilot.md`](./examples/browser-copilot.md). -- **Resumable turns (sandbox-free path)** — for a browser/edge copilot streaming the Router directly, buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. **Sandbox products don't need this** — the sandbox SDK already buffers + replays sessions (`streamPrompt` + `lastEventId`). See [`examples/resumable-turns.md`](./examples/resumable-turns.md). +- **Resumable turns (sandbox-free path)** — for a browser/edge copilot streaming the Router directly, buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. **Sandbox products don't need this** — the sandbox SDK already buffers + replays sessions (server-side reconnect via `box.streamPrompt`'s `lastEventId`; browser-direct via `box.mintScopedToken()` + `SessionGatewayClient`). See [`examples/resumable-turns.md`](./examples/resumable-turns.md). - **Composes the engine, never forks it** — `/eval` re-exports `@tangle-network/agent-eval`'s verifier; `/integrations` wraps the hub; `/tangle` and `/billing` take the tcloud client as a structural contract. Engines are **peer dependencies** — you pin the version, nothing is bundled. - **ESM, typed, zero runtime deps** in the substrate-free modules (`/runtime`, `/web`, `/crypto`, `/redact`, `/stream`). Ships with `.d.ts` and npm [provenance](https://www.npmjs.com/package/@tangle-network/agent-app#provenance). @@ -36,10 +36,16 @@ pnpm add @tangle-network/agent-eval @tangle-network/agent-integrations | Peer | Required by | Range | |---|---|---| | `@tangle-network/agent-eval` | `/eval`, `/eval-campaign`, `/profile`, `/knowledge` | `>=0.100.0` | -| `@tangle-network/agent-runtime` | `/runtime`, `/knowledge-loop`, runtime tool execution | `>=0.79.3` | -| `@tangle-network/agent-integrations` | `/integrations`, `/tangle` | `>=0.32.0` | +| `@tangle-network/agent-runtime` | `/runtime`, `/chat-routes` | `>=0.79.3` | +| `@tangle-network/agent-integrations` | `/integrations` | `>=0.44.0` | +| `@tangle-network/agent-interface` | `/interactions`, `/chat-store`, `/harness` | `>=0.15.0` | +| `@tangle-network/sandbox` | `/sandbox`, `/profile`, `/skills` | `>=0.9.7` | +| `@tangle-network/agent-knowledge` | `/knowledge-loop` | `>=1.7.0` | +| `@tangle-network/agent-profile-materialize` | `/skills-placement` | `>=0.6.0` | -Modules that do not import engine packages (`/tools`, `/web`, `/crypto`, `/redact`, `/stream`, `/billing`) need no peers. +All of these except `agent-eval`, `agent-integrations`, and `agent-interface` are declared **optional** peers, so a product that never imports the subpath installs nothing. `driveSandboxTurn` (`/sandbox`) calls `box.driveTurn`, which the SDK added in **0.10.5** — above the declared floor, so pin `@tangle-network/sandbox >= 0.10.5` yourself if you use it. + +Modules that import no engine package (`/tools`, `/web`, `/crypto`, `/redact`, `/stream`, `/billing`, `/tangle` — the last two take their client as a structural contract) need no peers. ## Quick start @@ -117,11 +123,13 @@ Three decisions cover most of the surface. **1. How does the turn run?** Pick the transport by who's watching, not by feature. +Each primitive is written `package → symbol`; three packages ship similarly-named turn functions, and AGENTS.md has the full [primitive table and the `runLoop` name-collision note](./AGENTS.md#turn-execution-primitives--which-one-when). + | Your turn | Use | Why | |---|---|---| -| **Interactive** — a user is watching a chat or copilot | `streamPrompt` held open for the turn; the sandbox gateway lets the browser attach directly | Worker lifetime ≈ turn length; a dropped tab replays the buffered tail on reconnect. | -| **Autonomous** — a mission step, queue job, cron, or inbound email, with nobody watching | `dispatchPrompt({ detach: true })` + poll from a durable driver. `runDetachedTurn` (`/chat-routes`) bridges that detached run into the live buffer, so a browser opening the session mid-run still tails it token-by-token | No consumer exists and Workers die in minutes; the platform runs the turn server-side and a crash re-dispatch is a lookup, not a second run. | -| **Eval / CI** — a long-lived harness process | `runAppToolLoop` / `streamPrompt` in-process | The process outlives the run; durability adds nothing — a failed run is re-run, not resumed. | +| **Interactive** — a user is watching a chat or copilot | sandbox → `box.streamPrompt()` held open for the turn, wrapped here as `streamSandboxPrompt` (`/sandbox`); for the browser leg, sandbox → `box.mintScopedToken()` + `SessionGatewayClient` (`@tangle-network/sandbox/session-gateway`) attaches the tab directly | Worker lifetime ≈ turn length; a dropped tab replays the buffered tail on reconnect. | +| **Autonomous** — a mission step, queue job, cron, or inbound email, with nobody watching | sandbox → `box.driveTurn()`, wrapped here as `driveSandboxTurn` (`/sandbox`), ticked from a durable driver; drop to raw `box.dispatchPrompt({ detach: true })` + `box.findCompletedTurn(turnId, { sessionId })` only when one pass is too coarse. `runDetachedTurn` (`/chat-routes`) bridges that detached run into the live buffer, so a browser opening the session mid-run still tails it token-by-token | No consumer exists and Workers die in minutes; the platform runs the turn server-side and a crash re-dispatch is a lookup, not a second run. | +| **Eval / CI** — a long-lived harness process | sandbox → `box.streamPrompt()` for a sandboxed harness; agent-runtime (root) → `runToolLoop` / `streamToolLoop` for an in-process model turn — `runAppToolLoop` / `streamAppToolLoop` (`/runtime`) are 1:1 aliases of those, not a second implementation | The process outlives the run; durability adds nothing — a failed run is re-run, not resumed. | **2. Assembled or à la carte?** `createChatTurnRoutes` (`/chat-routes`) wires the whole server chat turn — auth, store, streaming, replay, uploads, interactions — over typed seams. Reach for the individual modules (`/stream`, `/chat-store`, `/interactions`) only to compose something the assembled route doesn't cover. diff --git a/examples/resumable-turns.md b/examples/resumable-turns.md index 25e7ebd..44d0f74 100644 --- a/examples/resumable-turns.md +++ b/examples/resumable-turns.md @@ -7,11 +7,15 @@ every event as it's produced so a reconnecting client can replay the tail. ## ⚠️ First: are you on the sandbox? Then you almost certainly DON'T need this. **The `@tangle-network/sandbox` SDK already buffers + replays session streams.** -`streamPrompt` / `dispatchPrompt` buffer events server-side; a reconnecting -client replays with `lastEventId` (the `Last-Event-ID` header), and -`findCompletedTurn` is the idempotent completion check. Both gtm-agent and -creative-agent already use this — **do not hand-roll the buffer below over a -sandbox session.** See the `sandbox-sdk-integration` guidance. +`box.streamPrompt` / `box.dispatchPrompt` buffer events server-side; a +reconnecting **worker** resumes by passing `lastEventId` (forwarded as the +`Last-Event-ID` header), a **browser** attaches directly with +`box.mintScopedToken()` + `SessionGatewayClient` +(`@tangle-network/sandbox/session-gateway`, WebSocket, replay from +`lastEventId`), and `box.findCompletedTurn(turnId, { sessionId })` is the +idempotent completion check. Both gtm-agent and creative-agent already use this +— **do not hand-roll the buffer below over a sandbox session.** See the +`sandbox-sdk-integration` guidance. **This module is the resume story for the SANDBOX-FREE path only** — a browser or edge copilot streaming the Tangle Router (or any OpenAI-compatible endpoint) @@ -30,9 +34,9 @@ It is pure mechanism behind a storage seam — no peers. Storage is a | You're running… | Use this? | | --- | --- | -| **Sandbox-backed turn** (most products) | **No** — the SDK gateway already buffers + replays (`streamPrompt` + `lastEventId`). Use that. | +| **Sandbox-backed turn** (most products) | **No** — the SDK already buffers + replays: `box.streamPrompt` + `lastEventId` for a worker, `box.mintScopedToken()` + `SessionGatewayClient` for a browser. Use those. | | **Sandbox-free interactive turn** (browser/edge copilot on the Router directly) | **Yes** — there's no gateway; this is your resume mechanism. | -| **Autonomous turn** (mission, queue, cron) | Prefer `dispatchPrompt({ detach: true })` + poll. Buffer only if you also stream it to a watcher AND aren't sandbox-backed. | +| **Autonomous turn** (mission, queue, cron) | Prefer `box.driveTurn()` (agent-app's `driveSandboxTurn`) ticked from a durable driver, or raw `box.dispatchPrompt({ detach: true })` + poll. Buffer only if you also stream it to a watcher AND aren't sandbox-backed — and then use `runDetachedTurn` (`/chat-routes`), which does exactly that bridge. | | **Eval / CI** (long-lived process) | **No** — the harness is the consumer and outlives the run; a failed run is re-run, not resumed. | ## Pick a transport — who owns the producer?