@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.
- package/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- 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
|
+
}
|