@ixo/oracle-runtime-workers 0.25.0 → 0.27.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/package.json +3 -2
- package/src/attachments/extract.ts +108 -14
- package/src/attachments/index.ts +15 -0
- package/src/attachments/pdf-text.ts +99 -0
- package/src/attachments/pipeline.ts +47 -4
- package/src/attachments/sanitize.ts +14 -0
- package/src/attachments/view-cache.ts +95 -0
- package/src/attachments/view.ts +308 -3
- package/src/channels/auth.ts +2 -1
- package/src/channels/contract.ts +104 -0
- package/src/channels/replies.ts +58 -0
- package/src/channels/routing.ts +222 -0
- package/src/channels/turns.ts +1252 -16
- package/src/core/env.ts +17 -0
- package/src/do/contracts.ts +19 -0
- package/src/do/room-mirror.ts +8 -1
- package/src/do/run-coordinator.ts +27 -0
- package/src/do/transcript.ts +14 -11
- package/src/do/turn-messages.ts +54 -0
- package/src/do/user-oracle-do.ts +321 -135
- package/src/memory/session-history-indexer.ts +442 -103
- package/src/memory/user-context-fetcher.ts +13 -0
- package/src/memory/user-context-invalidation.ts +8 -2
- package/src/plugins/action-proposals/evidence.ts +7 -8
- package/src/plugins/memory/document-chunker.ts +313 -0
- package/src/plugins/memory/document-source.ts +229 -0
- package/src/plugins/memory/memory-engine-client.ts +352 -0
- package/src/plugins/memory/memory.plugin.ts +25 -2
- package/src/plugins/memory/oracle-document-tools.ts +677 -0
- package/src/plugins/tasks/middleware.ts +4 -9
- package/src/plugins/vfs/index.ts +11 -2
- package/src/plugins/vfs/vfs-content.ts +86 -22
- package/src/plugins/vfs/vfs.plugin.ts +28 -19
- package/src/shell/app.ts +70 -0
- package/src/sqlite/serialization.ts +20 -0
- package/src/sqlite/sessions-store.ts +43 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
3
|
+
import { MEMORY_CAPABILITY } from '../delegated-capabilities';
|
|
4
|
+
import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
|
|
5
|
+
import { memoryEntityDid } from './memory-entity';
|
|
6
|
+
|
|
7
|
+
export type KnowledgeSpaceType = 'public' | 'private';
|
|
8
|
+
|
|
9
|
+
export interface MemoryEngineClientDeps {
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
/** Per-request timeout (default 30 s). */
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
let warnedMissingEntityDid = false;
|
|
18
|
+
|
|
19
|
+
/** Forget that the missing-entity warning was logged (tests only). */
|
|
20
|
+
export function resetMemoryEngineWarningsForTests(): void {
|
|
21
|
+
warnedMissingEntityDid = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The DID sent as `x-oracle-entity-did`: the configured `ORACLE_ENTITY_DID`
|
|
26
|
+
* when it is a well-formed domain entity DID, never the `ORACLE_DID` fallback
|
|
27
|
+
* (see the memory engine contract in docs/configuration.md). Logs once per
|
|
28
|
+
* isolate when there is none.
|
|
29
|
+
*/
|
|
30
|
+
export function entityDidOrWarn(ctx: RuntimeContext): string | undefined {
|
|
31
|
+
const did = memoryEntityDid(ctx.config.ORACLE_ENTITY_DID);
|
|
32
|
+
if (did === undefined && !warnedMissingEntityDid) {
|
|
33
|
+
warnedMissingEntityDid = true;
|
|
34
|
+
ctx.logger.warn(
|
|
35
|
+
"[memory] documents unavailable: set ORACLE_ENTITY_DID to the oracle's domain entity DID (did:ixo:entity:<32 hex digits>)",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return did;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* An answer the Memory Engine refused (or a transport failure, `status` 0).
|
|
43
|
+
* `message` never carries the response body or the bearer; the engine's own
|
|
44
|
+
* `detail` is kept on the error so a tool can show it to the person, and is
|
|
45
|
+
* never logged.
|
|
46
|
+
*/
|
|
47
|
+
export class MemoryEngineError extends Error {
|
|
48
|
+
readonly status: number;
|
|
49
|
+
|
|
50
|
+
readonly code?: string;
|
|
51
|
+
|
|
52
|
+
readonly detail?: string;
|
|
53
|
+
|
|
54
|
+
readonly retryable: boolean;
|
|
55
|
+
|
|
56
|
+
readonly retryAfterSeconds?: number;
|
|
57
|
+
|
|
58
|
+
/** Parts the engine had queued before it failed (`PARTIAL_QUEUE`). */
|
|
59
|
+
readonly queued?: number;
|
|
60
|
+
|
|
61
|
+
constructor(init: {
|
|
62
|
+
status: number;
|
|
63
|
+
code?: string;
|
|
64
|
+
detail?: string;
|
|
65
|
+
retryable?: boolean;
|
|
66
|
+
retryAfterSeconds?: number;
|
|
67
|
+
queued?: number;
|
|
68
|
+
}) {
|
|
69
|
+
super(
|
|
70
|
+
`memory engine answered ${init.status}${init.code ? ` ${init.code}` : ''}`,
|
|
71
|
+
);
|
|
72
|
+
this.name = 'MemoryEngineError';
|
|
73
|
+
this.status = init.status;
|
|
74
|
+
if (init.code !== undefined) this.code = init.code;
|
|
75
|
+
if (init.detail !== undefined) this.detail = init.detail;
|
|
76
|
+
this.retryable = init.retryable ?? false;
|
|
77
|
+
if (init.retryAfterSeconds !== undefined) {
|
|
78
|
+
this.retryAfterSeconds = init.retryAfterSeconds;
|
|
79
|
+
}
|
|
80
|
+
if (init.queued !== undefined) this.queued = init.queued;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DocumentChunkInput {
|
|
85
|
+
label?: string;
|
|
86
|
+
content: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface AddDocumentRequest {
|
|
90
|
+
knowledge_space_type: KnowledgeSpaceType;
|
|
91
|
+
confirmed_insertion_from_user: true;
|
|
92
|
+
/** Present for a grouped document, absent for loose knowledge. */
|
|
93
|
+
document?: { id?: string; title: string; source_description?: string };
|
|
94
|
+
if_exists?: 'replace' | 'fail';
|
|
95
|
+
chunks: DocumentChunkInput[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const documentStatusSchema = z.enum([
|
|
99
|
+
'ingesting',
|
|
100
|
+
'ready',
|
|
101
|
+
'partial',
|
|
102
|
+
'deleting',
|
|
103
|
+
'delete_failed',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const addDocumentResponseSchema = z.object({
|
|
107
|
+
success: z.boolean().optional(),
|
|
108
|
+
queued: z.number(),
|
|
109
|
+
document: z
|
|
110
|
+
.object({
|
|
111
|
+
id: z.string(),
|
|
112
|
+
generation: z.number(),
|
|
113
|
+
chunk_count: z.number(),
|
|
114
|
+
status: documentStatusSchema,
|
|
115
|
+
replaced: z.boolean(),
|
|
116
|
+
/** Chunks of the replaced generation still being removed. */
|
|
117
|
+
retiring_chunks: z.number().optional(),
|
|
118
|
+
})
|
|
119
|
+
.optional(),
|
|
120
|
+
chunk_ids: z.array(z.string()).optional(),
|
|
121
|
+
});
|
|
122
|
+
export type AddDocumentResponse = z.infer<typeof addDocumentResponseSchema>;
|
|
123
|
+
|
|
124
|
+
const documentSummarySchema = z.object({
|
|
125
|
+
id: z.string(),
|
|
126
|
+
title: z.string(),
|
|
127
|
+
source_description: z.string().nullable().optional(),
|
|
128
|
+
generation: z.number(),
|
|
129
|
+
chunk_count: z.number(),
|
|
130
|
+
chunks_indexed: z.number(),
|
|
131
|
+
retiring_chunks: z.number().optional(),
|
|
132
|
+
status: documentStatusSchema,
|
|
133
|
+
/** Why the last deletion failed (status `delete_failed`). */
|
|
134
|
+
delete_error: z.string().nullable().optional(),
|
|
135
|
+
created_by: z.string().nullable().optional(),
|
|
136
|
+
updated_by: z.string().nullable().optional(),
|
|
137
|
+
created_at: z.string().nullable().optional(),
|
|
138
|
+
updated_at: z.string().nullable().optional(),
|
|
139
|
+
});
|
|
140
|
+
export type DocumentSummary = z.infer<typeof documentSummarySchema>;
|
|
141
|
+
|
|
142
|
+
const listDocumentsResponseSchema = z.object({
|
|
143
|
+
documents: z.array(documentSummarySchema),
|
|
144
|
+
});
|
|
145
|
+
export type ListDocumentsResponse = z.infer<typeof listDocumentsResponseSchema>;
|
|
146
|
+
|
|
147
|
+
const documentViewSchema = documentSummarySchema.extend({
|
|
148
|
+
chunks: z.array(z.unknown()).optional(),
|
|
149
|
+
});
|
|
150
|
+
export type DocumentView = z.infer<typeof documentViewSchema>;
|
|
151
|
+
|
|
152
|
+
const deleteDocumentResponseSchema = z.object({
|
|
153
|
+
success: z.boolean().optional(),
|
|
154
|
+
document: z.object({
|
|
155
|
+
id: z.string(),
|
|
156
|
+
status: documentStatusSchema,
|
|
157
|
+
chunks: z.number(),
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
export type DeleteDocumentResponse = z.infer<
|
|
161
|
+
typeof deleteDocumentResponseSchema
|
|
162
|
+
>;
|
|
163
|
+
|
|
164
|
+
export interface MemoryEngineClient {
|
|
165
|
+
addDocument(body: AddDocumentRequest): Promise<AddDocumentResponse>;
|
|
166
|
+
listDocuments(space: KnowledgeSpaceType): Promise<ListDocumentsResponse>;
|
|
167
|
+
getDocument(id: string, space: KnowledgeSpaceType): Promise<DocumentView>;
|
|
168
|
+
/** Deletion runs in the background; the answer says it was scheduled. */
|
|
169
|
+
deleteDocument(
|
|
170
|
+
id: string,
|
|
171
|
+
space: KnowledgeSpaceType,
|
|
172
|
+
): Promise<DeleteDocumentResponse['document']>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const configSchema = z.object({
|
|
176
|
+
MEMORY_ENGINE_URL: z.string().url(),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
function describe(error: unknown): string {
|
|
180
|
+
return error instanceof Error ? error.message : String(error);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseRetryAfter(res: Response): number | undefined {
|
|
184
|
+
const raw = res.headers.get('Retry-After');
|
|
185
|
+
if (!raw) return undefined;
|
|
186
|
+
const seconds = Number(raw);
|
|
187
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function detailText(detail: unknown): string | undefined {
|
|
191
|
+
if (typeof detail === 'string') return detail.slice(0, 500);
|
|
192
|
+
if (detail === undefined || detail === null) return undefined;
|
|
193
|
+
try {
|
|
194
|
+
return JSON.stringify(detail).slice(0, 500);
|
|
195
|
+
} catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const errorBodySchema = z.object({
|
|
201
|
+
code: z.string().optional(),
|
|
202
|
+
detail: z.unknown().optional(),
|
|
203
|
+
retryable: z.boolean().optional(),
|
|
204
|
+
queued: z.number().optional(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
async function toEngineError(res: Response): Promise<MemoryEngineError> {
|
|
208
|
+
let raw: unknown;
|
|
209
|
+
try {
|
|
210
|
+
raw = await res.json();
|
|
211
|
+
} catch {
|
|
212
|
+
raw = undefined;
|
|
213
|
+
}
|
|
214
|
+
const body = errorBodySchema.safeParse(raw);
|
|
215
|
+
const parsed = body.success ? body.data : {};
|
|
216
|
+
const detail = detailText(parsed.detail);
|
|
217
|
+
const retryAfterSeconds = parseRetryAfter(res);
|
|
218
|
+
return new MemoryEngineError({
|
|
219
|
+
status: res.status,
|
|
220
|
+
...(parsed.code !== undefined ? { code: parsed.code } : {}),
|
|
221
|
+
...(detail !== undefined ? { detail } : {}),
|
|
222
|
+
retryable: parsed.retryable === true,
|
|
223
|
+
...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
|
|
224
|
+
...(parsed.queued !== undefined ? { queued: parsed.queued } : {}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A client for the Memory Engine's document routes, authenticated as the
|
|
230
|
+
* current user (UCAN invocation minted once per client) and as the oracle's
|
|
231
|
+
* entity (`x-oracle-entity-did`). Returns `null`, after logging why, when the
|
|
232
|
+
* engine URL, the entity DID or an invocation is unavailable; a tool then tells
|
|
233
|
+
* the model the engine is not reachable.
|
|
234
|
+
*/
|
|
235
|
+
export async function createMemoryEngineClient(
|
|
236
|
+
ctx: RuntimeContext,
|
|
237
|
+
deps: MemoryEngineClientDeps = {},
|
|
238
|
+
): Promise<MemoryEngineClient | null> {
|
|
239
|
+
const config = configSchema.safeParse(ctx.config);
|
|
240
|
+
if (!config.success) {
|
|
241
|
+
ctx.logger.warn(
|
|
242
|
+
'[memory] documents unavailable: MEMORY_ENGINE_URL missing',
|
|
243
|
+
);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const engineUrl = config.data.MEMORY_ENGINE_URL.replace(/\/+$/, '');
|
|
247
|
+
const entityDid = entityDidOrWarn(ctx);
|
|
248
|
+
if (!entityDid) return null;
|
|
249
|
+
|
|
250
|
+
const serviceDid = await resolveServiceDidSafely(ctx, engineUrl, 'memory');
|
|
251
|
+
if (!serviceDid) return null;
|
|
252
|
+
const invocation = await mintInvocationSafely(
|
|
253
|
+
ctx,
|
|
254
|
+
{ did: serviceDid, capability: MEMORY_CAPABILITY.resource },
|
|
255
|
+
'memory',
|
|
256
|
+
{ can: MEMORY_CAPABILITY.action },
|
|
257
|
+
);
|
|
258
|
+
if (!invocation) return null;
|
|
259
|
+
|
|
260
|
+
const headers: Record<string, string> = {
|
|
261
|
+
Authorization: `Bearer ${invocation}`,
|
|
262
|
+
'X-Auth-Type': 'ucan',
|
|
263
|
+
'x-oracle-entity-did': entityDid,
|
|
264
|
+
};
|
|
265
|
+
if (ctx.session.roomId) headers['x-room-id'] = ctx.session.roomId;
|
|
266
|
+
|
|
267
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
268
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
269
|
+
|
|
270
|
+
async function request<T>(
|
|
271
|
+
method: 'GET' | 'POST' | 'DELETE',
|
|
272
|
+
path: string,
|
|
273
|
+
schema: z.ZodType<T>,
|
|
274
|
+
body?: unknown,
|
|
275
|
+
): Promise<T> {
|
|
276
|
+
let res: Response;
|
|
277
|
+
try {
|
|
278
|
+
res = await fetchImpl(`${engineUrl}${path}`, {
|
|
279
|
+
method,
|
|
280
|
+
headers:
|
|
281
|
+
body === undefined
|
|
282
|
+
? headers
|
|
283
|
+
: { ...headers, 'Content-Type': 'application/json' },
|
|
284
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
285
|
+
signal: AbortSignal.any([
|
|
286
|
+
ctx.abortSignal,
|
|
287
|
+
AbortSignal.timeout(timeoutMs),
|
|
288
|
+
]),
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// A cancelled turn is not an engine failure.
|
|
292
|
+
if (ctx.abortSignal.aborted) throw error;
|
|
293
|
+
const timedOut = error instanceof Error && error.name === 'TimeoutError';
|
|
294
|
+
throw new MemoryEngineError({
|
|
295
|
+
status: 0,
|
|
296
|
+
code: timedOut ? 'TIMEOUT' : 'NETWORK_ERROR',
|
|
297
|
+
detail: timedOut
|
|
298
|
+
? `no answer within ${timeoutMs} ms`
|
|
299
|
+
: describe(error).slice(0, 200),
|
|
300
|
+
retryable: true,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (!res.ok) throw await toEngineError(res);
|
|
304
|
+
let json: unknown;
|
|
305
|
+
try {
|
|
306
|
+
json = await res.json();
|
|
307
|
+
} catch {
|
|
308
|
+
throw new MemoryEngineError({
|
|
309
|
+
status: res.status,
|
|
310
|
+
code: 'MALFORMED_RESPONSE',
|
|
311
|
+
detail: 'the answer was not valid JSON',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const parsed = schema.safeParse(json);
|
|
315
|
+
if (!parsed.success) {
|
|
316
|
+
throw new MemoryEngineError({
|
|
317
|
+
status: res.status,
|
|
318
|
+
code: 'MALFORMED_RESPONSE',
|
|
319
|
+
detail: 'the answer did not have the expected shape',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return parsed.data;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const spaceQuery = (space: KnowledgeSpaceType): string =>
|
|
326
|
+
`knowledge_space_type=${space}`;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
addDocument: (body) =>
|
|
330
|
+
request('POST', '/knowledge/documents', addDocumentResponseSchema, body),
|
|
331
|
+
listDocuments: (space) =>
|
|
332
|
+
request(
|
|
333
|
+
'GET',
|
|
334
|
+
`/knowledge/documents?${spaceQuery(space)}`,
|
|
335
|
+
listDocumentsResponseSchema,
|
|
336
|
+
),
|
|
337
|
+
getDocument: (id, space) =>
|
|
338
|
+
request(
|
|
339
|
+
'GET',
|
|
340
|
+
`/knowledge/documents/${encodeURIComponent(id)}?${spaceQuery(space)}`,
|
|
341
|
+
documentViewSchema,
|
|
342
|
+
),
|
|
343
|
+
deleteDocument: async (id, space) => {
|
|
344
|
+
const out = await request(
|
|
345
|
+
'DELETE',
|
|
346
|
+
`/knowledge/documents/${encodeURIComponent(id)}?${spaceQuery(space)}`,
|
|
347
|
+
deleteDocumentResponseSchema,
|
|
348
|
+
);
|
|
349
|
+
return out.document;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type MemoryMcpFactory,
|
|
16
16
|
} from './memory-tools';
|
|
17
17
|
import { oracleEntityDidSchema } from './memory-entity';
|
|
18
|
+
import { createOracleDocumentTools } from './oracle-document-tools';
|
|
18
19
|
|
|
19
20
|
const configSchema = z.object({
|
|
20
21
|
MEMORY_MCP_URL: z.string().url('MEMORY_MCP_URL must be a valid HTTP(S) URL.'),
|
|
@@ -36,7 +37,8 @@ const manifest: PluginManifest = {
|
|
|
36
37
|
'You create an artifact for the user (file, document, edit, generated content): record what it is, what it is for, and the structural choices you made.',
|
|
37
38
|
'The user expresses satisfaction or dissatisfaction with something you produced: capture what worked or what did not — tone, length, structure, style — against that artifact entry.',
|
|
38
39
|
'The user references something they told you before, or something you made before.',
|
|
39
|
-
'A controller or owner of this oracle
|
|
40
|
+
'A controller or owner of this oracle TYPES into the chat something every user of the oracle should know (a policy, a procedure, reference material): tell them it will be shared with every user of the oracle (or, for the private space, with the members of the domain only: its controllers, owner, admins and DAO members), get their confirmation, then store it as oracle knowledge rather than as a memory about them. This line is only for knowledge typed in the chat.',
|
|
41
|
+
'A controller or owner of this oracle hands you a file (an attachment or a Library file) and asks you to store it as oracle knowledge: a file the person attached or a Library file ALWAYS goes to add_oracle_document, never to add_oracle_knowledge, even when its text is visible in the conversation. If the file is one coherent record (a board resolution, a contract, a report) group it as a document (group_as_document true); if it is a file where someone dumped many unrelated company facts to upload them easily, store it as loose facts (group_as_document false). When it is not obvious which, ask the person first, and always confirm the public or private space with them before writing. list_oracle_documents and delete_oracle_document show and remove grouped documents.',
|
|
40
42
|
],
|
|
41
43
|
whenNotToUse: [
|
|
42
44
|
'Ephemeral conversation-only state (use the current message thread).',
|
|
@@ -44,6 +46,7 @@ const manifest: PluginManifest = {
|
|
|
44
46
|
'Public web facts that are not specific to this user (use a web-search capability).',
|
|
45
47
|
'Anything the user asked you to forget or framed as temporary.',
|
|
46
48
|
'Oracle knowledge for something about one user: the shared knowledge is only for what every user of this oracle should know.',
|
|
49
|
+
'Storing a file the person did not ask you to store as oracle knowledge, or choosing between a grouped document and loose facts without asking when the file could be either.',
|
|
47
50
|
],
|
|
48
51
|
tags: ['memory', 'recall', 'artifacts', 'personalization'],
|
|
49
52
|
category: 'memory',
|
|
@@ -66,6 +69,15 @@ export interface MemoryPluginOptions {
|
|
|
66
69
|
* explicit list to drop one of them or to expose `delete_edge`.
|
|
67
70
|
*/
|
|
68
71
|
selectedTools?: readonly string[];
|
|
72
|
+
/**
|
|
73
|
+
* Surface `add_oracle_document`, `list_oracle_documents` and
|
|
74
|
+
* `delete_oracle_document` next to the upstream tools (default true). They
|
|
75
|
+
* talk to `MEMORY_ENGINE_URL` directly and are not part of
|
|
76
|
+
* {@link selectedTools}, and appear only when the upstream tools do (a user
|
|
77
|
+
* without a memory delegation gets neither). Pass `false` to keep documents
|
|
78
|
+
* out of the oracle.
|
|
79
|
+
*/
|
|
80
|
+
documentTools?: boolean;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
function resolveMemoryConfig(config: MergedConfig): {
|
|
@@ -109,10 +121,13 @@ export class MemoryPlugin extends OraclePlugin {
|
|
|
109
121
|
|
|
110
122
|
private readonly selectedTools: readonly string[];
|
|
111
123
|
|
|
124
|
+
private readonly documentTools: boolean;
|
|
125
|
+
|
|
112
126
|
constructor(options: MemoryPluginOptions = {}) {
|
|
113
127
|
super();
|
|
114
128
|
this.mcpFactory = options.mcpFactory ?? createDefaultMemoryMcpFactory;
|
|
115
129
|
this.selectedTools = options.selectedTools ?? DEFAULT_MEMORY_TOOLS;
|
|
130
|
+
this.documentTools = options.documentTools ?? true;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
override autoDetect(env: PluginEnv): boolean {
|
|
@@ -124,7 +139,15 @@ export class MemoryPlugin extends OraclePlugin {
|
|
|
124
139
|
override async getRequestTools(rtCtx: RuntimeContext): Promise<PluginTool[]> {
|
|
125
140
|
const { memoryMcpUrl, entityDid } = resolveMemoryConfig(rtCtx.config);
|
|
126
141
|
const factory = this.mcpFactory(memoryMcpUrl, entityDid);
|
|
127
|
-
|
|
142
|
+
const mcpTools = await fetchMemoryTools(rtCtx, factory, this.selectedTools);
|
|
143
|
+
// The document tools come first so a file never lands on the upstream
|
|
144
|
+
// add_oracle_knowledge by position.
|
|
145
|
+
// No upstream tools means memory is unavailable for this user (no
|
|
146
|
+
// delegation, unresolved service DID): the document tools would only
|
|
147
|
+
// answer "not reachable", so they are left out too.
|
|
148
|
+
return this.documentTools && mcpTools.length > 0
|
|
149
|
+
? [...createOracleDocumentTools(), ...mcpTools]
|
|
150
|
+
: mcpTools;
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
override getSharedState(): Record<
|