@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,87 @@
1
+ import { z } from 'zod';
2
+ import { clientSchemaToZod, NOOP_LOGGER } from '../core/utils';
3
+ import type { Logger } from '../plugin-api/types';
4
+
5
+ /**
6
+ * Shared adaptation layer between `@langchain/mcp-adapters` tools and the
7
+ * plugin API's `PluginTool` contract.
8
+ *
9
+ * `MultiServerMCPClient.getTools()` returns `DynamicStructuredTool[]` whose
10
+ * `schema` field carries the upstream server's raw JSON Schema (not a Zod
11
+ * schema). The Node runtime papered over this with a type cast; here the
12
+ * mismatch is resolved for real: {@link mcpToolZodSchema} converts the JSON
13
+ * Schema to Zod via the runtime's `clientSchemaToZod` (the same converter the
14
+ * core uses for client-declared tools), so the agent sees the upstream
15
+ * contract faithfully and strict typing holds without assertions.
16
+ */
17
+
18
+ /**
19
+ * The slice of a `DynamicStructuredTool` the adapters read. Declared
20
+ * structurally so `getTools()` results assign without casts and unit tests
21
+ * can satisfy it with plain objects.
22
+ */
23
+ export interface RawMcpClientTool {
24
+ name: string;
25
+ description: string;
26
+ schema?: unknown;
27
+ invoke(input: unknown): Promise<unknown>;
28
+ }
29
+
30
+ /** An upstream MCP tool with its schema normalised to Zod. */
31
+ export interface AdaptedMcpTool {
32
+ name: string;
33
+ description: string;
34
+ schema: z.ZodType;
35
+ invoke(input: unknown): Promise<unknown>;
36
+ }
37
+
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
40
+ }
41
+
42
+ /**
43
+ * Normalise an upstream tool's `schema` field to a Zod schema:
44
+ *
45
+ * - already Zod (a custom provider) → pass through verbatim;
46
+ * - a JSON Schema record (the `@langchain/mcp-adapters` default) →
47
+ * converted via `clientSchemaToZod`;
48
+ * - anything else / conversion failure → a permissive record schema so the
49
+ * tool stays callable (the upstream server validates args itself).
50
+ */
51
+ export function mcpToolZodSchema(
52
+ schema: unknown,
53
+ toolName: string,
54
+ logger: Logger = NOOP_LOGGER,
55
+ ): z.ZodType {
56
+ if (schema instanceof z.ZodType) return schema;
57
+ if (isRecord(schema)) {
58
+ const converted = clientSchemaToZod(schema, toolName, logger);
59
+ if (converted) return converted;
60
+ }
61
+ // Loud on purpose: with the permissive fallback the upstream server becomes
62
+ // the only validator, and the model never sees the tool's enums/required
63
+ // fields. A silent fallback hid exactly that once.
64
+ logger.warn(
65
+ `[mcp] tool "${toolName}": schema not convertible to Zod (${
66
+ isRecord(schema) ? 'conversion failed' : typeof schema
67
+ }) — using a permissive record schema; the upstream server validates args`,
68
+ );
69
+ return z.record(z.string(), z.unknown());
70
+ }
71
+
72
+ /**
73
+ * Adapt a client's tool list. Name and description pass through VERBATIM —
74
+ * the agent sees exactly the contract the upstream MCP server publishes; only
75
+ * the schema representation changes (JSON Schema → Zod).
76
+ */
77
+ export function adaptMcpClientTools(
78
+ tools: RawMcpClientTool[],
79
+ logger: Logger = NOOP_LOGGER,
80
+ ): AdaptedMcpTool[] {
81
+ return tools.map((t) => ({
82
+ name: t.name,
83
+ description: t.description,
84
+ schema: mcpToolZodSchema(t.schema, t.name, logger),
85
+ invoke: (input: unknown) => t.invoke(input),
86
+ }));
87
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ clearMemoryToolDefsCache,
3
+ createDefaultMemoryMcpFactory,
4
+ DEFAULT_MEMORY_TOOLS,
5
+ fetchMemoryTools,
6
+ MEMORY_ADD_MCP_NAME,
7
+ MEMORY_ADD_ORACLE_KNOWLEDGE_MCP_NAME,
8
+ MEMORY_CLEAR_MCP_NAME,
9
+ MEMORY_DELETE_EDGE_MCP_NAME,
10
+ MEMORY_DELETE_EPISODE_MCP_NAME,
11
+ MEMORY_SEARCH_MCP_NAME,
12
+ type MemoryMcpFactory,
13
+ type UpstreamMcpTool,
14
+ } from './memory-tools';
15
+ export { buildMemoryHeaders } from './memory-ucan';
16
+ export { MemoryPlugin, type MemoryPluginOptions } from './memory.plugin';
17
+ export type * from './types';
@@ -0,0 +1,463 @@
1
+ import { MultiServerMCPClient } from '@langchain/mcp-adapters';
2
+ import { clientSchemaToZod } from '../../core/utils';
3
+ import { withCallTimeout } from '../mcp-call-timeout';
4
+ import type { z } from 'zod';
5
+ import type { PluginTool, RuntimeContext } from '../../plugin-api/types';
6
+ import { adaptMcpClientTools } from '../mcp-tool-adapter';
7
+ import { buildMemoryHeaders } from './memory-ucan';
8
+
9
+ /**
10
+ * Tool names the upstream Memory Engine MCP server exposes (prefixed with the
11
+ * server name configured in `MultiServerMCPClient`). The plugin surfaces these
12
+ * upstream tools as-is — name, description, and schema all come from the
13
+ * upstream server. Wrapping them locally with a different shape (as an earlier
14
+ * iteration did) caused upstream-side schema rejections at invoke time
15
+ * because the wrapper schema didn't match the server's contract.
16
+ */
17
+ export const MEMORY_SEARCH_MCP_NAME = 'memory-engine__search_memory_engine';
18
+ export const MEMORY_ADD_MCP_NAME = 'memory-engine__add_memory';
19
+ export const MEMORY_ADD_ORACLE_KNOWLEDGE_MCP_NAME =
20
+ 'memory-engine__add_oracle_knowledge';
21
+ export const MEMORY_DELETE_EPISODE_MCP_NAME = 'memory-engine__delete_episode';
22
+ export const MEMORY_DELETE_EDGE_MCP_NAME = 'memory-engine__delete_edge';
23
+ export const MEMORY_CLEAR_MCP_NAME = 'memory-engine__clear';
24
+
25
+ /**
26
+ * Default selection: search, add, delete-episode, and clear — the user must
27
+ * always be able to ask for a full memory wipe, so the destructive `clear`
28
+ * stays in the default set. Forks that need org-owner knowledge writes (or
29
+ * want to drop `clear`) pass an explicit list via plugin options.
30
+ */
31
+ export const DEFAULT_MEMORY_TOOLS = [
32
+ MEMORY_SEARCH_MCP_NAME,
33
+ MEMORY_ADD_MCP_NAME,
34
+ MEMORY_DELETE_EPISODE_MCP_NAME,
35
+ MEMORY_CLEAR_MCP_NAME,
36
+ ] as const;
37
+
38
+ /**
39
+ * Minimal shape every upstream MCP tool exposes after adaptation
40
+ * ({@link adaptMcpClientTools} normalises the client's JSON-Schema tools to
41
+ * this). The plugin only depends on this slice; tests can satisfy it with a
42
+ * plain object.
43
+ */
44
+ export interface UpstreamMcpTool {
45
+ name: string;
46
+ description: string;
47
+ schema: z.ZodType;
48
+ invoke: (input: unknown) => Promise<unknown>;
49
+ }
50
+
51
+ export type MemoryMcpFactory = (
52
+ runCtx: RuntimeContext,
53
+ ) => Promise<UpstreamMcpTool[] | null>;
54
+
55
+ /**
56
+ * Tool *definitions* (name/description/schema) published by a Memory Engine
57
+ * MCP server. User-independent — the per-user part of a memory call is the
58
+ * auth headers, which are minted per request and applied at invoke time.
59
+ */
60
+ interface MemoryToolDef {
61
+ name: string;
62
+ description: string;
63
+ schema: z.ZodType;
64
+ }
65
+
66
+ interface CachedToolDefs {
67
+ defs: MemoryToolDef[];
68
+ expiresAt: number;
69
+ }
70
+
71
+ /**
72
+ * Definitions change only when the upstream server deploys, so a short TTL
73
+ * keeps the surface fresh while taking the MCP connect + tools/list network
74
+ * round-trip off nearly every chat turn.
75
+ */
76
+ const TOOL_DEFS_TTL_MS = 5 * 60 * 1000;
77
+
78
+ /** Cached upstream definitions, keyed by MCP URL (per isolate). */
79
+ const toolDefsCache = new Map<string, CachedToolDefs>();
80
+
81
+ /**
82
+ * MCP URLs with a background definition refresh in flight. Guards against a
83
+ * burst of turns each spawning its own connect when an entry expires.
84
+ */
85
+ const refreshInFlight = new Set<string>();
86
+
87
+ /** Test hook: drop all cached upstream tool definitions. */
88
+ export function clearMemoryToolDefsCache(): void {
89
+ toolDefsCache.clear();
90
+ refreshInFlight.clear();
91
+ }
92
+
93
+ /**
94
+ * How long a request's MCP client survives after its last invocation before
95
+ * being closed. There is no explicit request-end hook on the tool path, so
96
+ * idle-close is what bounds client lifetime — without it every turn that
97
+ * touched memory leaked a connected client (transport, sessions, tool set)
98
+ * for the life of the isolate.
99
+ */
100
+ const IDLE_CLIENT_CLOSE_MS = 5 * 60 * 1000;
101
+ /** Our own per-call limit (never given to the SDK — see `mcp-call-timeout.ts`). */
102
+ const MEMORY_TOOL_TIMEOUT_MS = 420_000;
103
+
104
+ function createMemoryMcpClient(
105
+ memoryMcpUrl: string,
106
+ headers: Record<string, string>,
107
+ ): MultiServerMCPClient {
108
+ return new MultiServerMCPClient({
109
+ useStandardContentBlocks: true,
110
+ prefixToolNameWithServerName: true,
111
+ mcpServers: {
112
+ 'memory-engine': {
113
+ type: 'http',
114
+ transport: 'http',
115
+ url: memoryMcpUrl,
116
+ headers,
117
+ reconnect: {
118
+ enabled: true,
119
+ maxAttempts: 3,
120
+ delayMs: 2000,
121
+ },
122
+ },
123
+ },
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Connect, snapshot the upstream tool *definitions*, and always close the
129
+ * client — defs are plain data, so the connection has nothing left to serve.
130
+ */
131
+ /**
132
+ * Log the raw upstream schema of `search_memory_engine` once per isolate —
133
+ * the artefact that settles whether an enum reached the model or a
134
+ * conversion fallback threw it away.
135
+ */
136
+ let schemaDump: MemorySchemaDump | null = null;
137
+
138
+ export interface MemorySchemaDump {
139
+ tool: string;
140
+ toolCount: number;
141
+ /** `JSON.stringify` of the schema exactly as `client.getTools()` handed it over. */
142
+ rawSchema: string;
143
+ /** Whether `clientSchemaToZod` accepts it (false = permissive-record fallback). */
144
+ convertible: boolean;
145
+ capturedAt: string;
146
+ }
147
+
148
+ /** The last captured upstream schema in this isolate (`GET /debug/storage`). */
149
+ export function getMemorySchemaDump(): MemorySchemaDump | null {
150
+ return schemaDump;
151
+ }
152
+
153
+ /** Snapshot `search_memory_engine`'s raw schema + whether it converts. */
154
+ export function buildMemorySchemaDump(
155
+ tools: ReadonlyArray<{ name: string; schema?: unknown }>,
156
+ ): MemorySchemaDump | null {
157
+ const tool = tools.find((t) => t.name.endsWith('search_memory_engine'));
158
+ if (!tool) return null;
159
+ let text: string;
160
+ try {
161
+ text = JSON.stringify(tool.schema);
162
+ } catch {
163
+ text = String(tool.schema);
164
+ }
165
+ const convertible =
166
+ typeof tool.schema === 'object' &&
167
+ tool.schema !== null &&
168
+ clientSchemaToZod(tool.schema as Record<string, unknown>, tool.name) !==
169
+ null;
170
+ return {
171
+ tool: tool.name,
172
+ toolCount: tools.length,
173
+ rawSchema: text.slice(0, 8000),
174
+ convertible,
175
+ capturedAt: new Date().toISOString(),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * On-demand capture for `GET /debug/memory-schema`: connect with the given
181
+ * auth headers, snapshot the tool schema, close.
182
+ */
183
+ export async function fetchMemorySchemaDump(
184
+ memoryMcpUrl: string,
185
+ headers: Record<string, string>,
186
+ ): Promise<MemorySchemaDump | null> {
187
+ const client = createMemoryMcpClient(memoryMcpUrl, headers);
188
+ try {
189
+ const dump = buildMemorySchemaDump(await client.getTools());
190
+ if (dump) schemaDump = dump;
191
+ return dump;
192
+ } finally {
193
+ await client.close().catch(() => undefined);
194
+ }
195
+ }
196
+
197
+ function dumpSchemaOnce(
198
+ tools: ReadonlyArray<{ name: string; schema?: unknown }>,
199
+ ): void {
200
+ if (schemaDump) return;
201
+ const tool = tools.find((t) => t.name.endsWith('search_memory_engine'));
202
+ if (!tool) return;
203
+ let text: string;
204
+ try {
205
+ text = JSON.stringify(tool.schema);
206
+ } catch {
207
+ text = String(tool.schema);
208
+ }
209
+ const convertible =
210
+ typeof tool.schema === 'object' &&
211
+ tool.schema !== null &&
212
+ clientSchemaToZod(tool.schema as Record<string, unknown>, tool.name) !==
213
+ null;
214
+ schemaDump = {
215
+ tool: tool.name,
216
+ toolCount: tools.length,
217
+ rawSchema: text.slice(0, 8000),
218
+ convertible,
219
+ capturedAt: new Date().toISOString(),
220
+ };
221
+ console.log(
222
+ `[memory] raw schema of ${tool.name} (${tools.length} tools, convertible=${convertible}): ${text.slice(0, 4000)}`,
223
+ );
224
+ }
225
+
226
+ async function listToolDefs(
227
+ memoryMcpUrl: string,
228
+ headers: Record<string, string>,
229
+ ): Promise<MemoryToolDef[]> {
230
+ const client = createMemoryMcpClient(memoryMcpUrl, headers);
231
+ try {
232
+ const raw = await client.getTools();
233
+ dumpSchemaOnce(raw);
234
+ const tools = adaptMcpClientTools(raw, console);
235
+ return tools.map(({ name, description, schema }) => ({
236
+ name,
237
+ description,
238
+ schema,
239
+ }));
240
+ } finally {
241
+ await client.close().catch(() => undefined);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Bind cached definitions to this request's auth headers. The MCP client is
247
+ * created lazily on the first actual invocation (and shared across the
248
+ * request's memory tools), so turns that never touch memory pay zero
249
+ * Memory-Engine round-trips. The client is closed after
250
+ * `IDLE_CLIENT_CLOSE_MS` without an invocation; a later call simply
251
+ * reconnects.
252
+ */
253
+ function buildLazyUpstreamTools(
254
+ defs: MemoryToolDef[],
255
+ memoryMcpUrl: string,
256
+ headers: Record<string, string>,
257
+ onTurnEnd?: (dispose: () => void | Promise<void>) => void,
258
+ ): UpstreamMcpTool[] {
259
+ let connection: Promise<{
260
+ client: MultiServerMCPClient;
261
+ byName: Map<string, UpstreamMcpTool>;
262
+ }> | null = null;
263
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
264
+
265
+ const closeConnection = (): void => {
266
+ const current = connection;
267
+ connection = null;
268
+ console.log(`[memory] closing MCP client (${current ? 'open' : 'none'})`);
269
+ if (idleTimer !== null) {
270
+ clearTimeout(idleTimer);
271
+ idleTimer = null;
272
+ }
273
+ if (!current) return;
274
+ void current
275
+ .then(({ client }) => client.close())
276
+ .then(
277
+ () => console.log('[memory] MCP client closed'),
278
+ (err: unknown) =>
279
+ console.warn(
280
+ `[memory] MCP client close failed: ${err instanceof Error ? err.message : String(err)}`,
281
+ ),
282
+ );
283
+ };
284
+
285
+ // Close when the turn ends (the client is per request anyway). The idle
286
+ // timer is only the fallback for hosts without a turn-end hook: a pending
287
+ // timer keeps a Durable Object resident and blocks hibernation.
288
+ const scheduleIdleClose = (): void => {
289
+ if (onTurnEnd) return;
290
+ if (idleTimer !== null) clearTimeout(idleTimer);
291
+ idleTimer = setTimeout(closeConnection, IDLE_CLIENT_CLOSE_MS);
292
+ };
293
+ let closeRegistered = false;
294
+ const registerTurnEndClose = (): void => {
295
+ if (!onTurnEnd || closeRegistered) return;
296
+ closeRegistered = true;
297
+ onTurnEnd(() => closeConnection());
298
+ };
299
+
300
+ const connect = (): NonNullable<typeof connection> => {
301
+ if (!connection) {
302
+ registerTurnEndClose();
303
+ connection = (async () => {
304
+ const client = createMemoryMcpClient(memoryMcpUrl, headers);
305
+ try {
306
+ const tools = adaptMcpClientTools(await client.getTools(), console);
307
+ return { client, byName: new Map(tools.map((t) => [t.name, t])) };
308
+ } catch (error) {
309
+ await client.close().catch(() => undefined);
310
+ throw error;
311
+ }
312
+ })();
313
+ // A failed connect must not poison the rest of the run — clear the
314
+ // memo so a later invocation retries with a fresh client.
315
+ connection.catch(() => {
316
+ connection = null;
317
+ });
318
+ }
319
+ return connection;
320
+ };
321
+
322
+ return defs.map((def) => ({
323
+ name: def.name,
324
+ description: def.description,
325
+ schema: def.schema,
326
+ invoke: async (input: unknown) => {
327
+ const { byName } = await connect();
328
+ const upstream = byName.get(def.name);
329
+ if (!upstream) {
330
+ scheduleIdleClose();
331
+ throw new Error(
332
+ `Memory Engine no longer exposes "${def.name}" — cached definition is stale, retry shortly.`,
333
+ );
334
+ }
335
+ const startedAt = Date.now();
336
+ try {
337
+ return await withCallTimeout(
338
+ () => upstream.invoke(input),
339
+ MEMORY_TOOL_TIMEOUT_MS,
340
+ `memory tool ${def.name}`,
341
+ closeConnection,
342
+ );
343
+ } catch (error) {
344
+ // The SSE layer only sees "tool did not complete" for a thrown tool;
345
+ // the reason has to be in the object's logs.
346
+ console.warn(
347
+ `[memory] tool ${def.name} failed after ${Date.now() - startedAt} ms: ${
348
+ error instanceof Error ? error.message : String(error)
349
+ }`,
350
+ );
351
+ throw error;
352
+ } finally {
353
+ scheduleIdleClose();
354
+ }
355
+ },
356
+ }));
357
+ }
358
+
359
+ /**
360
+ * Default factory: authenticates with the per-request UCAN headers from
361
+ * {@link buildMemoryHeaders}, then serves the upstream tool list.
362
+ *
363
+ * The first request per isolate connects and lists tools exactly like the
364
+ * old always-fetch path, then snapshots the definitions. Subsequent requests
365
+ * skip the network entirely and bind lazy tools that open their own
366
+ * authenticated client only when the agent actually calls one. An EXPIRED
367
+ * entry is served as-is (definitions only change on upstream deploys) while
368
+ * a background refresh re-snapshots it — TTL expiry never lands the
369
+ * connect + tools/list round-trip on a chat turn. Auth semantics are
370
+ * unchanged: header minting still happens (and gates the tool surface) on
371
+ * every request.
372
+ */
373
+ export function createDefaultMemoryMcpFactory(
374
+ memoryMcpUrl: string,
375
+ ): MemoryMcpFactory {
376
+ return async (runCtx) => {
377
+ const headers = await buildMemoryHeaders(runCtx, memoryMcpUrl);
378
+ if (!headers) return null;
379
+
380
+ const cached = toolDefsCache.get(memoryMcpUrl);
381
+ if (cached) {
382
+ if (
383
+ cached.expiresAt <= Date.now() &&
384
+ !refreshInFlight.has(memoryMcpUrl)
385
+ ) {
386
+ refreshInFlight.add(memoryMcpUrl);
387
+ void listToolDefs(memoryMcpUrl, headers)
388
+ .then((defs) => {
389
+ toolDefsCache.set(memoryMcpUrl, {
390
+ defs,
391
+ expiresAt: Date.now() + TOOL_DEFS_TTL_MS,
392
+ });
393
+ })
394
+ .catch((err: unknown) => {
395
+ // Keep serving the stale defs; the next expired-cache turn
396
+ // retries the refresh.
397
+ runCtx.logger.warn(
398
+ `[memory] background tool-defs refresh failed: ${err instanceof Error ? err.message : String(err)}`,
399
+ );
400
+ })
401
+ .finally(() => {
402
+ refreshInFlight.delete(memoryMcpUrl);
403
+ });
404
+ }
405
+ return buildLazyUpstreamTools(
406
+ cached.defs,
407
+ memoryMcpUrl,
408
+ headers,
409
+ runCtx.onTurnEnd,
410
+ );
411
+ }
412
+
413
+ const defs = await listToolDefs(memoryMcpUrl, headers);
414
+ toolDefsCache.set(memoryMcpUrl, {
415
+ defs,
416
+ expiresAt: Date.now() + TOOL_DEFS_TTL_MS,
417
+ });
418
+ // Cold path binds lazy tools too (instead of returning tools tied to the
419
+ // listing client, which is closed above) — the first actual invocation
420
+ // reconnects, exactly like the warm path.
421
+ return buildLazyUpstreamTools(
422
+ defs,
423
+ memoryMcpUrl,
424
+ headers,
425
+ runCtx.onTurnEnd,
426
+ );
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Adapt one upstream MCP tool into a `PluginTool`. Name, description, and
432
+ * schema are taken VERBATIM from upstream — the agent sees the same contract
433
+ * the Memory Engine server publishes, and our handler is a thin passthrough
434
+ * to `mcpTool.invoke`.
435
+ */
436
+ function adaptMcpTool(mcpTool: UpstreamMcpTool): PluginTool {
437
+ return {
438
+ name: mcpTool.name,
439
+ description: mcpTool.description,
440
+ schema: mcpTool.schema,
441
+ handler: async (args) => mcpTool.invoke(args),
442
+ };
443
+ }
444
+
445
+ /**
446
+ * Fetch the upstream Memory Engine MCP tools for this request and return them
447
+ * as `PluginTool[]`. Filtered by `selectedTools` so forks can scope the
448
+ * surface (e.g. drop `clear`, add `add_oracle_knowledge` for org-owner mode).
449
+ *
450
+ * Returns `[]` when the factory cannot mint auth — the agent then sees no
451
+ * memory tools rather than getting a half-built request to upstream.
452
+ */
453
+ export async function fetchMemoryTools(
454
+ runCtx: RuntimeContext,
455
+ factory: MemoryMcpFactory,
456
+ selectedTools: readonly string[] = DEFAULT_MEMORY_TOOLS,
457
+ ): Promise<PluginTool[]> {
458
+ const mcpTools = await factory(runCtx);
459
+ if (!mcpTools) return [];
460
+
461
+ const allow = new Set(selectedTools);
462
+ return mcpTools.filter((t) => allow.has(t.name)).map(adaptMcpTool);
463
+ }
@@ -0,0 +1,51 @@
1
+ import type { RuntimeContext } from '../../plugin-api/types';
2
+ import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
3
+
4
+ /**
5
+ * Build the headers needed to call the Memory Engine MCP server on behalf of
6
+ * the current user.
7
+ *
8
+ * 1. Resolve the memory service DID via `runCtx.ucan.resolveServiceDid`
9
+ * (delegates the did:web lookup + cache to the UCAN service).
10
+ * 2. Mint an `ixo:memory` invocation using the user's signing key.
11
+ * 3. Layer the resulting Bearer token plus the active `x-room-id`.
12
+ *
13
+ * UCAN-only — no Matrix-OpenID fallback. If resolution or minting fails for
14
+ * any reason (no signing key, no cached delegation, did:web unresolved,
15
+ * network error) the function returns `null` so callers degrade silently
16
+ * rather than leak a half-built request to the upstream service. All failures
17
+ * are contained by the shared UCAN helpers — nothing throws past this
18
+ * boundary. The "no delegation present" case logs at `debug`; a genuine
19
+ * resolve/mint failure logs at `warn`.
20
+ */
21
+ export async function buildMemoryHeaders(
22
+ runCtx: RuntimeContext,
23
+ memoryMcpUrl: string,
24
+ ): Promise<Record<string, string> | null> {
25
+ const memoryDid = await resolveServiceDidSafely(
26
+ runCtx,
27
+ memoryMcpUrl,
28
+ 'memory',
29
+ );
30
+ if (!memoryDid) return null;
31
+
32
+ const invocation = await mintInvocationSafely(
33
+ runCtx,
34
+ { did: memoryDid, capability: 'ixo:memory' },
35
+ 'memory',
36
+ // Claim the ability the user's delegation actually grants. A `'*'` claim is
37
+ // satisfiable only by a `'*'` grant — `'*'.startsWith('memory/')` is false.
38
+ { can: 'memory/*' },
39
+ );
40
+ if (!invocation) return null;
41
+
42
+ const headers: Record<string, string> = {
43
+ Authorization: `Bearer ${invocation}`,
44
+ 'X-Auth-Type': 'ucan',
45
+ 'User-Agent': 'LangChain-MCP-Client/1.0',
46
+ };
47
+ if (runCtx.session.roomId) {
48
+ headers['x-room-id'] = runCtx.session.roomId;
49
+ }
50
+ return headers;
51
+ }