@openviking/openclaw-plugin 2026.6.2 → 2026.6.17-dev.2

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 (108) hide show
  1. package/INSTALL-AGENT.md +4 -4
  2. package/INSTALL-ZH.md +68 -8
  3. package/INSTALL.md +12 -8
  4. package/README.md +45 -35
  5. package/adapters/http-transport.ts +3 -0
  6. package/adapters/resource-packager.ts +141 -0
  7. package/auto-recall.ts +258 -27
  8. package/client.ts +201 -177
  9. package/commands/setup.ts +200 -33
  10. package/config/feature-gates.json +10 -0
  11. package/config.ts +428 -85
  12. package/context-engine.ts +71 -1402
  13. package/dist/adapters/http-transport.js +1 -0
  14. package/dist/adapters/resource-packager.js +114 -0
  15. package/dist/auto-recall.js +201 -25
  16. package/dist/client.js +125 -161
  17. package/dist/commands/setup.js +170 -32
  18. package/dist/config.js +285 -64
  19. package/dist/context-engine.js +62 -1173
  20. package/dist/index.js +209 -1489
  21. package/dist/memory-ranking.js +35 -12
  22. package/dist/plugin/command-registration.js +5 -0
  23. package/dist/plugin/openviking-archive-tools.js +210 -0
  24. package/dist/plugin/openviking-bypass-runtime.js +8 -0
  25. package/dist/plugin/openviking-client-runtime.js +28 -0
  26. package/dist/plugin/openviking-command-args.js +146 -0
  27. package/dist/plugin/openviking-command-definitions.js +206 -0
  28. package/dist/plugin/openviking-context-engine-ref.js +11 -0
  29. package/dist/plugin/openviking-context-engine-registration.js +23 -0
  30. package/dist/plugin/openviking-feature-gates.js +250 -0
  31. package/dist/plugin/openviking-import-runtime.js +50 -0
  32. package/dist/plugin/openviking-import-tools.js +86 -0
  33. package/dist/plugin/openviking-lifecycle-hooks.js +33 -0
  34. package/dist/plugin/openviking-memory-recall-tools.js +233 -0
  35. package/dist/plugin/openviking-memory-tools.js +199 -0
  36. package/dist/plugin/openviking-query-config-command.js +203 -0
  37. package/dist/plugin/openviking-query-runtime.js +367 -0
  38. package/dist/plugin/openviking-query-tools.js +91 -0
  39. package/dist/plugin/openviking-recall-trace-runtime.js +387 -0
  40. package/dist/plugin/openviking-recall-trace-tools.js +38 -0
  41. package/dist/plugin/openviking-runtime-state.js +26 -0
  42. package/dist/plugin/openviking-runtime-utils.js +62 -0
  43. package/dist/plugin/openviking-services.js +20 -0
  44. package/dist/plugin/openviking-session-routing-runtime.js +61 -0
  45. package/dist/plugin/openviking-tool-result-tools.js +268 -0
  46. package/dist/plugin/recall-trace-routes.js +77 -0
  47. package/dist/plugin/tool-registration.js +23 -0
  48. package/dist/process-manager.js +1 -1
  49. package/dist/query-config.js +336 -0
  50. package/dist/recall-trace.js +309 -0
  51. package/dist/registries/recall-resource-types.js +58 -0
  52. package/dist/routing/identity-routing.js +152 -0
  53. package/dist/routing/memory-uri.js +7 -0
  54. package/dist/services/context-lifecycle-service.js +847 -0
  55. package/dist/services/context-message-adapter.js +340 -0
  56. package/dist/session-transcript-repair.js +0 -1
  57. package/dist/text-utils.js +14 -31
  58. package/index.ts +271 -1873
  59. package/install-manifest.json +10 -1
  60. package/memory-ranking.ts +46 -10
  61. package/openclaw.plugin.json +198 -36
  62. package/package.json +14 -4
  63. package/plugin/command-registration.ts +34 -0
  64. package/plugin/openviking-archive-tools.ts +301 -0
  65. package/plugin/openviking-bypass-runtime.ts +26 -0
  66. package/plugin/openviking-client-runtime.ts +74 -0
  67. package/plugin/openviking-command-args.ts +187 -0
  68. package/plugin/openviking-command-definitions.ts +294 -0
  69. package/plugin/openviking-context-engine-ref.ts +16 -0
  70. package/plugin/openviking-context-engine-registration.ts +97 -0
  71. package/plugin/openviking-feature-gates.ts +335 -0
  72. package/plugin/openviking-import-runtime.ts +105 -0
  73. package/plugin/openviking-import-tools.ts +133 -0
  74. package/plugin/openviking-lifecycle-hooks.ts +65 -0
  75. package/plugin/openviking-memory-recall-tools.ts +368 -0
  76. package/plugin/openviking-memory-tools.ts +299 -0
  77. package/plugin/openviking-query-config-command.ts +230 -0
  78. package/plugin/openviking-query-formatters.ts +108 -0
  79. package/plugin/openviking-query-runtime.ts +476 -0
  80. package/plugin/openviking-query-tools.ts +161 -0
  81. package/plugin/openviking-recall-trace-runtime.ts +482 -0
  82. package/plugin/openviking-recall-trace-tools.ts +99 -0
  83. package/plugin/openviking-runtime-state.ts +52 -0
  84. package/plugin/openviking-runtime-utils.ts +67 -0
  85. package/plugin/openviking-services.ts +49 -0
  86. package/plugin/openviking-session-routing-runtime.ts +105 -0
  87. package/plugin/openviking-tool-result-tools.ts +397 -0
  88. package/plugin/recall-trace-routes.ts +133 -0
  89. package/plugin/tool-registration.ts +51 -0
  90. package/process-manager.ts +1 -1
  91. package/query-config.ts +400 -0
  92. package/recall-trace.ts +442 -0
  93. package/registries/openviking-tools.ts +84 -0
  94. package/registries/recall-resource-types.ts +74 -0
  95. package/routing/identity-routing.ts +210 -0
  96. package/routing/memory-uri.ts +8 -0
  97. package/services/context-lifecycle-service.ts +1209 -0
  98. package/services/context-message-adapter.ts +375 -0
  99. package/services/setup/config-writer.ts +125 -0
  100. package/services/setup/exit-utils.ts +5 -0
  101. package/services/setup/package-metadata.ts +56 -0
  102. package/services/setup/probe-service.ts +143 -0
  103. package/services/setup/setup-flow.ts +306 -0
  104. package/services/setup/version-compatibility.ts +41 -0
  105. package/session-transcript-repair.ts +0 -2
  106. package/skills/install-openviking-memory/SKILL.md +123 -50
  107. package/skills/openviking-context-database/SKILL.md +286 -0
  108. package/text-utils.ts +15 -34
@@ -0,0 +1,368 @@
1
+ import { Type } from "@sinclair/typebox";
2
+
3
+ import type { FindResult, FindResultItem } from "../client.js";
4
+ import type { BuildMemoryLinesWithBudgetOptions } from "../auto-recall.js";
5
+ import type { RankingOptions } from "../memory-ranking.js";
6
+ import type { EffectiveQueryConfig, QueryConfigContext, RuntimeQueryParams } from "../query-config.js";
7
+ import type { RecallResourceType } from "../registries/recall-resource-types.js";
8
+ import type {
9
+ RecallTraceEntry,
10
+ RecallTraceResult,
11
+ } from "../recall-trace.js";
12
+
13
+ export type OpenVikingMemoryRecallToolContext = {
14
+ sessionKey?: string;
15
+ sessionId?: string;
16
+ agentId?: string;
17
+ senderId?: string;
18
+ };
19
+
20
+ export type OpenVikingMemoryRecallSession = {
21
+ sessionId?: string;
22
+ sessionKey?: string;
23
+ ovSessionId?: string;
24
+ agentId: string;
25
+ };
26
+
27
+ export type OpenVikingMemoryRecallClient = {
28
+ find: (
29
+ query: string,
30
+ options: {
31
+ targetUri?: string;
32
+ limit: number;
33
+ scoreThreshold: number;
34
+ contextType?: "memory" | "resource";
35
+ actorPeerId?: string;
36
+ },
37
+ ) => Promise<FindResult>;
38
+ read: (uri: string, agentId?: string) => Promise<string>;
39
+ getDefaultAgentId: () => string;
40
+ };
41
+
42
+ export type OpenVikingMemoryRecallToolsDeps = {
43
+ registerTool: (toolOrFactory: unknown, opts: { name: string }) => void;
44
+ getClient: () => Promise<OpenVikingMemoryRecallClient>;
45
+ queryConfigStore: {
46
+ getEffective: (
47
+ ctx: QueryConfigContext,
48
+ request?: RuntimeQueryParams,
49
+ ) => Promise<EffectiveQueryConfig>;
50
+ };
51
+ toQueryConfigContext: (session: OpenVikingMemoryRecallSession) => QueryConfigContext;
52
+ resolvePluginSessionRouting: (
53
+ ctx?: OpenVikingMemoryRecallToolContext,
54
+ ) => OpenVikingMemoryRecallSession;
55
+ isBypassedSession: (ctx?: OpenVikingMemoryRecallToolContext) => boolean;
56
+ makeBypassedToolResult: (toolName: string) => unknown;
57
+ resolveRecallSearchPlan: (
58
+ resourceTypes: unknown,
59
+ ctx: { ovSessionId?: string; agentId?: string },
60
+ ) => {
61
+ resourceTypes: RecallResourceType[];
62
+ searches: Array<{ resourceType: RecallResourceType; targetUri?: string; contextType: "memory" | "resource" }>;
63
+ skipped: Array<{ resourceType: RecallResourceType; reason: "missing_session" }>;
64
+ };
65
+ postProcessMemories: (
66
+ items: FindResultItem[],
67
+ options: { limit: number; scoreThreshold: number },
68
+ ) => FindResultItem[];
69
+ pickMemoriesForInjection: (
70
+ items: FindResultItem[],
71
+ limit: number,
72
+ queryText: string,
73
+ scoreThreshold: number,
74
+ rankingOptions?: RankingOptions,
75
+ ) => FindResultItem[];
76
+ buildMemoryLinesWithBudget: (
77
+ memories: FindResultItem[],
78
+ readFn: (uri: string) => Promise<string>,
79
+ options: BuildMemoryLinesWithBudgetOptions,
80
+ ) => Promise<{ lines: string[]; estimatedTokens: number }>;
81
+ inferRecallResourceType: (uri: string) => RecallResourceType | undefined;
82
+ createTraceId: (source: string) => string;
83
+ boundTraceQuery: (query: string, maxChars: number) => { query: string; queryTruncated?: boolean };
84
+ previewText: (value: unknown, maxChars: number) => string | undefined;
85
+ traceRecorder?: { recordAndFlush?: (entry: RecallTraceEntry) => Promise<unknown> };
86
+ cfg: {
87
+ recallTargetTypes: RecallResourceType[];
88
+ traceRecallMaxResultsPerSearch: number;
89
+ traceRecallPreviewChars: number;
90
+ traceRecallQueryMaxChars: number;
91
+ logFindRequests: boolean;
92
+ };
93
+ logger: {
94
+ info?: (message: string) => void;
95
+ };
96
+ };
97
+
98
+ function toTraceResult(
99
+ item: FindResultItem,
100
+ resultType: RecallTraceResult["resultType"],
101
+ deps: OpenVikingMemoryRecallToolsDeps,
102
+ ): RecallTraceResult {
103
+ return {
104
+ uri: item.uri,
105
+ resourceType: deps.inferRecallResourceType(item.uri),
106
+ category: item.category,
107
+ score: item.score,
108
+ level: item.level,
109
+ abstractPreview: deps.previewText(item.abstract || item.overview, deps.cfg.traceRecallPreviewChars),
110
+ resultType,
111
+ };
112
+ }
113
+
114
+ export function registerOpenVikingMemoryRecallTools(
115
+ deps: OpenVikingMemoryRecallToolsDeps,
116
+ ): void {
117
+ deps.registerTool(
118
+ (ctx: OpenVikingMemoryRecallToolContext) => ({
119
+ name: "memory_recall",
120
+ label: "Memory Recall (OpenViking)",
121
+ description:
122
+ "Search long-term memories from OpenViking. Use when you need past user preferences, facts, or decisions.",
123
+ parameters: Type.Object({
124
+ query: Type.String({ description: "Search query" }),
125
+ limit: Type.Optional(
126
+ Type.Number({ description: "Max results (default: plugin config)" }),
127
+ ),
128
+ scoreThreshold: Type.Optional(
129
+ Type.Number({ description: "Minimum score (0-1, default: plugin config)" }),
130
+ ),
131
+ targetUri: Type.Optional(
132
+ Type.String({ description: "Search scope URI (default: plugin config)" }),
133
+ ),
134
+ resourceTypes: Type.Optional(
135
+ Type.Array(Type.String({ description: "resource, user, or agent; used when targetUri is omitted" })),
136
+ ),
137
+ }),
138
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
139
+ if (deps.isBypassedSession(ctx)) {
140
+ return deps.makeBypassedToolResult("memory_recall");
141
+ }
142
+ const session = deps.resolvePluginSessionRouting(ctx);
143
+ const { query } = params as { query: string };
144
+ const queryConfig = await deps.queryConfigStore.getEffective(deps.toQueryConfigContext(session), {
145
+ recallLimit: typeof (params as { limit?: number }).limit === "number" ? (params as { limit: number }).limit : undefined,
146
+ scoreThreshold: typeof (params as { scoreThreshold?: number }).scoreThreshold === "number" ? (params as { scoreThreshold: number }).scoreThreshold : undefined,
147
+ targetUri: typeof (params as { targetUri?: string }).targetUri === "string" ? (params as { targetUri: string }).targetUri : undefined,
148
+ resourceTypes: Object.prototype.hasOwnProperty.call(params, "resourceTypes")
149
+ ? (params as { resourceTypes?: unknown }).resourceTypes as RuntimeQueryParams["resourceTypes"]
150
+ : undefined,
151
+ });
152
+ const limit = queryConfig.recallLimit;
153
+ const scoreThreshold = queryConfig.scoreThreshold;
154
+ const targetUri =
155
+ typeof (params as { targetUri?: string }).targetUri === "string"
156
+ ? (params as { targetUri: string }).targetUri
157
+ : queryConfig.targetUri;
158
+ const requestedResourceTypes = Object.prototype.hasOwnProperty.call(params, "resourceTypes")
159
+ ? (params as { resourceTypes?: unknown }).resourceTypes
160
+ : queryConfig.resourceTypes;
161
+ const requestLimit = queryConfig.candidateLimit;
162
+
163
+ const recallClient = await deps.getClient();
164
+ if (deps.cfg.logFindRequests) {
165
+ deps.logger.info?.(
166
+ `openviking: memory_recall X-OpenViking-Actor-Peer="${session.agentId}" ` +
167
+ `(plugin defaultAgentId="${recallClient.getDefaultAgentId()}" is unused when session context is present)`,
168
+ );
169
+ }
170
+
171
+ let result: FindResult;
172
+ let memoryRecallSearches: RecallTraceEntry["searches"] = [];
173
+ if (targetUri) {
174
+ result = await recallClient.find(
175
+ query,
176
+ {
177
+ targetUri,
178
+ limit: requestLimit,
179
+ scoreThreshold: 0,
180
+ actorPeerId: session.agentId,
181
+ },
182
+ );
183
+ const traceResults = [
184
+ ...(result.memories ?? []).map((item) => toTraceResult(item, "memory", deps)),
185
+ ...(result.resources ?? []).map((item) => toTraceResult(item, "resource", deps)),
186
+ ].slice(0, deps.cfg.traceRecallMaxResultsPerSearch);
187
+ memoryRecallSearches = [{
188
+ resourceType: deps.inferRecallResourceType(targetUri) ?? "resource",
189
+ targetUriInput: targetUri,
190
+ targetUriResolved: targetUri,
191
+ limit: requestLimit,
192
+ scoreThreshold,
193
+ durationMs: 0,
194
+ total: result.total ?? traceResults.length,
195
+ results: traceResults,
196
+ }];
197
+ } else {
198
+ const searchPlan = deps.resolveRecallSearchPlan(requestedResourceTypes ?? deps.cfg.recallTargetTypes, {
199
+ ovSessionId: session.ovSessionId,
200
+ agentId: session.agentId,
201
+ });
202
+ const settled = await Promise.allSettled(searchPlan.searches.map((search) =>
203
+ recallClient.find(
204
+ query,
205
+ {
206
+ targetUri: search.targetUri,
207
+ limit: requestLimit,
208
+ scoreThreshold: 0,
209
+ contextType: search.contextType,
210
+ actorPeerId: session.agentId,
211
+ },
212
+ ),
213
+ ));
214
+ const allMemories: FindResultItem[] = [];
215
+ for (let index = 0; index < settled.length; index += 1) {
216
+ const s = settled[index]!;
217
+ const search = searchPlan.searches[index]!;
218
+ if (s.status === "fulfilled") {
219
+ allMemories.push(...(s.value.memories ?? []), ...(s.value.resources ?? []));
220
+ memoryRecallSearches.push({
221
+ resourceType: search.resourceType,
222
+ targetUriInput: search.targetUri,
223
+ targetUriResolved: search.targetUri,
224
+ limit: requestLimit,
225
+ scoreThreshold,
226
+ durationMs: 0,
227
+ total: s.value.total ?? ((s.value.memories ?? []).length + (s.value.resources ?? []).length),
228
+ results: [
229
+ ...(s.value.memories ?? []).map((item) => toTraceResult(item, "memory", deps)),
230
+ ...(s.value.resources ?? []).map((item) => toTraceResult(item, "resource", deps)),
231
+ ].slice(0, deps.cfg.traceRecallMaxResultsPerSearch),
232
+ });
233
+ } else {
234
+ memoryRecallSearches.push({
235
+ resourceType: search.resourceType,
236
+ targetUriInput: search.targetUri,
237
+ targetUriResolved: search.targetUri,
238
+ limit: requestLimit,
239
+ scoreThreshold,
240
+ durationMs: 0,
241
+ total: 0,
242
+ results: [],
243
+ error: s.reason instanceof Error ? s.reason.message : String(s.reason),
244
+ });
245
+ }
246
+ }
247
+ const uniqueMemories = allMemories.filter((memory, index, self) =>
248
+ index === self.findIndex((m) => m.uri === memory.uri)
249
+ );
250
+ const leafOnly = uniqueMemories.filter((m) => !m.level || m.level === 2);
251
+ result = {
252
+ memories: leafOnly,
253
+ total: leafOnly.length,
254
+ };
255
+ }
256
+
257
+ const leafOnly = (result.memories ?? []).filter((m) => !m.level || m.level === 2);
258
+ const processed = deps.postProcessMemories(leafOnly, {
259
+ limit: requestLimit,
260
+ scoreThreshold,
261
+ });
262
+ const memories = deps.pickMemoriesForInjection(processed, limit, query, scoreThreshold, {
263
+ weights: queryConfig.rankingWeights,
264
+ categoryWeights: queryConfig.categoryWeights,
265
+ resourceTypeWeights: queryConfig.resourceTypeWeights,
266
+ });
267
+ const candidateTraceResults = leafOnly
268
+ .map((item) => toTraceResult(item, deps.inferRecallResourceType(item.uri) === "resource" ? "resource" : "memory", deps))
269
+ .slice(0, deps.cfg.traceRecallMaxResultsPerSearch);
270
+ const traceResourceTypes = [...new Set(
271
+ (targetUri ? [deps.inferRecallResourceType(targetUri)] : memoryRecallSearches.map((search) => search.resourceType))
272
+ .filter((resourceType): resourceType is RecallResourceType => Boolean(resourceType)),
273
+ )];
274
+ const recordMemoryRecallTrace = async (injectedUris: Set<string>) => {
275
+ await deps.traceRecorder?.recordAndFlush?.({
276
+ schemaVersion: "1.0",
277
+ traceId: deps.createTraceId("memory_recall"),
278
+ ts: Date.now(),
279
+ sessionId: session.sessionId,
280
+ sessionKey: session.sessionKey,
281
+ ovSessionId: session.ovSessionId,
282
+ agentId: session.agentId,
283
+ source: "memory_recall",
284
+ operationType: "semantic_find",
285
+ resourceTypes: traceResourceTypes.length > 0 ? traceResourceTypes : ["resource"],
286
+ trigger: deps.boundTraceQuery(query, deps.cfg.traceRecallQueryMaxChars),
287
+ searches: memoryRecallSearches.length > 0 ? memoryRecallSearches : [{
288
+ resourceType: "resource",
289
+ targetUriInput: targetUri,
290
+ targetUriResolved: targetUri ?? "viking://resources",
291
+ limit: requestLimit,
292
+ scoreThreshold,
293
+ durationMs: 0,
294
+ total: result.total ?? leafOnly.length,
295
+ results: candidateTraceResults,
296
+ }],
297
+ selected: memories.map((item) => ({
298
+ uri: item.uri,
299
+ resourceType: deps.inferRecallResourceType(item.uri),
300
+ category: item.category,
301
+ score: item.score,
302
+ abstractPreview: deps.previewText(item.abstract || item.overview, deps.cfg.traceRecallPreviewChars),
303
+ injected: injectedUris.has(item.uri),
304
+ displayed: injectedUris.has(item.uri),
305
+ })),
306
+ stats: {
307
+ candidateCount: leafOnly.length,
308
+ selectedCount: memories.length,
309
+ injectedCount: injectedUris.size,
310
+ },
311
+ });
312
+ };
313
+ if (memories.length === 0) {
314
+ await recordMemoryRecallTrace(new Set());
315
+ return {
316
+ content: [{ type: "text", text: "No relevant OpenViking memories found." }],
317
+ details: { count: 0, total: result.total ?? 0, scoreThreshold },
318
+ };
319
+ }
320
+ const { lines: memoryLines } = await deps.buildMemoryLinesWithBudget(
321
+ memories,
322
+ (uri) => recallClient.read(uri, session.agentId),
323
+ {
324
+ recallPreferAbstract: false,
325
+ recallMaxInjectedChars: queryConfig.maxInjectedChars,
326
+ },
327
+ );
328
+ if (memoryLines.length === 0) {
329
+ await recordMemoryRecallTrace(new Set());
330
+ return {
331
+ content: [
332
+ {
333
+ type: "text",
334
+ text: `No complete OpenViking memories fit recallMaxInjectedChars=${queryConfig.maxInjectedChars}.`,
335
+ },
336
+ ],
337
+ details: {
338
+ count: 0,
339
+ memories,
340
+ total: result.total ?? memories.length,
341
+ scoreThreshold,
342
+ requestLimit,
343
+ recallMaxInjectedChars: queryConfig.maxInjectedChars,
344
+ },
345
+ };
346
+ }
347
+ await recordMemoryRecallTrace(new Set(memories.slice(0, memoryLines.length).map((item) => item.uri)));
348
+ return {
349
+ content: [
350
+ {
351
+ type: "text",
352
+ text: `Found ${memoryLines.length} memories:\n\n${memoryLines.join("\n")}`,
353
+ },
354
+ ],
355
+ details: {
356
+ count: memoryLines.length,
357
+ memories,
358
+ total: result.total ?? memories.length,
359
+ scoreThreshold,
360
+ requestLimit,
361
+ recallMaxInjectedChars: queryConfig.maxInjectedChars,
362
+ },
363
+ };
364
+ },
365
+ }),
366
+ { name: "memory_recall" },
367
+ );
368
+ }
@@ -0,0 +1,299 @@
1
+ import { Type } from "@sinclair/typebox";
2
+
3
+ import type { CommitSessionResult, FindResultItem } from "../client.js";
4
+ import { clampScore, postProcessMemories } from "../memory-ranking.js";
5
+ import { isMemoryUri } from "../routing/memory-uri.js";
6
+
7
+ export type OpenVikingMemoryToolContext = {
8
+ sessionKey?: string;
9
+ sessionId?: string;
10
+ agentId?: string;
11
+ senderId?: string;
12
+ requesterSenderId?: string;
13
+ };
14
+
15
+ export type OpenVikingMemorySession = {
16
+ sessionId?: string;
17
+ sessionKey?: string;
18
+ ovSessionId?: string;
19
+ agentId: string;
20
+ };
21
+
22
+ export type OpenVikingMemoryClient = {
23
+ addSessionMessage: (
24
+ sessionId: string,
25
+ role: string,
26
+ parts: Array<{ type: "text"; text: string }>,
27
+ agentId?: string,
28
+ createdAt?: string,
29
+ roleId?: string,
30
+ ) => Promise<void>;
31
+ commitSession: (
32
+ sessionId: string,
33
+ options: { wait: true; agentId: string; keepRecentCount: number },
34
+ ) => Promise<CommitSessionResult>;
35
+ deleteUri: (uri: string, agentId?: string) => Promise<void>;
36
+ find: (
37
+ query: string,
38
+ options: { targetUri: string; limit: number; scoreThreshold: number },
39
+ agentId?: string,
40
+ ) => Promise<{ memories?: FindResultItem[] }>;
41
+ };
42
+
43
+ export type OpenVikingMemoryToolsDeps = {
44
+ registerTool: (toolOrFactory: unknown, opts: { name: string }) => void;
45
+ getClient: () => Promise<OpenVikingMemoryClient>;
46
+ normalizeSessionId: (sessionId: string) => string;
47
+ createTempSessionId: () => string;
48
+ extractSenderId: (ctx?: OpenVikingMemoryToolContext) => string | undefined;
49
+ toRoleId: (senderId?: string) => string | undefined;
50
+ resolvePluginSessionRouting: (ctx?: OpenVikingMemoryToolContext) => OpenVikingMemorySession;
51
+ isBypassedSession: (ctx?: OpenVikingMemoryToolContext) => boolean;
52
+ makeBypassedToolResult: (toolName: string) => unknown;
53
+ defaultTargetUri: string;
54
+ defaultRecallScoreThreshold: number;
55
+ logFindRequests: boolean;
56
+ logger: {
57
+ info?: (message: string) => void;
58
+ warn: (message: string) => void;
59
+ };
60
+ };
61
+
62
+ function totalCommitMemories(r: CommitSessionResult): number {
63
+ const m = r.memories_extracted;
64
+ if (!m || typeof m !== "object") return 0;
65
+ return Object.values(m).reduce((sum, n) => sum + (n ?? 0), 0);
66
+ }
67
+
68
+ export function registerOpenVikingMemoryTools(deps: OpenVikingMemoryToolsDeps): void {
69
+ deps.registerTool(
70
+ (ctx: OpenVikingMemoryToolContext) => ({
71
+ name: "memory_store",
72
+ label: "Memory Store (OpenViking)",
73
+ description:
74
+ "Store text in OpenViking memory pipeline by writing to a session and running memory extraction.",
75
+ parameters: Type.Object({
76
+ text: Type.String({ description: "Information to store as memory source text" }),
77
+ role: Type.Optional(Type.String({ description: "Session role, default user" })),
78
+ sessionId: Type.Optional(Type.String({ description: "Existing OpenViking session ID" })),
79
+ }),
80
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
81
+ if (deps.isBypassedSession(ctx)) {
82
+ return deps.makeBypassedToolResult("memory_store");
83
+ }
84
+ const session = deps.resolvePluginSessionRouting(ctx);
85
+ const { text } = params as { text: string };
86
+ const role =
87
+ typeof (params as { role?: string }).role === "string"
88
+ ? (params as { role: string }).role
89
+ : "user";
90
+ const explicitSessionId =
91
+ typeof (params as { sessionId?: unknown }).sessionId === "string" &&
92
+ (params as { sessionId: string }).sessionId.trim()
93
+ ? deps.normalizeSessionId((params as { sessionId: string }).sessionId)
94
+ : undefined;
95
+
96
+ if (deps.logFindRequests) {
97
+ deps.logger.info?.(
98
+ `openviking: memory_store invoked (textLength=${text?.length ?? 0}, sessionId=${explicitSessionId ?? "auto"})`,
99
+ );
100
+ }
101
+
102
+ let sessionId = explicitSessionId;
103
+ let usedTempSession = false;
104
+ try {
105
+ const client = await deps.getClient();
106
+ if (!sessionId) {
107
+ sessionId = deps.createTempSessionId();
108
+ usedTempSession = true;
109
+ }
110
+ const roleId = role === "user" ? deps.toRoleId(deps.extractSenderId(ctx)) : undefined;
111
+ await client.addSessionMessage(
112
+ sessionId,
113
+ role,
114
+ [{ type: "text", text }],
115
+ session.agentId,
116
+ undefined,
117
+ roleId,
118
+ );
119
+ const commitResult = await client.commitSession(sessionId, {
120
+ wait: true,
121
+ agentId: session.agentId,
122
+ keepRecentCount: 0,
123
+ });
124
+ const memoriesCount = totalCommitMemories(commitResult);
125
+ if (commitResult.status === "failed") {
126
+ deps.logger.warn(
127
+ `openviking: memory_store commit failed (sessionId=${sessionId}): ${commitResult.error ?? "unknown"}`,
128
+ );
129
+ return {
130
+ content: [{ type: "text", text: `Memory extraction failed for session ${sessionId}: ${commitResult.error ?? "unknown"}` }],
131
+ details: {
132
+ action: "failed",
133
+ sessionId,
134
+ status: "failed",
135
+ error: commitResult.error,
136
+ usedTempSession,
137
+ },
138
+ };
139
+ }
140
+ if (commitResult.status === "timeout") {
141
+ deps.logger.warn(
142
+ `openviking: memory_store commit timed out (sessionId=${sessionId}), task_id=${commitResult.task_id ?? "none"}. Memories may still be extracting in background.`,
143
+ );
144
+ return {
145
+ content: [{ type: "text", text: `Memory extraction timed out for session ${sessionId}. It may still complete in the background (task_id=${commitResult.task_id ?? "none"}).` }],
146
+ details: {
147
+ action: "timeout",
148
+ sessionId,
149
+ status: "timeout",
150
+ taskId: commitResult.task_id,
151
+ usedTempSession,
152
+ },
153
+ };
154
+ }
155
+ if (memoriesCount === 0) {
156
+ deps.logger.warn(
157
+ `openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}). ` +
158
+ "Check OpenViking server logs for embedding/extract errors (e.g. 401 API key, or extraction pipeline).",
159
+ );
160
+ } else {
161
+ deps.logger.info?.(`openviking: memory_store committed, memories=${memoriesCount}`);
162
+ }
163
+ return {
164
+ content: [
165
+ {
166
+ type: "text",
167
+ text: `Stored in OpenViking session ${sessionId} and committed ${memoriesCount} memories.`,
168
+ },
169
+ ],
170
+ details: {
171
+ action: "stored",
172
+ sessionId,
173
+ memoriesCount,
174
+ status: commitResult.status,
175
+ archived: commitResult.archived ?? false,
176
+ usedTempSession,
177
+ },
178
+ };
179
+ } catch (err) {
180
+ deps.logger.warn(`openviking: memory_store failed: ${String(err)}`);
181
+ throw err;
182
+ }
183
+ },
184
+ }),
185
+ { name: "memory_store" },
186
+ );
187
+
188
+ deps.registerTool(
189
+ (ctx: OpenVikingMemoryToolContext) => ({
190
+ name: "memory_forget",
191
+ label: "Memory Forget (OpenViking)",
192
+ description:
193
+ "Forget memory by URI, or search then delete when a strong single match is found.",
194
+ parameters: Type.Object({
195
+ uri: Type.Optional(Type.String({ description: "Exact memory URI to delete" })),
196
+ query: Type.Optional(Type.String({ description: "Search query to find memory URI" })),
197
+ targetUri: Type.Optional(
198
+ Type.String({ description: "Search scope URI (default: plugin config)" }),
199
+ ),
200
+ limit: Type.Optional(Type.Number({ description: "Search limit (default: 5)" })),
201
+ scoreThreshold: Type.Optional(
202
+ Type.Number({ description: "Minimum score (0-1, default: plugin config)" }),
203
+ ),
204
+ }),
205
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
206
+ if (deps.isBypassedSession(ctx)) {
207
+ return deps.makeBypassedToolResult("memory_forget");
208
+ }
209
+ const session = deps.resolvePluginSessionRouting(ctx);
210
+ const client = await deps.getClient();
211
+ const uri = (params as { uri?: string }).uri;
212
+ if (uri) {
213
+ if (!isMemoryUri(uri)) {
214
+ return {
215
+ content: [{ type: "text", text: `Refusing to delete non-memory URI: ${uri}` }],
216
+ details: { action: "rejected", uri },
217
+ };
218
+ }
219
+ await client.deleteUri(uri, session.agentId);
220
+ return {
221
+ content: [{ type: "text", text: `Forgotten: ${uri}` }],
222
+ details: { action: "deleted", uri },
223
+ };
224
+ }
225
+
226
+ const query = (params as { query?: string }).query;
227
+ if (!query) {
228
+ return {
229
+ content: [{ type: "text", text: "Provide uri or query." }],
230
+ details: { error: "missing_param" },
231
+ };
232
+ }
233
+
234
+ const limit =
235
+ typeof (params as { limit?: number }).limit === "number"
236
+ ? Math.max(1, Math.floor((params as { limit: number }).limit))
237
+ : 5;
238
+ const scoreThreshold =
239
+ typeof (params as { scoreThreshold?: number }).scoreThreshold === "number"
240
+ ? Math.max(0, Math.min(1, (params as { scoreThreshold: number }).scoreThreshold))
241
+ : deps.defaultRecallScoreThreshold;
242
+ const targetUri =
243
+ typeof (params as { targetUri?: string }).targetUri === "string"
244
+ ? (params as { targetUri: string }).targetUri
245
+ : deps.defaultTargetUri;
246
+ const requestLimit = Math.max(limit * 4, 20);
247
+
248
+ const result = await client.find(
249
+ query,
250
+ {
251
+ targetUri,
252
+ limit: requestLimit,
253
+ scoreThreshold: 0,
254
+ },
255
+ session.agentId,
256
+ );
257
+ const candidates = postProcessMemories(result.memories ?? [], {
258
+ limit: requestLimit,
259
+ scoreThreshold,
260
+ leafOnly: true,
261
+ }).filter((item) => isMemoryUri(item.uri));
262
+ if (candidates.length === 0) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: "No matching leaf memory candidates found. Try a more specific query.",
268
+ },
269
+ ],
270
+ details: { action: "none", scoreThreshold },
271
+ };
272
+ }
273
+ const top = candidates[0];
274
+ if (candidates.length === 1 && clampScore(top.score) >= 0.85) {
275
+ await client.deleteUri(top.uri, session.agentId);
276
+ return {
277
+ content: [{ type: "text", text: `Forgotten: ${top.uri}` }],
278
+ details: { action: "deleted", uri: top.uri, score: top.score ?? 0 },
279
+ };
280
+ }
281
+
282
+ const list = candidates
283
+ .map((item) => `- ${item.uri} (${(clampScore(item.score) * 100).toFixed(0)}%)`)
284
+ .join("\n");
285
+
286
+ return {
287
+ content: [
288
+ {
289
+ type: "text",
290
+ text: `Found ${candidates.length} candidates. Specify uri:\n${list}`,
291
+ },
292
+ ],
293
+ details: { action: "candidates", candidates, scoreThreshold, requestLimit },
294
+ };
295
+ },
296
+ }),
297
+ { name: "memory_forget" },
298
+ );
299
+ }