@tangle-network/agent-app 0.43.41 → 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.
- package/dist/assistant/index.d.ts +2 -1
- package/dist/assistant/index.js +3 -2
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-routes/index.d.ts +17 -3
- package/dist/chat-routes/index.js +91 -6
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-store/index.d.ts +4 -3
- package/dist/chat-store/index.js +5 -1
- package/dist/chat-store/index.js.map +1 -1
- package/dist/{chunk-Y4QHNQ75.js → chunk-6KVH5SP5.js} +110 -6
- package/dist/chunk-6KVH5SP5.js.map +1 -0
- package/dist/{chunk-XYWCGFII.js → chunk-B5JD3DXD.js} +48 -2
- package/dist/chunk-B5JD3DXD.js.map +1 -0
- package/dist/{chunk-I2R2XT4M.js → chunk-I2ATYB7R.js} +18 -2
- package/dist/chunk-I2ATYB7R.js.map +1 -0
- package/dist/chunk-SIXYZ2FB.js +101 -0
- package/dist/chunk-SIXYZ2FB.js.map +1 -0
- package/dist/{chunk-4TXDD6P2.js → chunk-XAWFPMAR.js} +38 -3
- package/dist/chunk-XAWFPMAR.js.map +1 -0
- package/dist/{chunk-KM766NN3.js → chunk-ZLHK25C3.js} +1766 -1223
- package/dist/chunk-ZLHK25C3.js.map +1 -0
- package/dist/chunk-ZU5GNSOJ.js +920 -0
- package/dist/chunk-ZU5GNSOJ.js.map +1 -0
- package/dist/{contract-DYbTzEDf.d.ts → contract-KfqJh_au.d.ts} +23 -2
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/durable-chat/index.d.ts +419 -0
- package/dist/durable-chat/index.js +59 -0
- package/dist/durable-chat/index.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +123 -45
- package/dist/interactions/index.d.ts +58 -3
- package/dist/interactions/index.js +7 -3
- package/dist/{parts-BcbitSNp.d.ts → parts-DjX0RRTS.d.ts} +9 -3
- package/dist/plans/index.d.ts +69 -0
- package/dist/plans/index.js +21 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/stream/index.js +3 -1
- package/dist/teams/index.js +9 -9
- package/dist/teams/invitations-api.js +3 -3
- package/dist/web-react/index.d.ts +252 -93
- package/dist/web-react/index.js +32 -3
- package/package.json +11 -1
- package/dist/chunk-4TXDD6P2.js.map +0 -1
- package/dist/chunk-I2R2XT4M.js.map +0 -1
- package/dist/chunk-KM766NN3.js.map +0 -1
- package/dist/chunk-XYWCGFII.js.map +0 -1
- package/dist/chunk-Y4QHNQ75.js.map +0 -1
|
@@ -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
|
-
|
|
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-
|
|
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":[]}
|