@openviking/openclaw-plugin 2026.7.15 → 2026.8.6-1

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/INSTALL-ZH.md CHANGED
@@ -203,6 +203,45 @@ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --forc
203
203
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
204
204
  ```
205
205
 
206
+ #### 无法执行 CLI 时直接配置文件
207
+
208
+ 如果容器内无法执行 `openclaw` CLI,可以把以下字段合并到 OpenClaw 实际读取的配置文件。设置了 `OPENCLAW_CONFIG_PATH` 时使用该路径;否则通常是 `$OPENCLAW_STATE_DIR/openclaw.json`(默认 `~/.openclaw/openclaw.json`)。
209
+
210
+ ```json
211
+ {
212
+ "plugins": {
213
+ "entries": {
214
+ "openviking": {
215
+ "enabled": true,
216
+ "config": {
217
+ "mode": "remote",
218
+ "baseUrl": "http://openviking:1933",
219
+ "apiKey": "<API_KEY>",
220
+ "peer_role": "assistant"
221
+ }
222
+ }
223
+ },
224
+ "slots": {
225
+ "contextEngine": "openviking"
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ - 插件必须已经安装;编辑前请备份配置,并把以上字段合并到现有 `plugins` 配置。
232
+ - 如果配置中已有 `plugins.allow`,把 `openviking` 追加进去;如果没有,不要仅为本插件新建 allowlist。
233
+ - `contextEngine` 是独占 slot;若已有其他 context engine,只有确认替换后再修改该字段。使用 root API key 时,还需在 `config` 中设置 `accountId` 和 `userId`。
234
+ - 容器连接其他服务时,`baseUrl` 应使用容器内可访问的服务地址,而不是 `127.0.0.1`。
235
+ - 推荐使用 `SecretRef` 对象作为 `apiKey`(而不是明文字符串),避免密钥直接落盘写入 `openclaw.json`。支持的形式与 OpenClaw 核心中 LLM/TTS/MCP 等 provider 配置使用的标准 `SecretRef` 一致:
236
+
237
+ | 类型 | 示例 | 说明 |
238
+ | --- | --- | --- |
239
+ | `env` | `{ "source": "env", "id": "OPENVIKING_API_KEY" }` | 启动时读取同名环境变量。 |
240
+ | `file` | `{ "source": "file", "id": "/etc/secrets/openviking.key" }` | 以 UTF-8 读取并去除首尾空白;`~` 可展开,适配 Kubernetes `secretKeyRef` 卷挂载、0600 权限文件。 |
241
+ | `exec` | 打包版插件不支持(应用市场安装扫描会拦截子进程执行) | 改用命令包一层环境变量:`OPENVIKING_API_KEY=$(op read op://vault/openviking/credential)`,然后配 `env`。 |
242
+
243
+ 仍可使用纯字符串形式(含 `${ENV_VAR}` 插值)作为向后兼容路径;此时请限制文件权限或通过受控 Secret 卷提供,并在修改后重启 Gateway、容器或 Pod。
244
+
206
245
  ### 3. 重启 OpenClaw Gateway
207
246
 
208
247
  ```bash
package/INSTALL.md CHANGED
@@ -147,6 +147,45 @@ If you want assistant messages to carry a prefixed `peer_id` and data-plane reca
147
147
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
148
148
  ```
149
149
 
150
+ #### Configure The File Directly When The CLI Is Unavailable
151
+
152
+ If the `openclaw` CLI cannot run inside the container, merge the following fields into the config file that OpenClaw actually reads. Use `OPENCLAW_CONFIG_PATH` when it is set; otherwise the file is usually `$OPENCLAW_STATE_DIR/openclaw.json` (default: `~/.openclaw/openclaw.json`).
153
+
154
+ ```json
155
+ {
156
+ "plugins": {
157
+ "entries": {
158
+ "openviking": {
159
+ "enabled": true,
160
+ "config": {
161
+ "mode": "remote",
162
+ "baseUrl": "http://openviking:1933",
163
+ "apiKey": "<API_KEY>",
164
+ "peer_role": "assistant"
165
+ }
166
+ }
167
+ },
168
+ "slots": {
169
+ "contextEngine": "openviking"
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ - The plugin must already be installed. Back up the file and merge these fields into the existing `plugins` config.
176
+ - If `plugins.allow` already exists, append `openviking`; otherwise, do not create an allowlist only for this plugin.
177
+ - `contextEngine` is an exclusive slot. If another context engine is configured, change it only after confirming the replacement. Root API keys also require `accountId` and `userId` in `config`.
178
+ - When connecting to another service from a container, use a `baseUrl` that is reachable from the container rather than `127.0.0.1`.
179
+ - Prefer a `SecretRef` for `apiKey` instead of a plaintext string so the key is never stored inside `openclaw.json`. Supported shapes match OpenClaw's standard `SecretRef` used by LLM/TTS/MCP provider configs:
180
+
181
+ | Shape | Example | Notes |
182
+ | --- | --- | --- |
183
+ | `env` | `{ "source": "env", "id": "OPENVIKING_API_KEY" }` | Reads the named env var at plugin load time. |
184
+ | `file` | `{ "source": "file", "id": "/etc/secrets/openviking.key" }` | Reads UTF-8, trims whitespace. `~` is expanded; ideal for Kubernetes `secretKeyRef` volumes and 0600-managed files. |
185
+ | `exec` | not supported in the packaged plugin (marketplace install scanners block subprocess execution) | Wrap the CLI instead: `OPENVIKING_API_KEY=$(op read op://vault/openviking/credential)` and use `env`. |
186
+
187
+ A plain `string` (including `${ENV_VAR}` interpolation) is still accepted, but only as a backward-compatibility path; in that case, restrict file permissions or provide the file through a managed Secret volume, then restart the Gateway, container, or Pod.
188
+
150
189
  ### 3. Restart OpenClaw Gateway
151
190
 
152
191
  ```bash
package/README.md CHANGED
@@ -46,6 +46,8 @@ Once installed, the plugin provides these agent tools:
46
46
  | `ov_read` | Read the full original content of one exact OpenViking URI |
47
47
  | `ov_multi_read` | Read the full original content of multiple OpenViking URIs |
48
48
  | `ov_list` | List OpenViking directories after search to inspect sibling chunks and overview files |
49
+ | `search_experience` | Search reusable execution experiences for the current OpenViking user |
50
+ | `read_experience` | Read one exact Experience returned by `search_experience` |
49
51
  | `openviking_tool_result_read` | Restore the full original content of an externalized tool result |
50
52
  | `openviking_tool_result_search` | Search inside an externalized tool result by keyword |
51
53
  | `openviking_tool_result_list` | List externalized tool results in the current session |
@@ -248,6 +250,8 @@ Beyond automatic behavior, the plugin exposes these tools directly:
248
250
  - `ov_read`: read one exact `viking://` URI returned by `ov_search` or `ov_list`
249
251
  - `ov_multi_read`: read multiple exact `viking://` URIs, useful for an overview plus sibling chunks
250
252
  - `ov_list`: list a hit's parent directory after `ov_search` to recover sibling chunks, `.overview.md`, and related split-document context
253
+ - `search_experience`: search reusable execution experiences for the current OpenViking user
254
+ - `read_experience`: read one exact Experience returned by `search_experience`
251
255
 
252
256
  They serve different roles:
253
257
 
@@ -261,6 +265,7 @@ They serve different roles:
261
265
  - `ov_read` turns a ranked hit into original evidence before answering precise documentation, codebase, configuration, or procedural questions
262
266
  - `ov_multi_read` reads overview and sibling chunks together when a split document needs more context than a single hit
263
267
  - `ov_list` complements `ov_search` when a ranked hit is only one chunk of a larger procedure or document
268
+ - `search_experience` and `read_experience` provide the exact tool-call contract used by the bundled Experience Memory skill and usage reporting
264
269
 
265
270
  `ov_archive_expand` is especially important because `assemble()` normally returns archive summaries and indexes, not the full raw transcript.
266
271
 
package/config.ts CHANGED
@@ -1,13 +1,38 @@
1
1
  import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
2
3
 
3
4
  import { getEnv } from "./runtime-utils.js";
4
5
 
6
+ /**
7
+ * Subset of OpenClaw's standard `SecretRef` shape supported by the OpenViking
8
+ * plugin. The full runtime SDK also recognises `source: "exec"` with a
9
+ * provider plugin, but the plugin self-hosts its own env/file resolution so
10
+ * users on pluginSdkVersion < 2026.6.x (no host-level secret-resolver) still
11
+ * get vault-free secret management.
12
+ *
13
+ * source: "env" -> `process.env[id]` (same as the bare `${ENV}` string
14
+ * interpolation, but declarative and visible to UI hints).
15
+ * source: "file" -> contents of `id` path (``~`` expansion supported),
16
+ * leading/trailing whitespace trimmed — convenient for
17
+ * Kubernetes `secretKeyRef` mount volumes and
18
+ * file-permission-only deployments.
19
+ * source: "exec" -> NOT supported in the packaged plugin. Marketplace
20
+ * install scanners (OpenClaw/ArkClaw) block any plugin
21
+ * whose shipped code can spawn subprocesses, so the exec
22
+ * resolver was removed from this build; configuring it
23
+ * fails with a clear error pointing at env/file instead.
24
+ */
25
+ export type OpenVikingSecretRef =
26
+ | { source: "env"; id: string }
27
+ | { source: "file"; id: string }
28
+ | { source: "exec"; provider: string; id: string };
29
+
5
30
  export type MemoryOpenVikingConfig = {
6
31
  mode?: "remote";
7
32
  baseUrl?: string;
8
33
  peer_role?: "none" | "assistant" | "person";
9
34
  peer_prefix?: string;
10
- apiKey?: string;
35
+ apiKey?: string | OpenVikingSecretRef;
11
36
  /** Optional HTTP headers merged into every OpenViking request. */
12
37
  headers?: Record<string, string>;
13
38
  /** Advanced option. Only needed when explicitly sending tenant identity headers. With a user key the server derives identity from the key. */
@@ -102,8 +127,10 @@ export type MemoryOpenVikingConfig = {
102
127
 
103
128
  /** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */
104
129
  export type ParsedMemoryOpenVikingConfig = Required<
105
- Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes">
130
+ Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey">
106
131
  > & {
132
+ /** parse() resolves SecretRef values, so the runtime shape is always a plain string. */
133
+ apiKey: string;
107
134
  agentExperience: Required<NonNullable<MemoryOpenVikingConfig["agentExperience"]>>;
108
135
  recallTargetTypes: Array<"resource" | "user" | "agent">;
109
136
  };
@@ -143,6 +170,8 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [
143
170
  "ov_read",
144
171
  "ov_multi_read",
145
172
  "ov_list",
173
+ "search_experience",
174
+ "read_experience",
146
175
  "memory_recall",
147
176
  "ov_recall_trace",
148
177
  "memory_store",
@@ -163,6 +192,7 @@ export const OPENVIKING_TOOL_GROUPS: Record<string, readonly OpenVikingToolName[
163
192
  default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES,
164
193
  memory: ["memory_recall", "memory_store", "memory_forget"],
165
194
  resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"],
195
+ experience: ["search_experience", "read_experience"],
166
196
  import: ["add_resource", "add_skill"],
167
197
  recall_trace: ["ov_recall_trace"],
168
198
  archive: ["ov_archive_search", "ov_archive_expand"],
@@ -188,6 +218,81 @@ function resolvePeerPrefix(configured: unknown): string {
188
218
  return DEFAULT_PEER_PREFIX;
189
219
  }
190
220
 
221
+ /**
222
+ * Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain
223
+ * strings pass through untouched so callers can compose this with the legacy
224
+ * `resolveEnvVars` pipeline without branching.
225
+ *
226
+ * Intentional behaviours:
227
+ * * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An
228
+ * unset / empty env var is an error — the user asked for a named secret
229
+ * so silent empty-string fallback would mask misconfigurations.
230
+ * * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace
231
+ * stripped (de-facto standard for files written with `echo xxx > key`).
232
+ * Missing / unreadable files propagate the original node error with an
233
+ * error prefix that names the OpenViking field, so the user knows which
234
+ * secretRef failed to load.
235
+ * * `exec`: rejected with a "not supported" error. The subprocess-based
236
+ * resolver was removed because marketplace install scanners block plugins
237
+ * whose shipped code can spawn processes; wrap the secret-manager CLI
238
+ * output into an env var or file instead (`OPENVIKING_API_KEY=$(op ...)`).
239
+ */
240
+ function resolveSecret(
241
+ value: string | OpenVikingSecretRef | undefined | null,
242
+ label: string,
243
+ ): string {
244
+ if (value === undefined || value === null) return "";
245
+ if (typeof value === "string") return value;
246
+ if (!value || typeof value !== "object") {
247
+ throw new Error(
248
+ `OpenViking ${label} must be a plain string or a SecretRef object ` +
249
+ `({source:"env"|"file", id})`,
250
+ );
251
+ }
252
+ const obj = value as Record<string, unknown>;
253
+ if (typeof obj.id !== "string" || !obj.id) {
254
+ throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`);
255
+ }
256
+ const id = obj.id;
257
+ switch (obj.source) {
258
+ case "env": {
259
+ const envValue = getEnv(id);
260
+ if (!envValue) {
261
+ throw new Error(
262
+ `OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`,
263
+ );
264
+ }
265
+ return envValue;
266
+ }
267
+ case "file": {
268
+ const resolvedPath = expandHomeDir(id);
269
+ try {
270
+ const contents = readFileSync(resolvedPath, "utf8");
271
+ return contents.trim();
272
+ } catch (err) {
273
+ const msg = err instanceof Error ? err.message : String(err);
274
+ throw new Error(
275
+ `OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`,
276
+ );
277
+ }
278
+ }
279
+ case "exec": {
280
+ throw new Error(
281
+ `OpenViking ${label} SecretRef exec source is not supported in the packaged plugin ` +
282
+ `(marketplace install scanners block subprocess execution). Export the secret to an ` +
283
+ `environment variable ({source:"env"}) or a file ({source:"file"}) instead, e.g. ` +
284
+ `OPENVIKING_API_KEY=$(op read ${id}).`,
285
+ );
286
+ }
287
+ default:
288
+ throw new Error(
289
+ `OpenViking ${label} SecretRef has unknown source "${String(
290
+ (obj as Record<string, unknown>).source,
291
+ )}". Supported: "env" | "file".`,
292
+ );
293
+ }
294
+ }
295
+
191
296
  function resolvePeerRole(configured: unknown) {
192
297
  if (typeof configured === "string") {
193
298
  const role = configured.trim().toLowerCase();
@@ -465,7 +570,14 @@ export const memoryOpenVikingConfigSchema = {
465
570
  const peerPrefix = resolvePeerPrefix(cfg.peer_prefix);
466
571
  const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl();
467
572
  const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, "");
468
- const rawApiKey = typeof cfg.apiKey === "string" ? cfg.apiKey : getEnv("OPENVIKING_API_KEY");
573
+ // Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback.
574
+ // A user writing a SecretRef has explicitly opted out of the bare env
575
+ // interpolation path, so the fallback kicks in only when nothing is
576
+ // configured at all (matches existing behaviour).
577
+ const rawApiKey: string | OpenVikingSecretRef | undefined =
578
+ cfg.apiKey !== undefined && cfg.apiKey !== null
579
+ ? (cfg.apiKey as string | OpenVikingSecretRef)
580
+ : getEnv("OPENVIKING_API_KEY") || undefined;
469
581
  const captureMode = cfg.captureMode;
470
582
  if (
471
583
  typeof captureMode !== "undefined" &&
@@ -508,7 +620,7 @@ export const memoryOpenVikingConfigSchema = {
508
620
  baseUrl: resolvedBaseUrl,
509
621
  peer_role: peerRole,
510
622
  peer_prefix: peerPrefix,
511
- apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
623
+ apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "",
512
624
  headers: toStringRecord(cfg.headers, "openviking config headers"),
513
625
  accountId,
514
626
  userId,
@@ -678,7 +790,10 @@ export const memoryOpenVikingConfigSchema = {
678
790
  label: "OpenViking API Key",
679
791
  sensitive: true,
680
792
  placeholder: "${OPENVIKING_API_KEY}",
681
- help: "Optional API key for OpenViking server",
793
+ help:
794
+ "Optional API key for OpenViking server. Accepts a plain string, " +
795
+ "${ENV_VAR} interpolation, or a SecretRef object ({source: env/file, id}). " +
796
+ "Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.",
682
797
  },
683
798
  headers: {
684
799
  label: "Headers",
@@ -836,7 +951,7 @@ export const memoryOpenVikingConfigSchema = {
836
951
  enabledTools: {
837
952
  label: "Enabled Tools",
838
953
  placeholder: "default",
839
- help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
954
+ help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
840
955
  advanced: true,
841
956
  },
842
957
  disabledTools: {
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
2
3
  import { getEnv } from "./runtime-utils.js";
3
4
  const DEFAULT_BASE_URL = "http://127.0.0.1:1933";
4
5
  const DEFAULT_TARGET_URI = "viking://user/memories";
@@ -34,6 +35,8 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [
34
35
  "ov_read",
35
36
  "ov_multi_read",
36
37
  "ov_list",
38
+ "search_experience",
39
+ "read_experience",
37
40
  "memory_recall",
38
41
  "ov_recall_trace",
39
42
  "memory_store",
@@ -53,6 +56,7 @@ export const OPENVIKING_TOOL_GROUPS = {
53
56
  default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES,
54
57
  memory: ["memory_recall", "memory_store", "memory_forget"],
55
58
  resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"],
59
+ experience: ["search_experience", "read_experience"],
56
60
  import: ["add_resource", "add_skill"],
57
61
  recall_trace: ["ov_recall_trace"],
58
62
  archive: ["ov_archive_search", "ov_archive_expand"],
@@ -76,6 +80,68 @@ function resolvePeerPrefix(configured) {
76
80
  }
77
81
  return DEFAULT_PEER_PREFIX;
78
82
  }
83
+ /**
84
+ * Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain
85
+ * strings pass through untouched so callers can compose this with the legacy
86
+ * `resolveEnvVars` pipeline without branching.
87
+ *
88
+ * Intentional behaviours:
89
+ * * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An
90
+ * unset / empty env var is an error — the user asked for a named secret
91
+ * so silent empty-string fallback would mask misconfigurations.
92
+ * * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace
93
+ * stripped (de-facto standard for files written with `echo xxx > key`).
94
+ * Missing / unreadable files propagate the original node error with an
95
+ * error prefix that names the OpenViking field, so the user knows which
96
+ * secretRef failed to load.
97
+ * * `exec`: rejected with a "not supported" error. The subprocess-based
98
+ * resolver was removed because marketplace install scanners block plugins
99
+ * whose shipped code can spawn processes; wrap the secret-manager CLI
100
+ * output into an env var or file instead (`OPENVIKING_API_KEY=$(op ...)`).
101
+ */
102
+ function resolveSecret(value, label) {
103
+ if (value === undefined || value === null)
104
+ return "";
105
+ if (typeof value === "string")
106
+ return value;
107
+ if (!value || typeof value !== "object") {
108
+ throw new Error(`OpenViking ${label} must be a plain string or a SecretRef object ` +
109
+ `({source:"env"|"file", id})`);
110
+ }
111
+ const obj = value;
112
+ if (typeof obj.id !== "string" || !obj.id) {
113
+ throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`);
114
+ }
115
+ const id = obj.id;
116
+ switch (obj.source) {
117
+ case "env": {
118
+ const envValue = getEnv(id);
119
+ if (!envValue) {
120
+ throw new Error(`OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`);
121
+ }
122
+ return envValue;
123
+ }
124
+ case "file": {
125
+ const resolvedPath = expandHomeDir(id);
126
+ try {
127
+ const contents = readFileSync(resolvedPath, "utf8");
128
+ return contents.trim();
129
+ }
130
+ catch (err) {
131
+ const msg = err instanceof Error ? err.message : String(err);
132
+ throw new Error(`OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`);
133
+ }
134
+ }
135
+ case "exec": {
136
+ throw new Error(`OpenViking ${label} SecretRef exec source is not supported in the packaged plugin ` +
137
+ `(marketplace install scanners block subprocess execution). Export the secret to an ` +
138
+ `environment variable ({source:"env"}) or a file ({source:"file"}) instead, e.g. ` +
139
+ `OPENVIKING_API_KEY=$(op read ${id}).`);
140
+ }
141
+ default:
142
+ throw new Error(`OpenViking ${label} SecretRef has unknown source "${String(obj.source)}". Supported: "env" | "file".`);
143
+ }
144
+ }
79
145
  function resolvePeerRole(configured) {
80
146
  if (typeof configured === "string") {
81
147
  const role = configured.trim().toLowerCase();
@@ -320,7 +386,13 @@ export const memoryOpenVikingConfigSchema = {
320
386
  const peerPrefix = resolvePeerPrefix(cfg.peer_prefix);
321
387
  const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl();
322
388
  const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, "");
323
- const rawApiKey = typeof cfg.apiKey === "string" ? cfg.apiKey : getEnv("OPENVIKING_API_KEY");
389
+ // Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback.
390
+ // A user writing a SecretRef has explicitly opted out of the bare env
391
+ // interpolation path, so the fallback kicks in only when nothing is
392
+ // configured at all (matches existing behaviour).
393
+ const rawApiKey = cfg.apiKey !== undefined && cfg.apiKey !== null
394
+ ? cfg.apiKey
395
+ : getEnv("OPENVIKING_API_KEY") || undefined;
324
396
  const captureMode = cfg.captureMode;
325
397
  if (typeof captureMode !== "undefined" &&
326
398
  captureMode !== "semantic" &&
@@ -342,7 +414,7 @@ export const memoryOpenVikingConfigSchema = {
342
414
  baseUrl: resolvedBaseUrl,
343
415
  peer_role: peerRole,
344
416
  peer_prefix: peerPrefix,
345
- apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
417
+ apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "",
346
418
  headers: toStringRecord(cfg.headers, "openviking config headers"),
347
419
  accountId,
348
420
  userId,
@@ -423,7 +495,9 @@ export const memoryOpenVikingConfigSchema = {
423
495
  label: "OpenViking API Key",
424
496
  sensitive: true,
425
497
  placeholder: "${OPENVIKING_API_KEY}",
426
- help: "Optional API key for OpenViking server",
498
+ help: "Optional API key for OpenViking server. Accepts a plain string, " +
499
+ "${ENV_VAR} interpolation, or a SecretRef object ({source: env/file, id}). " +
500
+ "Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.",
427
501
  },
428
502
  headers: {
429
503
  label: "Headers",
@@ -579,7 +653,7 @@ export const memoryOpenVikingConfigSchema = {
579
653
  enabledTools: {
580
654
  label: "Enabled Tools",
581
655
  placeholder: "default",
582
- help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
656
+ help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
583
657
  advanced: true,
584
658
  },
585
659
  disabledTools: {
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { registerOpenVikingCommands } from "./plugin/command-registration.js";
15
15
  import { registerRecallTraceRoutes as registerRecallTraceRouteAdapters } from "./plugin/recall-trace-routes.js";
16
16
  import { createOpenVikingService } from "./plugin/openviking-services.js";
17
17
  import { registerOpenVikingArchiveTools } from "./plugin/openviking-archive-tools.js";
18
+ import { registerOpenVikingExperienceTools } from "./plugin/openviking-experience-tools.js";
18
19
  import { createOpenVikingImportRuntime } from "./plugin/openviking-import-runtime.js";
19
20
  import { registerOpenVikingImportTools } from "./plugin/openviking-import-tools.js";
20
21
  import { registerOpenVikingLifecycleHooks } from "./plugin/openviking-lifecycle-hooks.js";
@@ -136,6 +137,14 @@ const contextEnginePlugin = {
136
137
  isBypassedSession,
137
138
  makeBypassedToolResult,
138
139
  });
140
+ registerOpenVikingExperienceTools({
141
+ registerTool: registerOpenVikingTool,
142
+ getClient,
143
+ resolvePluginSessionRouting,
144
+ isBypassedSession,
145
+ makeBypassedToolResult,
146
+ userId: cfg.userId,
147
+ });
139
148
  registerOpenVikingMemoryRecallTools({
140
149
  registerTool: registerOpenVikingTool,
141
150
  getClient,
@@ -0,0 +1,105 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ const EXPERIENCE_TARGET_URI = "viking://user/memories/experiences/";
3
+ const DEFAULT_LIMIT = 5;
4
+ const MAX_LIMIT = 20;
5
+ const EXPERIENCE_SIDECAR_FILENAMES = new Set([".abstract.md", ".overview.md", ".relations.json"]);
6
+ function clampLimit(value) {
7
+ const parsed = Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : DEFAULT_LIMIT;
8
+ return Math.max(1, Math.min(MAX_LIMIT, parsed));
9
+ }
10
+ function experienceRelativePath(uri) {
11
+ return uri.match(/^viking:\/\/user\/[^/]+\/memories\/experiences\/(.+)$/)?.[1] ?? "";
12
+ }
13
+ export function isCanonicalExperienceUri(uri, expectedUserId) {
14
+ const value = typeof uri === "string" ? uri.trim() : "";
15
+ if (!value || value.includes("?") || value.includes("#")) {
16
+ return false;
17
+ }
18
+ const owner = value.match(/^viking:\/\/user\/([^/]+)\//)?.[1] ?? "";
19
+ if (expectedUserId?.trim() && owner !== expectedUserId.trim()) {
20
+ return false;
21
+ }
22
+ const relative = experienceRelativePath(value);
23
+ const segments = relative.split("/");
24
+ const basename = segments.at(-1) ?? "";
25
+ return Boolean(relative
26
+ && segments.every((segment) => segment && segment !== "." && segment !== "..")
27
+ && !EXPERIENCE_SIDECAR_FILENAMES.has(basename));
28
+ }
29
+ function titleFromUri(uri) {
30
+ const basename = uri.split("/").at(-1) ?? uri;
31
+ const title = basename.replace(/\.md$/i, "");
32
+ try {
33
+ return decodeURIComponent(title);
34
+ }
35
+ catch {
36
+ return title;
37
+ }
38
+ }
39
+ function jsonToolResult(payload) {
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify(payload) }],
42
+ };
43
+ }
44
+ export function registerOpenVikingExperienceTools(deps) {
45
+ deps.registerTool((ctx) => ({
46
+ name: "search_experience",
47
+ label: "Search Experience (OpenViking)",
48
+ description: "Search reusable task-execution experiences for the current OpenViking user. " +
49
+ "Use before executing operational or multi-step tasks, then call read_experience for selected results.",
50
+ parameters: Type.Object({
51
+ query: Type.String({ description: "Current task, situation, or execution problem to search for" }),
52
+ limit: Type.Optional(Type.Integer({
53
+ description: `Maximum results to return. Default: ${DEFAULT_LIMIT}; maximum: ${MAX_LIMIT}`,
54
+ minimum: 1,
55
+ maximum: MAX_LIMIT,
56
+ })),
57
+ }, { additionalProperties: false }),
58
+ async execute(_toolCallId, params) {
59
+ if (deps.isBypassedSession(ctx)) {
60
+ return deps.makeBypassedToolResult("search_experience");
61
+ }
62
+ const query = typeof params.query === "string" ? params.query.trim() : "";
63
+ if (!query) {
64
+ throw new Error("query is required");
65
+ }
66
+ const session = deps.resolvePluginSessionRouting(ctx);
67
+ const client = await deps.getClient();
68
+ const result = await client.find(query, {
69
+ targetUri: EXPERIENCE_TARGET_URI,
70
+ limit: clampLimit(params.limit),
71
+ }, session.actorPeerId);
72
+ const results = (result.memories ?? [])
73
+ .filter((item) => isCanonicalExperienceUri(item.uri, deps.userId))
74
+ .map((item) => ({
75
+ uri: item.uri,
76
+ title: titleFromUri(item.uri),
77
+ score: typeof item.score === "number" ? item.score : 0,
78
+ snippet: item.abstract || item.overview || "",
79
+ }));
80
+ return jsonToolResult({ results });
81
+ },
82
+ }), { name: "search_experience" });
83
+ deps.registerTool((ctx) => ({
84
+ name: "read_experience",
85
+ label: "Read Experience (OpenViking)",
86
+ description: "Read the complete Markdown body of one Experience returned by search_experience and use it as execution guidance.",
87
+ parameters: Type.Object({
88
+ uri: Type.String({ description: "Exact canonical Experience URI returned by search_experience" }),
89
+ }, { additionalProperties: false }),
90
+ async execute(_toolCallId, params) {
91
+ if (deps.isBypassedSession(ctx)) {
92
+ return deps.makeBypassedToolResult("read_experience");
93
+ }
94
+ const uri = typeof params.uri === "string" ? params.uri.trim() : "";
95
+ if (!isCanonicalExperienceUri(uri, deps.userId)) {
96
+ throw new Error("uri must be a canonical OpenViking Experience URI returned by search_experience");
97
+ }
98
+ const session = deps.resolvePluginSessionRouting(ctx);
99
+ const client = await deps.getClient();
100
+ const content = await client.read(uri, session.actorPeerId);
101
+ const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
102
+ return jsonToolResult({ uri, content: text });
103
+ },
104
+ }), { name: "read_experience" });
105
+ }
package/index.ts CHANGED
@@ -19,6 +19,7 @@ import { registerOpenVikingCommands } from "./plugin/command-registration.js";
19
19
  import { registerRecallTraceRoutes as registerRecallTraceRouteAdapters } from "./plugin/recall-trace-routes.js";
20
20
  import { createOpenVikingService } from "./plugin/openviking-services.js";
21
21
  import { registerOpenVikingArchiveTools } from "./plugin/openviking-archive-tools.js";
22
+ import { registerOpenVikingExperienceTools } from "./plugin/openviking-experience-tools.js";
22
23
  import { createOpenVikingImportRuntime } from "./plugin/openviking-import-runtime.js";
23
24
  import { registerOpenVikingImportTools } from "./plugin/openviking-import-tools.js";
24
25
  import { registerOpenVikingLifecycleHooks } from "./plugin/openviking-lifecycle-hooks.js";
@@ -280,6 +281,15 @@ const contextEnginePlugin = {
280
281
  makeBypassedToolResult,
281
282
  });
282
283
 
284
+ registerOpenVikingExperienceTools({
285
+ registerTool: registerOpenVikingTool,
286
+ getClient,
287
+ resolvePluginSessionRouting,
288
+ isBypassedSession,
289
+ makeBypassedToolResult,
290
+ userId: cfg.userId,
291
+ });
292
+
283
293
  registerOpenVikingMemoryRecallTools({
284
294
  registerTool: registerOpenVikingTool,
285
295
  getClient,
@@ -41,6 +41,7 @@
41
41
  ".gitignore",
42
42
  "skills/install-openviking-memory/SKILL.md",
43
43
  "skills/openviking-context-database/SKILL.md",
44
+ "skills/ov-experience-memory/SKILL.md",
44
45
  "INSTALL-AGENT.md"
45
46
  ]
46
47
  },
@@ -3,6 +3,7 @@
3
3
  "name": "OpenViking",
4
4
  "kind": "context-engine",
5
5
  "description": "OpenClaw context-engine plugin for memory management — powered by OpenViking",
6
+ "icon": "https://raw.githubusercontent.com/volcengine/OpenViking/main/docs/images/ov-logo-icon.png",
6
7
  "activation": {
7
8
  "onStartup": true,
8
9
  "onCapabilities": [
@@ -12,11 +13,14 @@
12
13
  },
13
14
  "contracts": {
14
15
  "tools": [
16
+ "add_resource",
15
17
  "add_skill",
16
18
  "ov_search",
17
19
  "ov_read",
18
20
  "ov_multi_read",
19
21
  "ov_list",
22
+ "search_experience",
23
+ "read_experience",
20
24
  "memory_recall",
21
25
  "ov_recall_trace",
22
26
  "memory_store",
@@ -96,7 +100,7 @@
96
100
  "label": "OpenViking API Key",
97
101
  "sensitive": true,
98
102
  "placeholder": "${OPENVIKING_API_KEY}",
99
- "help": "Optional API key for OpenViking server"
103
+ "help": "Optional API key for OpenViking server. Accepts a plain string, ${ENV_VAR} interpolation, or a SecretRef object ({source: env/file, id}). Prefer a SecretRef so the key is never stored as plaintext in openclaw.json."
100
104
  },
101
105
  "headers": {
102
106
  "label": "Headers",
@@ -250,7 +254,7 @@
250
254
  "enabledTools": {
251
255
  "label": "Enabled Tools",
252
256
  "placeholder": "default",
253
- "help": "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
257
+ "help": "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
254
258
  "advanced": true
255
259
  },
256
260
  "disabledTools": {
@@ -290,7 +294,34 @@
290
294
  "description": "Deprecated and ignored. Tenant identity headers are controlled by explicit accountId/userId."
291
295
  },
292
296
  "apiKey": {
293
- "type": "string"
297
+ "oneOf": [
298
+ {
299
+ "type": "string",
300
+ "description": "Plain API key or ${ENV_VAR} interpolation. For secret-manager-managed keys, prefer the SecretRef object shape below."
301
+ },
302
+ {
303
+ "type": "object",
304
+ "title": "SecretRef (env)",
305
+ "description": "Read the API key from an environment variable.",
306
+ "additionalProperties": false,
307
+ "required": ["source", "id"],
308
+ "properties": {
309
+ "source": { "type": "string", "const": "env", "description": "Look up the key in process.env[id] at plugin load time. Equivalent to ${ENV} interpolation but declarative and visible in the UI." },
310
+ "id": { "type": "string", "description": "Name of the environment variable. Example: OPENVIKING_API_KEY." }
311
+ }
312
+ },
313
+ {
314
+ "type": "object",
315
+ "title": "SecretRef (file)",
316
+ "description": "Read the API key from a file on disk, e.g. a Kubernetes secretKeyRef mount volume or a 0600-permission managed file. ~ is expanded and trailing whitespace is trimmed.",
317
+ "additionalProperties": false,
318
+ "required": ["source", "id"],
319
+ "properties": {
320
+ "source": { "type": "string", "const": "file" },
321
+ "id": { "type": "string", "description": "Absolute file path or ~/relative path. Example: /etc/secrets/openviking.key." }
322
+ }
323
+ }
324
+ ]
294
325
  },
295
326
  "headers": {
296
327
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.7.15",
3
+ "version": "2026.8.6-1",
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",
@@ -0,0 +1,160 @@
1
+ import { Type } from "@sinclair/typebox";
2
+
3
+ import type { FindResult } from "../client.js";
4
+
5
+ const EXPERIENCE_TARGET_URI = "viking://user/memories/experiences/";
6
+ const DEFAULT_LIMIT = 5;
7
+ const MAX_LIMIT = 20;
8
+ const EXPERIENCE_SIDECAR_FILENAMES = new Set([".abstract.md", ".overview.md", ".relations.json"]);
9
+
10
+ export type OpenVikingExperienceToolContext = {
11
+ sessionKey?: string;
12
+ sessionId?: string;
13
+ agentId?: string;
14
+ senderId?: string;
15
+ requesterSenderId?: string;
16
+ };
17
+
18
+ export type OpenVikingExperienceSession = {
19
+ sessionId?: string;
20
+ sessionKey?: string;
21
+ ovSessionId?: string;
22
+ agentId: string;
23
+ actorPeerId?: string;
24
+ };
25
+
26
+ type OpenVikingExperienceClient = {
27
+ find: (
28
+ query: string,
29
+ options: { targetUri: string; limit: number; scoreThreshold?: number },
30
+ actorPeerId?: string,
31
+ ) => Promise<FindResult>;
32
+ read: (uri: string, actorPeerId?: string) => Promise<unknown>;
33
+ };
34
+
35
+ export type OpenVikingExperienceToolsDeps = {
36
+ registerTool: (toolOrFactory: unknown, opts: { name: string }) => void;
37
+ getClient: () => Promise<OpenVikingExperienceClient>;
38
+ resolvePluginSessionRouting: (ctx?: OpenVikingExperienceToolContext) => OpenVikingExperienceSession;
39
+ isBypassedSession: (ctx?: OpenVikingExperienceToolContext) => boolean;
40
+ makeBypassedToolResult: (toolName: string) => unknown;
41
+ userId?: string;
42
+ };
43
+
44
+ function clampLimit(value: unknown): number {
45
+ const parsed = Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : DEFAULT_LIMIT;
46
+ return Math.max(1, Math.min(MAX_LIMIT, parsed));
47
+ }
48
+
49
+ function experienceRelativePath(uri: string): string {
50
+ return uri.match(/^viking:\/\/user\/[^/]+\/memories\/experiences\/(.+)$/)?.[1] ?? "";
51
+ }
52
+
53
+ export function isCanonicalExperienceUri(uri: unknown, expectedUserId?: string): uri is string {
54
+ const value = typeof uri === "string" ? uri.trim() : "";
55
+ if (!value || value.includes("?") || value.includes("#")) {
56
+ return false;
57
+ }
58
+ const owner = value.match(/^viking:\/\/user\/([^/]+)\//)?.[1] ?? "";
59
+ if (expectedUserId?.trim() && owner !== expectedUserId.trim()) {
60
+ return false;
61
+ }
62
+ const relative = experienceRelativePath(value);
63
+ const segments = relative.split("/");
64
+ const basename = segments.at(-1) ?? "";
65
+ return Boolean(
66
+ relative
67
+ && segments.every((segment) => segment && segment !== "." && segment !== "..")
68
+ && !EXPERIENCE_SIDECAR_FILENAMES.has(basename)
69
+ );
70
+ }
71
+
72
+ function titleFromUri(uri: string): string {
73
+ const basename = uri.split("/").at(-1) ?? uri;
74
+ const title = basename.replace(/\.md$/i, "");
75
+ try {
76
+ return decodeURIComponent(title);
77
+ } catch {
78
+ return title;
79
+ }
80
+ }
81
+
82
+ function jsonToolResult(payload: Record<string, unknown>): {
83
+ content: Array<{ type: "text"; text: string }>;
84
+ } {
85
+ return {
86
+ content: [{ type: "text", text: JSON.stringify(payload) }],
87
+ };
88
+ }
89
+
90
+ export function registerOpenVikingExperienceTools(deps: OpenVikingExperienceToolsDeps): void {
91
+ deps.registerTool(
92
+ (ctx: OpenVikingExperienceToolContext) => ({
93
+ name: "search_experience",
94
+ label: "Search Experience (OpenViking)",
95
+ description:
96
+ "Search reusable task-execution experiences for the current OpenViking user. " +
97
+ "Use before executing operational or multi-step tasks, then call read_experience for selected results.",
98
+ parameters: Type.Object({
99
+ query: Type.String({ description: "Current task, situation, or execution problem to search for" }),
100
+ limit: Type.Optional(Type.Integer({
101
+ description: `Maximum results to return. Default: ${DEFAULT_LIMIT}; maximum: ${MAX_LIMIT}`,
102
+ minimum: 1,
103
+ maximum: MAX_LIMIT,
104
+ })),
105
+ }, { additionalProperties: false }),
106
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
107
+ if (deps.isBypassedSession(ctx)) {
108
+ return deps.makeBypassedToolResult("search_experience");
109
+ }
110
+ const query = typeof params.query === "string" ? params.query.trim() : "";
111
+ if (!query) {
112
+ throw new Error("query is required");
113
+ }
114
+ const session = deps.resolvePluginSessionRouting(ctx);
115
+ const client = await deps.getClient();
116
+ const result = await client.find(query, {
117
+ targetUri: EXPERIENCE_TARGET_URI,
118
+ limit: clampLimit(params.limit),
119
+ }, session.actorPeerId);
120
+ const results = (result.memories ?? [])
121
+ .filter((item) => isCanonicalExperienceUri(item.uri, deps.userId))
122
+ .map((item) => ({
123
+ uri: item.uri,
124
+ title: titleFromUri(item.uri),
125
+ score: typeof item.score === "number" ? item.score : 0,
126
+ snippet: item.abstract || item.overview || "",
127
+ }));
128
+ return jsonToolResult({ results });
129
+ },
130
+ }),
131
+ { name: "search_experience" },
132
+ );
133
+
134
+ deps.registerTool(
135
+ (ctx: OpenVikingExperienceToolContext) => ({
136
+ name: "read_experience",
137
+ label: "Read Experience (OpenViking)",
138
+ description:
139
+ "Read the complete Markdown body of one Experience returned by search_experience and use it as execution guidance.",
140
+ parameters: Type.Object({
141
+ uri: Type.String({ description: "Exact canonical Experience URI returned by search_experience" }),
142
+ }, { additionalProperties: false }),
143
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
144
+ if (deps.isBypassedSession(ctx)) {
145
+ return deps.makeBypassedToolResult("read_experience");
146
+ }
147
+ const uri = typeof params.uri === "string" ? params.uri.trim() : "";
148
+ if (!isCanonicalExperienceUri(uri, deps.userId)) {
149
+ throw new Error("uri must be a canonical OpenViking Experience URI returned by search_experience");
150
+ }
151
+ const session = deps.resolvePluginSessionRouting(ctx);
152
+ const client = await deps.getClient();
153
+ const content = await client.read(uri, session.actorPeerId);
154
+ const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
155
+ return jsonToolResult({ uri, content: text });
156
+ },
157
+ }),
158
+ { name: "read_experience" },
159
+ );
160
+ }
@@ -1,6 +1,7 @@
1
1
  export type OpenVikingToolGroup =
2
2
  | "memory"
3
3
  | "resource_query"
4
+ | "experience"
4
5
  | "import"
5
6
  | "recall_trace"
6
7
  | "archive"
@@ -13,6 +14,8 @@ export type OpenVikingToolName =
13
14
  | "ov_read"
14
15
  | "ov_multi_read"
15
16
  | "ov_list"
17
+ | "search_experience"
18
+ | "read_experience"
16
19
  | "memory_recall"
17
20
  | "ov_recall_trace"
18
21
  | "memory_store"
@@ -44,6 +47,8 @@ export const OPENVIKING_TOOL_SPECS = [
44
47
  { name: "ov_read", group: "resource_query", defaultEnabled: true },
45
48
  { name: "ov_multi_read", group: "resource_query", defaultEnabled: true },
46
49
  { name: "ov_list", group: "resource_query", defaultEnabled: true },
50
+ { name: "search_experience", group: "experience", defaultEnabled: true },
51
+ { name: "read_experience", group: "experience", defaultEnabled: true },
47
52
  { name: "memory_recall", group: "memory", defaultEnabled: true },
48
53
  { name: "ov_recall_trace", group: "recall_trace", defaultEnabled: true },
49
54
  { name: "memory_store", group: "memory", defaultEnabled: true },
@@ -64,6 +69,7 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = OPENVIKING_TOOL_SPECS
64
69
  const OPENVIKING_TOOL_GROUP_ORDER: readonly OpenVikingToolGroup[] = [
65
70
  "memory",
66
71
  "resource_query",
72
+ "experience",
67
73
  "import",
68
74
  "recall_trace",
69
75
  "archive",
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: ov-experience-memory
3
+ description: >
4
+ Use OpenViking experience memories during task execution. Search relevant
5
+ experiences with search_experience, read selected experiences with
6
+ read_experience, and leave standard tool parts in the committed session so
7
+ OpenViking can report recall and injection usage.
8
+ version: 2026.7.9
9
+ tags:
10
+ - openviking
11
+ - experience-memory
12
+ - agent-memory
13
+ - usage-reporting
14
+ ---
15
+
16
+ # OpenViking Experience Memory
17
+
18
+ Use this skill when the current user request starts or continues an executable
19
+ task, especially tasks involving tools, files, code changes, data operations,
20
+ workflow decisions, or multi-step actions.
21
+
22
+ Do not use this skill for casual chat, pure explanation, or one-off factual Q&A
23
+ that does not require operational guidance.
24
+
25
+ ## Runtime Contract
26
+
27
+ The agent runtime must expose two tools with these exact names:
28
+
29
+ - `search_experience`
30
+ - `read_experience`
31
+
32
+ OpenViking usage reporting recognizes only completed tool parts with these exact
33
+ tool names. Calls to generic `find`, `search`, `read`, `ov_search`, or `ov_read`
34
+ do not count as experience recall or injection events.
35
+
36
+ ## Tool: search_experience
37
+
38
+ Purpose: search reusable execution experiences from the OpenViking experience
39
+ library before assembling task context.
40
+
41
+ Input schema:
42
+
43
+ ```json
44
+ {
45
+ "query": "string",
46
+ "limit": 5
47
+ }
48
+ ```
49
+
50
+ Output schema:
51
+
52
+ ```json
53
+ {
54
+ "results": [
55
+ {
56
+ "uri": "viking://user/<current_user_id>/memories/experiences/example.md",
57
+ "title": "example",
58
+ "score": 0.82,
59
+ "snippet": "Short summary or matched situation"
60
+ }
61
+ ]
62
+ }
63
+ ```
64
+
65
+ Implementation:
66
+
67
+ The runtime tool calls OpenViking `POST /api/v1/search/find` with `target_uri`
68
+ fixed to the current-user shorthand `viking://user/memories/experiences/`.
69
+ Callers provide only `query` and optional `limit`; they cannot override or pass
70
+ `target_uri`. OpenViking resolves the fixed shorthand against the authenticated
71
+ request user. Return only canonical experience memory URIs for that user; never
72
+ hardcode `default` or another user ID.
73
+
74
+ Usage reporting:
75
+
76
+ A completed `search_experience` tool part is counted as an experience recall
77
+ event for every `results[].uri` value.
78
+
79
+ ## Tool: read_experience
80
+
81
+ Purpose: read the full Markdown body of a selected experience and inject it into
82
+ the agent prompt as task execution guidance.
83
+
84
+ Input schema:
85
+
86
+ ```json
87
+ {
88
+ "uri": "viking://user/<current_user_id>/memories/experiences/example.md"
89
+ }
90
+ ```
91
+
92
+ Output schema:
93
+
94
+ ```json
95
+ {
96
+ "uri": "viking://user/<current_user_id>/memories/experiences/example.md",
97
+ "content": "Experience Markdown body"
98
+ }
99
+ ```
100
+
101
+ Implementation:
102
+
103
+ Call OpenViking `GET /api/v1/content/read?uri=<encoded_uri>` for the selected
104
+ experience URI. Always pass the canonical URI returned by `search_experience`;
105
+ do not construct a URI with a hardcoded user ID. The returned content should be
106
+ inserted into the prompt as operational guidance, not as user profile facts.
107
+
108
+ Usage reporting:
109
+
110
+ A completed `read_experience` tool part is counted as an experience injection
111
+ event for `tool_input.uri` or `tool_output.uri`. In this design, reading an
112
+ experience through `read_experience` means the experience was injected into the
113
+ prompt.
114
+
115
+ ## Recommended Flow
116
+
117
+ 1. When a task begins, build a short query from the latest user instruction,
118
+ current plan, active skill name, and important tool/environment context.
119
+ 2. Call `search_experience` before final prompt assembly.
120
+ 3. Review returned titles/snippets and select only experiences likely to affect
121
+ execution.
122
+ 4. Call `read_experience` for selected experience URIs.
123
+ 5. Inject the returned Markdown into the prompt under an explicit experience
124
+ section.
125
+ 6. Continue task execution.
126
+ 7. Commit the session normally. The committed session must include the
127
+ `search_experience` and `read_experience` tool parts so OpenViking can report
128
+ usage.
129
+
130
+ ## Prompt Injection Format
131
+
132
+ Use a compact and explicit block:
133
+
134
+ ```text
135
+ <openviking-experience-memory>
136
+ The following guidance was retrieved from prior task execution experience.
137
+ Use it as operational guidance. Do not treat it as user identity or preference.
138
+
139
+ <experience uri="viking://user/<current_user_id>/memories/experiences/example.md">
140
+ ...experience markdown...
141
+ </experience>
142
+ </openviking-experience-memory>
143
+ ```
144
+
145
+ ## Commit Requirements
146
+
147
+ The session committed to OpenViking must preserve tool parts with:
148
+
149
+ - `tool_name`
150
+ - `tool_status`
151
+ - `tool_input`
152
+ - `tool_output`
153
+ - `tool_id`
154
+
155
+ Only `tool_status == "completed"` is counted. Failed, cancelled, or skipped tool
156
+ parts are ignored by usage reporting.