@tangle-network/agent-app 0.43.41 → 0.43.43
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/dist/assistant/index.d.ts +2 -1
- package/dist/assistant/index.js +3 -2
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-routes/index.d.ts +17 -3
- package/dist/chat-routes/index.js +91 -6
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-store/index.d.ts +4 -3
- package/dist/chat-store/index.js +5 -1
- package/dist/chat-store/index.js.map +1 -1
- package/dist/{chunk-Y4QHNQ75.js → chunk-6KVH5SP5.js} +110 -6
- package/dist/chunk-6KVH5SP5.js.map +1 -0
- package/dist/{chunk-XYWCGFII.js → chunk-B5JD3DXD.js} +48 -2
- package/dist/chunk-B5JD3DXD.js.map +1 -0
- package/dist/{chunk-I2R2XT4M.js → chunk-I2ATYB7R.js} +18 -2
- package/dist/chunk-I2ATYB7R.js.map +1 -0
- package/dist/{chunk-KM766NN3.js → chunk-PEUBCJXF.js} +1772 -1224
- package/dist/chunk-PEUBCJXF.js.map +1 -0
- package/dist/{chunk-UMSOSUEU.js → chunk-QYOD3K56.js} +2 -2
- package/dist/chunk-QYOD3K56.js.map +1 -0
- package/dist/chunk-SIXYZ2FB.js +101 -0
- package/dist/chunk-SIXYZ2FB.js.map +1 -0
- package/dist/{chunk-4TXDD6P2.js → chunk-XAWFPMAR.js} +38 -3
- package/dist/chunk-XAWFPMAR.js.map +1 -0
- package/dist/chunk-ZU5GNSOJ.js +920 -0
- package/dist/chunk-ZU5GNSOJ.js.map +1 -0
- package/dist/{contract-DYbTzEDf.d.ts → contract-KfqJh_au.d.ts} +23 -2
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/durable-chat/index.d.ts +419 -0
- package/dist/durable-chat/index.js +59 -0
- package/dist/durable-chat/index.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +124 -46
- package/dist/interactions/index.d.ts +58 -3
- package/dist/interactions/index.js +7 -3
- package/dist/object-store/index.d.ts +6 -4
- package/dist/object-store/index.js +1 -1
- package/dist/{parts-BcbitSNp.d.ts → parts-DjX0RRTS.d.ts} +9 -3
- package/dist/plans/index.d.ts +69 -0
- package/dist/plans/index.js +21 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/stream/index.js +3 -1
- package/dist/teams/index.js +9 -9
- package/dist/teams/invitations-api.js +3 -3
- package/dist/web-react/index.d.ts +252 -93
- package/dist/web-react/index.js +32 -3
- package/package.json +11 -1
- package/dist/chunk-4TXDD6P2.js.map +0 -1
- package/dist/chunk-I2R2XT4M.js.map +0 -1
- package/dist/chunk-KM766NN3.js.map +0 -1
- package/dist/chunk-UMSOSUEU.js.map +0 -1
- package/dist/chunk-XYWCGFII.js.map +0 -1
- package/dist/chunk-Y4QHNQ75.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/object-store/index.ts"],"sourcesContent":["/**\n * `/object-store` — a portable, secure object store for durable large\n * attachments (uploaded PDFs, images, exports) that are too big to ride a chat\n * turn body or live in KV.\n *\n * SECURITY POSTURE (this module guards a legal privilege wall). Every object key\n * carries an operator segment and a customer segment, and access is by a\n * short-lived HMAC-signed URL whose signature covers the EXACT key (operator +\n * customer + upload id + filename) AND the expiry. A tampered key, a swapped\n * customer segment, or a replayed-after-expiry URL all fail the signature or the\n * expiry check and are refused. The signing secret is a PARAMETER on every call\n * — this module reads NOTHING global (no `process.env`, no ambient config); if a\n * product forgets to bind the secret, {@link verifyObjectUrl} fails closed and\n * {@link signObjectUrl} throws rather than minting an unsigned URL.\n *\n * PURE MECHANISM behind two seams: an {@link ObjectStore} port (the R2 impl maps\n * 1:1 to a bucket held behind the structural {@link R2LikeBucket} — so\n * `@cloudflare/workers-types` never leaks into this package's public `.d.ts`)\n * and the signing `secret`. Reads and writes STREAM: {@link createR2ObjectStore}\n * returns the R2 object's `.body` stream directly and never calls\n * `.text()`/`.arrayBuffer()`, so a 15 MB PDF flows through the worker without\n * buffering the whole file into the isolate's heap.\n *\n * RECONCILIATION with the sandbox `storage` seam (`../sandbox` →\n * `SandboxRuntimeConfig.storage`): that is BYOS3/R2 *snapshot* storage — it\n * checkpoints and restores a sandbox box's filesystem for durable session\n * execution. This is a DIFFERENT concern: a content-addressed store for\n * user-facing attachments gated by a signed-URL privilege wall. They are not\n * interchangeable and must not be merged: one persists agent runtime state, the\n * other serves per-operator/per-customer documents to browsers. Do not route\n * artifact downloads through the snapshot bucket, or vice versa.\n */\n\nimport { constantTimeEqual, hmacSha256Base64Url } from '../crypto/web-token'\n\n// ── The store port + R2 impl ────────────────────────────────────────────────\n\n/** Options for a single {@link ObjectStore.put}. Both fields are optional; a\n * store that needs a fixed content length (e.g. R2 with a `ReadableStream`\n * body) uses `contentLength` when present. */\nexport interface PutObjectOptions {\n /** MIME type recorded with the object (returned by `get`/`head`). Advisory:\n * the proxied download route always serves `application/octet-stream`. */\n contentType?: string\n /** Byte length of `body`, when known. Some backends require it for a\n * streamed body; the R2 impl passes a known length through when supplied. */\n contentLength?: number\n}\n\n/** A retrieved object. `stream()` is the ONLY way to read the bytes — there is\n * deliberately no `text()`/`arrayBuffer()`, so a large object never buffers\n * into the isolate heap. */\nexport interface ObjectBody {\n /** The object's bytes as a `ReadableStream` (backed by R2's `.body`). */\n stream(): ReadableStream\n /** Size in bytes. */\n size: number\n /** Recorded MIME type, if any. */\n contentType?: string\n}\n\n/**\n * Portable object-store port. `get`/`head` return `null` on a miss and NEVER\n * throw for a missing key (a miss is a normal control-flow outcome, not an\n * error); `delete` is idempotent.\n */\nexport interface ObjectStore {\n put(key: string, body: ReadableStream | Uint8Array, opts?: PutObjectOptions): Promise<void>\n /** `null` on a miss — never throws for a missing key. */\n get(key: string): Promise<ObjectBody | null>\n /** `null` on a miss — never throws for a missing key. */\n head(key: string): Promise<{ size: number; contentType?: string } | null>\n delete(key: string): Promise<void>\n}\n\n/** Head/metadata shape of a stored object (structural match of R2's `R2Object`). */\nexport interface R2LikeObjectHead {\n size: number\n httpMetadata?: { contentType?: string }\n}\n\n/** Body shape of a retrieved object (structural match of R2's `R2ObjectBody`).\n * `body` is the streamed content — the impl reads this, never `.text()`. */\nexport interface R2LikeObjectBody extends R2LikeObjectHead {\n body: ReadableStream\n}\n\n/**\n * The minimal slice of Cloudflare's `R2Bucket` this module calls. A real\n * `R2Bucket` satisfies it structurally, so the consumer passes its binding\n * without this package ever importing `@cloudflare/workers-types` (which would\n * otherwise leak into the public `.d.ts`).\n */\nexport interface R2LikeBucket {\n put(\n key: string,\n value: ReadableStream | Uint8Array | ArrayBuffer,\n options?: { httpMetadata?: { contentType?: string } },\n ): Promise<unknown>\n get(key: string): Promise<R2LikeObjectBody | null>\n head(key: string): Promise<R2LikeObjectHead | null>\n delete(key: string): Promise<void>\n}\n\n/**\n * Map the {@link ObjectStore} port onto an R2 bucket 1:1. `get` STREAMS: it\n * returns the R2 object's `.body` behind `ObjectBody.stream()` and never calls\n * `.text()`/`.arrayBuffer()`, so a large file flows through the worker rather\n * than buffering into the isolate.\n */\nexport function createR2ObjectStore({ bucket }: { bucket: R2LikeBucket }): ObjectStore {\n return {\n async put(key, body, opts) {\n const options = opts?.contentType ? { httpMetadata: { contentType: opts.contentType } } : undefined\n await bucket.put(key, body, options)\n },\n async get(key) {\n const obj = await bucket.get(key)\n if (!obj) return null\n return {\n stream: () => obj.body,\n size: obj.size,\n contentType: obj.httpMetadata?.contentType,\n }\n },\n async head(key) {\n const obj = await bucket.head(key)\n if (!obj) return null\n return { size: obj.size, contentType: obj.httpMetadata?.contentType }\n },\n async delete(key) {\n await bucket.delete(key)\n },\n }\n}\n\n// ── Key construction + safety ───────────────────────────────────────────────\n\n/**\n * Assert a single object-key path SEGMENT (an operator id, customer id, or\n * upload id) is safe to interpolate into a key, and return it. Throws on the\n * traversal / injection shapes: `..` anywhere, a leading `/`, a backslash, or an\n * empty segment. Consumers should call this on caller-supplied identifiers\n * BEFORE any `get`/`put` so an attacker-controlled id can never widen the key\n * beyond its own operator+customer prefix.\n */\nexport function assertSafeKeySegment(s: string): string {\n if (s.length === 0) throw new Error('object-store: empty key segment')\n if (s.includes('..')) throw new Error(`object-store: unsafe key segment (contains \"..\") — ${s}`)\n if (s.startsWith('/')) throw new Error(`object-store: unsafe key segment (leading \"/\") — ${s}`)\n if (s.includes('\\\\')) throw new Error(`object-store: unsafe key segment (backslash) — ${s}`)\n return s\n}\n\n/** Strip a filename down to a safe leaf: drop any path, keep only\n * `[A-Za-z0-9._-]`, and remove leading dots so a `..`/`.hidden`/dot-only name\n * can neither traverse nor vanish. Never returns an empty string. */\nfunction sanitizeFilename(filename: string): string {\n const leaf = filename.split(/[/\\\\]/).pop() ?? ''\n const cleaned = leaf.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\\.+/, '')\n return cleaned.length > 0 ? cleaned : 'file'\n}\n\n/** Inputs to {@link objectKey}. `customerId` is optional — an unattributed\n * upload lands under the reserved `_unattributed` customer segment. */\nexport interface ObjectKeyParts {\n operatorId: string\n /** Optional — omitted/undefined groups the upload under `_unattributed`. */\n customerId?: string\n uploadId: string\n filename: string\n}\n\n/**\n * Build the canonical object key: `operator/customer/upload-filename`. The\n * operator, customer, and upload segments are asserted safe (throwing on\n * traversal); the filename is sanitized to a safe leaf. The customer segment\n * falls back to `_unattributed` when no customer is attributed. Because a signed\n * URL binds the EXACT key, the operator and customer segments are part of the\n * privilege wall — a caller cannot later swap them without breaking the\n * signature.\n */\nexport function objectKey({ operatorId, customerId, uploadId, filename }: ObjectKeyParts): string {\n const operator = assertSafeKeySegment(operatorId)\n const customer = customerId == null ? '_unattributed' : assertSafeKeySegment(customerId)\n const upload = assertSafeKeySegment(uploadId)\n return `${operator}/${customer}/${upload}-${sanitizeFilename(filename)}`\n}\n\n/**\n * Decode a key ONCE and assert every `/`-delimited segment is safe. Slash- and\n * percent-encoded forms of the same key canonicalize identically (`a/b` and\n * `a%2Fb` both → `a/b`), so a signature binds the key's MEANING, not its\n * on-the-wire spelling. Throws on malformed percent-encoding or an unsafe\n * segment. Our own keys never contain `%`, so the single decode is idempotent.\n */\nfunction canonicalizeObjectKey(raw: string): string {\n let decoded: string\n try {\n decoded = decodeURIComponent(raw)\n } catch {\n throw new Error('object-store: malformed key encoding')\n }\n for (const segment of decoded.split('/')) assertSafeKeySegment(segment)\n return decoded\n}\n\n// ── Signed URLs ─────────────────────────────────────────────────────────────\n\n/** The exact string the signature covers: a versioned JSON of the canonical key\n * and the expiry. JSON escaping makes it delimiter-injection-proof (a key\n * cannot forge the `exp` field). Both sign and verify build it identically. */\nfunction signingMessage(canonicalKey: string, exp: number): string {\n return JSON.stringify({ v: 1, exp, key: canonicalKey })\n}\n\n/** Arguments to {@link signObjectUrl}. */\nexport interface SignObjectUrlArgs {\n /** The exact object key to authorize (from {@link objectKey}). */\n key: string\n /** Absolute expiry in epoch MILLISECONDS (e.g. `Date.now() + 5 * 60_000` for a\n * ~5-minute TTL — the recommended default; keep it short). */\n exp: number\n /** HMAC signing secret. Must be non-empty — signing fails closed otherwise. */\n secret: string\n}\n\n/**\n * Mint a signed query string (`?key=…&exp=…&sig=…`) authorizing a download of\n * `key` until `exp`. The product appends it to its artifact route path\n * (`` `/artifacts${await signObjectUrl(...)}` ``); {@link createProxiedArtifactRoute}\n * / {@link verifyObjectUrl} read it straight off the request URL, so the route's\n * own mount path never matters.\n *\n * FAIL-CLOSED: throws if `secret` is empty (never mints an unsigned URL) and if\n * `key` is unsafe (traversal). Async because the HMAC primitive is WebCrypto.\n *\n * TTL: `exp` is caller-supplied on purpose (the caller knows the sensitivity);\n * keep it SHORT — ~5 minutes is the recommended default for a privilege-walled\n * document.\n */\nexport async function signObjectUrl({ key, exp, secret }: SignObjectUrlArgs): Promise<string> {\n if (!secret) throw new Error('object-store: signObjectUrl requires a non-empty secret (fail-closed)')\n const canonical = canonicalizeObjectKey(key)\n const sig = await hmacSha256Base64Url(signingMessage(canonical, exp), secret)\n const params = new URLSearchParams({ key: canonical, exp: String(exp), sig })\n return `?${params.toString()}`\n}\n\n/** Result of {@link verifyObjectUrl}: the canonical key on success, nothing\n * distinguishing on failure (so a rejection leaks no detail). */\nexport type VerifyObjectUrlResult = { ok: true; key: string } | { ok: false }\n\n/**\n * Verify a signed download request. Reads `key`/`exp`/`sig` off the request URL,\n * canonicalizes the key identically to the signer, recomputes the HMAC and\n * constant-time-compares it, and checks the expiry. Returns the canonical key on\n * success.\n *\n * FAIL-CLOSED everywhere: an empty `secret`, a missing/ malformed parameter, a\n * non-finite expiry, a signature mismatch, or an expired URL all return\n * `{ ok: false }` — and the mismatch path uses a constant-time compare so a\n * near-correct signature is not distinguishable by timing. Async because the\n * HMAC primitive is WebCrypto.\n */\nexport async function verifyObjectUrl(\n request: Request,\n { secret }: { secret: string },\n): Promise<VerifyObjectUrlResult> {\n if (!secret) return { ok: false }\n const url = new URL(request.url)\n const rawKey = url.searchParams.get('key')\n const expRaw = url.searchParams.get('exp')\n const sig = url.searchParams.get('sig')\n if (!rawKey || !expRaw || !sig) return { ok: false }\n\n let key: string\n try {\n key = canonicalizeObjectKey(rawKey)\n } catch {\n return { ok: false }\n }\n\n const exp = Number(expRaw)\n if (!Number.isFinite(exp)) return { ok: false }\n\n const expected = await hmacSha256Base64Url(signingMessage(key, exp), secret)\n if (!constantTimeEqual(expected, sig)) return { ok: false }\n // Signature is valid; enforce expiry last so the constant-time compare always\n // runs (an expired-but-otherwise-valid URL is refused just the same).\n if (Date.now() > exp) return { ok: false }\n return { ok: true, key }\n}\n\n// ── Proxied download route ──────────────────────────────────────────────────\n\n/**\n * Build a download handler that verifies a signed request and STREAMS the object\n * back with a conservative, non-executable content type. Status contract:\n *\n * - `400` — the `key` is missing or malformed (bad encoding / traversal). This\n * is an unauthenticated client error and leaks NOTHING about object existence.\n * - `403` — the signature is missing, wrong, or expired.\n * - `404` — the signature was valid but no object exists at that key (existence\n * is only ever revealed to a validly-signed request).\n * - `200` — streams the bytes with `Content-Disposition: attachment`,\n * `Content-Type: application/octet-stream`, and `X-Content-Type-Options:\n * nosniff`, so the worker never serves active/inline content.\n */\nexport function createProxiedArtifactRoute({\n store,\n secret,\n}: {\n store: ObjectStore\n secret: string\n}): (request: Request) => Promise<Response> {\n return async (request) => {\n // 1. Malformed/missing key → 400, decided BEFORE auth so it leaks no\n // existence signal (a malformed key can never carry a valid signature).\n const rawKey = new URL(request.url).searchParams.get('key')\n if (!rawKey) return new Response('Missing key', { status: 400 })\n try {\n canonicalizeObjectKey(rawKey)\n } catch {\n return new Response('Malformed key', { status: 400 })\n }\n\n // 2. Bad / expired signature → 403.\n const verified = await verifyObjectUrl(request, { secret })\n if (!verified.ok) return new Response('Forbidden', { status: 403 })\n\n // 3. Fetch by the VERIFIED canonical key. Miss → 404 (only reachable behind\n // a valid signature). Hit → stream with a conservative content type.\n const obj = await store.get(verified.key)\n if (!obj) return new Response('Not found', { status: 404 })\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/octet-stream',\n 'Content-Disposition': 'attachment',\n 'X-Content-Type-Options': 'nosniff',\n }\n if (Number.isFinite(obj.size) && obj.size >= 0) headers['Content-Length'] = String(obj.size)\n return new Response(obj.stream(), { status: 200, headers })\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stream/stream-normalizer.ts","../src/stream/turn-identity.ts","../src/stream/turn-buffer.ts"],"sourcesContent":["export type JsonRecord = Record<string, unknown>\n\nexport interface StreamEvent {\n type: string\n data?: JsonRecord\n}\n\nexport function asRecord(value: unknown): JsonRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as JsonRecord\n : undefined\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveToolId(part: JsonRecord): string {\n return String(\n part.id ??\n part.callID ??\n part.callId ??\n part.toolUseId ??\n part.toolCallId ??\n part.tool ??\n part.name ??\n `tool-${Date.now()}`,\n )\n}\n\nexport function resolveToolName(part: JsonRecord): string {\n return String(part.tool ?? part.name ?? 'tool')\n}\n\nexport function normalizeTime(value: unknown): JsonRecord | undefined {\n const record = asRecord(value)\n if (!record) return undefined\n\n const start = Number(record.start ?? record.startedAt ?? record.started_at)\n const end = Number(record.end ?? record.completedAt ?? record.completed_at)\n if (!Number.isFinite(start) && !Number.isFinite(end)) return undefined\n\n return {\n start: Number.isFinite(start) ? start : undefined,\n end: Number.isFinite(end) ? end : undefined,\n }\n}\n\nexport function normalizeToolEvent(event: StreamEvent): StreamEvent {\n if (event.type === 'tool_call' || event.type === 'tool.call') {\n const data = event.data ?? {}\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n input: data.arguments ?? data.input,\n status: 'running',\n },\n },\n }\n }\n\n if (event.type === 'tool_result' || event.type === 'tool.result') {\n const data = event.data ?? {}\n const error = asString(data.error)\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n output: data.output,\n error,\n status: error ? 'error' : 'completed',\n },\n },\n }\n }\n\n return event\n}\n\nexport function normalizePersistedPart(rawPart: JsonRecord): JsonRecord | null {\n const type = String(rawPart.type ?? '')\n\n if (type === 'text') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'text',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n // id: per-segment identity from the harness; absent on legacy parts,\n // which collapse to a single keyed segment. Never invented here.\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'reasoning') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'reasoning',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n time: normalizeTime(rawPart.time),\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'file' || type === 'image') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type,\n ...(id ? { id } : {}),\n ...(asString(rawPart.filename) ? { filename: asString(rawPart.filename) } : {}),\n ...(asString(rawPart.mediaType) ? { mediaType: asString(rawPart.mediaType) } : {}),\n ...(asString(rawPart.url) ? { url: asString(rawPart.url) } : {}),\n ...(asString(rawPart.path) ? { path: asString(rawPart.path) } : {}),\n ...(type === 'file' && asString(rawPart.content) ? { content: asString(rawPart.content) } : {}),\n }\n }\n\n if (type === 'step-start') {\n return { type: 'step-start' }\n }\n\n // The harness's per-step usage receipt. Dropping it here silently loses the\n // turn's token/cost accounting from the persisted transcript.\n if (type === 'step-finish') {\n const tokens = asRecord(rawPart.tokens)\n const cost = Number(rawPart.cost)\n return {\n type: 'step-finish',\n ...(asString(rawPart.reason) ? { reason: asString(rawPart.reason) } : {}),\n ...(tokens ? { tokens } : {}),\n ...(Number.isFinite(cost) ? { cost } : {}),\n }\n }\n\n if (type === 'subtask') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'subtask',\n prompt: asString(rawPart.prompt) ?? '',\n description: asString(rawPart.description) ?? '',\n agent: asString(rawPart.agent) ?? '',\n ...(id ? { id } : {}),\n }\n }\n\n // System-authored persisted parts (`/interactions` codecs) pass through\n // verbatim when a producer routes them here — they are already projections.\n if (type === 'interaction' || type === 'notice') {\n return rawPart\n }\n\n if (type === 'tool') {\n const state = asRecord(rawPart.state)\n const output = state?.output ?? rawPart.output\n const error = asString(state?.error ?? rawPart.error)\n const terminalError =\n state?.status === 'error' ||\n state?.status === 'failed' ||\n rawPart.status === 'error' ||\n rawPart.status === 'failed' ||\n Boolean(error)\n const status =\n state?.status === 'completed' || rawPart.status === 'completed'\n ? 'completed'\n : terminalError\n ? 'error'\n : output !== undefined\n ? 'completed'\n : 'running'\n\n return {\n type: 'tool',\n id: resolveToolId(rawPart),\n tool: resolveToolName(rawPart),\n callID:\n rawPart.callID != null || rawPart.callId != null\n ? String(rawPart.callID ?? rawPart.callId)\n : undefined,\n state: {\n status,\n input: state?.input ?? rawPart.input,\n output,\n error,\n metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),\n time: normalizeTime(state?.time ?? rawPart.time),\n },\n }\n }\n\n return null\n}\n\nexport function getPartKey(part: JsonRecord): string {\n const type = String(part.type ?? 'unknown')\n if (type === 'tool') {\n return `tool:${resolveToolId(part)}`\n }\n\n // Keyed by the part's OWN type so distinct kinds never merge into each\n // other. Untyped parts fall back to the text lane (legacy shape).\n const lane = type && type !== 'unknown' ? type : 'text'\n return `${lane}:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n}\n\n/** Shallow overlay that skips `undefined` incoming values, so a later partial\n * update never erases a field an earlier one captured. */\nfunction overlayDefined(base: JsonRecord, patch: JsonRecord): JsonRecord {\n const out: JsonRecord = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nexport function mergePersistedPart(existing: JsonRecord | undefined, incoming: JsonRecord, delta?: string): JsonRecord {\n const type = String(incoming.type ?? '')\n if (!existing) {\n if (type === 'text' && delta) {\n return { type: 'text', text: delta }\n }\n return incoming\n }\n\n if (type === 'text' && String(existing.type ?? '') === 'text') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n // An empty snapshot never erases accumulated text (matches reasoning).\n text: delta ? `${existingText}${delta}` : incomingText || existingText,\n }\n }\n\n if (type === 'reasoning' && String(existing.type ?? '') === 'reasoning') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,\n time: incoming.time ?? existing.time,\n }\n }\n\n if (type === 'tool' && String(existing.type ?? '') === 'tool') {\n const existingState = asRecord(existing.state) ?? {}\n const incomingState = asRecord(incoming.state) ?? {}\n // Overlay only DEFINED incoming fields: a normalized tool part always\n // carries `output`/`error` keys (undefined when not captured), so a plain\n // spread would clobber a completed tool's output with a later empty update.\n const mergedState = overlayDefined(existingState, incomingState)\n // A partial update with no captured status/output/error normalizes to\n // `running`; never let it downgrade a tool that already settled.\n const existingStatus = String(existingState.status ?? '')\n if (\n (existingStatus === 'completed' || existingStatus === 'error') &&\n String(incomingState.status ?? '') === 'running'\n ) {\n mergedState.status = existingStatus\n }\n return {\n ...overlayDefined(existing, incoming),\n state: mergedState,\n }\n }\n\n return incoming\n}\n\nexport function finalizeAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const parts = partOrder\n .map((key) => partMap.get(key))\n .filter((part): part is JsonRecord => Boolean(part))\n\n const textParts = parts.filter((part) => String(part.type ?? '') === 'text')\n\n if (textParts.length === 0) {\n if (finalText.trim()) {\n parts.push({ type: 'text', text: finalText })\n }\n return parts\n }\n\n // Id-less text parts form a single logical stream — the final text is\n // authoritative for it.\n if (!textParts.some((part) => asString(part.id))) {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'text') return part\n return {\n ...part,\n text: finalText || String(part.text ?? ''),\n }\n })\n }\n\n // Per-id text segments: invariant is concat(text parts) === persisted final\n // text, so segment boundaries survive without duplicating the answer into\n // every segment.\n const joined = textParts.map((part) => String(part.text ?? '')).join('')\n if (finalText === joined || finalText.trimEnd() === joined.trimEnd()) {\n return parts\n }\n\n if (finalText.startsWith(joined)) {\n // Final text extends the streamed segments (e.g. a failure diagnostic\n // appended after the stream) — persist the remainder as a trailing\n // id-less segment.\n return [...parts, { type: 'text', text: finalText.slice(joined.length) }]\n }\n\n // Final text replaced the streamed text outright. Keep non-text chronology;\n // collapse text to one authoritative segment at the last text position.\n const lastTextPart = textParts[textParts.length - 1]\n return parts\n .filter((part) => String(part.type ?? '') !== 'text' || part === lastTextPart)\n .map((part) => (part === lastTextPart ? { ...part, text: finalText } : part))\n}\n\nexport function encodeEvent(encoder: TextEncoder, event: StreamEvent): Uint8Array {\n return encoder.encode(`${JSON.stringify(event)}\\n`)\n}\n","import type { JsonRecord } from './stream-normalizer'\n\nexport interface PersistedChatMessageForTurn {\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts: Array<Record<string, unknown>> | null\n}\n\nexport interface ResolvedChatTurn {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: PersistedChatMessageForTurn[]\n userParts: JsonRecord[]\n}\n\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\nexport function messageHasTurnId(message: PersistedChatMessageForTurn, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\nexport function resolveChatTurn(input: {\n existingMessages: PersistedChatMessageForTurn[]\n userContent: string\n turnId?: string\n}): ResolvedChatTurn {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId)\n if (reusableIndex >= 0) {\n return {\n turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),\n shouldInsertUserMessage: false,\n priorMessages: existingMessages.slice(0, reusableIndex),\n userParts: buildUserTextParts(userContent, turnId),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages,\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: PersistedChatMessageForTurn[],\n userContent: string,\n turnId: string | undefined,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n }\n\n const latest = messages.at(-1)\n if (latest?.role === 'user' && latest.content === userContent) {\n return messages.length - 1\n }\n\n return -1\n}\n\nfunction countUserMessages(messages: PersistedChatMessageForTurn[]): number {\n return messages.filter((message) => message.role === 'user').length\n}\n","/**\n * Resumable chat turns — the router-path answer to \"streams resume on\n * disconnect\" (issue #27). A turn's loop events are teed into a store as they\n * stream; the turn keeps running under `ctx.waitUntil` when the client drops;\n * a reconnecting client replays the buffered tail by sequence number and\n * keeps following until the turn completes.\n *\n * POST /chat/stream → pumpBufferedTurn(...) + live NDJSON\n * GET /chat/stream/:turnId → replayTurnEvents({ fromSeq }) → NDJSON\n *\n * Storage is a structural seam ({@link TurnEventStore}); a D1 implementation\n * ships here because that's what Cloudflare products have (KV is unsuitable:\n * eventually consistent cross-isolate). Per-token deltas would mean hundreds\n * of rows per turn, so consecutive text/reasoning deltas are coalesced within\n * a flush window before they are persisted — replay yields slightly chunkier\n * deltas with identical concatenation.\n */\n\nexport type TurnStatus = 'running' | 'complete' | 'error'\n\nexport interface BufferedTurnEvent {\n seq: number\n /** The serialized event line (JSON string, no trailing newline). */\n event: string\n}\n\nexport interface TurnEventStore {\n append(turnId: string, events: BufferedTurnEvent[]): Promise<void>\n read(turnId: string, fromSeq: number): Promise<BufferedTurnEvent[]>\n /** Record turn lifecycle. `scopeId` (a thread/session id) is optional and lets\n * {@link TurnEventStore.listRunning} rediscover this turn after a client reload\n * loses the turnId; stores that don't track scope ignore it. */\n setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>\n getStatus(turnId: string): Promise<TurnStatus | null>\n /** Running turnIds for a scope, newest first — so a reloaded client (clientRunId\n * lost) can find and resume the in-flight turn. Optional: a store records it\n * only if `setStatus` was given a `scopeId`. */\n listRunning?(scopeId: string): Promise<string[]>\n}\n\n// ── coalescing ────────────────────────────────────────────────────────────\n\ntype AnyRecord = Record<string, unknown>\n\nfunction deltaTypeOf(ev: unknown): 'text' | 'reasoning' | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object') return null\n const inner = (e.kind === 'event' ? (e.event as AnyRecord | undefined) : e) as AnyRecord | undefined\n if (!inner || typeof inner !== 'object') return null\n if ((inner.type === 'text' || inner.type === 'reasoning') && typeof inner.text === 'string') {\n return inner.type\n }\n return null\n}\n\n/** Merge consecutive text/reasoning deltas of the same type into one event.\n * Concatenation-preserving: replaying the coalesced stream produces the same\n * accumulated text as the original. */\nexport function coalesceDeltas(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const type = deltaTypeOf(ev)\n const prev = out[out.length - 1]\n if (type && prev && deltaTypeOf(prev) === type) {\n const read = (x: unknown): AnyRecord =>\n ((x as AnyRecord).kind === 'event' ? (x as AnyRecord).event : x) as AnyRecord\n const merged = JSON.parse(JSON.stringify(prev)) as AnyRecord\n read(merged).text = String(read(prev).text) + String(read(ev).text)\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\nfunction asPartUpdate(ev: unknown): { partId: unknown; delta: unknown } | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object' || e.type !== 'message.part.updated') return null\n const data = e.data as AnyRecord | undefined\n if (!data || typeof data !== 'object') return null\n const part = data.part as AnyRecord | undefined\n const partId = part?.id ?? data.partId ?? part?.partId ?? null\n return { partId, delta: data.delta }\n}\n\n/**\n * Coalesce consecutive `message.part.updated` deltas for the SAME part into one\n * event. agent-runtime products stream `ChatStreamEvent` NDJSON\n * (`{type:'message.part.updated', data:{part, delta}}`); pumped through the\n * buffer with the default tool-loop coalescer, every per-token delta persists as\n * its own row because that coalescer never recognizes the shape. Pass this as\n * {@link PumpBufferedTurnOptions.coalesce} instead.\n *\n * Concatenation-preserving for BOTH consumer styles: the merged event keeps the\n * LATEST event's `data.part` (already the cumulative accumulation) and sets\n * `data.delta` to the concatenation of the merged deltas, so a client that\n * appends `delta` and one that reads the cumulative `part` both reconstruct the\n * identical final text.\n */\nexport function coalesceChatStreamEvents(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const cur = asPartUpdate(ev)\n const prevEv = out[out.length - 1]\n const prev = prevEv ? asPartUpdate(prevEv) : null\n if (cur && prev && cur.partId != null && cur.partId === prev.partId) {\n // Base the merged row on the latest event (its `part` is the most complete\n // accumulation); carry forward the summed delta.\n const merged = JSON.parse(JSON.stringify(ev)) as AnyRecord\n ;(merged.data as AnyRecord).delta = String(prev.delta ?? '') + String(cur.delta ?? '')\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\n// ── buffering core (the tap) ────────────────────────────────────────────────\n\nexport interface BufferedTurnOptions {\n store: TurnEventStore\n turnId: string\n /** Deliver one serialized line to the live client. Throwing here (client\n * disconnected) does NOT stop buffering — events keep persisting. */\n write?: (line: string) => Promise<void> | void\n /** Flush buffered events to the store at most this often. Default 400ms. */\n flushIntervalMs?: number\n /** Per-flush coalescer. Default {@link coalesceDeltas} (tool-loop text/reasoning\n * deltas). agent-runtime products streaming `ChatStreamEvent` pass\n * {@link coalesceChatStreamEvents} so per-token deltas don't each persist as a\n * row. Must be concatenation-preserving. */\n coalesce?: (events: unknown[]) => unknown[]\n /** Optional scope (thread/session id) recorded with the turn status, so\n * {@link TurnEventStore.listRunning} can find this turn after a reload. */\n scopeId?: string\n}\n\n/** A push-driven buffer for a turn whose producer the caller does NOT own. */\nexport interface BufferedTurnTap {\n /** Buffer one event: persist (coalesced, on the flush window) + best-effort\n * live-deliver. Wire to a push source's per-event hook (e.g. agent-runtime\n * `handleChatTurn`'s `hooks.onEvent`). Marks the turn 'running' on first call. */\n onEvent(raw: unknown): Promise<void>\n /** Settle the turn: final flush + set status. Call after the producer resolves\n * ('complete') or rejects ('error'). 'error' flushes what was produced first. */\n done(status?: Extract<TurnStatus, 'complete' | 'error'>): Promise<void>\n}\n\n/**\n * The buffering core. Sequence-numbers every event, delivers it to `write`\n * (best-effort — a disconnected client never stops buffering), and flushes to\n * the store in coalesced batches. Drives both transports:\n *\n * • {@link pumpBufferedTurn} — when you OWN an `AsyncIterable` producer.\n * • this tap (`onEvent`/`done`) — when the producer owns iteration and only\n * hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`\n * + the finished body). Durability stays here in the shell; the engine needs\n * no `TurnEventStore` seam.\n */\nexport function createBufferedTurnTap(opts: BufferedTurnOptions): BufferedTurnTap {\n const flushIntervalMs = opts.flushIntervalMs ?? 400\n const coalesce = opts.coalesce ?? coalesceDeltas\n const startedAt = Date.now()\n let seq = 0\n let clientGone = false\n let pending: unknown[] = []\n let lastFlush = Date.now()\n let started = false\n\n async function flush(): Promise<void> {\n if (pending.length === 0) return\n const batch = coalesce(pending)\n pending = []\n const rows = batch.map((ev) => ({ seq: ++seq, event: JSON.stringify(ev) }))\n await opts.store.append(opts.turnId, rows)\n lastFlush = Date.now()\n }\n\n async function ensureStarted(): Promise<void> {\n if (started) return\n started = true\n await opts.store.setStatus(opts.turnId, 'running', opts.scopeId)\n }\n\n return {\n async onEvent(raw) {\n await ensureStarted()\n // Stamp ms-since-turn-start so any stored turn is replayable AND traceable\n // (see ../trace) from the same buffered rows.\n const ev = raw && typeof raw === 'object' ? { ...(raw as Record<string, unknown>), _t: Date.now() - startedAt } : raw\n pending.push(ev)\n if (!clientGone && opts.write) {\n try {\n // Live delivery carries a provisional ordering hint, not the persisted\n // seq (coalescing changes seq assignment); clients resume with the\n // seqs from replay, or 0 for \"everything\".\n await opts.write(JSON.stringify(ev))\n } catch {\n clientGone = true\n }\n }\n if (Date.now() - lastFlush >= flushIntervalMs) await flush()\n },\n async done(status = 'complete') {\n await ensureStarted()\n if (status === 'error') {\n await flush().catch(() => {})\n await opts.store.setStatus(opts.turnId, 'error', opts.scopeId).catch(() => {})\n return\n }\n await flush()\n await opts.store.setStatus(opts.turnId, 'complete', opts.scopeId)\n },\n }\n}\n\n// ── pump (producer side) ──────────────────────────────────────────────────\n\nexport interface PumpBufferedTurnOptions extends BufferedTurnOptions {\n source: AsyncIterable<unknown>\n}\n\n/**\n * Drive a turn to completion regardless of the live client, when you OWN the\n * producer as an `AsyncIterable`. A thin driver over {@link createBufferedTurnTap}.\n * Returns a promise that resolves when the turn finishes — hand it to\n * `ctx.waitUntil` so a disconnect can't kill the turn. Never rejects on\n * client-write failure; a source error marks the turn 'error' (after flushing\n * what was produced) and rethrows.\n */\nexport async function pumpBufferedTurn(opts: PumpBufferedTurnOptions): Promise<void> {\n const tap = createBufferedTurnTap(opts)\n try {\n for await (const raw of opts.source) await tap.onEvent(raw)\n await tap.done('complete')\n } catch (err) {\n await tap.done('error')\n throw err\n }\n}\n\n// ── replay (consumer side) ────────────────────────────────────────────────\n\nexport interface ReplayTurnEventsOptions {\n store: TurnEventStore\n turnId: string\n /** Replay strictly after this sequence number (0 = from the beginning). */\n fromSeq?: number\n /** Poll cadence while the turn is still running. Default 500ms. */\n pollMs?: number\n /** Give up following a 'running' turn after this long. Default 120s. */\n timeoutMs?: number\n}\n\n/**\n * Yield buffered events after `fromSeq`, then keep polling while the turn is\n * still 'running' until it completes, errors, or times out. Terminates with a\n * final `{seq: -1, event: '{\"type\":\"turn_status\",...}'}` marker so clients\n * know why the replay ended.\n */\nexport async function* replayTurnEvents(opts: ReplayTurnEventsOptions): AsyncGenerator<BufferedTurnEvent> {\n const pollMs = opts.pollMs ?? 500\n const timeoutMs = opts.timeoutMs ?? 120_000\n let cursor = opts.fromSeq ?? 0\n const deadline = Date.now() + timeoutMs\n\n for (;;) {\n const batch = await opts.store.read(opts.turnId, cursor)\n for (const row of batch) {\n cursor = Math.max(cursor, row.seq)\n yield row\n }\n const status = await opts.store.getStatus(opts.turnId)\n if (status !== 'running') {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: status ?? 'unknown' }) }\n return\n }\n if (Date.now() >= deadline) {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: 'timeout' }) }\n return\n }\n await new Promise((r) => setTimeout(r, pollMs))\n }\n}\n\n// ── D1 store ──────────────────────────────────────────────────────────────\n\n/** Minimal structural D1 contract (Cloudflare `D1Database` satisfies it). */\nexport interface D1LikeForTurns {\n prepare(sql: string): {\n bind(...values: unknown[]): {\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n first<T = Record<string, unknown>>(): Promise<T | null>\n }\n }\n}\n\n/** Schema for the D1 store — append to the product's migrations. */\nexport const TURN_EVENTS_MIGRATION_SQL = `\nCREATE TABLE IF NOT EXISTS turn_events (\n turnId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n event TEXT NOT NULL,\n PRIMARY KEY (turnId, seq)\n);\nCREATE TABLE IF NOT EXISTS turn_status (\n turnId TEXT PRIMARY KEY,\n status TEXT NOT NULL,\n scopeId TEXT,\n updatedAt TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);\n`\n\n/** For deployments whose `turn_status` table predates `scopeId`/`listRunning` —\n * run once to add the column (the CREATE above already includes it for new\n * deployments). SQLite ignores a duplicate-add error if already applied. */\nexport const TURN_STATUS_SCOPE_MIGRATION_SQL = `ALTER TABLE turn_status ADD COLUMN scopeId TEXT;`\n\nexport function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore {\n return {\n async append(turnId, events) {\n if (!events.length) return\n // One multi-row insert per flush window keeps write volume bounded.\n const placeholders = events.map(() => '(?, ?, ?)').join(', ')\n const values = events.flatMap((e) => [turnId, e.seq, e.event])\n await db.prepare(`INSERT OR IGNORE INTO turn_events (turnId, seq, event) VALUES ${placeholders}`).bind(...values).run()\n },\n async read(turnId, fromSeq) {\n const { results } = await db\n .prepare('SELECT seq, event FROM turn_events WHERE turnId = ? AND seq > ? ORDER BY seq ASC')\n .bind(turnId, fromSeq)\n .all<{ seq: number; event: string }>()\n return results\n },\n async setStatus(turnId, status, scopeId) {\n // COALESCE preserves a scopeId set on the initial 'running' write when a\n // later 'complete'/'error' write passes none.\n await db\n .prepare(\n 'INSERT INTO turn_status (turnId, status, scopeId, updatedAt) VALUES (?, ?, ?, ?) ON CONFLICT(turnId) DO UPDATE SET status = excluded.status, scopeId = COALESCE(excluded.scopeId, turn_status.scopeId), updatedAt = excluded.updatedAt',\n )\n .bind(turnId, status, scopeId ?? null, new Date().toISOString())\n .run()\n },\n async getStatus(turnId) {\n const row = await db.prepare('SELECT status FROM turn_status WHERE turnId = ?').bind(turnId).first<{ status: TurnStatus }>()\n return row?.status ?? null\n },\n async listRunning(scopeId) {\n const { results } = await db\n .prepare(\"SELECT turnId FROM turn_status WHERE scopeId = ? AND status = 'running' ORDER BY updatedAt DESC\")\n .bind(scopeId)\n .all<{ turnId: string }>()\n return results.map((r) => r.turnId)\n },\n }\n}\n\n/** In-memory store for tests and keyless local dev. */\nexport function createMemoryTurnEventStore(): TurnEventStore {\n const events = new Map<string, BufferedTurnEvent[]>()\n const status = new Map<string, TurnStatus>()\n const scopes = new Map<string, string>()\n const order: string[] = []\n return {\n async append(turnId, rows) {\n const list = events.get(turnId) ?? []\n list.push(...rows)\n events.set(turnId, list)\n },\n async read(turnId, fromSeq) {\n return (events.get(turnId) ?? []).filter((e) => e.seq > fromSeq)\n },\n async setStatus(turnId, s, scopeId) {\n status.set(turnId, s)\n if (scopeId) scopes.set(turnId, scopeId)\n if (!order.includes(turnId)) order.push(turnId)\n },\n async getStatus(turnId) {\n return status.get(turnId) ?? null\n },\n async listRunning(scopeId) {\n // Newest first, mirroring the D1 store's `ORDER BY updatedAt DESC`.\n return [...order].reverse().filter((t) => status.get(t) === 'running' && scopes.get(t) === scopeId)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/interactions/sidecar.ts","../src/interactions/route.ts"],"sourcesContent":["/**\n * Server-side client for the sandbox sidecar's generic interaction routes\n * (`GET/POST {runtimeUrl}/agents/sessions/{sessionId}/interactions`). The\n * pinned sandbox SDK exposes only the question-specific `session().answer()`\n * convenience; these raw calls are backend-agnostic (question/permission/plan,\n * any harness) and carry explicit outcomes (accepted/declined).\n *\n * Server-only: the sidecar bearer must never reach browser code. The caller\n * supplies the connection as a structural value (runtime URL + bearer +\n * session id) — no sandbox-SDK import, so any box-resolution strategy works.\n */\n\nimport type { InteractionData, InteractionOutcome, InteractionRequestWire } from './contract'\n\nexport interface SidecarInteractionsError {\n code: string\n message: string\n /** Upstream HTTP status; 0 when the sidecar was unreachable. */\n status: number\n}\n\nexport type SidecarInteractionsResult<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: SidecarInteractionsError }\n\n/** Where and how to reach one session's interaction registry. */\nexport interface SidecarInteractionsConnection {\n runtimeUrl: string\n authToken?: string\n /** The sidecar agent-session id (the chat thread's session). */\n sessionId: string\n /** Request deadline. A pending interaction means the box is up and the\n * sidecar responsive; a short default keeps a wedged runtime from stalling\n * the answering request. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global fetch. */\n fetchImpl?: typeof fetch\n}\n\nconst DEFAULT_TIMEOUT_MS = 5_000\n\n/** Strips bearer tokens / key material before an upstream message is logged\n * or surfaced. */\nfunction sanitizeUpstreamMessage(input: unknown): string {\n const message = input instanceof Error ? input.message : String(input)\n return message\n .replace(/Bearer\\s+[^\\s]+/gi, 'Bearer [redacted]')\n .replace(/\\b(?:sk|pk|tc)[_-][A-Za-z0-9_-]{8,}\\b/g, '[redacted-key]')\n}\n\nasync function interactionsFetch(\n connection: SidecarInteractionsConnection,\n init: { method: 'GET' } | { method: 'POST'; body: Record<string, unknown> },\n): Promise<SidecarInteractionsResult<Record<string, unknown>>> {\n const doFetch = connection.fetchImpl ?? fetch\n const url = `${connection.runtimeUrl.replace(/\\/$/, '')}/agents/sessions/${encodeURIComponent(connection.sessionId)}/interactions`\n let response: Response\n try {\n response = await doFetch(url, {\n method: init.method,\n headers: {\n ...(connection.authToken ? { Authorization: `Bearer ${connection.authToken}` } : {}),\n ...(init.method === 'POST' ? { 'Content-Type': 'application/json' } : {}),\n },\n ...(init.method === 'POST' ? { body: JSON.stringify(init.body) } : {}),\n signal: AbortSignal.timeout(connection.timeoutMs ?? DEFAULT_TIMEOUT_MS),\n })\n } catch (err) {\n return {\n succeeded: false,\n error: { code: 'UPSTREAM_UNREACHABLE', message: sanitizeUpstreamMessage(err), status: 0 },\n }\n }\n const raw = await response.text().catch(() => '')\n let parsed: Record<string, unknown> = {}\n try {\n parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {}\n } catch {\n // Non-JSON error bodies (proxy 502 pages) fall through to the status check.\n }\n if (!response.ok) {\n const upstreamError = (parsed.error ?? {}) as { code?: unknown; message?: unknown }\n return {\n succeeded: false,\n error: {\n code: typeof upstreamError.code === 'string' && upstreamError.code ? upstreamError.code : 'UPSTREAM_ERROR',\n message: sanitizeUpstreamMessage(\n typeof upstreamError.message === 'string' && upstreamError.message\n ? upstreamError.message\n : `sidecar interactions ${init.method} failed (${response.status})`,\n ),\n status: response.status,\n },\n }\n }\n return { succeeded: true, value: parsed }\n}\n\n/** Outstanding (unanswered) interactions for the session — the sidecar's\n * registry is authoritative, so this is the reconnect/reload source of truth. */\nexport async function listSessionInteractions(\n connection: SidecarInteractionsConnection,\n): Promise<SidecarInteractionsResult<InteractionRequestWire[]>> {\n const result = await interactionsFetch(connection, { method: 'GET' })\n if (!result.succeeded) return result\n const data = result.value.data as { interactions?: unknown } | undefined\n if (!Array.isArray(data?.interactions)) {\n return {\n succeeded: false,\n error: { code: 'MALFORMED_RESPONSE', message: 'sidecar list returned no interactions array', status: 200 },\n }\n }\n return { succeeded: true, value: data.interactions as InteractionRequestWire[] }\n}\n\n/** Resolves one interaction. `data` is required by the sidecar only for\n * `accepted` outcomes and is validated fail-closed against the answerSpec\n * (400 INVALID_INTERACTION_ANSWER on mismatch). */\nexport async function respondToSessionInteraction(\n connection: SidecarInteractionsConnection,\n response: { id: string; outcome: InteractionOutcome; data?: InteractionData },\n): Promise<SidecarInteractionsResult<void>> {\n const result = await interactionsFetch(connection, {\n method: 'POST',\n body: {\n id: response.id,\n outcome: response.outcome,\n ...(response.data ? { data: response.data } : {}),\n },\n })\n if (!result.succeeded) return result\n return { succeeded: true, value: undefined }\n}\n","/**\n * Framework-neutral interaction-answer endpoints, lifted out of the per-app\n * route files (gtm `api.chat.interactions`, legal `api.chat.interactions`,\n * tax `api.sessions.$id.interactions` — three byte-similar forks):\n *\n * list(request) — GET: outstanding asks for a live turn (reload restore)\n * answer(request) — POST `{ id, outcome, data? }`: resolve one ask\n *\n * The product supplies ONE seam, `resolveConnection`: authenticate the caller,\n * authorize the thread/session, and resolve the sidecar connection. Everything\n * behind the seam is mechanism the forks kept re-fixing:\n *\n * - body validation (safe field keys, typed values),\n * - sidecar error → client contract mapping (every \"the ask is gone\" shape\n * becomes 410 so the card flips to its expired state),\n * - duplicate resolution: after answering, every other outstanding ask with\n * the same content signature (a re-emitted duplicate) gets the same answer,\n * - unblock verification: re-list and fail loud (503 INTERACTION_STILL_PENDING)\n * when the sidecar accepted the POST but the ask is still open,\n * - best-effort list: sidecar failures return `{ interactions: [],\n * unavailable }` so a reload restore never breaks the live stream.\n *\n * Handlers return web-standard `Response`s (Workers, Node 18+, Deno) — no\n * router import anywhere.\n */\n\nimport {\n interactionFromWireRequest,\n isSafeInteractionFieldKey,\n questionInteractionContentSignature,\n type InteractionData,\n} from './contract'\nimport {\n listSessionInteractions,\n respondToSessionInteraction,\n type SidecarInteractionsConnection,\n type SidecarInteractionsError,\n} from './sidecar'\n\n// A client resolves an ask by answering (`accepted`) or refusing (`declined`).\n// Withdrawal (`cancelled`) is an agent/broker outcome delivered via the\n// `interaction.cancel` event, never a client POST, so it is not accepted here.\nexport type InteractionClientOutcome = 'accepted' | 'declined'\n\nexport type InteractionAnswerBodyValidation =\n | { ok: true; id: string; outcome: InteractionClientOutcome; data?: InteractionData }\n | { ok: false; error: string }\n\n/** Validates the client POST body: `{ id, outcome, data? }` with\n * identifier-safe field keys and primitive/string-array values only. */\nexport function validateInteractionAnswerBody(body: Record<string, unknown>): InteractionAnswerBodyValidation {\n const id = typeof body.id === 'string' && body.id ? body.id : null\n if (!id) return { ok: false, error: 'Missing interaction id' }\n const outcome = body.outcome\n if (outcome !== 'accepted' && outcome !== 'declined') {\n return { ok: false, error: 'Invalid outcome: expected accepted or declined' }\n }\n if (body.data === undefined) return { ok: true, id, outcome }\n if (!body.data || typeof body.data !== 'object' || Array.isArray(body.data)) {\n return { ok: false, error: 'Invalid data: expected an object of field values' }\n }\n const data: InteractionData = {}\n for (const [key, value] of Object.entries(body.data)) {\n if (!isSafeInteractionFieldKey(key)) {\n return { ok: false, error: 'Invalid data: field names must contain only letters, numbers, underscores, or hyphens' }\n }\n const validValue =\n typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ||\n (Array.isArray(value) && value.every((item) => typeof item === 'string'))\n if (!validValue) {\n return { ok: false, error: 'Invalid data: field values must be strings, numbers, booleans, or string arrays' }\n }\n data[key] = value as InteractionData[string]\n }\n return { ok: true, id, outcome, data }\n}\n\nexport type InteractionRouteLogger = Pick<Console, 'warn' | 'error'>\n\n/** Sidecar error → the client-actionable contract. Every \"the ask is gone\"\n * shape maps to 410 so the card flips to its expired state — a raw 404/409\n * must never surface. */\nexport function mapInteractionRespondFailure(\n error: SidecarInteractionsError,\n logger: InteractionRouteLogger = console,\n): Response {\n if (error.code === 'INVALID_INTERACTION_ANSWER') {\n return Response.json(\n { code: 'INVALID_INTERACTION_ANSWER', error: 'This question needs an answer from the card above — pick one of the listed options.' },\n { status: 400 },\n )\n }\n if (error.status === 404) {\n return Response.json(\n { code: 'INTERACTION_EXPIRED', error: 'This question is no longer waiting for an answer.' },\n { status: 410 },\n )\n }\n if (error.status === 501 || error.code === 'NOT_IMPLEMENTED') {\n return Response.json(\n { code: 'INTERACTIONS_UNSUPPORTED', error: 'This agent backend cannot accept answers this way.' },\n { status: 501 },\n )\n }\n logger.error('[interactions] respond failed:', error)\n return Response.json(\n { code: 'INTERACTION_UPSTREAM_FAILED', error: 'Could not reach the agent. Try again.' },\n { status: 503 },\n )\n}\n\n/** The product seam's verdict for one request. `response` short-circuits with\n * a product-authored Response (401/404/429…); `unavailable` means the caller\n * is fine but the sandbox runtime is not reachable — the factory shapes that\n * per intent (empty list for `list`, 503 for `answer`). */\nexport type InteractionConnectionResolution =\n | { ok: true; connection: SidecarInteractionsConnection }\n | { ok: false; response: Response }\n | { ok: false; unavailable: string }\n\nexport interface ResolveInteractionConnectionArgs {\n request: Request\n intent: 'list' | 'answer'\n /** The parsed, validated POST body (answer intent only) so the resolver can\n * read product routing fields (workspaceId/threadId) without re-parsing. */\n body?: Record<string, unknown>\n}\n\nexport interface InteractionAnswerRouteOptions {\n /** Authenticate + authorize the caller and resolve the sidecar connection.\n * This is the only product-supplied step: session auth, workspace/thread\n * access, rate limiting, and box resolution all live here. */\n resolveConnection: (args: ResolveInteractionConnectionArgs) => Promise<InteractionConnectionResolution>\n logger?: InteractionRouteLogger\n}\n\nexport interface InteractionAnswerRoute {\n /** GET — outstanding interactions for a live turn. Failures return an empty\n * list with an explicit `unavailable` code: the caller is a best-effort\n * reload restore, and the live/replayed stream must stay untouched when the\n * sidecar cannot answer. */\n list: (request: Request) => Promise<Response>\n /** POST `{ id, outcome, data?, ...productFields }` — resolve one ask, answer\n * content-identical duplicates the same way, then re-list to prove the run\n * actually unblocked. */\n answer: (request: Request) => Promise<Response>\n}\n\nexport function createInteractionAnswerRoute(options: InteractionAnswerRouteOptions): InteractionAnswerRoute {\n const logger = options.logger ?? console\n\n async function list(request: Request): Promise<Response> {\n const resolution = await options.resolveConnection({ request, intent: 'list' })\n if (!resolution.ok) {\n if ('response' in resolution) return resolution.response\n return Response.json({ interactions: [], unavailable: resolution.unavailable })\n }\n const result = await listSessionInteractions(resolution.connection)\n if (!result.succeeded) {\n logger.warn('[interactions] list failed:', result.error)\n return Response.json({ interactions: [], unavailable: result.error.code })\n }\n return Response.json({ interactions: result.value })\n }\n\n async function answer(request: Request): Promise<Response> {\n const body = await request.json().catch(() => null) as Record<string, unknown> | null\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const validation = validateInteractionAnswerBody(body)\n if (!validation.ok) return Response.json({ error: validation.error }, { status: 400 })\n\n const resolution = await options.resolveConnection({ request, intent: 'answer', body })\n if (!resolution.ok) {\n if ('response' in resolution) return resolution.response\n return mapInteractionRespondFailure(\n { code: resolution.unavailable, message: 'sandbox runtime unavailable', status: 0 },\n logger,\n )\n }\n const connection = resolution.connection\n const answerPayload = {\n outcome: validation.outcome,\n ...(validation.data ? { data: validation.data } : {}),\n }\n\n // Snapshot the answered ask's content signature BEFORE resolving it, so any\n // content-identical duplicates still outstanding afterwards (the agent may\n // have re-emitted the same question N times) can be answered the same way.\n const before = await listSessionInteractions(connection)\n const answeredRequest = before.succeeded ? before.value.find((item) => item.id === validation.id) : undefined\n const answeredSignature = answeredRequest\n ? questionInteractionContentSignature(interactionFromWireRequest(answeredRequest))\n : null\n\n const result = await respondToSessionInteraction(connection, { id: validation.id, ...answerPayload })\n if (!result.succeeded) return mapInteractionRespondFailure(result.error, logger)\n\n let remaining = await listSessionInteractions(connection)\n if (remaining.succeeded && answeredSignature) {\n const duplicateRequests = remaining.value.filter((item) => {\n if (item.id === validation.id) return false\n return questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature\n })\n for (const duplicate of duplicateRequests) {\n const duplicateResult = await respondToSessionInteraction(connection, { id: duplicate.id, ...answerPayload })\n if (!duplicateResult.succeeded) break\n }\n if (duplicateRequests.length > 0) remaining = await listSessionInteractions(connection)\n }\n\n // Prove the run actually unblocked: neither the answered id nor any\n // content-duplicate may still be outstanding. If one is, the sidecar\n // accepted the POST but did not release the run — report it rather than\n // tell the user it was answered.\n if (remaining.succeeded && remaining.value.some((item) => item.id === validation.id || (\n answeredSignature && questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature\n ))) {\n logger.error('[interactions] respond returned ok but interaction is still pending:', {\n sessionId: connection.sessionId,\n interactionId: validation.id,\n })\n return Response.json(\n { code: 'INTERACTION_STILL_PENDING', error: 'The agent did not accept the answer. Try answering again.' },\n { status: 503 },\n )\n }\n\n return Response.json({ ok: true })\n }\n\n return { list, answer }\n}\n"],"mappings":"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","names":[]}
|