Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
22 changes: 17 additions & 5 deletions src/chat-routes/turn-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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'
Expand Down Expand Up @@ -236,14 +244,17 @@ export interface CreateChatTurnRoutesOptions<TContext = void> {
* product's own producer. May be async (box resolution). */
produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>
/** 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<TContext>
/** 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<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>
/** 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<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>
/** Deterministic run telemetry (start / complete / error) with identity and
* timing. Omit → no telemetry. */
Expand All @@ -253,7 +264,8 @@ export interface CreateChatTurnRoutesOptions<TContext = void> {
/** 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<void>
/** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).
* Live stream is never altered. */
Expand Down
9 changes: 9 additions & 0 deletions src/chat-routes/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down