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

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.
@@ -21,7 +21,6 @@ const NON_CONTENT_TEXT_RE = /^[\p{P}\p{S}\s]+$/u;
21
21
  const SUBAGENT_CONTEXT_RE = /^\s*\[Subagent Context\]/i;
22
22
  const MEMORY_INTENT_RE = /记住|记下|remember|save|store|偏好|preference|规则|rule|事实|fact/i;
23
23
  const QUESTION_CUE_RE = /[??]|\b(?:what|when|where|who|why|how|which|can|could|would|did|does|is|are)\b|^(?:请问|能否|可否|怎么|如何|什么时候|谁|什么|哪|是否)/i;
24
- export const CAPTURE_LIMIT = 3;
25
24
  function resolveCaptureMinLength(text) {
26
25
  return CJK_CHAR_REGEX.test(text) ? 4 : 10;
27
26
  }
@@ -232,21 +231,6 @@ export function extractTextsFromUserMessages(messages) {
232
231
  }
233
232
  return texts;
234
233
  }
235
- function formatToolUseBlock(b) {
236
- const name = typeof b.name === "string" ? b.name : "unknown";
237
- let inputStr = "";
238
- if (b.input !== undefined && b.input !== null) {
239
- try {
240
- inputStr = typeof b.input === "string" ? b.input : JSON.stringify(b.input);
241
- }
242
- catch {
243
- inputStr = String(b.input);
244
- }
245
- }
246
- return inputStr
247
- ? `[toolUse: ${name}]\n${inputStr}`
248
- : `[toolUse: ${name}]`;
249
- }
250
234
  function formatToolResultContent(content) {
251
235
  if (typeof content === "string")
252
236
  return content.trim();
@@ -270,40 +254,6 @@ function formatToolResultContent(content) {
270
254
  }
271
255
  return "";
272
256
  }
273
- /**
274
- * Extract text from a single message without a `[role]:` prefix.
275
- * Used by afterTurn to send messages with their actual role.
276
- */
277
- export function extractSingleMessageText(msg) {
278
- if (!msg || typeof msg !== "object")
279
- return "";
280
- const m = msg;
281
- const role = m.role;
282
- if (!role || role === "system")
283
- return "";
284
- if (role === "toolResult") {
285
- const toolName = typeof m.toolName === "string" ? m.toolName : "tool";
286
- const resultText = formatToolResultContent(m.content);
287
- return resultText ? `[${toolName} result]: ${resultText}` : "";
288
- }
289
- const content = m.content;
290
- if (typeof content === "string")
291
- return content.trim();
292
- if (Array.isArray(content)) {
293
- const parts = [];
294
- for (const block of content) {
295
- const b = block;
296
- if (b?.type === "text" && typeof b.text === "string") {
297
- parts.push(b.text.trim());
298
- }
299
- else if (b?.type === "toolUse") {
300
- parts.push(formatToolUseBlock(b));
301
- }
302
- }
303
- return parts.join("\n");
304
- }
305
- return "";
306
- }
307
257
  /**
308
258
  * 提取消息中的一个 part 的文本内容,并清理时间戳等噪音
309
259
  */
@@ -394,7 +344,6 @@ export function extractNewTurnMessages(messages, startIndex) {
394
344
  continue;
395
345
  }
396
346
  // user/assistant -> type: "text"
397
- // 统一 role 为 user
398
347
  const content = msg.content;
399
348
  const text = extractPartText(content);
400
349
  if (text) {
@@ -412,6 +361,37 @@ export function extractNewTurnMessages(messages, startIndex) {
412
361
  });
413
362
  }
414
363
  }
364
+ else {
365
+ /// 如果原始消息有 toolCall,提取所有工具名并生成占位符
366
+ if (role === "assistant" && Array.isArray(content)) {
367
+ const toolNames = [];
368
+ // 收集所有 toolCall 的 tool_name
369
+ for (const block of content) {
370
+ const b = block;
371
+ const blockType = b?.type;
372
+ if (blockType === "toolCall" ||
373
+ blockType === "toolUse" ||
374
+ blockType === "tool_use" ||
375
+ blockType === "tool_call") {
376
+ const name = b?.name || b?.toolName;
377
+ if (name && typeof name === "string" && name.trim()) {
378
+ toolNames.push(name.trim());
379
+ }
380
+ }
381
+ }
382
+ // 只有找到 toolCall 时才添加占位符
383
+ if (toolNames.length > 0) {
384
+ const toolNamesStr = toolNames.join(", ");
385
+ result.push({
386
+ role: "assistant",
387
+ parts: [{
388
+ type: "text",
389
+ text: `[tool: ${toolNamesStr}]`,
390
+ }],
391
+ });
392
+ }
393
+ }
394
+ }
415
395
  }
416
396
  return { messages: result, newCount: count };
417
397
  }
package/index.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  import {
23
23
  clampScore,
24
24
  postProcessMemories,
25
- formatMemoryLines,
25
+ pickMemoriesForInjection,
26
26
  } from "./memory-ranking.js";
27
27
  import { withTimeout } from "./process-manager.js";
28
28
  import {
@@ -150,6 +150,44 @@ type MemorySearchInput = {
150
150
  limit?: number;
151
151
  };
152
152
 
153
+ type ToolResultRef = {
154
+ sessionId: string;
155
+ toolResultId: string;
156
+ ref: string;
157
+ };
158
+
159
+ function parseToolResultRef(value: unknown): ToolResultRef | null {
160
+ const raw = typeof value === "string" ? value.trim() : "";
161
+ if (!raw) {
162
+ return null;
163
+ }
164
+ const match = raw.match(/^viking:\/\/session\/([^/]+)\/tool-results\/([^/?#]+)(?:[?#].*)?$/);
165
+ if (!match) {
166
+ return null;
167
+ }
168
+ const sessionId = decodeURIComponent(match[1]!);
169
+ const toolResultId = decodeURIComponent(match[2]!);
170
+ if (!sessionId || !toolResultId) {
171
+ return null;
172
+ }
173
+ return {
174
+ sessionId,
175
+ toolResultId,
176
+ ref: `viking://session/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}`,
177
+ };
178
+ }
179
+
180
+ function getOptionalInteger(value: unknown, fallback: number): number {
181
+ if (typeof value !== "number" || !Number.isFinite(value)) {
182
+ return fallback;
183
+ }
184
+ return Math.floor(value);
185
+ }
186
+
187
+ function getPositiveInteger(value: unknown, fallback: number): number {
188
+ return Math.max(1, getOptionalInteger(value, fallback));
189
+ }
190
+
153
191
  type OpenClawPluginApi = {
154
192
  pluginConfig?: unknown;
155
193
  logger: PluginLogger;
@@ -1090,29 +1128,58 @@ const contextEnginePlugin = {
1090
1128
  };
1091
1129
  }
1092
1130
 
1093
- const memories = postProcessMemories(result.memories ?? [], {
1094
- limit,
1131
+ const leafOnly = (result.memories ?? []).filter((m) => !m.level || m.level === 2);
1132
+ const processed = postProcessMemories(leafOnly, {
1133
+ limit: requestLimit,
1095
1134
  scoreThreshold,
1096
1135
  });
1136
+ const memories = pickMemoriesForInjection(processed, limit, query);
1097
1137
  if (memories.length === 0) {
1098
1138
  return {
1099
1139
  content: [{ type: "text", text: "No relevant OpenViking memories found." }],
1100
1140
  details: { count: 0, total: result.total ?? 0, scoreThreshold },
1101
1141
  };
1102
1142
  }
1143
+ const { lines: memoryLines } = await buildMemoryLinesWithBudget(
1144
+ memories,
1145
+ (uri) => recallClient.read(uri, session.agentId),
1146
+ {
1147
+ recallPreferAbstract: false,
1148
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
1149
+ },
1150
+ );
1151
+ if (memoryLines.length === 0) {
1152
+ return {
1153
+ content: [
1154
+ {
1155
+ type: "text",
1156
+ text: `No complete OpenViking memories fit recallMaxInjectedChars=${cfg.recallMaxInjectedChars}.`,
1157
+ },
1158
+ ],
1159
+ details: {
1160
+ count: 0,
1161
+ memories,
1162
+ total: result.total ?? memories.length,
1163
+ scoreThreshold,
1164
+ requestLimit,
1165
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
1166
+ },
1167
+ };
1168
+ }
1103
1169
  return {
1104
1170
  content: [
1105
1171
  {
1106
1172
  type: "text",
1107
- text: `Found ${memories.length} memories:\n\n${formatMemoryLines(memories)}`,
1173
+ text: `Found ${memoryLines.length} memories:\n\n${memoryLines.join("\n")}`,
1108
1174
  },
1109
1175
  ],
1110
1176
  details: {
1111
- count: memories.length,
1177
+ count: memoryLines.length,
1112
1178
  memories,
1113
1179
  total: result.total ?? memories.length,
1114
1180
  scoreThreshold,
1115
1181
  requestLimit,
1182
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
1116
1183
  },
1117
1184
  };
1118
1185
  },
@@ -1531,6 +1598,285 @@ const contextEnginePlugin = {
1531
1598
  },
1532
1599
  }), { name: "ov_archive_expand" });
1533
1600
 
1601
+ api.registerTool(
1602
+ (ctx: ToolContext) => ({
1603
+ name: "openviking_tool_result_read",
1604
+ label: "Tool Result Read (OpenViking)",
1605
+ description:
1606
+ "Restore the full original content of a tool result that was externalized by OpenViking. " +
1607
+ "Use when a previous tool result was externalized and only a preview is visible — " +
1608
+ "the preview contains a [tool-result-ref] or viking://session/.../tool-results/... URI. " +
1609
+ "\"Read\" tool returns the same truncated preview; this tool returns the complete content. " +
1610
+ "To read all content: pass offset=0 and a limit large enough to cover the whole result " +
1611
+ "(e.g. limit=100000). Use offset/limit for paging only when you need a specific section.",
1612
+ parameters: Type.Object({
1613
+ tool_output_ref: Type.String({
1614
+ description:
1615
+ "Exact OV URI from the preview, e.g. viking://session/<session_id>/tool-results/<tool_result_id>",
1616
+ }),
1617
+ offset: Type.Optional(Type.Number({ description: "Unicode character offset. Default: 0" })),
1618
+ limit: Type.Optional(Type.Number({ description: "Maximum Unicode characters to read. Default: 20000" })),
1619
+ }),
1620
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
1621
+ if (isBypassedSession(ctx)) {
1622
+ return makeBypassedToolResult("openviking_tool_result_read");
1623
+ }
1624
+ const session = resolvePluginSessionRouting(ctx);
1625
+ if (!session.ovSessionId) {
1626
+ return {
1627
+ content: [{ type: "text", text: "Error: no active session." }],
1628
+ details: { error: "no_session" },
1629
+ };
1630
+ }
1631
+
1632
+ const parsed = parseToolResultRef(params.tool_output_ref ?? params.ref ?? params.uri);
1633
+ if (!parsed) {
1634
+ return {
1635
+ content: [{ type: "text", text: "Error: tool_output_ref must be a viking://session/.../tool-results/... URI." }],
1636
+ details: { error: "invalid_tool_output_ref" },
1637
+ };
1638
+ }
1639
+ if (parsed.sessionId !== session.ovSessionId) {
1640
+ return {
1641
+ content: [{ type: "text", text: "Error: refusing to read a tool result from another session." }],
1642
+ details: {
1643
+ error: "session_mismatch",
1644
+ requestedSessionId: parsed.sessionId,
1645
+ currentSessionId: session.ovSessionId,
1646
+ },
1647
+ };
1648
+ }
1649
+
1650
+ const offset = Math.max(0, getOptionalInteger(params.offset, 0));
1651
+ const limit = getOptionalInteger(params.limit, 20_000);
1652
+ if (limit < -1) {
1653
+ return {
1654
+ content: [{ type: "text", text: "Error: limit must be -1 or greater than or equal to 0." }],
1655
+ details: { error: "invalid_limit", limit },
1656
+ };
1657
+ }
1658
+
1659
+ try {
1660
+ const client = await getClient();
1661
+ const result = await client.readToolResult(
1662
+ session.ovSessionId,
1663
+ parsed.toolResultId,
1664
+ { offset, limit, includeMetadata: true },
1665
+ session.agentId,
1666
+ );
1667
+ const returnedChars = result.content.length;
1668
+ const nextOffset = result.offset + returnedChars;
1669
+ const text = result.content || "(empty tool result chunk)";
1670
+ return {
1671
+ content: [{ type: "text", text }],
1672
+ details: {
1673
+ action: "read",
1674
+ tool_output_ref: parsed.ref,
1675
+ tool_result_id: result.tool_result_id,
1676
+ offset: result.offset,
1677
+ limit: result.limit,
1678
+ returned_chars: returnedChars,
1679
+ total_chars: result.total_chars,
1680
+ has_more: result.has_more,
1681
+ next_offset: result.has_more ? nextOffset : null,
1682
+ metadata: result.metadata ?? null,
1683
+ },
1684
+ };
1685
+ } catch (err) {
1686
+ const msg = err instanceof Error ? err.message : String(err);
1687
+ api.logger.warn?.(`openviking: openviking_tool_result_read failed: ${msg}`);
1688
+ return {
1689
+ content: [{ type: "text", text: `Failed to read tool result: ${msg}` }],
1690
+ details: { error: msg, tool_output_ref: parsed.ref },
1691
+ };
1692
+ }
1693
+ },
1694
+ }),
1695
+ { name: "openviking_tool_result_read" },
1696
+ );
1697
+
1698
+ api.registerTool(
1699
+ (ctx: ToolContext) => ({
1700
+ name: "openviking_tool_result_search",
1701
+ label: "Tool Result Search (OpenViking)",
1702
+ description:
1703
+ "Search inside an externalized tool result for a keyword. " +
1704
+ "Use when you need to find specific content in a large externalized result, " +
1705
+ "before reading it with openviking_tool_result_read. " +
1706
+ "Returns matching snippets with their character offsets.",
1707
+ parameters: Type.Object({
1708
+ tool_output_ref: Type.String({
1709
+ description:
1710
+ "Exact OV URI from the preview, e.g. viking://session/<session_id>/tool-results/<tool_result_id>",
1711
+ }),
1712
+ query: Type.String({ description: "Keyword or exact text to search for" }),
1713
+ limit: Type.Optional(Type.Number({ description: "Maximum matches. Default: 20" })),
1714
+ context_chars: Type.Optional(Type.Number({ description: "Characters around each match. Default: 300" })),
1715
+ }),
1716
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
1717
+ if (isBypassedSession(ctx)) {
1718
+ return makeBypassedToolResult("openviking_tool_result_search");
1719
+ }
1720
+ const session = resolvePluginSessionRouting(ctx);
1721
+ if (!session.ovSessionId) {
1722
+ return {
1723
+ content: [{ type: "text", text: "Error: no active session." }],
1724
+ details: { error: "no_session" },
1725
+ };
1726
+ }
1727
+
1728
+ const parsed = parseToolResultRef(params.tool_output_ref ?? params.ref ?? params.uri);
1729
+ if (!parsed) {
1730
+ return {
1731
+ content: [{ type: "text", text: "Error: tool_output_ref must be a viking://session/.../tool-results/... URI." }],
1732
+ details: { error: "invalid_tool_output_ref" },
1733
+ };
1734
+ }
1735
+ if (parsed.sessionId !== session.ovSessionId) {
1736
+ return {
1737
+ content: [{ type: "text", text: "Error: refusing to search a tool result from another session." }],
1738
+ details: {
1739
+ error: "session_mismatch",
1740
+ requestedSessionId: parsed.sessionId,
1741
+ currentSessionId: session.ovSessionId,
1742
+ },
1743
+ };
1744
+ }
1745
+
1746
+ const query = String(params.query ?? "").trim();
1747
+ if (!query) {
1748
+ return {
1749
+ content: [{ type: "text", text: "Error: query is required." }],
1750
+ details: { error: "missing_param", param: "query" },
1751
+ };
1752
+ }
1753
+ const limit = getPositiveInteger(params.limit, 20);
1754
+ const contextChars = Math.max(
1755
+ 0,
1756
+ getOptionalInteger(params.context_chars ?? params.contextChars, 300),
1757
+ );
1758
+
1759
+ try {
1760
+ const client = await getClient();
1761
+ const result = await client.searchToolResult(
1762
+ session.ovSessionId,
1763
+ parsed.toolResultId,
1764
+ query,
1765
+ { limit, contextChars },
1766
+ session.agentId,
1767
+ );
1768
+ const matches = result.matches ?? [];
1769
+ const text = matches.length
1770
+ ? [
1771
+ `Found ${matches.length} match(es) for "${query}" in ${parsed.ref}:`,
1772
+ "",
1773
+ ...matches.map((match, index) =>
1774
+ `## Match ${index + 1} (offset ${match.offset})\n${match.snippet}`,
1775
+ ),
1776
+ ].join("\n")
1777
+ : `No matches found for "${query}" in ${parsed.ref}.`;
1778
+ return {
1779
+ content: [{ type: "text", text }],
1780
+ details: {
1781
+ action: "searched",
1782
+ tool_output_ref: parsed.ref,
1783
+ tool_result_id: result.tool_result_id,
1784
+ query,
1785
+ match_count: matches.length,
1786
+ matches,
1787
+ },
1788
+ };
1789
+ } catch (err) {
1790
+ const msg = err instanceof Error ? err.message : String(err);
1791
+ api.logger.warn?.(`openviking: openviking_tool_result_search failed: ${msg}`);
1792
+ return {
1793
+ content: [{ type: "text", text: `Failed to search tool result: ${msg}` }],
1794
+ details: { error: msg, tool_output_ref: parsed.ref, query },
1795
+ };
1796
+ }
1797
+ },
1798
+ }),
1799
+ { name: "openviking_tool_result_search" },
1800
+ );
1801
+
1802
+ api.registerTool(
1803
+ (ctx: ToolContext) => ({
1804
+ name: "openviking_tool_result_list",
1805
+ label: "Tool Result List (OpenViking)",
1806
+ description:
1807
+ "List externalized tool results for the current session. " +
1808
+ "Use to discover available refs before calling openviking_tool_result_read. " +
1809
+ "Optionally filter by tool_name to narrow down results.",
1810
+ parameters: Type.Object({
1811
+ tool_name: Type.Optional(Type.String({ description: "Optional exact tool name filter" })),
1812
+ limit: Type.Optional(Type.Number({ description: "Maximum results. Default: 50" })),
1813
+ }),
1814
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
1815
+ if (isBypassedSession(ctx)) {
1816
+ return makeBypassedToolResult("openviking_tool_result_list");
1817
+ }
1818
+ const session = resolvePluginSessionRouting(ctx);
1819
+ if (!session.ovSessionId) {
1820
+ return {
1821
+ content: [{ type: "text", text: "Error: no active session." }],
1822
+ details: { error: "no_session" },
1823
+ };
1824
+ }
1825
+
1826
+ const toolName =
1827
+ typeof params.tool_name === "string" && params.tool_name.trim()
1828
+ ? params.tool_name.trim()
1829
+ : typeof params.toolName === "string" && params.toolName.trim()
1830
+ ? params.toolName.trim()
1831
+ : undefined;
1832
+ const limit = getPositiveInteger(params.limit, 50);
1833
+
1834
+ try {
1835
+ const client = await getClient();
1836
+ const result = await client.listToolResults(
1837
+ session.ovSessionId,
1838
+ { toolName, limit },
1839
+ session.agentId,
1840
+ );
1841
+ const items = result.tool_results ?? [];
1842
+ const text = items.length
1843
+ ? [
1844
+ `Found ${items.length} externalized tool result(s) in current session:`,
1845
+ "",
1846
+ ...items.map((item, index) => {
1847
+ const ref = typeof item.storage_uri === "string" ? item.storage_uri : "(missing ref)";
1848
+ const name = typeof item.tool_name === "string" ? item.tool_name : "tool";
1849
+ const chars = typeof item.original_chars === "number" ? item.original_chars : "unknown";
1850
+ const created = typeof item.created_at === "string" ? ` created_at=${item.created_at}` : "";
1851
+ return `${index + 1}. ${name} original_chars=${chars}${created}\nref: ${ref}`;
1852
+ }),
1853
+ ].join("\n")
1854
+ : toolName
1855
+ ? `No externalized tool results found for tool "${toolName}" in current session.`
1856
+ : "No externalized tool results found in current session.";
1857
+ return {
1858
+ content: [{ type: "text", text }],
1859
+ details: {
1860
+ action: "listed",
1861
+ session_id: session.ovSessionId,
1862
+ tool_name: toolName ?? null,
1863
+ count: items.length,
1864
+ tool_results: items,
1865
+ },
1866
+ };
1867
+ } catch (err) {
1868
+ const msg = err instanceof Error ? err.message : String(err);
1869
+ api.logger.warn?.(`openviking: openviking_tool_result_list failed: ${msg}`);
1870
+ return {
1871
+ content: [{ type: "text", text: `Failed to list tool results: ${msg}` }],
1872
+ details: { error: msg, session_id: session.ovSessionId, tool_name: toolName ?? null },
1873
+ };
1874
+ }
1875
+ },
1876
+ }),
1877
+ { name: "openviking_tool_result_list" },
1878
+ );
1879
+
1534
1880
  let contextEngineRef: ContextEngineWithCommit | null = null;
1535
1881
  const sessionAgentResolver = createSessionAgentResolver(cfg.agent_prefix);
1536
1882
  const rememberSessionAgentId = (ctx: SessionAgentLookup) => {
@@ -19,7 +19,10 @@
19
19
  "memory_store",
20
20
  "memory_forget",
21
21
  "ov_archive_search",
22
- "ov_archive_expand"
22
+ "ov_archive_expand",
23
+ "openviking_tool_result_read",
24
+ "openviking_tool_result_search",
25
+ "openviking_tool_result_list"
23
26
  ]
24
27
  },
25
28
  "commandAliases": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.5.10-dev.3",
3
+ "version": "2026.5.21",
4
4
  "displayName": "OpenViking",
5
5
  "description": "OpenClaw context-engine plugin — OpenViking AI memory, long-term memory, knowledge base, semantic search, RAG, context engine. 长期记忆 · 知识库 · 语义检索 · 上下文引擎",
6
6
  "type": "module",
@@ -30,6 +30,7 @@
30
30
  "INSTALL.md",
31
31
  "INSTALL-ZH.md",
32
32
  "INSTALL-AGENT.md",
33
+ "images/",
33
34
  "skills/"
34
35
  ],
35
36
  "scripts": {
@@ -48,7 +49,7 @@
48
49
  },
49
50
  "repository": {
50
51
  "type": "git",
51
- "url": "git+https://github.com/volcengine/OpenViking.git"
52
+ "url": "https://github.com/volcengine/OpenViking"
52
53
  },
53
54
  "homepage": "https://github.com/volcengine/OpenViking/tree/main/examples/openclaw-plugin",
54
55
  "license": "MIT",