@openviking/openclaw-plugin 2026.5.10-dev.3 → 2026.5.26

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/dist/client.js CHANGED
@@ -245,6 +245,36 @@ export class OpenVikingClient {
245
245
  async read(uri, agentId) {
246
246
  return this.request(`/api/v1/content/read?uri=${encodeURIComponent(uri)}`, {}, agentId);
247
247
  }
248
+ async readToolResult(sessionId, toolResultId, options, agentId) {
249
+ const params = new URLSearchParams();
250
+ if (options?.offset !== undefined)
251
+ params.set("offset", String(options.offset));
252
+ if (options?.limit !== undefined)
253
+ params.set("limit", String(options.limit));
254
+ if (options?.includeMetadata !== undefined) {
255
+ params.set("include_metadata", String(options.includeMetadata));
256
+ }
257
+ const query = params.toString();
258
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}${query ? `?${query}` : ""}`, {}, agentId);
259
+ }
260
+ async searchToolResult(sessionId, toolResultId, queryText, options, agentId) {
261
+ const params = new URLSearchParams({ q: queryText });
262
+ if (options?.limit !== undefined)
263
+ params.set("limit", String(options.limit));
264
+ if (options?.contextChars !== undefined) {
265
+ params.set("context_chars", String(options.contextChars));
266
+ }
267
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}/search?${params.toString()}`, {}, agentId);
268
+ }
269
+ async listToolResults(sessionId, options, agentId) {
270
+ const params = new URLSearchParams();
271
+ if (options?.toolName)
272
+ params.set("tool_name", options.toolName);
273
+ if (options?.limit !== undefined)
274
+ params.set("limit", String(options.limit));
275
+ const query = params.toString();
276
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results${query ? `?${query}` : ""}`, {}, agentId);
277
+ }
248
278
  async uploadTempFile(filePath, agentId) {
249
279
  const fileBytes = await readFile(filePath);
250
280
  const form = new FormData();
@@ -29,9 +29,6 @@ function allocateContextBudget(totalBudget, instructionTokens = 0) {
29
29
  const sessionContext = Math.max(usableBudget - archiveMemory, 0);
30
30
  return { archiveMemory, sessionContext, reserved };
31
31
  }
32
- function estimateTokens(messages) {
33
- return Math.max(1, messages.length * 80);
34
- }
35
32
  function roughEstimate(messages) {
36
33
  return Math.ceil(JSON.stringify(messages).length / 4);
37
34
  }
@@ -265,6 +262,10 @@ export function convertToAgentMessages(msg) {
265
262
  const toolName = typeof p.tool_name === "string" ? p.tool_name : undefined;
266
263
  const status = typeof p.tool_status === "string" ? p.tool_status : "unknown";
267
264
  const output = typeof p.tool_output === "string" ? p.tool_output : "";
265
+ const ref = typeof p.tool_output_ref === "string" ? p.tool_output_ref : "";
266
+ const originalChars = typeof p.tool_output_original_chars === "number"
267
+ ? p.tool_output_original_chars
268
+ : undefined;
268
269
  if (toolId) {
269
270
  // Structured path: emit canonical toolCall + toolResult pair (works for any role)
270
271
  toolCallBlocks.push({
@@ -273,9 +274,15 @@ export function convertToAgentMessages(msg) {
273
274
  name: toolName ?? "unknown",
274
275
  arguments: p.tool_input ?? {},
275
276
  });
276
- const resultText = (status === "completed" || status === "error")
277
+ let resultText = (status === "completed" || status === "error")
277
278
  ? (output || "(no output)")
278
279
  : "(interrupted — tool did not complete)";
280
+ if (ref && !resultText.includes(ref)) {
281
+ const suffix = originalChars !== undefined
282
+ ? `\n[tool-result-ref] ${ref} original_chars=${originalChars}`
283
+ : `\n[tool-result-ref] ${ref}`;
284
+ resultText += suffix;
285
+ }
279
286
  const resultPayload = {
280
287
  role: "toolResult",
281
288
  toolCallId: toolId,
@@ -330,6 +337,32 @@ export function convertToAgentMessages(msg) {
330
337
  }
331
338
  return result;
332
339
  }
340
+ function isToolOnlyMessage(msg) {
341
+ return msg.role === "user" && msg.parts.length > 0 && msg.parts.every((part) => part.type === "tool");
342
+ }
343
+ function coalesceConsecutiveToolMessages(messages) {
344
+ const result = [];
345
+ let pendingTools;
346
+ const flush = () => {
347
+ if (pendingTools) {
348
+ result.push(pendingTools);
349
+ pendingTools = undefined;
350
+ }
351
+ };
352
+ for (const msg of messages) {
353
+ if (isToolOnlyMessage(msg)) {
354
+ if (!pendingTools) {
355
+ pendingTools = { role: "user", parts: [] };
356
+ }
357
+ pendingTools.parts.push(...msg.parts);
358
+ continue;
359
+ }
360
+ flush();
361
+ result.push(msg);
362
+ }
363
+ flush();
364
+ return result;
365
+ }
333
366
  function normalizeAssistantContent(messages) {
334
367
  for (let i = 0; i < messages.length; i++) {
335
368
  const msg = messages[i];
@@ -965,7 +998,8 @@ export function createMemoryOpenVikingContextEngine(params) {
965
998
  afterTurnParams.prePromptMessageCount >= 0
966
999
  ? afterTurnParams.prePromptMessageCount
967
1000
  : 0;
968
- const { messages: extractedMessages, newCount } = extractNewTurnMessages(messages, start);
1001
+ const { messages: extractedMessagesRaw, newCount } = extractNewTurnMessages(messages, start);
1002
+ const extractedMessages = coalesceConsecutiveToolMessages(extractedMessagesRaw);
969
1003
  if (extractedMessages.length === 0) {
970
1004
  diag("afterTurn_skip", OVSessionId, {
971
1005
  reason: "no_new_turn_messages",
@@ -1055,9 +1089,7 @@ export function createMemoryOpenVikingContextEngine(params) {
1055
1089
  if (commitResult.task_id && cfg.logFindRequests) {
1056
1090
  logger.info(`openviking: Phase2 memory extraction runs asynchronously on the server (task_id=${commitResult.task_id}). ` +
1057
1091
  "memories_extracted appears only after that task completes — not in this immediate response.");
1058
- if (cfg.logFindRequests) {
1059
- void pollPhase2ExtractionOutcome(getClient, commitResult.task_id, agentId, logger, OVSessionId);
1060
- }
1092
+ void pollPhase2ExtractionOutcome(getClient, commitResult.task_id, agentId, logger, OVSessionId);
1061
1093
  }
1062
1094
  }
1063
1095
  catch (err) {
package/dist/index.js CHANGED
@@ -4,10 +4,39 @@ import { registerSetupCli } from "./commands/setup.js";
4
4
  import { OpenVikingClient, isMemoryUri } from "./client.js";
5
5
  import { formatMessageFaithful, toRoleId } from "./context-engine.js";
6
6
  import { compileSessionPatterns, shouldBypassSession, } from "./text-utils.js";
7
- import { clampScore, postProcessMemories, formatMemoryLines, } from "./memory-ranking.js";
7
+ import { clampScore, postProcessMemories, pickMemoriesForInjection, } from "./memory-ranking.js";
8
8
  import { createMemoryOpenVikingContextEngine, openClawSessionToOvStorageId, openClawSessionRefToOvStorageId, } from "./context-engine.js";
9
9
  import { buildMemoryLines, buildMemoryLinesWithBudget, estimateTokenCount, prepareRecallQuery, } from "./auto-recall.js";
10
10
  export { buildMemoryLines, buildMemoryLinesWithBudget, estimateTokenCount, prepareRecallQuery, };
11
+ function parseToolResultRef(value) {
12
+ const raw = typeof value === "string" ? value.trim() : "";
13
+ if (!raw) {
14
+ return null;
15
+ }
16
+ const match = raw.match(/^viking:\/\/session\/([^/]+)\/tool-results\/([^/?#]+)(?:[?#].*)?$/);
17
+ if (!match) {
18
+ return null;
19
+ }
20
+ const sessionId = decodeURIComponent(match[1]);
21
+ const toolResultId = decodeURIComponent(match[2]);
22
+ if (!sessionId || !toolResultId) {
23
+ return null;
24
+ }
25
+ return {
26
+ sessionId,
27
+ toolResultId,
28
+ ref: `viking://session/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}`,
29
+ };
30
+ }
31
+ function getOptionalInteger(value, fallback) {
32
+ if (typeof value !== "number" || !Number.isFinite(value)) {
33
+ return fallback;
34
+ }
35
+ return Math.floor(value);
36
+ }
37
+ function getPositiveInteger(value, fallback) {
38
+ return Math.max(1, getOptionalInteger(value, fallback));
39
+ }
11
40
  const DEFAULT_OPENCLAW_AGENT_ID = "main";
12
41
  /**
13
42
  * OpenViking `UserIdentifier` allows only [a-zA-Z0-9_-] for agent_id
@@ -176,13 +205,13 @@ export function parseAddSkillCommandArgs(args) {
176
205
  timeout: getNumberFlag(parsed.flags, "timeout"),
177
206
  };
178
207
  }
179
- export function parseMemorySearchCommandArgs(args) {
208
+ export function parseOVSearchCommandArgs(args) {
180
209
  const parsed = parseFlagArgs(args);
181
- // `/memory-search` only accepts a single query string, so positional segments are
210
+ // `/ov-search` only accepts a single query string, so positional segments are
182
211
  // always re-joined to preserve unquoted multi-word searches.
183
212
  const query = parsed.positionals.join(" ").trim();
184
213
  if (!query) {
185
- throw new Error('Usage: /memory-search "<query>" [--uri URI] [--limit N]');
214
+ throw new Error('Usage: /ov-search "<query>" [--uri URI] [--limit N]');
186
215
  }
187
216
  return {
188
217
  query,
@@ -453,7 +482,7 @@ const contextEnginePlugin = {
453
482
  total: memories.length + resources.length + skills.length,
454
483
  };
455
484
  };
456
- const formatMemorySearchRows = (result) => {
485
+ const formatOVSearchRows = (result) => {
457
486
  const truncateSummary = (value, maxChars = 220) => {
458
487
  const collapsed = value.replace(/\s+/g, " ").trim();
459
488
  if (collapsed.length <= maxChars) {
@@ -490,7 +519,7 @@ const contextEnginePlugin = {
490
519
  }),
491
520
  ];
492
521
  };
493
- const formatMemorySearchText = (query, uri, result) => {
522
+ const formatOVSearchText = (query, uri, result) => {
494
523
  if ((result.total ?? 0) <= 0) {
495
524
  const scope = uri ? ` under ${uri}` : "";
496
525
  return `No OpenViking resource or skill results found for "${query}"${scope}.`;
@@ -499,11 +528,11 @@ const contextEnginePlugin = {
499
528
  const lines = [
500
529
  `Found ${result.total ?? 0} OpenViking results for "${query}"${scope}`,
501
530
  "",
502
- ...formatMemorySearchRows(result),
531
+ ...formatOVSearchRows(result),
503
532
  ].filter((line, index, all) => line || (all[index - 1] && all[index + 1]));
504
533
  return lines.join("\n");
505
534
  };
506
- const memorySearchOpenViking = async (input, agentId) => {
535
+ const searchOpenViking = async (input, agentId) => {
507
536
  const query = input.query.trim();
508
537
  if (!query) {
509
538
  throw new Error("query is required");
@@ -543,7 +572,7 @@ const contextEnginePlugin = {
543
572
  result = mergeFindResults(successful);
544
573
  }
545
574
  return {
546
- content: [{ type: "text", text: formatMemorySearchText(query, input.uri, result) }],
575
+ content: [{ type: "text", text: formatOVSearchText(query, input.uri, result) }],
547
576
  details: {
548
577
  action: "searched",
549
578
  query,
@@ -610,8 +639,8 @@ const contextEnginePlugin = {
610
639
  },
611
640
  }), { name: "add_skill" });
612
641
  api.registerTool((ctx) => ({
613
- name: "memory_search",
614
- label: "Memory Search (OpenViking)",
642
+ name: "ov_search",
643
+ label: "Search (OpenViking)",
615
644
  description: "Search OpenViking resources and skills. Use after importing, or when the user asks to search OpenViking resources or skills.",
616
645
  parameters: Type.Object({
617
646
  query: Type.String({ description: "Search query" }),
@@ -620,16 +649,16 @@ const contextEnginePlugin = {
620
649
  }),
621
650
  async execute(_toolCallId, params) {
622
651
  if (isBypassedSession(ctx)) {
623
- return makeBypassedToolResult("memory_search");
652
+ return makeBypassedToolResult("ov_search");
624
653
  }
625
654
  const session = resolvePluginSessionRouting(ctx);
626
- return memorySearchOpenViking({
655
+ return searchOpenViking({
627
656
  query: String(params.query ?? ""),
628
657
  uri: typeof params.uri === "string" ? params.uri : undefined,
629
658
  limit: typeof params.limit === "number" ? params.limit : undefined,
630
659
  }, session.agentId);
631
660
  },
632
- }), { name: "memory_search" });
661
+ }), { name: "ov_search" });
633
662
  api.registerCommand?.({
634
663
  name: "add-resource",
635
664
  description: "Add a resource into OpenViking.",
@@ -671,22 +700,22 @@ const contextEnginePlugin = {
671
700
  },
672
701
  });
673
702
  api.registerCommand?.({
674
- name: "memory-search",
703
+ name: "ov-search",
675
704
  description: "Search OpenViking resources and skills.",
676
705
  acceptsArgs: true,
677
706
  handler: async (ctx) => {
678
707
  try {
679
708
  if (isBypassedSession(ctx)) {
680
- const bypassed = makeBypassedToolResult("memory_search");
709
+ const bypassed = makeBypassedToolResult("ov_search");
681
710
  return { text: bypassed.content[0].text, details: bypassed.details };
682
711
  }
683
712
  const session = resolvePluginSessionRouting(ctx);
684
- const input = parseMemorySearchCommandArgs(ctx.args ?? "");
685
- const result = await memorySearchOpenViking(input, session.agentId);
713
+ const input = parseOVSearchCommandArgs(ctx.args ?? "");
714
+ const result = await searchOpenViking(input, session.agentId);
686
715
  return { text: result.content[0].text, details: result.details };
687
716
  }
688
717
  catch (err) {
689
- return { text: `OpenViking memory search failed: ${err instanceof Error ? err.message : String(err)}` };
718
+ return { text: `OpenViking search failed: ${err instanceof Error ? err.message : String(err)}` };
690
719
  }
691
720
  },
692
721
  });
@@ -764,29 +793,54 @@ const contextEnginePlugin = {
764
793
  total: leafOnly.length,
765
794
  };
766
795
  }
767
- const memories = postProcessMemories(result.memories ?? [], {
768
- limit,
796
+ const leafOnly = (result.memories ?? []).filter((m) => !m.level || m.level === 2);
797
+ const processed = postProcessMemories(leafOnly, {
798
+ limit: requestLimit,
769
799
  scoreThreshold,
770
800
  });
801
+ const memories = pickMemoriesForInjection(processed, limit, query);
771
802
  if (memories.length === 0) {
772
803
  return {
773
804
  content: [{ type: "text", text: "No relevant OpenViking memories found." }],
774
805
  details: { count: 0, total: result.total ?? 0, scoreThreshold },
775
806
  };
776
807
  }
808
+ const { lines: memoryLines } = await buildMemoryLinesWithBudget(memories, (uri) => recallClient.read(uri, session.agentId), {
809
+ recallPreferAbstract: false,
810
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
811
+ });
812
+ if (memoryLines.length === 0) {
813
+ return {
814
+ content: [
815
+ {
816
+ type: "text",
817
+ text: `No complete OpenViking memories fit recallMaxInjectedChars=${cfg.recallMaxInjectedChars}.`,
818
+ },
819
+ ],
820
+ details: {
821
+ count: 0,
822
+ memories,
823
+ total: result.total ?? memories.length,
824
+ scoreThreshold,
825
+ requestLimit,
826
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
827
+ },
828
+ };
829
+ }
777
830
  return {
778
831
  content: [
779
832
  {
780
833
  type: "text",
781
- text: `Found ${memories.length} memories:\n\n${formatMemoryLines(memories)}`,
834
+ text: `Found ${memoryLines.length} memories:\n\n${memoryLines.join("\n")}`,
782
835
  },
783
836
  ],
784
837
  details: {
785
- count: memories.length,
838
+ count: memoryLines.length,
786
839
  memories,
787
840
  total: result.total ?? memories.length,
788
841
  scoreThreshold,
789
842
  requestLimit,
843
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
790
844
  },
791
845
  };
792
846
  },
@@ -1133,6 +1187,242 @@ const contextEnginePlugin = {
1133
1187
  }
1134
1188
  },
1135
1189
  }), { name: "ov_archive_expand" });
1190
+ api.registerTool((ctx) => ({
1191
+ name: "openviking_tool_result_read",
1192
+ label: "Tool Result Read (OpenViking)",
1193
+ description: "Restore the full original content of a tool result that was externalized by OpenViking. " +
1194
+ "Use when a previous tool result was externalized and only a preview is visible — " +
1195
+ "the preview contains a [tool-result-ref] or viking://session/.../tool-results/... URI. " +
1196
+ "\"Read\" tool returns the same truncated preview; this tool returns the complete content. " +
1197
+ "To read all content: pass offset=0 and a limit large enough to cover the whole result " +
1198
+ "(e.g. limit=100000). Use offset/limit for paging only when you need a specific section.",
1199
+ parameters: Type.Object({
1200
+ tool_output_ref: Type.String({
1201
+ description: "Exact OV URI from the preview, e.g. viking://session/<session_id>/tool-results/<tool_result_id>",
1202
+ }),
1203
+ offset: Type.Optional(Type.Number({ description: "Unicode character offset. Default: 0" })),
1204
+ limit: Type.Optional(Type.Number({ description: "Maximum Unicode characters to read. Default: 20000" })),
1205
+ }),
1206
+ async execute(_toolCallId, params) {
1207
+ if (isBypassedSession(ctx)) {
1208
+ return makeBypassedToolResult("openviking_tool_result_read");
1209
+ }
1210
+ const session = resolvePluginSessionRouting(ctx);
1211
+ if (!session.ovSessionId) {
1212
+ return {
1213
+ content: [{ type: "text", text: "Error: no active session." }],
1214
+ details: { error: "no_session" },
1215
+ };
1216
+ }
1217
+ const parsed = parseToolResultRef(params.tool_output_ref ?? params.ref ?? params.uri);
1218
+ if (!parsed) {
1219
+ return {
1220
+ content: [{ type: "text", text: "Error: tool_output_ref must be a viking://session/.../tool-results/... URI." }],
1221
+ details: { error: "invalid_tool_output_ref" },
1222
+ };
1223
+ }
1224
+ if (parsed.sessionId !== session.ovSessionId) {
1225
+ return {
1226
+ content: [{ type: "text", text: "Error: refusing to read a tool result from another session." }],
1227
+ details: {
1228
+ error: "session_mismatch",
1229
+ requestedSessionId: parsed.sessionId,
1230
+ currentSessionId: session.ovSessionId,
1231
+ },
1232
+ };
1233
+ }
1234
+ const offset = Math.max(0, getOptionalInteger(params.offset, 0));
1235
+ const limit = getOptionalInteger(params.limit, 20_000);
1236
+ if (limit < -1) {
1237
+ return {
1238
+ content: [{ type: "text", text: "Error: limit must be -1 or greater than or equal to 0." }],
1239
+ details: { error: "invalid_limit", limit },
1240
+ };
1241
+ }
1242
+ try {
1243
+ const client = await getClient();
1244
+ const result = await client.readToolResult(session.ovSessionId, parsed.toolResultId, { offset, limit, includeMetadata: true }, session.agentId);
1245
+ const returnedChars = result.content.length;
1246
+ const nextOffset = result.offset + returnedChars;
1247
+ const text = result.content || "(empty tool result chunk)";
1248
+ return {
1249
+ content: [{ type: "text", text }],
1250
+ details: {
1251
+ action: "read",
1252
+ tool_output_ref: parsed.ref,
1253
+ tool_result_id: result.tool_result_id,
1254
+ offset: result.offset,
1255
+ limit: result.limit,
1256
+ returned_chars: returnedChars,
1257
+ total_chars: result.total_chars,
1258
+ has_more: result.has_more,
1259
+ next_offset: result.has_more ? nextOffset : null,
1260
+ metadata: result.metadata ?? null,
1261
+ },
1262
+ };
1263
+ }
1264
+ catch (err) {
1265
+ const msg = err instanceof Error ? err.message : String(err);
1266
+ api.logger.warn?.(`openviking: openviking_tool_result_read failed: ${msg}`);
1267
+ return {
1268
+ content: [{ type: "text", text: `Failed to read tool result: ${msg}` }],
1269
+ details: { error: msg, tool_output_ref: parsed.ref },
1270
+ };
1271
+ }
1272
+ },
1273
+ }), { name: "openviking_tool_result_read" });
1274
+ api.registerTool((ctx) => ({
1275
+ name: "openviking_tool_result_search",
1276
+ label: "Tool Result Search (OpenViking)",
1277
+ description: "Search inside an externalized tool result for a keyword. " +
1278
+ "Use when you need to find specific content in a large externalized result, " +
1279
+ "before reading it with openviking_tool_result_read. " +
1280
+ "Returns matching snippets with their character offsets.",
1281
+ parameters: Type.Object({
1282
+ tool_output_ref: Type.String({
1283
+ description: "Exact OV URI from the preview, e.g. viking://session/<session_id>/tool-results/<tool_result_id>",
1284
+ }),
1285
+ query: Type.String({ description: "Keyword or exact text to search for" }),
1286
+ limit: Type.Optional(Type.Number({ description: "Maximum matches. Default: 20" })),
1287
+ context_chars: Type.Optional(Type.Number({ description: "Characters around each match. Default: 300" })),
1288
+ }),
1289
+ async execute(_toolCallId, params) {
1290
+ if (isBypassedSession(ctx)) {
1291
+ return makeBypassedToolResult("openviking_tool_result_search");
1292
+ }
1293
+ const session = resolvePluginSessionRouting(ctx);
1294
+ if (!session.ovSessionId) {
1295
+ return {
1296
+ content: [{ type: "text", text: "Error: no active session." }],
1297
+ details: { error: "no_session" },
1298
+ };
1299
+ }
1300
+ const parsed = parseToolResultRef(params.tool_output_ref ?? params.ref ?? params.uri);
1301
+ if (!parsed) {
1302
+ return {
1303
+ content: [{ type: "text", text: "Error: tool_output_ref must be a viking://session/.../tool-results/... URI." }],
1304
+ details: { error: "invalid_tool_output_ref" },
1305
+ };
1306
+ }
1307
+ if (parsed.sessionId !== session.ovSessionId) {
1308
+ return {
1309
+ content: [{ type: "text", text: "Error: refusing to search a tool result from another session." }],
1310
+ details: {
1311
+ error: "session_mismatch",
1312
+ requestedSessionId: parsed.sessionId,
1313
+ currentSessionId: session.ovSessionId,
1314
+ },
1315
+ };
1316
+ }
1317
+ const query = String(params.query ?? "").trim();
1318
+ if (!query) {
1319
+ return {
1320
+ content: [{ type: "text", text: "Error: query is required." }],
1321
+ details: { error: "missing_param", param: "query" },
1322
+ };
1323
+ }
1324
+ const limit = getPositiveInteger(params.limit, 20);
1325
+ const contextChars = Math.max(0, getOptionalInteger(params.context_chars ?? params.contextChars, 300));
1326
+ try {
1327
+ const client = await getClient();
1328
+ const result = await client.searchToolResult(session.ovSessionId, parsed.toolResultId, query, { limit, contextChars }, session.agentId);
1329
+ const matches = result.matches ?? [];
1330
+ const text = matches.length
1331
+ ? [
1332
+ `Found ${matches.length} match(es) for "${query}" in ${parsed.ref}:`,
1333
+ "",
1334
+ ...matches.map((match, index) => `## Match ${index + 1} (offset ${match.offset})\n${match.snippet}`),
1335
+ ].join("\n")
1336
+ : `No matches found for "${query}" in ${parsed.ref}.`;
1337
+ return {
1338
+ content: [{ type: "text", text }],
1339
+ details: {
1340
+ action: "searched",
1341
+ tool_output_ref: parsed.ref,
1342
+ tool_result_id: result.tool_result_id,
1343
+ query,
1344
+ match_count: matches.length,
1345
+ matches,
1346
+ },
1347
+ };
1348
+ }
1349
+ catch (err) {
1350
+ const msg = err instanceof Error ? err.message : String(err);
1351
+ api.logger.warn?.(`openviking: openviking_tool_result_search failed: ${msg}`);
1352
+ return {
1353
+ content: [{ type: "text", text: `Failed to search tool result: ${msg}` }],
1354
+ details: { error: msg, tool_output_ref: parsed.ref, query },
1355
+ };
1356
+ }
1357
+ },
1358
+ }), { name: "openviking_tool_result_search" });
1359
+ api.registerTool((ctx) => ({
1360
+ name: "openviking_tool_result_list",
1361
+ label: "Tool Result List (OpenViking)",
1362
+ description: "List externalized tool results for the current session. " +
1363
+ "Use to discover available refs before calling openviking_tool_result_read. " +
1364
+ "Optionally filter by tool_name to narrow down results.",
1365
+ parameters: Type.Object({
1366
+ tool_name: Type.Optional(Type.String({ description: "Optional exact tool name filter" })),
1367
+ limit: Type.Optional(Type.Number({ description: "Maximum results. Default: 50" })),
1368
+ }),
1369
+ async execute(_toolCallId, params) {
1370
+ if (isBypassedSession(ctx)) {
1371
+ return makeBypassedToolResult("openviking_tool_result_list");
1372
+ }
1373
+ const session = resolvePluginSessionRouting(ctx);
1374
+ if (!session.ovSessionId) {
1375
+ return {
1376
+ content: [{ type: "text", text: "Error: no active session." }],
1377
+ details: { error: "no_session" },
1378
+ };
1379
+ }
1380
+ const toolName = typeof params.tool_name === "string" && params.tool_name.trim()
1381
+ ? params.tool_name.trim()
1382
+ : typeof params.toolName === "string" && params.toolName.trim()
1383
+ ? params.toolName.trim()
1384
+ : undefined;
1385
+ const limit = getPositiveInteger(params.limit, 50);
1386
+ try {
1387
+ const client = await getClient();
1388
+ const result = await client.listToolResults(session.ovSessionId, { toolName, limit }, session.agentId);
1389
+ const items = result.tool_results ?? [];
1390
+ const text = items.length
1391
+ ? [
1392
+ `Found ${items.length} externalized tool result(s) in current session:`,
1393
+ "",
1394
+ ...items.map((item, index) => {
1395
+ const ref = typeof item.storage_uri === "string" ? item.storage_uri : "(missing ref)";
1396
+ const name = typeof item.tool_name === "string" ? item.tool_name : "tool";
1397
+ const chars = typeof item.original_chars === "number" ? item.original_chars : "unknown";
1398
+ const created = typeof item.created_at === "string" ? ` created_at=${item.created_at}` : "";
1399
+ return `${index + 1}. ${name} original_chars=${chars}${created}\nref: ${ref}`;
1400
+ }),
1401
+ ].join("\n")
1402
+ : toolName
1403
+ ? `No externalized tool results found for tool "${toolName}" in current session.`
1404
+ : "No externalized tool results found in current session.";
1405
+ return {
1406
+ content: [{ type: "text", text }],
1407
+ details: {
1408
+ action: "listed",
1409
+ session_id: session.ovSessionId,
1410
+ tool_name: toolName ?? null,
1411
+ count: items.length,
1412
+ tool_results: items,
1413
+ },
1414
+ };
1415
+ }
1416
+ catch (err) {
1417
+ const msg = err instanceof Error ? err.message : String(err);
1418
+ api.logger.warn?.(`openviking: openviking_tool_result_list failed: ${msg}`);
1419
+ return {
1420
+ content: [{ type: "text", text: `Failed to list tool results: ${msg}` }],
1421
+ details: { error: msg, session_id: session.ovSessionId, tool_name: toolName ?? null },
1422
+ };
1423
+ }
1424
+ },
1425
+ }), { name: "openviking_tool_result_list" });
1136
1426
  let contextEngineRef = null;
1137
1427
  const sessionAgentResolver = createSessionAgentResolver(cfg.agent_prefix);
1138
1428
  const rememberSessionAgentId = (ctx) => {