@tangle-network/agent-app 0.44.28 → 0.44.30

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 (50) hide show
  1. package/dist/assistant/index.d.ts +8 -6
  2. package/dist/assistant/index.js +6 -3
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/{attachment-validation-Dvc_Livy.d.ts → attachment-validation-CNkH91Gs.d.ts} +1 -1
  5. package/dist/chat-react/index.d.ts +192 -0
  6. package/dist/chat-react/index.js +179 -0
  7. package/dist/chat-react/index.js.map +1 -0
  8. package/dist/chat-routes/index.d.ts +7 -6
  9. package/dist/chat-routes/index.js +13 -9
  10. package/dist/chat-routes/index.js.map +1 -1
  11. package/dist/chat-store/index.d.ts +6 -5
  12. package/dist/chat-store/index.js +2 -1
  13. package/dist/chat-store/index.js.map +1 -1
  14. package/dist/chunk-4OO7P3ZF.js +292 -0
  15. package/dist/chunk-4OO7P3ZF.js.map +1 -0
  16. package/dist/chunk-4PUMUTLU.js +158 -0
  17. package/dist/chunk-4PUMUTLU.js.map +1 -0
  18. package/dist/{chunk-7775L5NN.js → chunk-BAC2B2KI.js} +170 -7
  19. package/dist/chunk-BAC2B2KI.js.map +1 -0
  20. package/dist/{chunk-PRKSYTMQ.js → chunk-GCH3BUAZ.js} +31 -317
  21. package/dist/chunk-GCH3BUAZ.js.map +1 -0
  22. package/dist/{chunk-BI6NKSO4.js → chunk-IYLJS6VW.js} +3 -84
  23. package/dist/chunk-IYLJS6VW.js.map +1 -0
  24. package/dist/chunk-QY4BRKRJ.js +84 -0
  25. package/dist/chunk-QY4BRKRJ.js.map +1 -0
  26. package/dist/{chunk-C3SRFZGL.js → chunk-QYAQGCHF.js} +2 -154
  27. package/dist/chunk-QYAQGCHF.js.map +1 -0
  28. package/dist/{contract-CEewO6DI.d.ts → contract-CQNvv5th.d.ts} +1 -1
  29. package/dist/interactions/index.d.ts +2 -2
  30. package/dist/{parts-fyPPdDdK.d.ts → parts-7fbe2rj8.d.ts} +4 -274
  31. package/dist/{queue-vRI0Qx3X.d.ts → queue-VTBA5ONX.d.ts} +1 -1
  32. package/dist/sandbox/index.d.ts +253 -1
  33. package/dist/sandbox/index.js +9 -1
  34. package/dist/stream/index.d.ts +3 -3
  35. package/dist/{stream-normalizer-DnuqkZvw.d.ts → stream-normalizer-CnPnMaTp.d.ts} +1 -1
  36. package/dist/teams/index.js +5 -5
  37. package/dist/teams/invitations-api.js +4 -4
  38. package/dist/teams-react/index.js +3 -3
  39. package/dist/{types-DB82fktc.d.ts → types-CCeYywdS.d.ts} +1 -1
  40. package/dist/use-file-mentions-E6a7_cbH.d.ts +98 -0
  41. package/dist/web-react/index.d.ts +13 -104
  42. package/dist/web-react/index.js +17 -12
  43. package/dist/wire-DSp4LzEE.d.ts +272 -0
  44. package/dist/work-product/index.d.ts +3 -3
  45. package/dist/work-product-react/index.d.ts +1 -1
  46. package/package.json +6 -1
  47. package/dist/chunk-7775L5NN.js.map +0 -1
  48. package/dist/chunk-BI6NKSO4.js.map +0 -1
  49. package/dist/chunk-C3SRFZGL.js.map +0 -1
  50. package/dist/chunk-PRKSYTMQ.js.map +0 -1
@@ -0,0 +1,158 @@
1
+ import {
2
+ persistedPartToWorkProduct
3
+ } from "./chunk-ZVEEWGDK.js";
4
+ import {
5
+ mentionKindForPath
6
+ } from "./chunk-QYAQGCHF.js";
7
+ import {
8
+ persistedPartToInteraction
9
+ } from "./chunk-3ZK5IJSW.js";
10
+ import {
11
+ persistedPartToPlan,
12
+ planToPersistedPart
13
+ } from "./chunk-YJMCRXQQ.js";
14
+
15
+ // src/chat-store/parts.ts
16
+ function toChatMessageParts(parts) {
17
+ const out = [];
18
+ for (const part of parts) {
19
+ const typed = toChatMessagePart(part);
20
+ if (typed) out.push(typed);
21
+ }
22
+ return out;
23
+ }
24
+ var str = (value) => typeof value === "string";
25
+ function toChatMessagePart(part) {
26
+ if (!part || typeof part !== "object") return null;
27
+ const type = part.type;
28
+ switch (type) {
29
+ case "text":
30
+ case "reasoning":
31
+ return str(part.text) ? part : null;
32
+ case "tool":
33
+ return str(part.id) && str(part.tool) && part.state && typeof part.state === "object" ? part : null;
34
+ case "file":
35
+ case "image":
36
+ return part;
37
+ case "subtask":
38
+ return str(part.prompt) && str(part.description) && str(part.agent) ? part : null;
39
+ case "step-start":
40
+ return { type: "step-start" };
41
+ case "step-finish":
42
+ return part;
43
+ case "interaction":
44
+ return persistedPartToInteraction(part) ? part : null;
45
+ case "notice":
46
+ return str(part.id) && str(part.noticeKind) && str(part.text) ? part : null;
47
+ case "plan": {
48
+ const plan = persistedPartToPlan(part);
49
+ return plan ? { ...part, ...planToPersistedPart(plan) } : null;
50
+ }
51
+ case "work_product":
52
+ return persistedPartToWorkProduct(part) ? part : null;
53
+ case "mention":
54
+ return isChatMentionPart(part) ? part : null;
55
+ case void 0:
56
+ return null;
57
+ default: {
58
+ const _exhaustive = type;
59
+ void _exhaustive;
60
+ return null;
61
+ }
62
+ }
63
+ }
64
+ function isChatToolPart(part) {
65
+ return part.type === "tool";
66
+ }
67
+ function isChatTextPart(part) {
68
+ return part.type === "text";
69
+ }
70
+ function isChatInteractionPart(part) {
71
+ return part.type === "interaction";
72
+ }
73
+ function isChatPlanPart(part) {
74
+ return part.type === "plan";
75
+ }
76
+ function isChatWorkProductPart(part) {
77
+ return part.type === "work_product";
78
+ }
79
+ function isChatStepFinishPart(part) {
80
+ return part.type === "step-finish";
81
+ }
82
+ function isChatMentionPart(part) {
83
+ if (!part || typeof part !== "object") return false;
84
+ const record = part;
85
+ if (record.size !== void 0 && (typeof record.size !== "number" || !Number.isFinite(record.size))) {
86
+ return false;
87
+ }
88
+ return record.type === "mention" && typeof record.path === "string" && record.path.length > 0 && typeof record.name === "string" && record.name.trim().length > 0 && (record.mentionKind === "image" || record.mentionKind === "file");
89
+ }
90
+ function mentionPartsFromMessageParts(parts) {
91
+ if (!parts) return [];
92
+ return parts.filter(isChatMentionPart);
93
+ }
94
+ function mentionInputToPart(input) {
95
+ const part = {
96
+ type: "mention",
97
+ mentionKind: mentionKindForPath(input.path),
98
+ path: input.path,
99
+ name: input.name
100
+ };
101
+ if (typeof input.size === "number" && Number.isFinite(input.size)) part.size = input.size;
102
+ return part;
103
+ }
104
+ function attachmentKindForMime(mime) {
105
+ return mime?.startsWith("image/") ? "image" : "file";
106
+ }
107
+ function isChatAttachmentPart(part) {
108
+ if (!part || typeof part !== "object") return false;
109
+ const record = part;
110
+ return (record.type === "image" || record.type === "file") && typeof record.path === "string" && record.path.length > 0;
111
+ }
112
+ function attachmentInputToPart(input) {
113
+ const part = { type: input.kind, path: input.path, name: input.name };
114
+ if (typeof input.size === "number" && Number.isFinite(input.size)) part.size = input.size;
115
+ if (input.mediaType) part.mediaType = input.mediaType;
116
+ return part;
117
+ }
118
+ function attachmentPartsFromMessageParts(parts) {
119
+ if (!parts) return [];
120
+ return parts.filter(isChatAttachmentPart);
121
+ }
122
+ var DEFAULT_ATTACHMENT_PROMPT_HEADER = "Attached files (already saved to the workspace vault \u2014 read them from these paths):";
123
+ var PROMPT_BLOCK_CONTROL_CHARS = /[\x00-\x1F\x7F]/g;
124
+ function buildAttachmentPromptBlock(atts, header = DEFAULT_ATTACHMENT_PROMPT_HEADER) {
125
+ if (atts.length === 0) return "";
126
+ const clean = (value) => value.replace(PROMPT_BLOCK_CONTROL_CHARS, " ");
127
+ const lines = atts.map((a) => `- ${clean(a.name)} (vault: ${clean(a.path)})`);
128
+ return `
129
+
130
+ ${header}
131
+ ${lines.join("\n")}`;
132
+ }
133
+ function historyContentWithAttachments(message, header) {
134
+ const attachmentParts = attachmentPartsFromMessageParts(message.parts);
135
+ if (attachmentParts.length === 0) return message.content;
136
+ return `${message.content}${buildAttachmentPromptBlock(attachmentParts, header)}`;
137
+ }
138
+
139
+ export {
140
+ toChatMessageParts,
141
+ isChatToolPart,
142
+ isChatTextPart,
143
+ isChatInteractionPart,
144
+ isChatPlanPart,
145
+ isChatWorkProductPart,
146
+ isChatStepFinishPart,
147
+ isChatMentionPart,
148
+ mentionPartsFromMessageParts,
149
+ mentionInputToPart,
150
+ attachmentKindForMime,
151
+ isChatAttachmentPart,
152
+ attachmentInputToPart,
153
+ attachmentPartsFromMessageParts,
154
+ DEFAULT_ATTACHMENT_PROMPT_HEADER,
155
+ buildAttachmentPromptBlock,
156
+ historyContentWithAttachments
157
+ };
158
+ //# sourceMappingURL=chunk-4PUMUTLU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-store/parts.ts"],"sourcesContent":["/**\n * The stored shape of `message.parts` — one typed vocabulary for every part a\n * product persists into a chat transcript. NOT an ad-hoc union reverse-\n * engineered from product schemas; each member is matched field-for-field to\n * its canonical source:\n *\n * - `text` / `reasoning` / `tool`: the persisted projection `/stream`'s\n * `normalizePersistedPart` produces from the harness lane's\n * `message.part.updated` events (ADC sidecar\n * `apps/sidecar/src/events/session-events.ts:56` wraps the canonical part in\n * an `{id, sessionID, messageID}` envelope; the projection strips the\n * session/message ids and keeps the per-segment part id).\n * - `file` / `image` / `step-start` / `step-finish`: the sidecar's canonical\n * `MessagePartSchema` members (ADC\n * `apps/sidecar/src/schemas/agent-schemas.ts:50-154`); `step-finish` carries\n * the harness's per-step usage receipt — tokens\n * `{total, input, output, reasoning, cache{write, read}}` + `cost` — which is\n * also the shape the message-level token/cost columns mirror.\n * - `subtask`: `@tangle-network/agent-interface`'s `SubtaskPart` (a spawned\n * sub-agent task).\n * - `interaction` / `notice`: the persisted-part codecs in\n * `/web-react`'s chat-interactions contract (`interactionToPersistedPart`,\n * `noticePart`) — type-only imports, one source of truth for their statuses\n * and field shapes.\n * - `plan`: the durable-plan projection in `/plans`, derived from the sandbox\n * SDK's authoritative plan lifecycle.\n * - `work_product`: the persisted work-product anchor card in\n * `/work-product`'s contract (`workProductToPersistedPart` /\n * `persistedPartToWorkProduct`) — SYSTEM-authored on the ready transition\n * and updated on a reviewer verdict; no prompt teaches an agent to author\n * one.\n * - `mention`: an `@`-picked reference to a file that already lives in the\n * workspace sandbox (`FileMention` in `/chat-routes`'s wire contract, plus\n * the image/file discriminant). Neither transport lane produces it — the\n * turn route persists it from the request's `mentions` field — but it is a\n * part a product persists into a transcript, so it belongs in this\n * vocabulary rather than in a parallel one.\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 persistedPartToWorkProduct,\n type WorkProductPersistedPart,\n} from '../work-product/types'\nimport {\n persistedPartToPlan,\n planToPersistedPart,\n type ChatPlanPersistedPart,\n} from '../plans/index'\n// `./wire` is import-free by construction, so this edge costs the store\n// nothing and keeps ONE image-extension table for the whole mention layer.\nimport {\n mentionKindForPath,\n type ChatAttachmentInput,\n type ChatAttachmentKind,\n type ChatMentionKind,\n type FileMention,\n} from '../chat-routes/wire'\n\nexport type { ChatMentionKind, ChatAttachmentKind }\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\n/** Define a reasoning part of a chat with text content and optional metadata fields */\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\n/** Describe the current state and data of a chat tool including status, input, output, and metadata */\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\n/** Define a chat component representing a tool with its state and optional call identifier */\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. `name` is the\n * attachment-flavored display label an uploaded/promoted file carries (see\n * {@link ChatAttachmentPart}) — absent on a harness-emitted file part, which\n * uses `filename`. */\nexport interface ChatFilePart {\n type: 'file'\n id?: string\n filename?: string\n name?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\n/** Define properties for an image part within a chat message including optional metadata fields */\nexport interface ChatImagePart {\n type: 'image'\n filename?: string\n name?: string\n mediaType?: string\n url?: string\n path?: string\n}\n\n/** Define a subtask part of a chat with prompt, description, agent, and optional identifier */\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\n/** Define a chat step finish part indicating completion with optional reason, tokens, and cost */\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\n/** Resolve a chat plan part by aliasing it to the persisted chat plan part type */\nexport type ChatPlanPart = ChatPlanPersistedPart\n\n/** Persisted work-product anchor card — byte-matches\n * `workProductToPersistedPart` in `/work-product`'s contract. Written by the\n * PLATFORM on the ready transition (and updated on a verdict); never\n * authored by a prompt. */\nexport type ChatWorkProductPart = WorkProductPersistedPart\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/**\n * A file the user `@`-mentioned on this turn: a workspace-relative path into\n * the sandbox, never bytes. `type: 'mention'` is its own discriminant\n * precisely so it does NOT collide with the `file`/`image` attachment parts —\n * an attachment carries content the product uploaded, a mention points at\n * something the box already has, and a transcript renders them differently\n * (an inline pill, not an attachment card).\n *\n * `path` is the identity: mentioning one file twice in a turn folds to one\n * part. `turnId` is optional and set by products that rebuild a turn's\n * mentions on retry.\n */\nexport interface ChatMentionPart {\n type: 'mention'\n mentionKind: ChatMentionKind\n path: string\n name: string\n size?: number\n turnId?: 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>\ntype _CodecEmitsStorableWorkProductPart = MutuallyAssignable<WorkProductPersistedPart, ChatWorkProductPart>\ntype _StoredWorkProductPartFeedsCodec = MutuallyAssignable<ChatWorkProductPart, WorkProductPersistedPart>\n\n/** Represent parts of a chat message including text, reasoning, tools, files, images, subtasks, steps, interactions, notices, plans, and mentions */\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 | ChatWorkProductPart\n | ChatMentionPart\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 'work_product':\n // Validate via the codec but keep the original row (extra fields the\n // product persisted round-trip, matching the interaction case).\n return persistedPartToWorkProduct(part) ? (part as unknown as ChatWorkProductPart) : null\n case 'mention':\n return isChatMentionPart(part) ? part : null\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\n/** Resolve whether a ChatMessagePart is specifically a ChatToolPart based on its type property */\nexport function isChatToolPart(part: ChatMessagePart): part is ChatToolPart {\n return part.type === 'tool'\n}\n\n/** Resolve whether a chat message part is a text part based on its type property */\nexport function isChatTextPart(part: ChatMessagePart): part is ChatTextPart {\n return part.type === 'text'\n}\n\n/** Resolve whether a ChatMessagePart is a ChatInteractionPart based on its type property */\nexport function isChatInteractionPart(part: ChatMessagePart): part is ChatInteractionPart {\n return part.type === 'interaction'\n}\n\n/** Resolve whether a chat message part is a persisted chat plan part */\nexport function isChatPlanPart(part: ChatMessagePart): part is ChatPlanPart {\n return part.type === 'plan'\n}\n\n/** Resolve whether a chat message part is a persisted work-product anchor */\nexport function isChatWorkProductPart(part: ChatMessagePart): part is ChatWorkProductPart {\n return part.type === 'work_product'\n}\n\n/** Determine if a chat message part represents the completion of a chat step */\nexport function isChatStepFinishPart(part: ChatMessagePart): part is ChatStepFinishPart {\n return part.type === 'step-finish'\n}\n\n/** Widened to `unknown` — unlike its siblings this guard also runs over raw\n * untyped stored rows (a transcript renderer reads `message.parts` before the\n * typed projection), which is exactly what {@link mentionPartsFromMessageParts}\n * needs. `path` and `name` carry the pill; a row missing either is unrenderable.\n *\n * Mirrors the write contract exactly (`parseFileMention` in `/chat-routes`,\n * then {@link mentionInputToPart}): a blank `name` is rejected there and so is\n * rejected here, and `size` — optional, but typed `number` once present — is\n * type-checked so `'12'` or `null` cannot ride through the guard wearing a\n * type it does not have. Negative sizes are NOT re-rejected: the wire screens\n * them, `mentionInputToPart` trusts its input, and a read guard stricter than\n * what the writer can emit would drop rows it produced itself. */\nexport function isChatMentionPart(part: unknown): part is ChatMentionPart {\n if (!part || typeof part !== 'object') return false\n const record = part as Record<string, unknown>\n if (record.size !== undefined && (typeof record.size !== 'number' || !Number.isFinite(record.size))) {\n return false\n }\n return (\n record.type === 'mention' &&\n typeof record.path === 'string' &&\n record.path.length > 0 &&\n typeof record.name === 'string' &&\n record.name.trim().length > 0 &&\n (record.mentionKind === 'image' || record.mentionKind === 'file')\n )\n}\n\n/** Every mention part on one message, in stored order. The projection a\n * transcript renderer runs before deciding which mentions the message text\n * already shows inline (see `segmentMentionContent` in `/web-react`). */\nexport function mentionPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | ReadonlyArray<ChatMessagePart> | null | undefined,\n): ChatMentionPart[] {\n if (!parts) return []\n return (parts as ReadonlyArray<unknown>).filter(isChatMentionPart)\n}\n\n/** A validated wire mention (`parseFileMentions` in `/chat-routes`) as the\n * part the turn route persists. An absent/non-finite `size` is DROPPED rather\n * than stored as `undefined`, so a stored row never carries a key that means\n * nothing. */\nexport function mentionInputToPart(input: FileMention): ChatMentionPart {\n const part: ChatMentionPart = {\n type: 'mention',\n mentionKind: mentionKindForPath(input.path),\n path: input.path,\n name: input.name,\n }\n if (typeof input.size === 'number' && Number.isFinite(input.size)) part.size = input.size\n return part\n}\n\n// ── attachments ──────────────────────────────────────────────────────────\n//\n// An attachment is a file the product uploaded into its own store (vault /\n// object-store) ahead of the turn — content the box did not already have,\n// unlike a `@`-mention. It persists NOT as a new `ChatMessagePart` union\n// member but as an attachment-flavored `file`/`image` part: `type` is the\n// existing `image`/`file` discriminant, with `path` + `name` load-bearing.\n// That reuse is deliberate — a transcript already renders `file`/`image`\n// parts, and `toChatMessagePart` passes them through by cast (fields survive),\n// so an attachment slots into the same stored `parts` array without a parallel\n// vocabulary. These helpers own the input→part projection, the path-keyed\n// stream key, and the agent-facing prompt block, all byte-identical to the gtm\n// source they were lifted from (gtm-agent#618 adoption reproduces its rows and\n// dispatched prompt bytes through them).\n\n/** `image` for any `image/*` mime, `file` for everything else (including an\n * absent/empty mime) — the same split the composer and the persisted-part\n * discriminant both key on. */\nexport function attachmentKindForMime(mime?: string): ChatAttachmentKind {\n return mime?.startsWith('image/') ? 'image' : 'file'\n}\n\n/** Stream/transcript part key for an attachment — ONE declaration, owned by\n * `/stream`'s normalizer (whose `getPartKey` routes path-bearing file/image\n * parts through it). Re-exported here (not redefined) so the root barrel's\n * star-exports of both modules resolve to the same symbol instead of\n * colliding (TS2308 in the dts build). */\nexport { attachmentPartKey } from '../stream/stream-normalizer'\n\n/**\n * Persisted attachment part: structurally an attachment-flavored `ChatFilePart`\n * / `ChatImagePart` (`path` + `name` promoted to required) rather than a\n * separate union member — see the section note above. The index signature keeps\n * it a `Record<string, unknown>` so it drops into the same untyped `parts`\n * arrays every other stored part rides in, and lets the `type`-driven\n * constructors below build it without union-discriminant gymnastics.\n */\nexport interface ChatAttachmentPart {\n type: ChatAttachmentKind\n path: string\n name: string\n size?: number\n mediaType?: string\n turnId?: string\n [key: string]: unknown\n}\n\n/** True for any `image`/`file` part carrying a non-empty string `path`.\n * DELIBERATELY also matches a sidecar path-bearing file part (a mention-style\n * `file` part with only `path`) — the guard is a structural attachment shape,\n * not proof of provenance; a caller that needs to exclude those filters\n * upstream. The `path.length > 0` check diverges from gtm's original guard\n * (which only checked `typeof`), mirroring sibling {@link isChatMentionPart}:\n * read-side robustness against a stored `{type:'file', path:''}` row — which\n * would otherwise render a malformed `(vault: )` pointer line — justifies the\n * small divergence from byte-for-byte gtm parity here; no legitimate write\n * path ever produces an empty `path`. */\nexport function isChatAttachmentPart(part: unknown): part is ChatAttachmentPart {\n if (!part || typeof part !== 'object') return false\n const record = part as Record<string, unknown>\n return (\n (record.type === 'image' || record.type === 'file') &&\n typeof record.path === 'string' &&\n record.path.length > 0\n )\n}\n\n/** Drop absent/empty optional fields rather than persisting `undefined`/`''`\n * — keeps stored parts minimal. */\nexport function attachmentInputToPart(input: ChatAttachmentInput): ChatAttachmentPart {\n const part: ChatAttachmentPart = { type: input.kind, path: input.path, name: input.name }\n if (typeof input.size === 'number' && Number.isFinite(input.size)) part.size = input.size\n if (input.mediaType) part.mediaType = input.mediaType\n return part\n}\n\n/** Every attachment part on one message's stored `parts`, in stored order. */\nexport function attachmentPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | null | undefined,\n): ChatAttachmentPart[] {\n if (!parts) return []\n return parts.filter(isChatAttachmentPart)\n}\n\n/** gtm's exact default header for {@link buildAttachmentPromptBlock} — kept\n * byte-identical because it feeds dispatched prompts (gtm-agent#618\n * reproduces the prompt bytes through this module). Override only with intent. */\nexport const DEFAULT_ATTACHMENT_PROMPT_HEADER =\n 'Attached files (already saved to the workspace vault — read them from these paths):'\n\n/** Same C0-control/DEL sweep as `/chat-routes`'s `resolve-attachments.ts`\n * (kept as a separate literal rather than a shared import — this module is\n * the read side and must stay usable over PERSISTED rows the wire validator\n * never saw). Replaced with a single space, not stripped or collapsed: this\n * is defense-in-depth for legacy/corrupt stored rows that bypassed wire\n * validation, not the primary control — a name/path a `resolveChatAttachments`\n * call validated today never contains one, so this is a no-op on legitimate\n * input and gtm byte-compat holds. */\nconst PROMPT_BLOCK_CONTROL_CHARS = /[\\x00-\\x1F\\x7F]/g\n\n/**\n * The agent-facing pointer block appended to the dispatched prompt — never\n * persisted in `message.content`. Empty array → `''` so callers can append\n * unconditionally. The sole producer of this text: both the current turn's\n * dispatch and history projection ({@link historyContentWithAttachments}) build\n * it from here, so the two can never drift. `header` overrides the first line\n * ({@link DEFAULT_ATTACHMENT_PROMPT_HEADER}); the per-file line format is fixed.\n *\n * Every interpolated `name`/`path` is swept for control characters first\n * (newline/carriage-return/tab included) and each one replaced with a single\n * space — a single-line-per-file guarantee. Without it, a control character\n * riding through on a PERSISTED row (`historyContentWithAttachments` reads\n * whatever is already stored; a legacy or hand-edited row was never re-checked\n * against `resolveChatAttachments`) could inject extra lines — fabricated\n * pointer entries or instructions — into a prompt the agent trusts verbatim.\n * The wire-side validator is the primary control; this is the backstop for\n * rows it never saw.\n */\nexport function buildAttachmentPromptBlock(\n atts: ReadonlyArray<Pick<ChatAttachmentPart, 'name' | 'path'>>,\n header: string = DEFAULT_ATTACHMENT_PROMPT_HEADER,\n): string {\n if (atts.length === 0) return ''\n const clean = (value: string) => value.replace(PROMPT_BLOCK_CONTROL_CHARS, ' ')\n const lines = atts.map((a) => `- ${clean(a.name)} (vault: ${clean(a.path)})`)\n return `\\n\\n${header}\\n${lines.join('\\n')}`\n}\n\n/**\n * History projection for a persisted message: `content` unchanged when it\n * carries no attachment parts, else the block is appended once. The\n * no-double-annotation guarantee holds under the invariant that the block is\n * never persisted INTO `message.content` — it is appended only at read time,\n * by this function and the current turn's dispatch, both sourcing from\n * `attachmentPartsFromMessageParts` — never by content inspection. This\n * function does not (and cannot) detect an already-embedded block by scanning\n * `content` for its text: a message whose stored `content` happens to already\n * contain the block's bytes, alongside attachment parts, still gets the block\n * appended a second time. See the pinned test for that boundary.\n */\nexport function historyContentWithAttachments(\n message: {\n content: string\n parts?: ReadonlyArray<Record<string, unknown>> | null\n },\n header?: string,\n): string {\n const attachmentParts = attachmentPartsFromMessageParts(message.parts)\n if (attachmentParts.length === 0) return message.content\n return `${message.content}${buildAttachmentPromptBlock(attachmentParts, header)}`\n}\n"],"mappings":"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","names":[]}
@@ -304,6 +304,11 @@ function createWorkspaceSandboxConnectionHandler(opts) {
304
304
  });
305
305
  };
306
306
  }
307
+ function sandboxSidecarProxyUrl(baseUrl, sandboxId) {
308
+ if (!baseUrl) throw new Error("baseUrl is required");
309
+ if (!sandboxId) throw new Error("sandboxId is required");
310
+ return new URL(`/v1/sidecar-proxy/${encodeURIComponent(sandboxId)}`, baseUrl).toString().replace(/\/+$/, "");
311
+ }
307
312
  function createWorkspaceSandboxRuntimeProxyHandler(opts) {
308
313
  return async function handleWorkspaceSandboxRuntimeProxy({ request, params }) {
309
314
  const user = await opts.requireUser(request);
@@ -325,10 +330,7 @@ function createWorkspaceSandboxRuntimeProxyHandler(opts) {
325
330
  const runtimeConnection = await opts.getSandboxRuntimeConnection?.({ request, userId: user.id, workspaceId, sandboxId });
326
331
  const directRuntimeConnection = runtimeConnection?.runtimeUrl && runtimeConnection.authToken ? runtimeConnection : null;
327
332
  const credentials = directRuntimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
328
- const upstreamUrl = directRuntimeConnection ? new URL(encodedRuntimePath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(
329
- `/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${encodedRuntimePath}`,
330
- credentials.baseUrl
331
- );
333
+ const upstreamUrl = directRuntimeConnection ? new URL(encodedRuntimePath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(encodedRuntimePath, `${sandboxSidecarProxyUrl(credentials.baseUrl, sandboxId)}/`);
332
334
  upstreamUrl.search = requestUrl.search;
333
335
  const headers = buildSandboxRuntimeProxyHeaders(
334
336
  request.headers,
@@ -405,17 +407,41 @@ function createWorkspaceSandboxTerminalUpgradeHandler(opts) {
405
407
  const runtimeConnection = await opts.getSandboxRuntimeConnection?.({ request, userId: user.id, workspaceId, sandboxId });
406
408
  const directRuntimeConnection = runtimeConnection?.runtimeUrl && runtimeConnection.authToken ? runtimeConnection : null;
407
409
  const credentials = directRuntimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
408
- const upstreamUrl = directRuntimeConnection ? new URL(subPath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${subPath}`, credentials.baseUrl);
410
+ const upstreamUrl = directRuntimeConnection ? new URL(subPath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(subPath, `${sandboxSidecarProxyUrl(credentials.baseUrl, sandboxId)}/`);
409
411
  upstreamUrl.search = url.search;
410
412
  const upstreamHeaders = new Headers(request.headers);
411
413
  const upstreamBearer = directRuntimeConnection?.authToken ?? credentials.apiKey;
412
414
  upstreamHeaders.set("Authorization", `Bearer ${upstreamBearer}`);
413
415
  upstreamHeaders.delete("host");
416
+ const browserProtocol = selectedBearerSubprotocol(request.headers.get("Sec-WebSocket-Protocol"));
414
417
  stripBearerSubprotocol(upstreamHeaders);
415
418
  const fetchImpl = opts.fetch ?? fetch;
416
- return fetchImpl(upstreamUrl.toString(), { method: request.method, headers: upstreamHeaders });
419
+ const upstream = await fetchImpl(upstreamUrl.toString(), { method: request.method, headers: upstreamHeaders });
420
+ const echo = terminalUpgradeSubprotocolEcho(upstream, browserProtocol);
421
+ if (!echo) return upstream;
422
+ return new Response(null, {
423
+ status: echo.status,
424
+ statusText: echo.statusText,
425
+ headers: echo.headers,
426
+ webSocket: upstream.webSocket ?? null
427
+ });
417
428
  };
418
429
  }
430
+ function terminalUpgradeSubprotocolEcho(upstream, browserProtocol) {
431
+ if (upstream.status !== 101 || !browserProtocol) return null;
432
+ if (upstream.headers.has("Sec-WebSocket-Protocol")) return null;
433
+ const headers = new Headers(upstream.headers);
434
+ headers.set("Sec-WebSocket-Protocol", browserProtocol);
435
+ return { status: upstream.status, statusText: upstream.statusText ?? "", headers };
436
+ }
437
+ function selectedBearerSubprotocol(value) {
438
+ if (!value) return null;
439
+ for (const part of value.split(",")) {
440
+ const protocol = part.trim();
441
+ if (protocol.toLowerCase().startsWith(BEARER_SUBPROTOCOL_PREFIX)) return protocol;
442
+ }
443
+ return null;
444
+ }
419
445
  var DEFAULT_RUNTIME_PROXY_HEADERS = ["accept", "content-type", "last-event-id", "x-session-id"];
420
446
  function buildSandboxRuntimeProxyHeaders(source, sandboxApiKey, forwardHeaders = DEFAULT_RUNTIME_PROXY_HEADERS) {
421
447
  const headers = new Headers();
@@ -484,6 +510,139 @@ function validateTerminalSubject(subject) {
484
510
  if (!subject.sandboxId) throw new Error("sandboxId is required");
485
511
  }
486
512
 
513
+ // src/sandbox/prewarm.ts
514
+ function prewarmKey(scope) {
515
+ return `${scope.workspaceId}::${scope.harness}`;
516
+ }
517
+ function errText(err) {
518
+ return err instanceof Error ? err.message : String(err);
519
+ }
520
+ function createSandboxPrewarmer(shell, options) {
521
+ const mode = options.mode ?? "resume-only";
522
+ const claimTtlSeconds = options.claimTtlSeconds ?? 180;
523
+ const failureCooldownMs = options.failureCooldownMs ?? 6e4;
524
+ const now = options.now ?? (() => Date.now());
525
+ const claim = options.claim === "single-isolate-only" ? null : options.claim;
526
+ const inFlight = /* @__PURE__ */ new Map();
527
+ const failures = /* @__PURE__ */ new Map();
528
+ const emit = (event) => {
529
+ try {
530
+ options.onEvent?.(event);
531
+ } catch {
532
+ }
533
+ };
534
+ async function releaseClaim(key) {
535
+ if (!claim) return;
536
+ try {
537
+ await claim.release(key);
538
+ } catch {
539
+ }
540
+ }
541
+ async function runWarm(key, scope, holdsClaim) {
542
+ const startedAt = now();
543
+ emit({ type: "started", key, workspaceId: scope.workspaceId });
544
+ try {
545
+ const box = await ensureWorkspaceSandbox(shell, {
546
+ workspaceId: scope.workspaceId,
547
+ userId: scope.userId,
548
+ harness: scope.harness,
549
+ billingOwnerId: scope.billingOwnerId
550
+ });
551
+ const ms = now() - startedAt;
552
+ failures.delete(key);
553
+ emit({ type: "succeeded", key, workspaceId: scope.workspaceId, boxId: box.id, ms });
554
+ return { ok: true, boxId: box.id, ms };
555
+ } catch (err) {
556
+ const ms = now() - startedAt;
557
+ const error = errText(err);
558
+ failures.set(key, { error, until: now() + failureCooldownMs });
559
+ emit({ type: "failed", key, workspaceId: scope.workspaceId, error, ms });
560
+ return { ok: false, error, ms };
561
+ } finally {
562
+ inFlight.delete(key);
563
+ if (holdsClaim) await releaseClaim(key);
564
+ }
565
+ }
566
+ return {
567
+ async prewarm(scope) {
568
+ const key = prewarmKey(scope);
569
+ if (inFlight.has(key)) {
570
+ emit({ type: "skipped", key, workspaceId: scope.workspaceId, outcome: "already-warming" });
571
+ return { outcome: "already-warming" };
572
+ }
573
+ const failure = failures.get(key);
574
+ if (failure && now() < failure.until) {
575
+ emit({ type: "skipped", key, workspaceId: scope.workspaceId, outcome: "cooling-down" });
576
+ return { outcome: "cooling-down" };
577
+ }
578
+ let settle = () => {
579
+ };
580
+ const reservation = new Promise((resolve) => {
581
+ settle = resolve;
582
+ });
583
+ inFlight.set(key, reservation);
584
+ const abandon = (outcome) => {
585
+ inFlight.delete(key);
586
+ settle({ ok: false, error: outcome, ms: 0 });
587
+ emit({ type: "skipped", key, workspaceId: scope.workspaceId, outcome });
588
+ return { outcome };
589
+ };
590
+ try {
591
+ const peek = await peekWorkspaceSandbox(shell, {
592
+ workspaceId: scope.workspaceId,
593
+ userId: scope.userId
594
+ });
595
+ if (peek.status === "running") return abandon("already-running");
596
+ if (peek.status === "absent" && mode === "resume-only") {
597
+ return abandon("absent-and-resume-only");
598
+ }
599
+ if (options.shouldPrewarm) {
600
+ const allowed = await options.shouldPrewarm(scope);
601
+ if (!allowed) return abandon("declined-by-policy");
602
+ }
603
+ let holdsClaim = false;
604
+ if (claim) {
605
+ holdsClaim = await claim.acquire(key, claimTtlSeconds);
606
+ if (!holdsClaim) return abandon("warming-elsewhere");
607
+ }
608
+ const warm = runWarm(key, scope, holdsClaim);
609
+ void warm.then(settle);
610
+ return { outcome: "started", completion: warm };
611
+ } catch (err) {
612
+ const error = errText(err);
613
+ inFlight.delete(key);
614
+ failures.set(key, { error, until: now() + failureCooldownMs });
615
+ settle({ ok: false, error, ms: 0 });
616
+ emit({ type: "failed", key, workspaceId: scope.workspaceId, error, ms: 0 });
617
+ return { outcome: "cooling-down" };
618
+ }
619
+ },
620
+ async readiness(scope) {
621
+ const key = prewarmKey(scope);
622
+ const peek = await peekWorkspaceSandbox(shell, {
623
+ workspaceId: scope.workspaceId,
624
+ userId: scope.userId
625
+ });
626
+ if (peek.status === "running") return { status: "ready", boxId: peek.box.id };
627
+ if (inFlight.has(key)) return { status: "warming" };
628
+ if (claim?.isHeld) {
629
+ try {
630
+ if (await claim.isHeld(key)) return { status: "warming" };
631
+ } catch {
632
+ }
633
+ }
634
+ const failure = failures.get(key);
635
+ if (failure && now() < failure.until) {
636
+ return { status: "failed", error: failure.error, retryAfterMs: failure.until - now() };
637
+ }
638
+ return { status: "absent" };
639
+ },
640
+ clearFailure(scope) {
641
+ failures.delete(prewarmKey(scope));
642
+ }
643
+ };
644
+ }
645
+
487
646
  // src/sandbox/index.ts
488
647
  var DEFAULT_SANDBOX_DIRECT_KEY_NAMES = [
489
648
  "TCLOUD_SANDBOX_API_KEY",
@@ -1726,15 +1885,19 @@ export {
1726
1885
  createSandboxTerminalToken,
1727
1886
  verifySandboxTerminalToken,
1728
1887
  createWorkspaceSandboxConnectionHandler,
1888
+ sandboxSidecarProxyUrl,
1729
1889
  createWorkspaceSandboxRuntimeProxyHandler,
1730
1890
  matchSandboxTerminalWsPath,
1731
1891
  isSandboxTerminalWsUpgrade,
1732
1892
  createWorkspaceSandboxTerminalUpgradeHandler,
1893
+ terminalUpgradeSubprotocolEcho,
1894
+ selectedBearerSubprotocol,
1733
1895
  buildSandboxRuntimeProxyHeaders,
1734
1896
  encodeSandboxRuntimePath,
1735
1897
  bearerToken,
1736
1898
  bearerSubprotocolToken,
1737
1899
  terminalTokenFromRequest,
1900
+ createSandboxPrewarmer,
1738
1901
  resolveSandboxClientCredentials,
1739
1902
  DEFAULT_SANDBOX_RESOURCES,
1740
1903
  getClient,
@@ -1778,4 +1941,4 @@ export {
1778
1941
  isTerminalPromptEvent,
1779
1942
  detectInteractiveQuestion
1780
1943
  };
1781
- //# sourceMappingURL=chunk-7775L5NN.js.map
1944
+ //# sourceMappingURL=chunk-BAC2B2KI.js.map