@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.
Files changed (52) hide show
  1. package/dist/assistant/index.d.ts +2 -1
  2. package/dist/assistant/index.js +3 -2
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/chat-routes/index.d.ts +17 -3
  5. package/dist/chat-routes/index.js +91 -6
  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-Y4QHNQ75.js → chunk-6KVH5SP5.js} +110 -6
  11. package/dist/chunk-6KVH5SP5.js.map +1 -0
  12. package/dist/{chunk-XYWCGFII.js → chunk-B5JD3DXD.js} +48 -2
  13. package/dist/chunk-B5JD3DXD.js.map +1 -0
  14. package/dist/{chunk-I2R2XT4M.js → chunk-I2ATYB7R.js} +18 -2
  15. package/dist/chunk-I2ATYB7R.js.map +1 -0
  16. package/dist/{chunk-KM766NN3.js → chunk-PEUBCJXF.js} +1772 -1224
  17. package/dist/chunk-PEUBCJXF.js.map +1 -0
  18. package/dist/{chunk-UMSOSUEU.js → chunk-QYOD3K56.js} +2 -2
  19. package/dist/chunk-QYOD3K56.js.map +1 -0
  20. package/dist/chunk-SIXYZ2FB.js +101 -0
  21. package/dist/chunk-SIXYZ2FB.js.map +1 -0
  22. package/dist/{chunk-4TXDD6P2.js → chunk-XAWFPMAR.js} +38 -3
  23. package/dist/chunk-XAWFPMAR.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/index.d.ts +5 -3
  32. package/dist/index.js +124 -46
  33. package/dist/interactions/index.d.ts +58 -3
  34. package/dist/interactions/index.js +7 -3
  35. package/dist/object-store/index.d.ts +6 -4
  36. package/dist/object-store/index.js +1 -1
  37. package/dist/{parts-BcbitSNp.d.ts → parts-DjX0RRTS.d.ts} +9 -3
  38. package/dist/plans/index.d.ts +69 -0
  39. package/dist/plans/index.js +21 -0
  40. package/dist/plans/index.js.map +1 -0
  41. package/dist/stream/index.js +3 -1
  42. package/dist/teams/index.js +9 -9
  43. package/dist/teams/invitations-api.js +3 -3
  44. package/dist/web-react/index.d.ts +252 -93
  45. package/dist/web-react/index.js +32 -3
  46. package/package.json +11 -1
  47. package/dist/chunk-4TXDD6P2.js.map +0 -1
  48. package/dist/chunk-I2R2XT4M.js.map +0 -1
  49. package/dist/chunk-KM766NN3.js.map +0 -1
  50. package/dist/chunk-UMSOSUEU.js.map +0 -1
  51. package/dist/chunk-XYWCGFII.js.map +0 -1
  52. package/dist/chunk-Y4QHNQ75.js.map +0 -1
@@ -32,7 +32,7 @@ function createR2ObjectStore({ bucket }) {
32
32
  function assertSafeKeySegment(s) {
33
33
  if (s.length === 0) throw new Error("object-store: empty key segment");
34
34
  if (s.includes("..")) throw new Error(`object-store: unsafe key segment (contains "..") \u2014 ${s}`);
35
- if (s.startsWith("/")) throw new Error(`object-store: unsafe key segment (leading "/") \u2014 ${s}`);
35
+ if (s.includes("/")) throw new Error(`object-store: unsafe key segment (contains "/") \u2014 ${s}`);
36
36
  if (s.includes("\\")) throw new Error(`object-store: unsafe key segment (backslash) \u2014 ${s}`);
37
37
  return s;
38
38
  }
@@ -121,4 +121,4 @@ export {
121
121
  verifyObjectUrl,
122
122
  createProxiedArtifactRoute
123
123
  };
124
- //# sourceMappingURL=chunk-UMSOSUEU.js.map
124
+ //# sourceMappingURL=chunk-QYOD3K56.js.map
@@ -0,0 +1 @@
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, ANY `/` (a segment must be a\n * single path component — a leading, interior, or trailing slash all widen the\n * key by injecting extra levels), a backslash, or an empty segment. Consumers\n * should call this on caller-supplied identifiers BEFORE any `get`/`put` so an\n * attacker-controlled id can never widen the key beyond its own operator+\n * 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 // Reject ANY '/' — leading, interior, or trailing. A key segment is a single\n // path component; an interior slash (`a/b`) would silently add a key level and\n // widen the operator/customer prefix, so it is refused just like a leading one.\n if (s.includes('/')) throw new Error(`object-store: unsafe key segment (contains \"/\") — ${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":[]}
@@ -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":";AAQO,IAAM,uBAAuB;AAqCpC,IAAM,gBAAqC,oBAAI,IAAI;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,SAAS,OAAgD;AAChE,SAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAC7D,QACA;AACN;AAEA,SAAS,eAAe,QAAiC,KAA4B;AACnF,QAAM,QAAQ,OAAO,GAAG;AACxB,SAAO,OAAO,UAAU,YAAY,QAAQ,QAAQ;AACtD;AAEA,SAAS,UAAU,QAAiC,eAAiD;AACnG,QAAM,SAAS,eAAe,QAAQ,QAAQ,KAAK,eAAe,QAAQ,IAAI;AAC9E,QAAM,WAAW,OAAO;AACxB,QAAM,OAAO,eAAe,QAAQ,MAAM;AAC1C,QAAM,cAAc,eAAe,QAAQ,aAAa;AACxD,QAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AACnE,MACE,CAAC,UACD,OAAO,aAAa,YACpB,CAAC,OAAO,UAAU,QAAQ,KAC1B,WAAW,KACX,CAAC,QACD,CAAC,eACD,CAAC,UACD,CAAC,cAAc,IAAI,MAAM,GACzB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA,GAAI,OAAO,OAAO,UAAU,YAAY,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,IAClF;AAAA,IACA;AAAA,IACA,GAAI,OAAO,YAAY,OAAO,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,OAAO,QAAQ,IACxF,EAAE,UAAU,OAAO,SAAoC,IACvD,CAAC;AAAA,IACL,GAAI,OAAO,OAAO,cAAc,YAAY,OAAO,YAAY,EAAE,WAAW,OAAO,UAAU,IAAI,CAAC;AAAA,EACpG;AAEA,MAAI,WAAW,YAAa,QAAO,EAAE,GAAG,QAAQ,QAAQ,YAAY;AACpE,MAAI,WAAW,UAAW,QAAO,EAAE,GAAG,QAAQ,QAAQ,UAAU;AAChE,MAAI,WAAW,YAAY;AACzB,UAAM,YAAY,eAAe,QAAQ,WAAW;AACpD,WAAO,YAAY,EAAE,GAAG,QAAQ,QAAQ,UAAU,IAAI;AAAA,EACxD;AACA,MAAI,WAAW,YAAY;AACzB,UAAM,YAAY,eAAe,QAAQ,WAAW;AACpD,UAAM,WAAW,eAAe,QAAQ,UAAU;AAClD,WAAO,aAAa,WAAW,EAAE,GAAG,QAAQ,QAAQ,WAAW,SAAS,IAAI;AAAA,EAC9E;AACA,MAAI,WAAW,cAAc;AAC3B,UAAM,eAAe,eAAe,QAAQ,cAAc;AAC1D,UAAM,qBAAqB,eAAe,QAAQ,oBAAoB;AACtE,WAAO,gBAAgB,qBACnB,EAAE,GAAG,QAAQ,QAAQ,cAAc,mBAAmB,IACtD;AAAA,EACN;AAEA,QAAM,cAAc,eAAe,QAAQ,aAAa;AACxD,QAAM,kBAAkB,eAAe,QAAQ,iBAAiB,KAAK,eAAe,QAAQ,QAAQ;AACpG,SAAO,eAAe,kBAClB,EAAE,GAAG,QAAQ,QAAQ,aAAa,aAAa,gBAAgB,IAC/D;AACN;AAEO,SAAS,YAAY,QAAwB;AAClD,SAAO,QAAQ,MAAM;AACvB;AAEO,SAAS,gBAAgB,QAAgB,UAA0B;AACxE,SAAO,QAAQ,MAAM,aAAa,QAAQ;AAC5C;AAEO,SAAS,mBAAmB,QAAgB,SAA0C;AAC3F,SAAO,QAAQ,MAAM,IAAI,OAAO;AAClC;AAGO,SAAS,wBAAwB,MAAsB,IAA6B;AACzF,MAAI,SAAS,GAAI,QAAO;AACxB,MAAI,SAAS,YAAa,QAAO,OAAO,aAAa,OAAO,gBAAgB,OAAO;AACnF,MAAI,SAAS,UAAW,QAAO,OAAO,cAAc,OAAO,cAAc,OAAO,gBAAgB,OAAO;AACvG,SAAO;AACT;AAEO,SAAS,oBAAoB,MAAuC;AACzE,SAAO,EAAE,MAAM,QAAQ,GAAG,KAAK;AACjC;AAEO,SAAS,oBAAoB,MAAgD;AAClF,MAAI,OAAO,KAAK,QAAQ,EAAE,MAAM,OAAQ,QAAO;AAC/C,SAAO,UAAU,IAAI;AACvB;AAKO,SAAS,wBAAwB,OAA0C;AAChF,QAAM,OAAO,SAAS,KAAK;AAC3B,MAAI,CAAC,QAAQ,KAAK,SAAS,sBAAsB;AAC/C,WAAO,EAAE,WAAW,OAAO,OAAO,8BAA8B;AAAA,EAClE;AACA,QAAM,OAAO,SAAS,SAAS,KAAK,UAAU,GAAG,IAAI,KAAK,SAAS,SAAS,KAAK,IAAI,GAAG,IAAI;AAC5F,MAAI,CAAC,MAAM;AACT,WAAO,EAAE,WAAW,OAAO,OAAO,uCAAuC;AAAA,EAC3E;AACA,QAAM,SAAS,UAAU,MAAM,SAAS;AACxC,SAAO,SACH,EAAE,WAAW,MAAM,OAAO,OAAO,IACjC,EAAE,WAAW,OAAO,OAAO,gDAAgD;AACjF;","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":";AAWA;AAAA,EACE;AAAA,OAKK;AAQA,IAAM,oBAAoB;AAE1B,IAAM,2BAA2B;AAGjC,IAAM,6BAA6B;AAK1C,IAAM,+BAAoD,oBAAI,IAAI,CAAC,YAAY,MAAM,CAAC;AAE/E,SAAS,4BAA4B,MAAuB;AACjE,SAAO,6BAA6B,IAAI,IAAI;AAC9C;AAIO,SAAS,0BAA0B,KAAsB;AAC9D,SAAO,mBAAmB,KAAK,GAAG,KAAK,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ;AACjG;AAsCO,SAAS,wBAAwB,OAA+C;AACrF,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,WAAO,EAAE,WAAW,OAAO,OAAO,wCAAwC;AAAA,EAC5E;AACA,QAAM,UAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,SAAS,KAAK,OAAO,QAAQ,KAAgC,GAAG;AAC/E,QAAI,CAAC,0BAA0B,GAAG,GAAG;AACnC,aAAO,EAAE,WAAW,OAAO,OAAO,oDAAoD,GAAG,GAAG;AAAA,IAC9F;AACA,UAAM,QAAQ,OAAO,cAAc,YAAY,OAAO,cAAc,YAAY,OAAO,cAAc,aAClG,MAAM,QAAQ,SAAS,KAAK,UAAU,MAAM,CAAC,SAAS,OAAO,SAAS,QAAQ;AACjF,QAAI,CAAC,OAAO;AACV,aAAO,EAAE,WAAW,OAAO,OAAO,sBAAsB,GAAG,sDAAsD;AAAA,IACnH;AACA,YAAQ,GAAG,IAAI,MAAM,QAAQ,SAAS,IAAI,CAAC,GAAG,SAAS,IAAI;AAAA,EAC7D;AACA,SAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAC3C;AAgBO,SAAS,4BAA4B,QAAwC;AAClF,SAAO,WAAW;AACpB;AAIO,SAAS,+BACd,MACA,IACS;AACT,SAAO,SAAS,aAAa,OAAO;AACtC;AAGO,SAAS,gBAAgB,QAAmD;AACjF,SAAO,WAAW,YAAY,YAAY;AAC5C;AAEA,SAAS,YAAY,OAAyB;AAC5C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,WAAW;AACtD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,KAAgC,EAC5C,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,MAAM,KAAK,cAAc,KAAK,CAAC,EACnD,IAAI,CAAC,CAAC,KAAK,MAAM,MAAM,CAAC,KAAK,YAAY,MAAM,CAAC,CAAC;AAAA,EACtD;AACF;AAEA,SAAS,0BAA0B,OAAmC;AACpE,UAAQ,SAAS,IAAI,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACjD;AAGO,SAAS,oCAAoC,aAA6C;AAC/F,MAAI,YAAY,SAAS,WAAY,QAAO;AAC5C,SAAO,KAAK,UAAU,YAAY;AAAA,IAChC,MAAM,YAAY;AAAA,IAClB,OAAO,0BAA0B,YAAY,KAAK;AAAA,IAClD,MAAM,0BAA0B,YAAY,IAAI;AAAA,IAChD,QAAQ,YAAY;AAAA,EACtB,CAAC,CAAC;AACJ;AAEO,SAAS,oCAAoC,cAAoD;AACtG,QAAM,OAAO,oBAAI,IAAY;AAC7B,SAAO,aAAa,OAAO,CAAC,gBAAgB;AAC1C,UAAM,YAAY,oCAAoC,WAAW;AACjE,QAAI,CAAC,UAAW,QAAO;AACvB,QAAI,KAAK,IAAI,SAAS,EAAG,QAAO;AAChC,SAAK,IAAI,SAAS;AAClB,WAAO;AAAA,EACT,CAAC;AACH;AAaO,SAAS,wBAAwB,MAAmE;AACzG,QAAM,UAAU,MAAM;AACtB,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,WAAO,EAAE,WAAW,OAAO,OAAO,8CAA8C;AAAA,EAClF;AACA,QAAM,aAAa,yBAAyB,UAAU,OAAO;AAC7D,MAAI,CAAC,WAAW,SAAS;AACvB,WAAO,EAAE,WAAW,OAAO,OAAO,kCAAkC,WAAW,MAAM,OAAO,GAAG;AAAA,EACjG;AACA,SAAO,EAAE,WAAW,MAAM,OAAO,QAAkC;AACrE;AAOO,SAAS,uBACd,MACyF;AACzF,QAAM,KAAK,OAAO,MAAM,OAAO,YAAY,KAAK,KAAK,KAAK,KAAK;AAC/D,MAAI,CAAC,GAAI,QAAO,EAAE,WAAW,OAAO,OAAO,yCAAyC;AACpF,QAAM,SAAS,OAAO,MAAM,WAAW,YAAY,KAAK,SAAS,KAAK,SAAS;AAC/E,SAAO,EAAE,WAAW,MAAM,OAAO,EAAE,IAAI,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC,EAAG,EAAE;AACzE;AAQO,SAAS,qBAAqB,OAAsC;AACzE,MAAI,MAAM,SAAS,OAAQ,QAAO;AAClC,MAAI,MAAM,SAAS,SAAU,QAAQ,MAA0B,gBAAgB;AAC/E,SAAO;AACT;AASO,SAAS,yBAAyB,SAAsD;AAC7F,QAAM,aAAuC,CAAC;AAC9C,aAAW,eAAe,SAAS;AAKjC,QAAI,YAAY,SAAS,WAAY;AACrC,UAAM,QAAQ,YAAY,OAAO,KAAK,oBAAoB,KAAK,YAAY,OAAO,CAAC;AACnF,QAAI,CAAC,MAAO;AACZ,eAAW,KAAK,EAAE,eAAe,YAAY,IAAI,MAAM,CAAC;AAAA,EAC1D;AACA,SAAO;AACT;AAIO,SAAS,mBAAmB,OAA6B,MAA+B;AAC7F,SAAO,EAAE,CAAC,MAAM,IAAI,GAAG,MAAM,SAAS,WAAW,CAAC,IAAI,IAAI,KAAK;AACjE;AAMO,SAAS,mBAAmB,IAAoB;AACrD,SAAO,eAAe,EAAE;AAC1B;AAEO,SAAS,cAAc,IAAoB;AAChD,SAAO,UAAU,EAAE;AACrB;AAiCO,SAAS,WAAW,YAAwB,IAAY,MAAmC;AAChG,SAAO,EAAE,MAAM,UAAU,IAAI,YAAY,KAAK;AAChD;AAGO,SAAS,2BAA2B,SAAkD;AAC3F,SAAO;AAAA,IACL,IAAI,QAAQ;AAAA,IACZ,MAAM,QAAQ;AAAA,IACd,OAAO,QAAQ;AAAA,IACf,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,QAAQ,QAAQ,WAAW;AAAA,IAC3B,QAAQ;AAAA,EACV;AACF;AAGO,SAAS,2BACd,SACA,QACA,cACA,SAC0B;AAC1B,QAAM,gBAAgB,YAAY,SAAY,SAAY,wBAAwB,OAAO;AACzF,MAAI,iBAAiB,CAAC,cAAc,UAAW,OAAM,IAAI,UAAU,cAAc,KAAK;AACtF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,IAAI,QAAQ;AAAA,IACZ,MAAM,QAAQ;AAAA,IACd,OAAO,QAAQ;AAAA,IACf,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,YAAY,EAAE,QAAQ,QAAQ,WAAW,OAAO;AAAA,IAChD;AAAA,IACA,GAAI,eAAe,YAAY,EAAE,SAAS,cAAc,MAAM,IAAI,CAAC;AAAA,IACnE,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,EACzC;AACF;AAIO,SAAS,wBACd,OACA,wBACgC;AAChC,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,QAAI,OAAO,KAAK,QAAQ,EAAE,MAAM,iBAAiB,OAAO,KAAK,OAAO,SAAU,QAAO;AACrF,QAAI,CAAC,OAAO,UAAU,eAAe,KAAK,wBAAwB,KAAK,EAAE,EAAG,QAAO;AACnF,UAAM,aAAa,uBAAuB,KAAK,EAAE;AACjD,QAAI,eAAe,OAAW,QAAO;AACrC,UAAM,SAAS,wBAAwB,UAAU;AACjD,QAAI,CAAC,OAAO,UAAW,OAAM,IAAI,UAAU,OAAO,KAAK;AACvD,WAAO,EAAE,GAAG,MAAM,SAAS,OAAO,MAAM;AAAA,EAC1C,CAAC;AACH;AAIO,SAAS,2BAA2B,MAAuD;AAChG,MAAI,OAAO,KAAK,QAAQ,EAAE,MAAM,cAAe,QAAO;AACtD,QAAM,KAAK,OAAO,KAAK,OAAO,YAAY,KAAK,KAAK,KAAK,KAAK;AAC9D,QAAM,OAAO,OAAO,KAAK,SAAS,YAAY,KAAK,OAAO,KAAK,OAAO;AACtE,QAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAC5D,QAAM,aAAa,KAAK;AACxB,QAAM,SAAS,MAAM,QAAQ,YAAY,MAAM,IAAK,WAAW,SAAoC;AACnG,QAAM,SAAS,KAAK;AACpB,QAAM,cAAc,UAAU,CAAC,WAAW,YAAY,YAAY,aAAa,SAAS,EAAE,SAAS,MAAM;AACzG,QAAM,gBAAgB,KAAK,YAAY,SAAY,SAAY,wBAAwB,KAAK,OAAO;AACnG,MAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,eAAgB,iBAAiB,CAAC,cAAc,UAAY,QAAO;AACnG,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,OAAO,KAAK,SAAS,YAAY,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,IACxE;AAAA,IACA;AAAA,IACA,GAAI,eAAe,YAAY,EAAE,SAAS,cAAc,MAAM,IAAI,CAAC;AAAA,IACnE,GAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,eAAe,EAAE,cAAc,KAAK,aAAa,IAAI,CAAC;AAAA,EAC1G;AACF;","names":[]}