@tangle-network/agent-app 0.43.40 → 0.43.42

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.
Files changed (53) hide show
  1. package/dist/assistant/index.d.ts +3 -2
  2. package/dist/assistant/index.js +4 -3
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/chat-routes/index.d.ts +19 -5
  5. package/dist/chat-routes/index.js +100 -7
  6. package/dist/chat-routes/index.js.map +1 -1
  7. package/dist/chat-store/index.d.ts +4 -3
  8. package/dist/chat-store/index.js +5 -1
  9. package/dist/chat-store/index.js.map +1 -1
  10. package/dist/chunk-2EO7CPL3.js +191 -0
  11. package/dist/chunk-2EO7CPL3.js.map +1 -0
  12. package/dist/{chunk-Y4QHNQ75.js → chunk-6KVH5SP5.js} +110 -6
  13. package/dist/chunk-6KVH5SP5.js.map +1 -0
  14. package/dist/{chunk-XYWCGFII.js → chunk-B5JD3DXD.js} +48 -2
  15. package/dist/chunk-B5JD3DXD.js.map +1 -0
  16. package/dist/{chunk-I2R2XT4M.js → chunk-I2ATYB7R.js} +18 -2
  17. package/dist/chunk-I2ATYB7R.js.map +1 -0
  18. package/dist/chunk-SIXYZ2FB.js +101 -0
  19. package/dist/chunk-SIXYZ2FB.js.map +1 -0
  20. package/dist/{chunk-4TXDD6P2.js → chunk-XAWFPMAR.js} +38 -3
  21. package/dist/chunk-XAWFPMAR.js.map +1 -0
  22. package/dist/{chunk-D4HB72W2.js → chunk-ZLHK25C3.js} +1890 -1213
  23. package/dist/chunk-ZLHK25C3.js.map +1 -0
  24. package/dist/chunk-ZU5GNSOJ.js +920 -0
  25. package/dist/chunk-ZU5GNSOJ.js.map +1 -0
  26. package/dist/{contract-DYbTzEDf.d.ts → contract-KfqJh_au.d.ts} +23 -2
  27. package/dist/design-canvas-react/index.js +4 -4
  28. package/dist/durable-chat/index.d.ts +419 -0
  29. package/dist/durable-chat/index.js +59 -0
  30. package/dist/durable-chat/index.js.map +1 -0
  31. package/dist/file-index-Bw_IQE_G.d.ts +194 -0
  32. package/dist/index.d.ts +5 -3
  33. package/dist/index.js +123 -45
  34. package/dist/interactions/index.d.ts +58 -3
  35. package/dist/interactions/index.js +7 -3
  36. package/dist/{parts-BcbitSNp.d.ts → parts-DjX0RRTS.d.ts} +9 -3
  37. package/dist/plans/index.d.ts +69 -0
  38. package/dist/plans/index.js +21 -0
  39. package/dist/plans/index.js.map +1 -0
  40. package/dist/stream/index.js +3 -1
  41. package/dist/teams/index.js +9 -9
  42. package/dist/teams/invitations-api.js +3 -3
  43. package/dist/web-react/index.d.ts +345 -94
  44. package/dist/web-react/index.js +50 -5
  45. package/package.json +11 -1
  46. package/dist/chunk-4TXDD6P2.js.map +0 -1
  47. package/dist/chunk-5SV5PSU7.js +0 -80
  48. package/dist/chunk-5SV5PSU7.js.map +0 -1
  49. package/dist/chunk-D4HB72W2.js.map +0 -1
  50. package/dist/chunk-I2R2XT4M.js.map +0 -1
  51. package/dist/chunk-XYWCGFII.js.map +0 -1
  52. package/dist/chunk-Y4QHNQ75.js.map +0 -1
  53. package/dist/wire-BaUF66AS.d.ts +0 -61
@@ -1,3 +1,14 @@
1
+ import {
2
+ canTransitionInteractionStatus,
3
+ persistedPartToInteraction
4
+ } from "./chunk-XAWFPMAR.js";
5
+ import {
6
+ canTransitionPlanStatus,
7
+ persistedPartToPlan,
8
+ planPartKey,
9
+ planToPersistedPart
10
+ } from "./chunk-SIXYZ2FB.js";
11
+
1
12
  // src/stream/stream-normalizer.ts
2
13
  function asRecord(value) {
3
14
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -115,7 +126,14 @@ function normalizePersistedPart(rawPart) {
115
126
  ...id ? { id } : {}
116
127
  };
117
128
  }
118
- if (type === "interaction" || type === "notice") {
129
+ if (type === "interaction") {
130
+ return persistedPartToInteraction(rawPart) ? rawPart : null;
131
+ }
132
+ if (type === "plan") {
133
+ const plan = persistedPartToPlan(rawPart);
134
+ return plan ? { ...rawPart, ...planToPersistedPart(plan) } : null;
135
+ }
136
+ if (type === "notice") {
119
137
  return rawPart;
120
138
  }
121
139
  if (type === "tool") {
@@ -146,6 +164,7 @@ function getPartKey(part) {
146
164
  if (type === "tool") {
147
165
  return `tool:${resolveToolId(part)}`;
148
166
  }
167
+ if (type === "plan") return planPartKey(String(part.planId ?? ""));
149
168
  const lane = type && type !== "unknown" ? type : "text";
150
169
  return `${lane}:${String(part.id ?? part.partId ?? part.index ?? "current")}`;
151
170
  }
@@ -197,6 +216,33 @@ function mergePersistedPart(existing, incoming, delta) {
197
216
  state: mergedState
198
217
  };
199
218
  }
219
+ if (type === "interaction" && String(existing.type ?? "") === "interaction") {
220
+ const merged = overlayDefined(existing, incoming);
221
+ const existingStatus = existing.status;
222
+ const incomingStatus = incoming.status;
223
+ if (existingStatus && incomingStatus && existingStatus !== incomingStatus && !canTransitionInteractionStatus(existingStatus, incomingStatus)) {
224
+ merged.status = existingStatus;
225
+ }
226
+ if (incoming.answers === void 0 && existing.answers !== void 0) {
227
+ merged.answers = existing.answers;
228
+ }
229
+ return merged;
230
+ }
231
+ if (type === "plan" && String(existing.type ?? "") === "plan") {
232
+ const existingRevision = Number(existing.revision);
233
+ const incomingRevision = Number(incoming.revision);
234
+ if (Number.isInteger(existingRevision) && Number.isInteger(incomingRevision)) {
235
+ if (incomingRevision < existingRevision) return existing;
236
+ if (incomingRevision > existingRevision) return incoming;
237
+ }
238
+ const merged = overlayDefined(existing, incoming);
239
+ const existingStatus = existing.status;
240
+ const incomingStatus = incoming.status;
241
+ if (existingStatus && incomingStatus && existingStatus !== incomingStatus && !canTransitionPlanStatus(existingStatus, incomingStatus)) {
242
+ merged.status = existingStatus;
243
+ }
244
+ return merged;
245
+ }
200
246
  return incoming;
201
247
  }
202
248
  function finalizeAssistantParts(partOrder, partMap, finalText) {
@@ -524,4 +570,4 @@ export {
524
570
  createD1TurnEventStore,
525
571
  createMemoryTurnEventStore
526
572
  };
527
- //# sourceMappingURL=chunk-XYWCGFII.js.map
573
+ //# sourceMappingURL=chunk-B5JD3DXD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stream/stream-normalizer.ts","../src/stream/turn-identity.ts","../src/stream/turn-buffer.ts"],"sourcesContent":["import {\n canTransitionInteractionStatus,\n persistedPartToInteraction,\n type ChatInteractionStatus,\n} from '../interactions/contract'\nimport {\n canTransitionPlanStatus,\n persistedPartToPlan,\n planPartKey,\n planToPersistedPart,\n type ChatPlanStatus,\n} from '../plans/index'\n\nexport 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 if (type === 'interaction') {\n return persistedPartToInteraction(rawPart) ? rawPart : null\n }\n\n if (type === 'plan') {\n const plan = persistedPartToPlan(rawPart)\n return plan ? { ...rawPart, ...planToPersistedPart(plan) } : null\n }\n\n // System-authored notices pass through verbatim; `/chat-store` owns their\n // final typed validation before persistence.\n if (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 if (type === 'plan') return planPartKey(String(part.planId ?? ''))\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 if (type === 'interaction' && String(existing.type ?? '') === 'interaction') {\n const merged = overlayDefined(existing, incoming)\n const existingStatus = existing.status as ChatInteractionStatus | undefined\n const incomingStatus = incoming.status as ChatInteractionStatus | undefined\n if (\n existingStatus &&\n incomingStatus &&\n existingStatus !== incomingStatus &&\n !canTransitionInteractionStatus(existingStatus, incomingStatus)\n ) {\n merged.status = existingStatus\n }\n if (incoming.answers === undefined && existing.answers !== undefined) {\n merged.answers = existing.answers\n }\n return merged\n }\n\n if (type === 'plan' && String(existing.type ?? '') === 'plan') {\n const existingRevision = Number(existing.revision)\n const incomingRevision = Number(incoming.revision)\n if (Number.isInteger(existingRevision) && Number.isInteger(incomingRevision)) {\n if (incomingRevision < existingRevision) return existing\n if (incomingRevision > existingRevision) return incoming\n }\n const merged = overlayDefined(existing, incoming)\n const existingStatus = existing.status as ChatPlanStatus | undefined\n const incomingStatus = incoming.status as ChatPlanStatus | undefined\n if (\n existingStatus &&\n incomingStatus &&\n existingStatus !== incomingStatus &&\n !canTransitionPlanStatus(existingStatus, incomingStatus)\n ) {\n merged.status = existingStatus\n }\n return merged\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,3 +1,11 @@
1
+ import {
2
+ persistedPartToInteraction
3
+ } from "./chunk-XAWFPMAR.js";
4
+ import {
5
+ persistedPartToPlan,
6
+ planToPersistedPart
7
+ } from "./chunk-SIXYZ2FB.js";
8
+
1
9
  // src/chat-store/parts.ts
2
10
  function toChatMessageParts(parts) {
3
11
  const out = [];
@@ -27,9 +35,13 @@ function toChatMessagePart(part) {
27
35
  case "step-finish":
28
36
  return part;
29
37
  case "interaction":
30
- return str(part.id) && str(part.kind) && str(part.title) && str(part.status) && part.answerSpec && typeof part.answerSpec === "object" ? part : null;
38
+ return persistedPartToInteraction(part) ? part : null;
31
39
  case "notice":
32
40
  return str(part.id) && str(part.noticeKind) && str(part.text) ? part : null;
41
+ case "plan": {
42
+ const plan = persistedPartToPlan(part);
43
+ return plan ? { ...part, ...planToPersistedPart(plan) } : null;
44
+ }
33
45
  case void 0:
34
46
  return null;
35
47
  default: {
@@ -48,6 +60,9 @@ function isChatTextPart(part) {
48
60
  function isChatInteractionPart(part) {
49
61
  return part.type === "interaction";
50
62
  }
63
+ function isChatPlanPart(part) {
64
+ return part.type === "plan";
65
+ }
51
66
  function isChatStepFinishPart(part) {
52
67
  return part.type === "step-finish";
53
68
  }
@@ -57,6 +72,7 @@ export {
57
72
  isChatToolPart,
58
73
  isChatTextPart,
59
74
  isChatInteractionPart,
75
+ isChatPlanPart,
60
76
  isChatStepFinishPart
61
77
  };
62
- //# sourceMappingURL=chunk-I2R2XT4M.js.map
78
+ //# sourceMappingURL=chunk-I2ATYB7R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-store/parts.ts"],"sourcesContent":["/**\n * The stored shape of `message.parts` — one typed vocabulary for every part a\n * product persists into a chat transcript. NOT an ad-hoc union reverse-\n * engineered from product schemas; each member is matched field-for-field to\n * its canonical source:\n *\n * - `text` / `reasoning` / `tool`: the persisted projection `/stream`'s\n * `normalizePersistedPart` produces from the harness lane's\n * `message.part.updated` events (ADC sidecar\n * `apps/sidecar/src/events/session-events.ts:56` wraps the canonical part in\n * an `{id, sessionID, messageID}` envelope; the projection strips the\n * session/message ids and keeps the per-segment part id).\n * - `file` / `image` / `step-start` / `step-finish`: the sidecar's canonical\n * `MessagePartSchema` members (ADC\n * `apps/sidecar/src/schemas/agent-schemas.ts:50-154`); `step-finish` carries\n * the harness's per-step usage receipt — tokens\n * `{total, input, output, reasoning, cache{write, read}}` + `cost` — which is\n * also the shape the message-level token/cost columns mirror.\n * - `subtask`: `@tangle-network/agent-interface`'s `SubtaskPart` (a spawned\n * sub-agent task).\n * - `interaction` / `notice`: the persisted-part codecs in\n * `/web-react`'s chat-interactions contract (`interactionToPersistedPart`,\n * `noticePart`) — type-only imports, one source of truth for their statuses\n * and field shapes.\n * - `plan`: the durable-plan projection in `/plans`, derived from the sandbox\n * SDK's authoritative plan lifecycle.\n *\n * `@tangle-network/agent-interface` exports the canonical wire `Part` union,\n * but its `PartBase` requires the `sessionID`/`messageID` stream envelope that\n * is deliberately NOT persisted, so the stored union is defined here as the\n * envelope-free projection (a type-level coverage check against the peer's\n * `Part['type']` lives in the tests). Contribute-down candidate: if\n * agent-interface grows envelope-free persisted-part types, re-export them\n * here and delete these definitions.\n *\n * Two transport lanes serialize into this SAME stored shape:\n * - harness lane: canonical `message.part.updated` parts, merged/normalized by\n * `/stream` (`mergePersistedPart`, `finalizeAssistantParts`);\n * - router/openai-compat lane: `text_delta`/`tool_call` stream events are\n * mapped INTO canonical part events first (`/runtime`'s `toLoopEvents` +\n * `/stream`'s `normalizeToolEvent`) and then persisted identically — the\n * store never sees a router-specific shape.\n */\n\nimport type { Part as HarnessWirePart } from '@tangle-network/agent-interface'\nimport type {\n ChatInteractionField,\n ChatInteractionStatus,\n InteractionAnswers,\n InteractionPersistedPart,\n NoticeKind,\n NoticePersistedPart,\n} from '../web-react/chat-interactions'\nimport { persistedPartToInteraction } from '../interactions/contract'\nimport {\n persistedPartToPlan,\n planToPersistedPart,\n type ChatPlanPersistedPart,\n} from '../plans/index'\n\n/** Start/end wall-clock millis, as normalized by `/stream`'s `normalizeTime`. */\nexport interface ChatPartTime {\n start?: number\n end?: number\n}\n\n/** `id` is the harness's per-segment identity; absent on legacy/router parts,\n * which collapse to a single logical text stream. Never invented client-side. */\nexport interface ChatTextPart {\n type: 'text'\n text: string\n id?: string\n}\n\nexport interface ChatReasoningPart {\n type: 'reasoning'\n text: string\n id?: string\n time?: ChatPartTime\n}\n\n/** Superset of the sidecar's status enum (`pending|running|completed|failed`)\n * and agent-interface's `ToolState` statuses; `error` is the persisted\n * terminal form `/stream`'s `normalizePersistedPart` settles on. */\nexport type ChatToolStatus = 'pending' | 'running' | 'completed' | 'error' | 'failed'\n\nexport interface ChatToolState {\n status: ChatToolStatus\n input?: unknown\n output?: unknown\n error?: string\n title?: string\n metadata?: Record<string, unknown>\n time?: ChatPartTime\n}\n\nexport interface ChatToolPart {\n type: 'tool'\n id: string\n tool: string\n callID?: string\n state: ChatToolState\n}\n\n/** Union of the sidecar's legacy (path-based) and AI-SDK (url-based) file\n * shapes; response-side every field besides `type` is optional. */\nexport interface ChatFilePart {\n type: 'file'\n id?: string\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\nexport interface ChatImagePart {\n type: 'image'\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n}\n\nexport interface ChatSubtaskPart {\n type: 'subtask'\n prompt: string\n description: string\n agent: string\n id?: string\n}\n\n/** OpenCode step-boundary marker — no renderable text; preserved so mappers\n * never coerce it into a \"[object Object]\" text part. */\nexport interface ChatStepStartPart {\n type: 'step-start'\n}\n\n/** Per-step usage receipt as the harness reports it (sidecar\n * `StepFinishPartSchema`). The message-level token/cost columns are this\n * shape flattened. */\nexport interface ChatUsageTokens {\n total?: number\n input?: number\n output?: number\n reasoning?: number\n cache?: {\n write?: number\n read?: number\n }\n}\n\nexport interface ChatStepFinishPart {\n type: 'step-finish'\n reason?: string\n tokens?: ChatUsageTokens\n cost?: number\n}\n\n/** Persisted human-in-the-loop ask — byte-matches\n * `interactionToPersistedPart` in `/web-react`'s chat-interactions contract. */\nexport interface ChatInteractionPart {\n type: 'interaction'\n id: string\n kind: string\n title: string\n body?: string\n answerSpec: { fields: ChatInteractionField[] }\n status: ChatInteractionStatus\n answers?: InteractionAnswers\n cancelReason?: string\n}\n\nexport type ChatPlanPart = ChatPlanPersistedPart\n\n/** Persisted one-line transcript notice — byte-matches `noticePart` in\n * `/web-react`'s chat-interactions contract. */\nexport interface ChatNoticePart {\n type: 'notice'\n id: string\n noticeKind: NoticeKind\n text: string\n}\n\n// The \"byte-matches\" claims above, enforced at compile time: the interaction\n// contract's codec output types and the stored part types must stay mutually\n// assignable, so a codec field added on one side without the other fails here.\ntype MutuallyAssignable<A extends B, B> = A\ntype _CodecEmitsStorableInteractionPart = MutuallyAssignable<InteractionPersistedPart, ChatInteractionPart>\ntype _StoredInteractionPartFeedsCodec = MutuallyAssignable<ChatInteractionPart, InteractionPersistedPart>\ntype _CodecEmitsStorableNoticePart = MutuallyAssignable<NoticePersistedPart, ChatNoticePart>\ntype _StoredNoticePartFeedsCodec = MutuallyAssignable<ChatNoticePart, NoticePersistedPart>\ntype _CodecEmitsStorablePlanPart = MutuallyAssignable<ChatPlanPersistedPart, ChatPlanPart>\ntype _StoredPlanPartFeedsCodec = MutuallyAssignable<ChatPlanPart, ChatPlanPersistedPart>\n\nexport type ChatMessagePart =\n | ChatTextPart\n | ChatReasoningPart\n | ChatToolPart\n | ChatFilePart\n | ChatImagePart\n | ChatSubtaskPart\n | ChatStepStartPart\n | ChatStepFinishPart\n | ChatInteractionPart\n | ChatNoticePart\n | ChatPlanPart\n\n/** Every canonical harness wire-part kind must be storable — compile-time\n * guarantee that a new agent-interface part kind cannot silently fall out of\n * the persisted vocabulary. */\nexport type StorableHarnessPartKind = HarnessWirePart['type'] & ChatMessagePart['type']\n\n/**\n * The typed projection at the `/stream` → `/chat-store` boundary. The stream\n * normalizers (`normalizePersistedPart`/`mergePersistedPart`/\n * `finalizeAssistantParts`) deliberately produce untyped `JsonRecord`s — they\n * normalize wire shapes and do not own the stored vocabulary. THIS module\n * owns it, so this is where rows gain the `ChatMessagePart` type: each entry\n * is validated against its kind's required fields and narrowed, junk is\n * dropped, and — enforced by the exhaustiveness check below — no storable\n * kind can silently fall out (the step-finish/interaction trap).\n */\nexport function toChatMessageParts(parts: Array<Record<string, unknown>>): ChatMessagePart[] {\n const out: ChatMessagePart[] = []\n for (const part of parts) {\n const typed = toChatMessagePart(part)\n if (typed) out.push(typed)\n }\n return out\n}\n\nconst str = (value: unknown): value is string => typeof value === 'string'\n\nfunction toChatMessagePart(part: Record<string, unknown>): ChatMessagePart | null {\n if (!part || typeof part !== 'object') return null\n const type = part.type as ChatMessagePart['type'] | undefined\n switch (type) {\n case 'text':\n case 'reasoning':\n return str(part.text) ? (part as unknown as ChatTextPart | ChatReasoningPart) : null\n case 'tool':\n return str(part.id) && str(part.tool) && part.state && typeof part.state === 'object'\n ? (part as unknown as ChatToolPart)\n : null\n case 'file':\n case 'image':\n return part as unknown as ChatFilePart | ChatImagePart\n case 'subtask':\n return str(part.prompt) && str(part.description) && str(part.agent)\n ? (part as unknown as ChatSubtaskPart)\n : null\n case 'step-start':\n return { type: 'step-start' }\n case 'step-finish':\n return part as unknown as ChatStepFinishPart\n case 'interaction':\n return persistedPartToInteraction(part) ? (part as unknown as ChatInteractionPart) : null\n case 'notice':\n return str(part.id) && str(part.noticeKind) && str(part.text)\n ? (part as unknown as ChatNoticePart)\n : null\n case 'plan': {\n const plan = persistedPartToPlan(part)\n return plan ? ({ ...part, ...planToPersistedPart(plan) } as ChatPlanPart) : null\n }\n case undefined:\n return null\n default: {\n // Compile-time exhaustiveness: a new ChatMessagePart kind that is not\n // handled above makes `type` non-never here and this line fails.\n const _exhaustive: never = type\n void _exhaustive\n return null\n }\n }\n}\n\nexport function isChatToolPart(part: ChatMessagePart): part is ChatToolPart {\n return part.type === 'tool'\n}\n\nexport function isChatTextPart(part: ChatMessagePart): part is ChatTextPart {\n return part.type === 'text'\n}\n\nexport function isChatInteractionPart(part: ChatMessagePart): part is ChatInteractionPart {\n return part.type === 'interaction'\n}\n\nexport function isChatPlanPart(part: ChatMessagePart): part is ChatPlanPart {\n return part.type === 'plan'\n}\n\nexport function isChatStepFinishPart(part: ChatMessagePart): part is ChatStepFinishPart {\n return part.type === 'step-finish'\n}\n"],"mappings":";;;;;;;;;AA+NO,SAAS,mBAAmB,OAA0D;AAC3F,QAAM,MAAyB,CAAC;AAChC,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,kBAAkB,IAAI;AACpC,QAAI,MAAO,KAAI,KAAK,KAAK;AAAA,EAC3B;AACA,SAAO;AACT;AAEA,IAAM,MAAM,CAAC,UAAoC,OAAO,UAAU;AAElE,SAAS,kBAAkB,MAAuD;AAChF,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,OAAO,KAAK;AAClB,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,aAAO,IAAI,KAAK,IAAI,IAAK,OAAuD;AAAA,IAClF,KAAK;AACH,aAAO,IAAI,KAAK,EAAE,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,SAAS,OAAO,KAAK,UAAU,WACxE,OACD;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI,KAAK,KAAK,IAC7D,OACD;AAAA,IACN,KAAK;AACH,aAAO,EAAE,MAAM,aAAa;AAAA,IAC9B,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,2BAA2B,IAAI,IAAK,OAA0C;AAAA,IACvF,KAAK;AACH,aAAO,IAAI,KAAK,EAAE,KAAK,IAAI,KAAK,UAAU,KAAK,IAAI,KAAK,IAAI,IACvD,OACD;AAAA,IACN,KAAK,QAAQ;AACX,YAAM,OAAO,oBAAoB,IAAI;AACrC,aAAO,OAAQ,EAAE,GAAG,MAAM,GAAG,oBAAoB,IAAI,EAAE,IAAqB;AAAA,IAC9E;AAAA,IACA,KAAK;AACH,aAAO;AAAA,IACT,SAAS;AAGP,YAAM,cAAqB;AAC3B,WAAK;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEO,SAAS,eAAe,MAA6C;AAC1E,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,eAAe,MAA6C;AAC1E,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,sBAAsB,MAAoD;AACxF,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,eAAe,MAA6C;AAC1E,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,qBAAqB,MAAmD;AACtF,SAAO,KAAK,SAAS;AACvB;","names":[]}
@@ -0,0 +1,101 @@
1
+ // src/plans/index.ts
2
+ var PLAN_SUBMITTED_EVENT = "plan.submitted";
3
+ var PLAN_STATUSES = /* @__PURE__ */ new Set([
4
+ "preparing",
5
+ "pending",
6
+ "approved",
7
+ "rejected",
8
+ "superseded",
9
+ "withdrawn"
10
+ ]);
11
+ function asRecord(value) {
12
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
13
+ }
14
+ function requiredString(record, key) {
15
+ const value = record[key];
16
+ return typeof value === "string" && value ? value : null;
17
+ }
18
+ function parsePlan(record, defaultStatus) {
19
+ const planId = requiredString(record, "planId") ?? requiredString(record, "id");
20
+ const revision = record.revision;
21
+ const body = requiredString(record, "body");
22
+ const submittedAt = requiredString(record, "submittedAt");
23
+ const status = typeof record.status === "string" ? record.status : defaultStatus;
24
+ if (!planId || typeof revision !== "number" || !Number.isInteger(revision) || revision < 1 || !body || !submittedAt || !status || !PLAN_STATUSES.has(status)) {
25
+ return null;
26
+ }
27
+ const common = {
28
+ planId,
29
+ revision,
30
+ ...typeof record.title === "string" && record.title ? { title: record.title } : {},
31
+ body,
32
+ submittedAt,
33
+ ...record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? { metadata: record.metadata } : {},
34
+ ...typeof record.decidedBy === "string" && record.decidedBy ? { decidedBy: record.decidedBy } : {}
35
+ };
36
+ if (status === "preparing") return { ...common, status: "preparing" };
37
+ if (status === "pending") return { ...common, status: "pending" };
38
+ if (status === "approved") {
39
+ const decidedAt = requiredString(record, "decidedAt");
40
+ return decidedAt ? { ...common, status, decidedAt } : null;
41
+ }
42
+ if (status === "rejected") {
43
+ const decidedAt = requiredString(record, "decidedAt");
44
+ const feedback = requiredString(record, "feedback");
45
+ return decidedAt && feedback ? { ...common, status, decidedAt, feedback } : null;
46
+ }
47
+ if (status === "superseded") {
48
+ const supersededAt = requiredString(record, "supersededAt");
49
+ const supersededByPlanId = requiredString(record, "supersededByPlanId");
50
+ return supersededAt && supersededByPlanId ? { ...common, status, supersededAt, supersededByPlanId } : null;
51
+ }
52
+ const withdrawnAt = requiredString(record, "withdrawnAt");
53
+ const withdrawnReason = requiredString(record, "withdrawnReason") ?? requiredString(record, "reason");
54
+ return withdrawnAt && withdrawnReason ? { ...common, status: "withdrawn", withdrawnAt, withdrawnReason } : null;
55
+ }
56
+ function planPartKey(planId) {
57
+ return `plan:${planId}`;
58
+ }
59
+ function planRevisionKey(planId, revision) {
60
+ return `plan:${planId}:revision:${revision}`;
61
+ }
62
+ function planFollowUpTurnId(planId, outcome) {
63
+ return `plan:${planId}:${outcome}`;
64
+ }
65
+ function canTransitionPlanStatus(from, to) {
66
+ if (from === to) return true;
67
+ if (from === "preparing") return to === "pending" || to === "superseded" || to === "withdrawn";
68
+ if (from === "pending") return to === "approved" || to === "rejected" || to === "superseded" || to === "withdrawn";
69
+ return false;
70
+ }
71
+ function planToPersistedPart(plan) {
72
+ return { type: "plan", ...plan };
73
+ }
74
+ function persistedPartToPlan(part) {
75
+ if (String(part.type ?? "") !== "plan") return null;
76
+ return parsePlan(part);
77
+ }
78
+ function parsePlanSubmittedEvent(event) {
79
+ const root = asRecord(event);
80
+ if (!root || root.type !== PLAN_SUBMITTED_EVENT) {
81
+ return { succeeded: false, error: "event is not plan.submitted" };
82
+ }
83
+ const plan = asRecord(asRecord(root.properties)?.plan) ?? asRecord(asRecord(root.data)?.plan);
84
+ if (!plan) {
85
+ return { succeeded: false, error: "plan.submitted event carried no plan" };
86
+ }
87
+ const parsed = parsePlan(plan, "pending");
88
+ return parsed ? { succeeded: true, value: parsed } : { succeeded: false, error: "plan.submitted event carried a malformed plan" };
89
+ }
90
+
91
+ export {
92
+ PLAN_SUBMITTED_EVENT,
93
+ planPartKey,
94
+ planRevisionKey,
95
+ planFollowUpTurnId,
96
+ canTransitionPlanStatus,
97
+ planToPersistedPart,
98
+ persistedPartToPlan,
99
+ parsePlanSubmittedEvent
100
+ };
101
+ //# sourceMappingURL=chunk-SIXYZ2FB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plans/index.ts"],"sourcesContent":["/**\n * Durable-plan application-shell contract.\n *\n * The sandbox SDK owns the authoritative plan lifecycle and decision APIs.\n * This module only projects that substrate state into chat events and\n * persisted message parts that products can render and restore.\n */\n\nexport const PLAN_SUBMITTED_EVENT = 'plan.submitted' as const\n\n/** Sandbox plan lifecycle. `preparing` is the transient submission state and\n * is retained in the projection even though the chat card normally starts at\n * `pending`. */\nexport type ChatPlanStatus = 'preparing' | 'pending' | 'approved' | 'rejected' | 'superseded' | 'withdrawn'\n\ntype ChatPlanBase = {\n /** The sandbox SDK calls this field `id`; the transcript projection names\n * it explicitly so it cannot collide with a message-part id. */\n planId: string\n revision: number\n title?: string\n body: string\n submittedAt: string\n /** Provider/Sandbox metadata is deliberately opaque to this package. */\n metadata?: Record<string, unknown>\n decidedBy?: string\n}\n\n/** Browser/persisted projection of the sandbox SDK's durable-plan union. */\nexport type ChatPlan = ChatPlanBase & (\n | { status: 'preparing' }\n | { status: 'pending' }\n | { status: 'approved'; decidedAt: string }\n | { status: 'rejected'; decidedAt: string; feedback: string }\n | { status: 'superseded'; supersededAt: string; supersededByPlanId: string }\n | { status: 'withdrawn'; withdrawnAt: string; withdrawnReason: string }\n)\n\n/** Canonical transcript part for one durable plan. */\nexport type ChatPlanPersistedPart = ChatPlan & { type: 'plan' }\n\nexport type ParsePlanSubmittedResult =\n | { succeeded: true; value: ChatPlan }\n | { succeeded: false; error: string }\n\nconst PLAN_STATUSES: ReadonlySet<string> = new Set([\n 'preparing',\n 'pending',\n 'approved',\n 'rejected',\n 'superseded',\n 'withdrawn',\n])\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as Record<string, unknown>\n : null\n}\n\nfunction requiredString(record: Record<string, unknown>, key: string): string | null {\n const value = record[key]\n return typeof value === 'string' && value ? value : null\n}\n\nfunction parsePlan(record: Record<string, unknown>, defaultStatus?: ChatPlanStatus): ChatPlan | null {\n const planId = requiredString(record, 'planId') ?? requiredString(record, 'id')\n const revision = record.revision\n const body = requiredString(record, 'body')\n const submittedAt = requiredString(record, 'submittedAt')\n const status = typeof record.status === 'string' ? record.status : defaultStatus\n if (\n !planId ||\n typeof revision !== 'number' ||\n !Number.isInteger(revision) ||\n revision < 1 ||\n !body ||\n !submittedAt ||\n !status ||\n !PLAN_STATUSES.has(status)\n ) {\n return null\n }\n\n const common = {\n planId,\n revision,\n ...(typeof record.title === 'string' && record.title ? { title: record.title } : {}),\n body,\n submittedAt,\n ...(record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)\n ? { metadata: record.metadata as Record<string, unknown> }\n : {}),\n ...(typeof record.decidedBy === 'string' && record.decidedBy ? { decidedBy: record.decidedBy } : {}),\n }\n\n if (status === 'preparing') return { ...common, status: 'preparing' }\n if (status === 'pending') return { ...common, status: 'pending' }\n if (status === 'approved') {\n const decidedAt = requiredString(record, 'decidedAt')\n return decidedAt ? { ...common, status, decidedAt } : null\n }\n if (status === 'rejected') {\n const decidedAt = requiredString(record, 'decidedAt')\n const feedback = requiredString(record, 'feedback')\n return decidedAt && feedback ? { ...common, status, decidedAt, feedback } : null\n }\n if (status === 'superseded') {\n const supersededAt = requiredString(record, 'supersededAt')\n const supersededByPlanId = requiredString(record, 'supersededByPlanId')\n return supersededAt && supersededByPlanId\n ? { ...common, status, supersededAt, supersededByPlanId }\n : null\n }\n\n const withdrawnAt = requiredString(record, 'withdrawnAt')\n const withdrawnReason = requiredString(record, 'withdrawnReason') ?? requiredString(record, 'reason')\n return withdrawnAt && withdrawnReason\n ? { ...common, status: 'withdrawn', withdrawnAt, withdrawnReason }\n : null\n}\n\nexport function planPartKey(planId: string): string {\n return `plan:${planId}`\n}\n\nexport function planRevisionKey(planId: string, revision: number): string {\n return `plan:${planId}:revision:${revision}`\n}\n\nexport function planFollowUpTurnId(planId: string, outcome: 'approved' | 'rejected'): string {\n return `plan:${planId}:${outcome}`\n}\n\n/** Plan status is monotonic: only a pending plan can settle. */\nexport function canTransitionPlanStatus(from: ChatPlanStatus, to: ChatPlanStatus): boolean {\n if (from === to) return true\n if (from === 'preparing') return to === 'pending' || to === 'superseded' || to === 'withdrawn'\n if (from === 'pending') return to === 'approved' || to === 'rejected' || to === 'superseded' || to === 'withdrawn'\n return false\n}\n\nexport function planToPersistedPart(plan: ChatPlan): ChatPlanPersistedPart {\n return { type: 'plan', ...plan }\n}\n\nexport function persistedPartToPlan(part: Record<string, unknown>): ChatPlan | null {\n if (String(part.type ?? '') !== 'plan') return null\n return parsePlan(part)\n}\n\n/** Parses both direct sandbox events (`data.plan`) and session envelopes\n * (`properties.plan`). Session identity is carried by the surrounding stream\n * and is deliberately not required in the payload. */\nexport function parsePlanSubmittedEvent(event: unknown): ParsePlanSubmittedResult {\n const root = asRecord(event)\n if (!root || root.type !== PLAN_SUBMITTED_EVENT) {\n return { succeeded: false, error: 'event is not plan.submitted' }\n }\n const plan = asRecord(asRecord(root.properties)?.plan) ?? asRecord(asRecord(root.data)?.plan)\n if (!plan) {\n return { succeeded: false, error: 'plan.submitted event carried no plan' }\n }\n const parsed = parsePlan(plan, 'pending')\n return parsed\n ? { succeeded: true, value: parsed }\n : { succeeded: false, error: 'plan.submitted event carried a malformed plan' }\n}\n"],"mappings":"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","names":[]}
@@ -12,6 +12,23 @@ function isRenderableInteractionKind(kind) {
12
12
  function isSafeInteractionFieldKey(key) {
13
13
  return /^[A-Za-z0-9_-]+$/.test(key) && key !== "__proto__" && key !== "constructor" && key !== "prototype";
14
14
  }
15
+ function parseInteractionAnswers(value) {
16
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17
+ return { succeeded: false, error: "interaction answers must be an object" };
18
+ }
19
+ const answers = {};
20
+ for (const [key, selection] of Object.entries(value)) {
21
+ if (!isSafeInteractionFieldKey(key)) {
22
+ return { succeeded: false, error: `interaction answers contain an unsafe field key: ${key}` };
23
+ }
24
+ const valid = typeof selection === "string" || typeof selection === "number" || typeof selection === "boolean" || Array.isArray(selection) && selection.every((item) => typeof item === "string");
25
+ if (!valid) {
26
+ return { succeeded: false, error: `interaction answer ${key} must be a string, number, boolean, or string array` };
27
+ }
28
+ answers[key] = Array.isArray(selection) ? [...selection] : selection;
29
+ }
30
+ return { succeeded: true, value: answers };
31
+ }
15
32
  function isTerminalInteractionStatus(status) {
16
33
  return status !== "pending";
17
34
  }
@@ -104,7 +121,9 @@ function interactionFromWireRequest(request) {
104
121
  status: "pending"
105
122
  };
106
123
  }
107
- function interactionToPersistedPart(request, status, cancelReason) {
124
+ function interactionToPersistedPart(request, status, cancelReason, answers) {
125
+ const parsedAnswers = answers === void 0 ? void 0 : parseInteractionAnswers(answers);
126
+ if (parsedAnswers && !parsedAnswers.succeeded) throw new TypeError(parsedAnswers.error);
108
127
  return {
109
128
  type: "interaction",
110
129
  id: request.id,
@@ -113,9 +132,21 @@ function interactionToPersistedPart(request, status, cancelReason) {
113
132
  ...request.body ? { body: request.body } : {},
114
133
  answerSpec: { fields: request.answerSpec.fields },
115
134
  status,
135
+ ...parsedAnswers?.succeeded ? { answers: parsedAnswers.value } : {},
116
136
  ...cancelReason ? { cancelReason } : {}
117
137
  };
118
138
  }
139
+ function stampInteractionAnswers(parts, answersByInteractionId) {
140
+ return parts.map((part) => {
141
+ if (String(part.type ?? "") !== "interaction" || typeof part.id !== "string") return part;
142
+ if (!Object.prototype.hasOwnProperty.call(answersByInteractionId, part.id)) return part;
143
+ const rawAnswers = answersByInteractionId[part.id];
144
+ if (rawAnswers === void 0) return part;
145
+ const parsed = parseInteractionAnswers(rawAnswers);
146
+ if (!parsed.succeeded) throw new TypeError(parsed.error);
147
+ return { ...part, answers: parsed.value };
148
+ });
149
+ }
119
150
  function persistedPartToInteraction(part) {
120
151
  if (String(part.type ?? "") !== "interaction") return null;
121
152
  const id = typeof part.id === "string" && part.id ? part.id : null;
@@ -125,7 +156,8 @@ function persistedPartToInteraction(part) {
125
156
  const fields = Array.isArray(answerSpec?.fields) ? answerSpec.fields : null;
126
157
  const status = part.status;
127
158
  const validStatus = status && ["pending", "answered", "declined", "cancelled", "expired"].includes(status);
128
- if (!id || !kind || !fields || !validStatus) return null;
159
+ const parsedAnswers = part.answers === void 0 ? void 0 : parseInteractionAnswers(part.answers);
160
+ if (!id || !kind || !fields || !validStatus || parsedAnswers && !parsedAnswers.succeeded) return null;
129
161
  return {
130
162
  id,
131
163
  kind,
@@ -133,6 +165,7 @@ function persistedPartToInteraction(part) {
133
165
  ...typeof part.body === "string" && part.body ? { body: part.body } : {},
134
166
  fields,
135
167
  status,
168
+ ...parsedAnswers?.succeeded ? { answers: parsedAnswers.value } : {},
136
169
  ...typeof part.cancelReason === "string" && part.cancelReason ? { cancelReason: part.cancelReason } : {}
137
170
  };
138
171
  }
@@ -143,6 +176,7 @@ export {
143
176
  INTERACTION_RESOLVED_EVENT,
144
177
  isRenderableInteractionKind,
145
178
  isSafeInteractionFieldKey,
179
+ parseInteractionAnswers,
146
180
  isTerminalInteractionStatus,
147
181
  canTransitionInteractionStatus,
148
182
  cancelStatusFor,
@@ -158,6 +192,7 @@ export {
158
192
  noticePart,
159
193
  interactionFromWireRequest,
160
194
  interactionToPersistedPart,
195
+ stampInteractionAnswers,
161
196
  persistedPartToInteraction
162
197
  };
163
- //# sourceMappingURL=chunk-4TXDD6P2.js.map
198
+ //# sourceMappingURL=chunk-XAWFPMAR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/interactions/contract.ts"],"sourcesContent":["// Shared contract for agent interaction events (kind: \"question\" et al) — the\n// generalized human-in-the-loop primitive on the chat stream. Framework-agnostic\n// (no server- or React-only imports) so a producer (chat route) and a consumer\n// (stream parser, transcript, question card) agree on one wire shape and one\n// persisted-part shape.\n//\n// An `interaction` event means the run is BLOCKED inside the sidecar's\n// InteractionBroker until the user answers, the agent withdraws the ask\n// (`interaction.cancel`), or the broker times out. A pending interaction is\n// \"waiting on the user\", not \"model working\".\n\nimport {\n InteractionRequestSchema,\n type InteractionData,\n type InteractionField,\n type InteractionOutcome,\n type InteractionRequest,\n} from '@tangle-network/agent-interface'\n\nexport type { InteractionData, InteractionOutcome, InteractionRequest }\n\n// ---------------------------------------------------------------------------\n// Event names\n\n/** Sidecar → client: the agent raised an ask; data = `{ request }`. */\nexport const INTERACTION_EVENT = 'interaction' as const\n/** Sidecar → client: the ask was withdrawn; data = `{ id, reason? }`. */\nexport const INTERACTION_CANCEL_EVENT = 'interaction.cancel' as const\n/** An ask was answered; data = `{ id, status }`. In the wire contract so a\n * server broadcast and a client-local mark share one event name. */\nexport const INTERACTION_RESOLVED_EVENT = 'interaction.resolved' as const\n\n/** Interaction kinds a product typically renders a card for. Anything else is\n * auto-declined by the chat producer's safety net and never reaches a client.\n * A pure default; a product may substitute its own renderable set. */\nconst RENDERABLE_INTERACTION_KINDS: ReadonlySet<string> = new Set(['question', 'plan'])\n\nexport function isRenderableInteractionKind(kind: string): boolean {\n return RENDERABLE_INTERACTION_KINDS.has(kind)\n}\n\n/** Answer/field keys the sidecar will accept: identifier-safe and never a\n * prototype-pollution vector. */\nexport function isSafeInteractionFieldKey(key: string): boolean {\n return /^[A-Za-z0-9_-]+$/.test(key) && key !== '__proto__' && key !== 'constructor' && key !== 'prototype'\n}\n\n// ---------------------------------------------------------------------------\n// Field types\n//\n// `allowCustom` (a select that also accepts a write-in value) is defined by\n// newer agent-interface schemas; older pinned schemas strip unknown keys on\n// parse. The wire/persisted field types below carry the flag so a card can gate\n// its write-in input, and `parseInteractionRequest` returns the RAW payload\n// (schema-validated, not schema-parsed) so the flag survives.\n\nexport type ChatSelectField = Extract<InteractionField, { type: 'select' }> & {\n allowCustom?: boolean\n}\nexport type ChatInteractionField = Exclude<InteractionField, { type: 'select' }> | ChatSelectField\n\n/** `InteractionRequest` whose select fields may carry `allowCustom`. */\nexport type InteractionRequestWire = Omit<InteractionRequest, 'answerSpec'> & {\n answerSpec: { fields: ChatInteractionField[] }\n}\n\n// ---------------------------------------------------------------------------\n// Interaction lifecycle\n\nexport type ChatInteractionStatus = 'pending' | 'answered' | 'declined' | 'cancelled' | 'expired'\n\n/** Accepted field selections keyed by answer-spec field name. */\n/** Accepted answer values. Select fields historically used `string[]`; the\n * wider scalar union is additive and lets durable stores preserve text,\n * numeric, and boolean answer fields without coercion. */\nexport type InteractionAnswerValue = string | number | boolean | string[]\nexport type InteractionAnswers = Record<string, InteractionAnswerValue>\n\nexport type ParseInteractionAnswersResult =\n | { succeeded: true; value: InteractionAnswers }\n | { succeeded: false; error: string }\n\n/** Strictly validates and copies persisted answer selections. */\nexport function parseInteractionAnswers(value: unknown): ParseInteractionAnswersResult {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return { succeeded: false, error: 'interaction answers must be an object' }\n }\n const answers: InteractionAnswers = {}\n for (const [key, selection] of Object.entries(value as Record<string, unknown>)) {\n if (!isSafeInteractionFieldKey(key)) {\n return { succeeded: false, error: `interaction answers contain an unsafe field key: ${key}` }\n }\n const valid = typeof selection === 'string' || typeof selection === 'number' || typeof selection === 'boolean' ||\n (Array.isArray(selection) && selection.every((item) => typeof item === 'string'))\n if (!valid) {\n return { succeeded: false, error: `interaction answer ${key} must be a string, number, boolean, or string array` }\n }\n answers[key] = Array.isArray(selection) ? [...selection] : selection\n }\n return { succeeded: true, value: answers }\n}\n\n/** The client/persisted view of one ask. `fields` come verbatim off the wire. */\nexport interface ChatInteraction {\n id: string\n kind: string\n title: string\n body?: string\n fields: ChatInteractionField[]\n status: ChatInteractionStatus\n /** Accepted selections, restored with the transcript after reload. */\n answers?: InteractionAnswers\n /** Set when status came from an `interaction.cancel` (e.g. \"timeout\"). */\n cancelReason?: string\n}\n\nexport function isTerminalInteractionStatus(status: ChatInteractionStatus): boolean {\n return status !== 'pending'\n}\n\n/** Statuses only move forward (pending → terminal); a replayed/stale `pending`\n * must never resurrect a resolved card. */\nexport function canTransitionInteractionStatus(\n from: ChatInteractionStatus,\n to: ChatInteractionStatus,\n): boolean {\n return from === 'pending' && to !== from\n}\n\n/** Maps an `interaction.cancel` reason to the card's terminal status. */\nexport function cancelStatusFor(reason: string | undefined): ChatInteractionStatus {\n return reason === 'timeout' ? 'expired' : 'cancelled'\n}\n\nfunction stableValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(stableValue)\n if (!value || typeof value !== 'object') return value\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, nested]) => [key, stableValue(nested)]),\n )\n}\n\nfunction normalizedInteractionText(value: string | undefined): string {\n return (value ?? '').replace(/\\s+/g, ' ').trim()\n}\n\n/** Content identity for duplicate safety nets. Excludes volatile ids/statuses. */\nexport function questionInteractionContentSignature(interaction: ChatInteraction): string | null {\n if (interaction.kind !== 'question') return null\n return JSON.stringify(stableValue({\n kind: interaction.kind,\n title: normalizedInteractionText(interaction.title),\n body: normalizedInteractionText(interaction.body),\n fields: interaction.fields,\n }))\n}\n\nexport function dedupeQuestionInteractionsByContent(interactions: ChatInteraction[]): ChatInteraction[] {\n const seen = new Set<string>()\n return interactions.filter((interaction) => {\n const signature = questionInteractionContentSignature(interaction)\n if (!signature) return true\n if (seen.has(signature)) return false\n seen.add(signature)\n return true\n })\n}\n\n// ---------------------------------------------------------------------------\n// Wire parsing — typed outcomes, fail loud at the caller (log + skip; never a\n// half-rendered card).\n\nexport type ParseInteractionResult =\n | { succeeded: true; value: InteractionRequestWire }\n | { succeeded: false; error: string }\n\n/** Parses an `interaction` event's data (`{ request }`). Validates the shape\n * with the agent-interface schema but returns the raw request so a field a\n * pinned schema predates (`allowCustom`) survives. */\nexport function parseInteractionRequest(data: Record<string, unknown> | undefined): ParseInteractionResult {\n const request = data?.request\n if (!request || typeof request !== 'object') {\n return { succeeded: false, error: 'interaction event carried no request object' }\n }\n const validation = InteractionRequestSchema.safeParse(request)\n if (!validation.success) {\n return { succeeded: false, error: `malformed interaction request: ${validation.error.message}` }\n }\n return { succeeded: true, value: request as InteractionRequestWire }\n}\n\nexport interface InteractionCancelData {\n id: string\n reason?: string\n}\n\nexport function parseInteractionCancel(\n data: Record<string, unknown> | undefined,\n): { succeeded: true; value: InteractionCancelData } | { succeeded: false; error: string } {\n const id = typeof data?.id === 'string' && data.id ? data.id : null\n if (!id) return { succeeded: false, error: 'interaction.cancel event carried no id' }\n const reason = typeof data?.reason === 'string' && data.reason ? data.reason : undefined\n return { succeeded: true, value: { id, ...(reason ? { reason } : {}) } }\n}\n\n// ---------------------------------------------------------------------------\n// Composer-as-answer delivery: while asks are pending the composer never\n// blocks — typed text is delivered verbatim to every open ask and the agent\n// decides what it means. No interpretation here; the sidecar validates answers\n// fail-closed (invalid free text on an option-only ask → 400).\n\nexport function fieldAcceptsFreeText(field: ChatInteractionField): boolean {\n if (field.type === 'text') return true\n if (field.type === 'select') return (field as ChatSelectField).allowCustom === true\n return false\n}\n\nexport interface ComposerAnswerDelivery {\n interactionId: string\n field: ChatInteractionField\n}\n\n/** One delivery per pending ask: the first free-text-capable field, else the\n * first field. Zero-field asks are skipped (nothing to carry the text). */\nexport function composerAnswerDeliveries(pending: ChatInteraction[]): ComposerAnswerDelivery[] {\n const deliveries: ComposerAnswerDelivery[] = []\n for (const interaction of pending) {\n // Only questions take a composer-routed answer. A non-question ask (a plan)\n // is POSTed as outcome:\"accepted\" when answered — routing composer text to\n // it would silently APPROVE it. Approval is an explicit card click, so the\n // composer skips it and the plan card stays the only path.\n if (interaction.kind !== 'question') continue\n const field = interaction.fields.find(fieldAcceptsFreeText) ?? interaction.fields[0]\n if (!field) continue\n deliveries.push({ interactionId: interaction.id, field })\n }\n return deliveries\n}\n\n/** Shapes composer text into the respond payload for the routed field\n * (select answers are string arrays on the wire; text answers are strings). */\nexport function composerAnswerData(field: ChatInteractionField, text: string): InteractionData {\n return { [field.name]: field.type === 'select' ? [text] : text }\n}\n\n// ---------------------------------------------------------------------------\n// Part keys + codecs (persisted `messages.parts` entries and live stream parts\n// share these shapes).\n\nexport function interactionPartKey(id: string): string {\n return `interaction:${id}`\n}\n\nexport function noticePartKey(id: string): string {\n return `notice:${id}`\n}\n\nexport type NoticeKind = 'warning' | 'auto-declined'\n\n/**\n * Persisted-part shapes the codecs below produce — the SAME rows\n * `/chat-store`'s `ChatInteractionPart`/`ChatNoticePart` store, typed at the\n * source so a product pushing them into a `ChatMessagePart[]` transcript needs\n * no cast. Type aliases (not interfaces) on purpose: the implicit index\n * signature keeps them assignable to the `Record<string, unknown>` these\n * codecs previously returned, so existing consumers stay source-compatible.\n */\nexport type InteractionPersistedPart = {\n type: 'interaction'\n id: string\n kind: string\n title: string\n body?: string\n answerSpec: { fields: ChatInteractionField[] }\n status: ChatInteractionStatus\n answers?: InteractionAnswers\n cancelReason?: string\n}\n\nexport type NoticePersistedPart = {\n type: 'notice'\n id: string\n noticeKind: NoticeKind\n text: string\n}\n\n/** Builds the persisted/streamed `notice` part — a one-line transcript notice\n * explaining an out-of-band event (warning, auto-declined interaction). */\nexport function noticePart(noticeKind: NoticeKind, id: string, text: string): NoticePersistedPart {\n return { type: 'notice', id, noticeKind, text }\n}\n\n/** Reads a wire request into the client's pending `ChatInteraction`. */\nexport function interactionFromWireRequest(request: InteractionRequestWire): ChatInteraction {\n return {\n id: request.id,\n kind: request.kind,\n title: request.title,\n ...(request.body ? { body: request.body } : {}),\n fields: request.answerSpec.fields,\n status: 'pending',\n }\n}\n\n/** Builds the persisted/streamed `interaction` part from a wire request. */\nexport function interactionToPersistedPart(\n request: InteractionRequestWire,\n status: ChatInteractionStatus,\n cancelReason?: string,\n answers?: InteractionAnswers,\n): InteractionPersistedPart {\n const parsedAnswers = answers === undefined ? undefined : parseInteractionAnswers(answers)\n if (parsedAnswers && !parsedAnswers.succeeded) throw new TypeError(parsedAnswers.error)\n return {\n type: 'interaction',\n id: request.id,\n kind: request.kind,\n title: request.title,\n ...(request.body ? { body: request.body } : {}),\n answerSpec: { fields: request.answerSpec.fields },\n status,\n ...(parsedAnswers?.succeeded ? { answers: parsedAnswers.value } : {}),\n ...(cancelReason ? { cancelReason } : {}),\n }\n}\n\n/** Stamps accepted values onto matching persisted interaction parts without\n * mutating the caller's transcript or answer maps. */\nexport function stampInteractionAnswers(\n parts: Array<Record<string, unknown>>,\n answersByInteractionId: Readonly<Record<string, InteractionAnswers>>,\n): Array<Record<string, unknown>> {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'interaction' || typeof part.id !== 'string') return part\n if (!Object.prototype.hasOwnProperty.call(answersByInteractionId, part.id)) return part\n const rawAnswers = answersByInteractionId[part.id]\n if (rawAnswers === undefined) return part\n const parsed = parseInteractionAnswers(rawAnswers)\n if (!parsed.succeeded) throw new TypeError(parsed.error)\n return { ...part, answers: parsed.value }\n })\n}\n\n/** Reads a persisted/streamed `interaction` part back into a `ChatInteraction`.\n * Returns null (caller logs) when the part is not one of ours. */\nexport function persistedPartToInteraction(part: Record<string, unknown>): ChatInteraction | null {\n if (String(part.type ?? '') !== 'interaction') return null\n const id = typeof part.id === 'string' && part.id ? part.id : null\n const kind = typeof part.kind === 'string' && part.kind ? part.kind : null\n const title = typeof part.title === 'string' ? part.title : ''\n const answerSpec = part.answerSpec as { fields?: unknown } | undefined\n const fields = Array.isArray(answerSpec?.fields) ? (answerSpec.fields as ChatInteractionField[]) : null\n const status = part.status as ChatInteractionStatus | undefined\n const validStatus = status && ['pending', 'answered', 'declined', 'cancelled', 'expired'].includes(status)\n const parsedAnswers = part.answers === undefined ? undefined : parseInteractionAnswers(part.answers)\n if (!id || !kind || !fields || !validStatus || (parsedAnswers && !parsedAnswers.succeeded)) return null\n return {\n id,\n kind,\n title,\n ...(typeof part.body === 'string' && part.body ? { body: part.body } : {}),\n fields,\n status,\n ...(parsedAnswers?.succeeded ? { answers: parsedAnswers.value } : {}),\n ...(typeof part.cancelReason === 'string' && part.cancelReason ? { cancelReason: part.cancelReason } : {}),\n }\n}\n"],"mappings":"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","names":[]}