@openviking/openclaw-plugin 2026.7.15 → 2026.8.6-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL-ZH.md +39 -0
- package/INSTALL.md +39 -0
- package/README.md +5 -0
- package/config.ts +121 -6
- package/dist/config.js +78 -4
- package/dist/index.js +9 -0
- package/dist/plugin/openviking-experience-tools.js +105 -0
- package/index.ts +10 -0
- package/install-manifest.json +1 -0
- package/openclaw.plugin.json +34 -3
- package/package.json +1 -1
- package/plugin/openviking-experience-tools.ts +160 -0
- package/registries/openviking-tools.ts +6 -0
- package/skills/ov-experience-memory/SKILL.md +156 -0
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
package/install-manifest.json
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
-
"
|
|
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.
|
|
3
|
+
"version": "2026.8.6-2",
|
|
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.
|