@ixo/oracle-runtime-workers 0.2.0

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 (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Transcript → client DTO. Port of `@ixo/common`'s
3
+ * `transformGraphStateMessageToListMessageResponse` so `GET /messages/:id`
4
+ * returns the exact shape the Portal / client SDK already consume.
5
+ */
6
+ import type {
7
+ AIMessage,
8
+ BaseMessage,
9
+ ToolMessage,
10
+ } from '@langchain/core/messages';
11
+ import {
12
+ isAttachmentPlaceholderText,
13
+ isAttachmentViewMessage,
14
+ } from '../attachments/retention';
15
+
16
+ export interface ToolCallDto {
17
+ name: string;
18
+ id: string;
19
+ args: unknown;
20
+ status?: 'isRunning' | 'done';
21
+ output?: string;
22
+ }
23
+
24
+ export interface AttachmentMeta {
25
+ filename: string;
26
+ mimetype: string;
27
+ size?: number;
28
+ mxcUri?: string;
29
+ eventId?: string;
30
+ category?: string;
31
+ }
32
+
33
+ export interface MessageDto {
34
+ id: string;
35
+ type: 'ai' | 'human';
36
+ content: string;
37
+ toolCalls?: ToolCallDto[];
38
+ reasoning?: string;
39
+ isComplete?: boolean;
40
+ isReasoning?: boolean;
41
+ attachment?: AttachmentMeta;
42
+ attachments?: AttachmentMeta[];
43
+ }
44
+
45
+ export interface ListMessagesResponse {
46
+ messages: MessageDto[];
47
+ }
48
+
49
+ const UUID_RE =
50
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
51
+
52
+ /** Stable id for a message without one — sha256 hex of its text (as the Node runtime does). */
53
+ export async function uuidFromString(str: string): Promise<string> {
54
+ if (UUID_RE.test(str)) return str;
55
+ const digest = await crypto.subtle.digest(
56
+ 'SHA-256',
57
+ new TextEncoder().encode(str),
58
+ );
59
+ return Array.from(new Uint8Array(digest))
60
+ .map((b) => b.toString(16).padStart(2, '0'))
61
+ .join('');
62
+ }
63
+
64
+ function reasoningToText(reasoning: unknown): string | undefined {
65
+ if (typeof reasoning === 'string') return reasoning || undefined;
66
+ if (
67
+ reasoning &&
68
+ typeof reasoning === 'object' &&
69
+ Array.isArray((reasoning as { summary?: unknown }).summary)
70
+ ) {
71
+ const text = (reasoning as { summary: unknown[] }).summary
72
+ .map((part) =>
73
+ part &&
74
+ typeof part === 'object' &&
75
+ typeof (part as { text?: unknown }).text === 'string'
76
+ ? (part as { text: string }).text
77
+ : '',
78
+ )
79
+ .join('');
80
+ return text || undefined;
81
+ }
82
+ return undefined;
83
+ }
84
+
85
+ export function contentToText(content: BaseMessage['content']): string {
86
+ if (typeof content === 'string') return content;
87
+ if (Array.isArray(content)) {
88
+ return content
89
+ .map((part) => {
90
+ if (typeof part === 'string') return part;
91
+ if (part && typeof part === 'object' && 'text' in part) {
92
+ const text = (part as { text?: unknown }).text;
93
+ // The retention placeholder is model-facing only; the transcript
94
+ // keeps listing the attachment from its metadata.
95
+ return typeof text === 'string' && !isAttachmentPlaceholderText(text)
96
+ ? text
97
+ : '';
98
+ }
99
+ return '';
100
+ })
101
+ .join('');
102
+ }
103
+ return String(content);
104
+ }
105
+
106
+ /** The summarization middleware's bookkeeping message — never shown to users. */
107
+ export function isSummarizationMessage(message: BaseMessage): boolean {
108
+ const kw = message.additional_kwargs as Record<string, unknown> | undefined;
109
+ if (kw?.lc_summary_message === true || kw?.summary === true) return true;
110
+ const text = contentToText(message.content);
111
+ return message.type === 'system' && /summary of the conversation/i.test(text);
112
+ }
113
+
114
+ export async function transformTranscript(
115
+ messages: BaseMessage[],
116
+ ): Promise<ListMessagesResponse> {
117
+ const acc: MessageDto[] = [];
118
+ for (const message of messages) {
119
+ const toolMsg = message.type === 'tool' ? (message as ToolMessage) : null;
120
+ const kw = (message.additional_kwargs ?? {}) as Record<string, unknown>;
121
+ const isExtractionContext = message.type === 'ai' && Boolean(kw.attachment);
122
+
123
+ if (
124
+ message.type !== 'system' &&
125
+ message.type !== 'tool' &&
126
+ !kw.isError &&
127
+ !isExtractionContext &&
128
+ // `view_attachment` re-attaching an offloaded payload — model-facing.
129
+ !isAttachmentViewMessage(message)
130
+ ) {
131
+ const reasoning = reasoningToText(kw.reasoning);
132
+ const attachments =
133
+ message.type === 'human'
134
+ ? ((kw.attachments as AttachmentMeta[] | undefined) ??
135
+ (kw.attachment ? [kw.attachment as AttachmentMeta] : undefined))
136
+ : undefined;
137
+ const attachment = attachments?.[0];
138
+ const textContent = contentToText(message.content);
139
+ const toolCalls = (message as AIMessage).tool_calls;
140
+ const dto: MessageDto = {
141
+ type: message.type === 'ai' ? 'ai' : 'human',
142
+ content: textContent,
143
+ id: await uuidFromString(message.id ?? textContent),
144
+ isComplete: true,
145
+ isReasoning: false,
146
+ };
147
+ if (toolCalls?.length) {
148
+ dto.toolCalls = [];
149
+ for (const tc of toolCalls) {
150
+ dto.toolCalls.push({
151
+ name: tc.name,
152
+ args: tc.args,
153
+ id: tc.id ?? (await uuidFromString(JSON.stringify(tc.args))),
154
+ output: undefined,
155
+ });
156
+ }
157
+ }
158
+ if (reasoning) dto.reasoning = reasoning;
159
+ if (attachment) dto.attachment = attachment;
160
+ if (attachments?.length) dto.attachments = attachments;
161
+ acc.push(dto);
162
+ }
163
+
164
+ if (toolMsg) {
165
+ const kwargs = (
166
+ toolMsg as unknown as {
167
+ lc_kwargs?: { tool_call_id?: string; args?: unknown };
168
+ }
169
+ ).lc_kwargs;
170
+ const toolCallId =
171
+ toolMsg.tool_call_id ??
172
+ kwargs?.tool_call_id ??
173
+ (await uuidFromString(JSON.stringify(kwargs?.args)));
174
+ const idx = acc.findIndex((m) =>
175
+ m.toolCalls?.some((t) => t.id === toolCallId),
176
+ );
177
+ const el = idx !== -1 ? acc[idx] : undefined;
178
+ if (el) {
179
+ el.toolCalls = el.toolCalls?.map((t) =>
180
+ t.id === toolCallId
181
+ ? {
182
+ ...t,
183
+ output: JSON.stringify(toolMsg.content),
184
+ status: 'done' as const,
185
+ }
186
+ : t,
187
+ );
188
+ }
189
+ }
190
+ }
191
+ return { messages: acc };
192
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `POST /messages/:id` body validation — the Workers counterpart of Node's
3
+ * `SendMessageDto` under a `ValidationPipe({ whitelist, forbidNonWhitelisted })`
4
+ * behind express body-parser: malformed JSON, unknown top-level fields and a
5
+ * missing `message` are all 400s, never a failed turn or a 500.
6
+ */
7
+
8
+ /** Top-level fields of Node's `SendMessageDto`; anything else is rejected. */
9
+ export const TURN_BODY_KEYS: ReadonlySet<string> = new Set([
10
+ 'message',
11
+ 'stream',
12
+ 'returnAllMessages',
13
+ 'model',
14
+ 'tools',
15
+ 'agActions',
16
+ 'metadata',
17
+ 'timezone',
18
+ 'homeServer',
19
+ 'mcpInvocations',
20
+ 'attachments',
21
+ 'requestId',
22
+ ]);
23
+
24
+ export interface TurnBody {
25
+ message: string;
26
+ stream?: boolean;
27
+ returnAllMessages?: boolean;
28
+ timezone?: string;
29
+ model?: string;
30
+ /**
31
+ * Free-form client metadata (Node's `SendMessageDto.metadata`); the runtime
32
+ * reads `editorRoomId`, `spaceId`, `sessionRunId` and `currentEntityDid`.
33
+ */
34
+ metadata?: Record<string, unknown> & {
35
+ editorRoomId?: string;
36
+ spaceId?: string;
37
+ sessionRunId?: string;
38
+ currentEntityDid?: string;
39
+ };
40
+ tools?: Array<{
41
+ name: string;
42
+ description: string;
43
+ schema: Record<string, unknown>;
44
+ }>;
45
+ agActions?: Array<{
46
+ name: string;
47
+ description: string;
48
+ schema: Record<string, unknown>;
49
+ hasRender?: boolean;
50
+ }>;
51
+ attachments?: unknown[];
52
+ }
53
+
54
+ export type ParsedTurnBody =
55
+ | { ok: true; body: TurnBody }
56
+ | { ok: false; status: 400; message: string };
57
+
58
+ const reject = (message: string): ParsedTurnBody => ({
59
+ ok: false,
60
+ status: 400,
61
+ message,
62
+ });
63
+
64
+ /** Parse and validate a raw request body. Never throws. */
65
+ export function parseTurnBody(raw: string): ParsedTurnBody {
66
+ let parsed: unknown;
67
+ try {
68
+ parsed = JSON.parse(raw);
69
+ } catch {
70
+ return reject('Invalid JSON body');
71
+ }
72
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
73
+ return reject('Request body must be a JSON object');
74
+ }
75
+ const record = parsed as Record<string, unknown>;
76
+ const unknownProp = Object.keys(record).find((k) => !TURN_BODY_KEYS.has(k));
77
+ if (unknownProp !== undefined) {
78
+ return reject(`property ${unknownProp} should not exist`);
79
+ }
80
+ if (typeof record.message !== 'string' || record.message.length === 0) {
81
+ return reject('message is required');
82
+ }
83
+ return { ok: true, body: record as unknown as TurnBody };
84
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Request `metadata` → agent state, the Node runtime's `agent-builder` rules:
3
+ *
4
+ * - `editorRoomId` / `spaceId` / `currentEntityDid` update the thread's
5
+ * checkpointed value when the request carries them, otherwise the prior
6
+ * value stands;
7
+ * - `sessionRunId` follows the room: a request that names the editor room
8
+ * also defines its session run, and "no run" must not fall back to one
9
+ * remembered from an earlier flow — so it is written whenever
10
+ * `editorRoomId` is, even as `undefined`;
11
+ * - an active editor context (room or space, from the request or the
12
+ * checkpoint) seeds the editor plugin into `loadedPlugins`, so its tools
13
+ * pass the capability gate without an explicit `load_capability` step.
14
+ *
15
+ * Metadata crosses the Durable Object boundary as a JSON string; only string
16
+ * values are honoured, anything else is ignored.
17
+ */
18
+
19
+ export const EDITOR_PLUGIN_NAME = 'editor';
20
+
21
+ export interface TurnMetadata {
22
+ editorRoomId?: string;
23
+ spaceId?: string;
24
+ sessionRunId?: string;
25
+ currentEntityDid?: string;
26
+ }
27
+
28
+ const KEYS = [
29
+ 'editorRoomId',
30
+ 'spaceId',
31
+ 'sessionRunId',
32
+ 'currentEntityDid',
33
+ ] as const;
34
+
35
+ export function parseTurnMetadata(json: string | undefined): TurnMetadata {
36
+ if (!json) return {};
37
+ let parsed: unknown;
38
+ try {
39
+ parsed = JSON.parse(json);
40
+ } catch {
41
+ return {};
42
+ }
43
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
44
+ return {};
45
+ const record: Record<string, unknown> = { ...parsed };
46
+ const out: TurnMetadata = {};
47
+ for (const key of KEYS) {
48
+ const value = record[key];
49
+ if (typeof value === 'string' && value.length > 0) out[key] = value;
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /** The checkpointed values the mapping falls back to. */
55
+ export interface PriorMetadataState {
56
+ editorRoomId?: string;
57
+ spaceId?: string;
58
+ sessionRunId?: string;
59
+ currentEntityDid?: string;
60
+ loadedPlugins?: string[];
61
+ }
62
+
63
+ function priorString(
64
+ prior: Record<string, unknown>,
65
+ key: keyof PriorMetadataState,
66
+ ): string | undefined {
67
+ const value = prior[key];
68
+ return typeof value === 'string' ? value : undefined;
69
+ }
70
+
71
+ export function priorMetadataState(
72
+ prior: Record<string, unknown>,
73
+ ): PriorMetadataState {
74
+ const loaded = prior['loadedPlugins'];
75
+ return {
76
+ editorRoomId: priorString(prior, 'editorRoomId'),
77
+ spaceId: priorString(prior, 'spaceId'),
78
+ sessionRunId: priorString(prior, 'sessionRunId'),
79
+ currentEntityDid: priorString(prior, 'currentEntityDid'),
80
+ ...(Array.isArray(loaded)
81
+ ? { loadedPlugins: loaded.filter((p) => typeof p === 'string') }
82
+ : {}),
83
+ };
84
+ }
85
+
86
+ export function editorContextActive(
87
+ meta: TurnMetadata,
88
+ prior: PriorMetadataState,
89
+ ): boolean {
90
+ return Boolean(
91
+ (meta.editorRoomId ?? prior.editorRoomId) ||
92
+ (meta.spaceId ?? prior.spaceId),
93
+ );
94
+ }
95
+
96
+ /** Fields for the build-time state (`history.state`): request over checkpoint. */
97
+ export function metadataBuildState(
98
+ meta: TurnMetadata,
99
+ prior: PriorMetadataState,
100
+ ): {
101
+ editorRoomId?: string;
102
+ sessionRunId?: string;
103
+ spaceId?: string;
104
+ currentEntityDid?: string;
105
+ loadedPlugins: string[];
106
+ } {
107
+ const loaded = prior.loadedPlugins ?? [];
108
+ return {
109
+ editorRoomId: meta.editorRoomId ?? prior.editorRoomId,
110
+ sessionRunId:
111
+ meta.editorRoomId !== undefined ? meta.sessionRunId : prior.sessionRunId,
112
+ spaceId: meta.spaceId ?? prior.spaceId,
113
+ currentEntityDid: meta.currentEntityDid ?? prior.currentEntityDid,
114
+ loadedPlugins: editorContextActive(meta, prior)
115
+ ? Array.from(new Set([...loaded, EDITOR_PLUGIN_NAME]))
116
+ : loaded,
117
+ };
118
+ }
119
+
120
+ /** Fields for the graph input: only what THIS request carries is written. */
121
+ export function metadataGraphInput(
122
+ meta: TurnMetadata,
123
+ prior: PriorMetadataState,
124
+ ): {
125
+ editorRoomId?: string;
126
+ sessionRunId?: string;
127
+ spaceId?: string;
128
+ currentEntityDid?: string;
129
+ loadedPlugins?: string[];
130
+ } {
131
+ return {
132
+ ...(meta.editorRoomId !== undefined && {
133
+ editorRoomId: meta.editorRoomId,
134
+ // Written even when undefined so a room without a run clears the
135
+ // checkpointed run id rather than keeping a stale one.
136
+ sessionRunId: meta.sessionRunId,
137
+ }),
138
+ ...(meta.spaceId !== undefined && { spaceId: meta.spaceId }),
139
+ ...(meta.currentEntityDid !== undefined && {
140
+ currentEntityDid: meta.currentEntityDid,
141
+ }),
142
+ ...(editorContextActive(meta, prior) && {
143
+ loadedPlugins: [EDITOR_PLUGIN_NAME],
144
+ }),
145
+ };
146
+ }