diff --git a/AGENTS.md b/AGENTS.md index 373d974..36d4e1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ Corollary — **extend, never duplicate.** Before writing anything that complete | `/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`) | -| `/chat-routes` | the assembled server chat vertical (#188 Phase 1): `createChatTurnRoutes()` — body parse/validate → injected `authorize` seam → producer seam → turn-buffer tap wired BY DEFAULT → NDJSON `Response` + replay endpoint + composed `/interactions` answer endpoints + `/chat-store` persistence (user row on send, assistant row with parts/usage on completion); six optional product seams — `turnLock` (single-flight acquire/release), `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn` (observe/augment producer input), `lifecycle` (deterministic start/complete/error telemetry, idempotent + settled even on a synchronous pre-stream throw), `heartbeat` (keepalive during silent producer waits), `onRawEvent` (raw producer-event tap) — plus `transformFinalText` (pre-persist redaction applied to BOTH the final-text scalar AND every persisted assistant TEXT part, so `/redact` closes the at-rest PII leak, not just the streamed scalar) and run-failure surfacing (`onTurnComplete` receives `failed`/`failureReason` from a terminal `error`/`session.run.failed` event so an errored turn is skipped for billing + rendered as an error row, never billed/marked-complete with empty text); `createSandboxChatProducer` (raw sandbox events → client vocabulary + persisted parts + usage receipt, non-renderable-ask auto-decline); `createUploadRoute` (multipart → inline `data:` part ≤700 KiB, else base64 write through a structural sandbox sink + path-ref part — the >1 MiB gateway cap makes the two-step mandatory); import-free `./wire` contract (`ChatTurnRequestPayload`, inline-part byte gate) re-exported via `/web-react`'s chat-stream | peer `@tangle-network/agent-runtime` (`handleChatTurn` IS the turn engine — zero loop logic here; subpath-only, not in the root barrel); composes `/stream`, `/chat-store`, `/interactions`, `/web`. Reference assembly: `examples/chat-app.md` | +| `/chat-routes` | the assembled server chat vertical (#188 Phase 1): `createChatTurnRoutes()` — body parse/validate → injected `authorize` seam → producer seam → turn-buffer tap wired BY DEFAULT → NDJSON `Response` + replay endpoint + composed `/interactions` answer endpoints + `/chat-store` persistence (user row on send, assistant row with parts/usage on completion); six optional product seams (`turnLock`/`contextGate`/`beforeTurn`/`onRawEvent` are `@experimental` — single-consumer, proven by gtm (#200), kept FLAT for back-compat; `lifecycle`/`heartbeat` are stable) — `turnLock` (single-flight acquire/release), `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn` (observe/augment producer input), `lifecycle` (deterministic start/complete/error telemetry, idempotent + settled even on a synchronous pre-stream throw), `heartbeat` (keepalive during silent producer waits), `onRawEvent` (raw producer-event tap) — plus `transformFinalText` (pre-persist redaction applied to BOTH the final-text scalar AND every persisted assistant TEXT part, so `/redact` closes the at-rest PII leak, not just the streamed scalar) and run-failure surfacing (`onTurnComplete` receives `failed`/`failureReason` from a terminal `error`/`session.run.failed` event so an errored turn is skipped for billing + rendered as an error row, never billed/marked-complete with empty text); `createSandboxChatProducer` (raw sandbox events → client vocabulary + persisted parts + usage receipt, non-renderable-ask auto-decline); `createUploadRoute` (sole consumer: the `--chat` scaffold — fleet apps keep their own durable-vault routes; multipart → inline `data:` part ≤700 KiB, else base64 write through a structural sandbox sink + path-ref part — the >1 MiB gateway cap makes the two-step mandatory); import-free `./wire` contract (`ChatTurnRequestPayload`, inline-part byte gate) re-exported via `/web-react`'s chat-stream | peer `@tangle-network/agent-runtime` (`handleChatTurn` IS the turn engine — zero loop logic here; subpath-only, not in the root barrel); composes `/stream`, `/chat-store`, `/interactions`, `/web`. Reference assembly: `examples/chat-app.md` | | `/tangle` | app-registration consent URL + cached broker-token provider | structural `TangleAppsClient` (from agent-integrations) | | `/billing` | per-workspace budget-capped key manager (mint/rotate/rollover/usage) | structural tcloud provisioner + store + crypto seams | | `/preflight` | deploy-time secret-liveness probes: `runPreflight(probes)` → per-probe verdict + latency + overall pass/fail (any critical fail → fail); standard builders `routerChatProbe`/`sandboxAuthProbe`/`httpHeadProbe` (explicit config, read nothing global); `formatPreflightReport` table + the `agent-app-preflight` bin reading `preflight.config.mjs`. Binds at DEPLOY time (the one place with real secrets — CI can't hold them); failures name the exact secret to rotate | — (server-only; product declares probes from `process.env`) | diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffbbd49..08fcd0b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -88,7 +88,7 @@ consumer of L0/L1 installs none of them): `konva`/`react-konva` → only | Integration-hub `/exec` calls | `integrations` | | Per-workspace key mint/rotate/budget | `billing` | | Resumable chat turns (buffer/replay/coalesce) | `stream` — see [`examples/resumable-turns.md`](./examples/resumable-turns.md) | -| The whole assembled chat turn route (auth → persist → stream → interactions) | `chat-routes` — `createChatTurnRoutes` (peer `agent-runtime`). Product seams: `turnLock` · `contextGate` · `beforeTurn` · `lifecycle` · `heartbeat` · `onRawEvent`, plus `transformFinalText` (pre-persist redaction over the final-text scalar AND every persisted TEXT part) and `onTurnComplete(failed, failureReason)` run-failure surfacing | +| The whole assembled chat turn route (auth → persist → stream → interactions) | `chat-routes` — `createChatTurnRoutes` (peer `agent-runtime`). Product seams (`@experimental`, single-consumer/gtm #200: `turnLock` · `contextGate` · `beforeTurn` · `onRawEvent`; stable: `lifecycle` · `heartbeat`), plus `transformFinalText` (pre-persist redaction over the final-text scalar AND every persisted TEXT part) and `onTurnComplete(failed, failureReason)` run-failure surfacing | | Flow traces / waterfalls | `trace` | | Chat UI + run/observability components | `web-react` | | Agent asks a human mid-run (question/plan cards, answer route) | `interactions` (server + contract) + `web-react` (cards/hook) | diff --git a/src/chat-routes/turn-routes.ts b/src/chat-routes/turn-routes.ts index 995e5ca..5e568c6 100644 --- a/src/chat-routes/turn-routes.ts +++ b/src/chat-routes/turn-routes.ts @@ -20,7 +20,7 @@ * no router import. Auth/access is one injected `authorize` seam, composable * with `/app-auth` guards but not coupled to them. * - * Five optional product seams let a complex turn-orchestrator compose the + * Six optional product seams let a complex turn-orchestrator compose the * vertical instead of hand-rolling a generator — each omittable to the exact * behavior above: `turnLock` (single-flight acquire/release around the turn), * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn` @@ -29,6 +29,14 @@ * producer waits), plus `onRawEvent` (the raw producer events, for telemetry). * `handleChatTurn` stays the engine — the seams only wrap its input, its * producer stream, and its settle. + * + * Seam stability: `lifecycle` and `heartbeat` are generic and stable. + * `turnLock`, `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental` + * — proven by a single consumer (gtm's chat vertical, #200) and may change once + * a second consumer exercises them. They stay FLAT top-level options (not + * grouped under a `hooks` object): that grouping would break the shipped + * consumer's call for no mechanism gain, and this package's exports are + * additive-only. */ import { deriveExecutionId, handleChatTurn } from '@tangle-network/agent-runtime' @@ -236,14 +244,17 @@ export interface CreateChatTurnRoutesOptions { * product's own producer. May be async (box resolution). */ produce(args: ChatTurnProduceArgs): ChatTurnRouteProducer | Promise /** Single-flight lock acquired before any side effect and released once when - * the turn settles (including short-circuit/throw). Omit → no lock. */ + * the turn settles (including short-circuit/throw). Omit → no lock. + * @experimental Single-consumer (gtm, #200); shape may change. */ turnLock?: ChatTurnLock /** Pre-turn readiness gate that can short-circuit with a product `Response` * before the producer runs (the user row is already persisted). Runs after - * `turnLock.acquire`, before `beforeTurn`. Omit → always proceed. */ + * `turnLock.acquire`, before `beforeTurn`. Omit → always proceed. + * @experimental Single-consumer (gtm, #200); shape may change. */ contextGate?(args: ChatTurnProduceArgs): ChatTurnGateResult | Promise /** Observe the assembled producer input and optionally augment it (rewrite - * the prompt / prior messages) before the producer runs. Omit → no change. */ + * the prompt / prior messages) before the producer runs. Omit → no change. + * @experimental Single-consumer (gtm, #200); shape may change. */ beforeTurn?(args: ChatTurnProduceArgs): ChatTurnInputPatch | void | Promise /** Deterministic run telemetry (start / complete / error) with identity and * timing. Omit → no telemetry. */ @@ -253,7 +264,8 @@ export interface CreateChatTurnRoutesOptions { /** Observe each event the producer emits, before the engine frames it and * before any heartbeat injection (the raw sidecar-producer events, for * telemetry). Never alters the stream; errors are swallowed. Distinct from - * `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes. */ + * `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes. + * @experimental Single-consumer (gtm, #200); shape may change. */ onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`). * Live stream is never altered. */ diff --git a/src/chat-routes/upload.ts b/src/chat-routes/upload.ts index 8c95fcb..d04a081 100644 --- a/src/chat-routes/upload.ts +++ b/src/chat-routes/upload.ts @@ -11,6 +11,15 @@ * so a large file can never ride the prompt POST. * * The sink is structural (no sandbox-SDK import); products pass `box.fs`. + * + * @remarks Sole consumer today is the `--chat` scaffold (`create-agent-app + * --chat` → `template-chat/src/chat.ts`), the reference multimodal path. The + * fleet apps (gtm/tax/legal/insurance) each keep their OWN upload route into a + * durable vault (KV, or AES-GCM-encrypted R2) — a different persistence model + * from this route's inline-`data:`-or-ephemeral-sandbox-workspace split, so + * they don't (and shouldn't) route through it. This stays the scaffold's proven + * upload pattern, not a fleet primitive; keep that distinction in mind before + * widening its surface. */ import type { ChatTurnFilePartInput } from './wire'