@openviking/dsh-memory-plugin 0.1.0
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/README.md +129 -0
- package/capture.mjs +104 -0
- package/client.mjs +206 -0
- package/config.mjs +161 -0
- package/cordis.patch.yml +9 -0
- package/index.mjs +56 -0
- package/lifecycle.mjs +8 -0
- package/package.json +60 -0
- package/runtime.mjs +316 -0
- package/shared/capture-utils.mjs +515 -0
- package/shared/credentials.mjs +228 -0
- package/shared/debug-log.mjs +58 -0
- package/shared/pending-queue.mjs +451 -0
- package/shared/plugin-config.mjs +90 -0
- package/shared/profile-inject.mjs +294 -0
- package/shared/recall-compress-core.mjs +195 -0
- package/shared/recall-core.mjs +609 -0
- package/shared/retryable.mjs +9 -0
- package/shared/session-model.mjs +57 -0
- package/shared/setup-wizard.mjs +111 -0
- package/shared/uri-guard.mjs +56 -0
- package/shared/workspace-peer.mjs +16 -0
- package/tools.mjs +246 -0
- package/uri-guard.mjs +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# OpenViking Memory for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
An installable DeepSeek Harness bundle that adds OpenViking auto-recall, session capture, `viking://` URI protection, and model-invocable memory tools.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- `@deepseek-ai/dsh` `0.1.0-rc.6`
|
|
8
|
+
- Node.js `^22.19.0` or `>=24`
|
|
9
|
+
- A reachable OpenViking server
|
|
10
|
+
|
|
11
|
+
The bundle has no runtime npm dependencies. Its tool and message structures
|
|
12
|
+
come from the DSH constructors (`defineTool` from `@deepseek-ai/dsh-tools`,
|
|
13
|
+
`createUserMessage` from `@deepseek-ai/dsh-llm`) behind exact-pinned
|
|
14
|
+
peerDependencies that DSH exposes through its profile fallback at boot time, so
|
|
15
|
+
the definitions track DSH's contracts instead of hand-built object shapes.
|
|
16
|
+
It is tested against `0.1.0-rc.6`; install that exact DSH release because
|
|
17
|
+
prerelease package dist-tags are not synchronized across the package family.
|
|
18
|
+
|
|
19
|
+
## Why injection uses pre-step user messages, not the system prompt
|
|
20
|
+
|
|
21
|
+
Recall and profile context enter through the `agent/pre-step` waterfall as
|
|
22
|
+
durable, source-attributed user messages (`source: { kind: 'plugin', … }`).
|
|
23
|
+
They are deliberately **not** added to the system prompt: a DSH preset whose
|
|
24
|
+
persona declares `complete: true` (the stock `minimal` preset does) restores
|
|
25
|
+
that persona as the sole prompt section after assembly, silently discarding
|
|
26
|
+
every other contribution — a system-prompt-based memory plugin loses its
|
|
27
|
+
context under such presets with no error. Pre-step injection also makes each
|
|
28
|
+
injection a session event that replays, is visible to compaction, and never
|
|
29
|
+
reaches `request/header`.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
From the OpenViking repository:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dsh plugin --profile default add ./examples/dsh-memory-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or install the published package:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile default add @openviking/dsh-memory-plugin
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Confirm that the profile includes the bundle:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh --profile default --dump-config
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The package patch mounts the runtime inside a Cordis group with an isolated `openvikingMemory` service.
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
OpenViking credentials use the same resolution order as the other memory plugins:
|
|
56
|
+
|
|
57
|
+
1. `OPENVIKING_*` environment variables
|
|
58
|
+
2. `~/.openviking/ovcli.conf`
|
|
59
|
+
3. `~/.openviking/ov.conf`
|
|
60
|
+
|
|
61
|
+
Common environment variables:
|
|
62
|
+
|
|
63
|
+
| Variable | Purpose |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `OPENVIKING_URL` / `OPENVIKING_BASE_URL` | OpenViking server endpoint |
|
|
66
|
+
| `OPENVIKING_API_KEY` / `OPENVIKING_BEARER_TOKEN` | Bearer credential |
|
|
67
|
+
| `OPENVIKING_ACCOUNT` | Trusted-mode account |
|
|
68
|
+
| `OPENVIKING_USER` | Trusted-mode user |
|
|
69
|
+
| `OPENVIKING_PEER_ID` | Explicit actor peer |
|
|
70
|
+
| `OPENVIKING_WORKSPACE_PEER` | Derive a peer from each DSH session workspace by default |
|
|
71
|
+
| `OPENVIKING_RECALL_PEER_SCOPE` | `all` for cross-workspace recall or `actor` for isolation |
|
|
72
|
+
|
|
73
|
+
The patch can also carry plugin config:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- insert:
|
|
77
|
+
- id: openviking-memory
|
|
78
|
+
name: '@deepseek-ai/cordis-plugin-group'
|
|
79
|
+
group: true
|
|
80
|
+
isolate:
|
|
81
|
+
openvikingMemory: true
|
|
82
|
+
config:
|
|
83
|
+
- id: openviking-memory-runtime
|
|
84
|
+
name: '@openviking/dsh-memory-plugin'
|
|
85
|
+
config:
|
|
86
|
+
endpoint: http://127.0.0.1:1933
|
|
87
|
+
recallTokenBudget: 2000
|
|
88
|
+
scoreThreshold: 0.35
|
|
89
|
+
captureToolResults: false
|
|
90
|
+
commitTokenThreshold: 20000
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Behavior
|
|
94
|
+
|
|
95
|
+
- `agent/session-start` injects the OpenViking profile and available-memory index through `agent.inject()`.
|
|
96
|
+
- `agent/pre-step` retrieves with the current step input and appends a durable plugin message to that same step.
|
|
97
|
+
- `session/event` captures user, assistant, and optionally tool-result messages without scraping a transcript.
|
|
98
|
+
- `turn/end` checks the OpenViking pending-token threshold and commits when required.
|
|
99
|
+
- Failed writes enter the shared OpenViking pending queue for replay at the next session start.
|
|
100
|
+
- `tools/pre-execute` blocks DSH filesystem and shell tools from treating `viking://` URIs as local paths.
|
|
101
|
+
|
|
102
|
+
Each DSH session maps to `dsh-<session-id>` in OpenViking. Workspace-derived actor peers are resolved per session and sent on every session-specific request.
|
|
103
|
+
|
|
104
|
+
## Tools
|
|
105
|
+
|
|
106
|
+
The bundle registers:
|
|
107
|
+
|
|
108
|
+
- `viking_search`
|
|
109
|
+
- `viking_read`
|
|
110
|
+
- `viking_browse`
|
|
111
|
+
- `viking_remember`
|
|
112
|
+
- `viking_forget`
|
|
113
|
+
- `viking_add_resource`
|
|
114
|
+
- `viking_archive_expand`
|
|
115
|
+
|
|
116
|
+
`viking_forget` performs permanent deletion. The calling model should use it only when the user explicitly requests deletion.
|
|
117
|
+
|
|
118
|
+
## Testing
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm ci # installs the exact-pinned dsh devDependencies the tests exercise
|
|
122
|
+
npm test # node --test *.test.mjs — runs in the repo's PR workflow
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`live-recall.test.mjs` is an opt-in end-to-end gate against a real OpenViking
|
|
126
|
+
server: it stores a sentinel memory through a session commit, waits for
|
|
127
|
+
extraction, and asserts recall returns that sentinel — the property no stub
|
|
128
|
+
can certify. Enable it with `OPENVIKING_E2E=1` plus the normal credential
|
|
129
|
+
chain; it skips otherwise (including in CI until a server secret exists).
|
package/capture.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractPartsFromPayload,
|
|
3
|
+
extractTextFromPayload,
|
|
4
|
+
shouldCaptureText,
|
|
5
|
+
} from "./shared/capture-utils.mjs";
|
|
6
|
+
|
|
7
|
+
export const OPENVIKING_PLUGIN_SOURCE = "openviking-memory";
|
|
8
|
+
|
|
9
|
+
export function captureEvent(event, config, toolNames = new Map()) {
|
|
10
|
+
if (!event || typeof event !== "object") return null;
|
|
11
|
+
if (event.type === "tool/call") {
|
|
12
|
+
if (config.captureToolResults === true) {
|
|
13
|
+
toolNames.set(String(event.data.callId), event.data.name);
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const message = eventMessage(event);
|
|
19
|
+
if (!message) return null;
|
|
20
|
+
const toolCallId = event.type === "tool/result"
|
|
21
|
+
? String(message.source?.callId || message.content?.[0]?.toolCallId || "")
|
|
22
|
+
: "";
|
|
23
|
+
try {
|
|
24
|
+
return captureMessage(event, message, config, toolNames);
|
|
25
|
+
} finally {
|
|
26
|
+
if (toolCallId) toolNames.delete(toolCallId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function captureMessage(event, message, config, toolNames) {
|
|
31
|
+
// Whitelist by source: plugin-injected user messages (this plugin's recall
|
|
32
|
+
// blocks, time-context snapshots, any other plugin's context) are model
|
|
33
|
+
// input, not human input — mirroring them would launder synthetic text
|
|
34
|
+
// into memory as if a person said it.
|
|
35
|
+
if (message.source?.kind === "plugin") return null;
|
|
36
|
+
if (message.role === "assistant" && config.captureAssistantTurns === false) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (message.source?.kind === "tool" && config.captureToolResults !== true) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
44
|
+
const toolNameById = Object.fromEntries(toolNames);
|
|
45
|
+
const rawText = extractTextFromPayload(message, {
|
|
46
|
+
toolMaxChars: config.captureToolMaxChars,
|
|
47
|
+
});
|
|
48
|
+
const parts = extractPartsFromPayload(message, {
|
|
49
|
+
toolMaxChars: config.captureToolMaxChars,
|
|
50
|
+
toolNameById,
|
|
51
|
+
});
|
|
52
|
+
const decision = shouldCaptureText(rawText, role, config);
|
|
53
|
+
const structuredParts = parts.filter(part => part?.type !== "text");
|
|
54
|
+
if (!decision.shouldCapture && structuredParts.length === 0) return null;
|
|
55
|
+
|
|
56
|
+
const hasTextPart = parts.some(part => part?.type === "text");
|
|
57
|
+
const bodyParts = [
|
|
58
|
+
...(hasTextPart && decision.shouldCapture && decision.text
|
|
59
|
+
? [{ type: "text", text: decision.text }]
|
|
60
|
+
: []),
|
|
61
|
+
...structuredParts,
|
|
62
|
+
];
|
|
63
|
+
const payload = bodyParts.length > 0
|
|
64
|
+
? { role, parts: bodyParts }
|
|
65
|
+
: { role, content: decision.text };
|
|
66
|
+
const createdAt = eventCreatedAt(event);
|
|
67
|
+
if (createdAt) payload.created_at = createdAt;
|
|
68
|
+
if (config.peerId) payload.peer_id = config.peerId;
|
|
69
|
+
return payload;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function promptText(messages) {
|
|
73
|
+
return (messages || [])
|
|
74
|
+
.filter(message => !(
|
|
75
|
+
message?.source?.kind === "plugin"
|
|
76
|
+
&& message.source.plugin === OPENVIKING_PLUGIN_SOURCE
|
|
77
|
+
))
|
|
78
|
+
.map(message => extractTextFromPayload(message))
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join("\n\n")
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function eventMessage(event) {
|
|
85
|
+
switch (event.type) {
|
|
86
|
+
case "user/message":
|
|
87
|
+
return event.data;
|
|
88
|
+
case "assistant/message":
|
|
89
|
+
case "tool/result":
|
|
90
|
+
return event.data?.message;
|
|
91
|
+
default:
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function eventCreatedAt(event) {
|
|
97
|
+
const time = Number(event?.time);
|
|
98
|
+
if (!Number.isFinite(time) || time < 0) return "";
|
|
99
|
+
try {
|
|
100
|
+
return new Date(time).toISOString();
|
|
101
|
+
} catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
}
|
package/client.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
export class OpenVikingClient {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.config = config;
|
|
4
|
+
this.connected = false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
headers(options = {}) {
|
|
8
|
+
const headers = { "Content-Type": "application/json" };
|
|
9
|
+
if (this.config.apiKey) headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
10
|
+
if (this.config.account) headers["X-OpenViking-Account"] = this.config.account;
|
|
11
|
+
if (this.config.user) headers["X-OpenViking-User"] = this.config.user;
|
|
12
|
+
const actorPeerId = options.actorPeerId ?? this.config.peerId;
|
|
13
|
+
if (actorPeerId) headers["X-OpenViking-Actor-Peer"] = actorPeerId;
|
|
14
|
+
if (this.config.userAgent) headers["User-Agent"] = this.config.userAgent;
|
|
15
|
+
return headers;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async fetchJSON(path, init = {}, options = {}) {
|
|
19
|
+
const timeoutMs = options.timeoutMs ?? this.config.requestTimeoutMs;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`${this.config.endpoint}${path}`, {
|
|
24
|
+
...init,
|
|
25
|
+
headers: {
|
|
26
|
+
...this.headers(options),
|
|
27
|
+
...(init.headers || {}),
|
|
28
|
+
},
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
const body = await response.json().catch(() => ({}));
|
|
32
|
+
const traceId = body?.result?.trace_id
|
|
33
|
+
|| body?.error?.trace_id
|
|
34
|
+
|| body?.trace_id
|
|
35
|
+
|| undefined;
|
|
36
|
+
if (!response.ok || body?.status === "error") {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
result: null,
|
|
40
|
+
status: response.status,
|
|
41
|
+
error: body?.error || { message: `HTTP ${response.status}` },
|
|
42
|
+
traceId,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
result: body?.result ?? body,
|
|
48
|
+
status: response.status,
|
|
49
|
+
traceId,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
result: null,
|
|
55
|
+
status: 0,
|
|
56
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
57
|
+
};
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async health() {
|
|
64
|
+
return (await this.healthResult()).ok;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async healthResult() {
|
|
68
|
+
const response = await this.fetchJSON("/health", {}, { timeoutMs: 5000 });
|
|
69
|
+
this.connected = response.ok;
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async ensureSession(sessionId, actorPeerId) {
|
|
74
|
+
const response = await this.ensureSessionResult(sessionId, actorPeerId);
|
|
75
|
+
return response.ok
|
|
76
|
+
|| (response.status === 409 && response.error?.code === "ALREADY_EXISTS");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async ensureSessionResult(sessionId, actorPeerId) {
|
|
80
|
+
const response = await this.fetchJSON("/api/v1/sessions", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({ session_id: sessionId }),
|
|
83
|
+
}, { actorPeerId });
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getSession(sessionId, actorPeerId) {
|
|
88
|
+
const response = await this.fetchJSON(
|
|
89
|
+
`/api/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
90
|
+
{},
|
|
91
|
+
{ timeoutMs: 5000, actorPeerId },
|
|
92
|
+
);
|
|
93
|
+
return response.ok ? response.result : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getSessionArchive(sessionId, archiveId, actorPeerId) {
|
|
97
|
+
const response = await this.fetchJSON(
|
|
98
|
+
`/api/v1/sessions/${encodeURIComponent(sessionId)}/archives/${encodeURIComponent(archiveId)}`,
|
|
99
|
+
{},
|
|
100
|
+
{ actorPeerId },
|
|
101
|
+
);
|
|
102
|
+
return response.ok ? response.result : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async addMessage(sessionId, payload, actorPeerId) {
|
|
106
|
+
return this.fetchJSON(
|
|
107
|
+
`/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
108
|
+
{
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: JSON.stringify(payload),
|
|
111
|
+
},
|
|
112
|
+
{ actorPeerId },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async commitSession(sessionId, actorPeerId, options = {}) {
|
|
117
|
+
return this.fetchJSON(
|
|
118
|
+
`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
|
|
119
|
+
{
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify({ keep_recent_count: this.config.commitKeepRecentCount }),
|
|
122
|
+
},
|
|
123
|
+
{ timeoutMs: options.timeoutMs ?? 30000, actorPeerId },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async find(query, options = {}) {
|
|
128
|
+
const body = { query };
|
|
129
|
+
if (options.targetUri) body.target_uri = options.targetUri;
|
|
130
|
+
if (options.limit) body.limit = options.limit;
|
|
131
|
+
if (options.scoreThreshold !== undefined) body.score_threshold = options.scoreThreshold;
|
|
132
|
+
const response = await this.fetchJSON("/api/v1/search/find", {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
}, { actorPeerId: options.actorPeerId });
|
|
136
|
+
if (!response.ok || !response.result) return [];
|
|
137
|
+
|
|
138
|
+
const results = [];
|
|
139
|
+
for (const bucket of ["memories", "resources", "skills"]) {
|
|
140
|
+
const entries = response.result[bucket];
|
|
141
|
+
if (!Array.isArray(entries)) continue;
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
results.push({
|
|
144
|
+
uri: entry?.uri || "",
|
|
145
|
+
contextType: entry?.context_type
|
|
146
|
+
|| (bucket === "memories" ? "memory" : bucket === "skills" ? "skill" : "resource"),
|
|
147
|
+
score: Number(entry?.score || 0),
|
|
148
|
+
abstract: entry?.abstract || "",
|
|
149
|
+
overview: entry?.overview || null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async read(uri, level, actorPeerId) {
|
|
157
|
+
const endpoint = level === "abstract"
|
|
158
|
+
? "abstract"
|
|
159
|
+
: level === "overview"
|
|
160
|
+
? "overview"
|
|
161
|
+
: "read";
|
|
162
|
+
const response = await this.fetchJSON(
|
|
163
|
+
`/api/v1/content/${endpoint}?uri=${encodeURIComponent(uri)}`,
|
|
164
|
+
{},
|
|
165
|
+
{ actorPeerId },
|
|
166
|
+
);
|
|
167
|
+
return response.ok ? response.result : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async list(uri, actorPeerId) {
|
|
171
|
+
const response = await this.fetchJSON(
|
|
172
|
+
`/api/v1/fs/ls?uri=${encodeURIComponent(uri)}&output=original`,
|
|
173
|
+
{},
|
|
174
|
+
{ actorPeerId },
|
|
175
|
+
);
|
|
176
|
+
return response.ok && Array.isArray(response.result) ? response.result : [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async stat(uri, actorPeerId) {
|
|
180
|
+
const response = await this.fetchJSON(
|
|
181
|
+
`/api/v1/fs/stat?uri=${encodeURIComponent(uri)}`,
|
|
182
|
+
{},
|
|
183
|
+
{ actorPeerId },
|
|
184
|
+
);
|
|
185
|
+
return response.ok ? response.result : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async forget(uri, recursive = false, actorPeerId) {
|
|
189
|
+
const response = await this.fetchJSON(
|
|
190
|
+
`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=${recursive}`,
|
|
191
|
+
{ method: "DELETE" },
|
|
192
|
+
{ actorPeerId },
|
|
193
|
+
);
|
|
194
|
+
return response.ok;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async addResource(path, reason, actorPeerId) {
|
|
198
|
+
const body = { path };
|
|
199
|
+
if (reason) body.reason = reason;
|
|
200
|
+
const response = await this.fetchJSON("/api/v1/resources", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: JSON.stringify(body),
|
|
203
|
+
}, { timeoutMs: 30000, actorPeerId });
|
|
204
|
+
return response.ok ? response.result : null;
|
|
205
|
+
}
|
|
206
|
+
}
|
package/config.mjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { buildUserAgent, resolveOpenVikingCredentials } from "./shared/credentials.mjs";
|
|
2
|
+
import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs";
|
|
3
|
+
|
|
4
|
+
export const PLUGIN_VERSION = "0.1.0";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
7
|
+
endpoint: "http://127.0.0.1:1933",
|
|
8
|
+
apiKey: "",
|
|
9
|
+
account: "",
|
|
10
|
+
user: "",
|
|
11
|
+
peerId: "",
|
|
12
|
+
userAgent: "",
|
|
13
|
+
workspacePeer: true,
|
|
14
|
+
recallPeerScope: "all",
|
|
15
|
+
recallQueryExpansion: "auto",
|
|
16
|
+
recallQueryExpansionConfigured: false,
|
|
17
|
+
syncTurns: true,
|
|
18
|
+
recallTokenBudget: 2000,
|
|
19
|
+
recallMaxContentChars: 500,
|
|
20
|
+
recallPreferAbstract: true,
|
|
21
|
+
recallLimit: 10,
|
|
22
|
+
recallLimitConfigured: false,
|
|
23
|
+
scoreThreshold: 0.35,
|
|
24
|
+
minQueryLength: 3,
|
|
25
|
+
profileTokenBudget: 10000,
|
|
26
|
+
commitTokenThreshold: 20000,
|
|
27
|
+
commitKeepRecentCount: 10,
|
|
28
|
+
captureToolResults: false,
|
|
29
|
+
captureMode: "semantic",
|
|
30
|
+
captureMaxLength: 24000,
|
|
31
|
+
captureToolMaxChars: 1000000,
|
|
32
|
+
captureAssistantTurns: true,
|
|
33
|
+
requestTimeoutMs: 10000,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function resolveConfig(input = {}, env = process.env, cwd = process.cwd()) {
|
|
37
|
+
const credentials = resolveOpenVikingCredentials(env);
|
|
38
|
+
const explicitPeerId = input.peerId || credentials.peerId;
|
|
39
|
+
const config = {
|
|
40
|
+
...DEFAULT_CONFIG,
|
|
41
|
+
...input,
|
|
42
|
+
endpoint: input.endpoint || credentials.baseUrl || DEFAULT_CONFIG.endpoint,
|
|
43
|
+
apiKey: input.apiKey || credentials.apiKey,
|
|
44
|
+
account: input.account || credentials.account,
|
|
45
|
+
user: input.user || credentials.user,
|
|
46
|
+
peerId: explicitPeerId,
|
|
47
|
+
explicitPeerId,
|
|
48
|
+
userAgent: buildUserAgent("dsh", PLUGIN_VERSION),
|
|
49
|
+
recallLimitConfigured: Object.prototype.hasOwnProperty.call(input, "recallLimit"),
|
|
50
|
+
recallQueryExpansionConfigured: Object.prototype.hasOwnProperty.call(input, "recallQueryExpansion"),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (env.OPENVIKING_WORKSPACE_PEER !== undefined) {
|
|
54
|
+
config.workspacePeer = environmentBoolean(
|
|
55
|
+
env.OPENVIKING_WORKSPACE_PEER,
|
|
56
|
+
config.workspacePeer,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (env.OPENVIKING_RECALL_PEER_SCOPE) {
|
|
60
|
+
config.recallPeerScope = env.OPENVIKING_RECALL_PEER_SCOPE;
|
|
61
|
+
}
|
|
62
|
+
if (env.OPENVIKING_RECALL_QUERY_EXPANSION) {
|
|
63
|
+
config.recallQueryExpansion = env.OPENVIKING_RECALL_QUERY_EXPANSION;
|
|
64
|
+
config.recallQueryExpansionConfigured = true;
|
|
65
|
+
}
|
|
66
|
+
if (env.OPENVIKING_RECALL_LIMIT) {
|
|
67
|
+
config.recallLimit = env.OPENVIKING_RECALL_LIMIT;
|
|
68
|
+
config.recallLimitConfigured = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
config.endpoint = String(config.endpoint || DEFAULT_CONFIG.endpoint).replace(/\/+$/, "");
|
|
72
|
+
config.workspacePeer = config.workspacePeer !== false;
|
|
73
|
+
config.peerId = resolveEffectivePeerId({ cfg: config, cwd }).peerId;
|
|
74
|
+
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all";
|
|
75
|
+
config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto";
|
|
76
|
+
config.recallLimit = clampInteger(config.recallLimit, 1, 50, DEFAULT_CONFIG.recallLimit);
|
|
77
|
+
config.recallMaxContentChars = clampInteger(
|
|
78
|
+
config.recallMaxContentChars,
|
|
79
|
+
100,
|
|
80
|
+
5000,
|
|
81
|
+
DEFAULT_CONFIG.recallMaxContentChars,
|
|
82
|
+
);
|
|
83
|
+
config.recallTokenBudget = clampInteger(
|
|
84
|
+
config.recallTokenBudget,
|
|
85
|
+
200,
|
|
86
|
+
50000,
|
|
87
|
+
DEFAULT_CONFIG.recallTokenBudget,
|
|
88
|
+
);
|
|
89
|
+
config.scoreThreshold = clampNumber(
|
|
90
|
+
config.scoreThreshold,
|
|
91
|
+
0,
|
|
92
|
+
1,
|
|
93
|
+
DEFAULT_CONFIG.scoreThreshold,
|
|
94
|
+
);
|
|
95
|
+
config.minQueryLength = clampInteger(
|
|
96
|
+
config.minQueryLength,
|
|
97
|
+
1,
|
|
98
|
+
64,
|
|
99
|
+
DEFAULT_CONFIG.minQueryLength,
|
|
100
|
+
);
|
|
101
|
+
config.profileTokenBudget = clampInteger(
|
|
102
|
+
config.profileTokenBudget,
|
|
103
|
+
500,
|
|
104
|
+
50000,
|
|
105
|
+
DEFAULT_CONFIG.profileTokenBudget,
|
|
106
|
+
);
|
|
107
|
+
config.commitTokenThreshold = clampInteger(
|
|
108
|
+
config.commitTokenThreshold,
|
|
109
|
+
1000,
|
|
110
|
+
1000000,
|
|
111
|
+
DEFAULT_CONFIG.commitTokenThreshold,
|
|
112
|
+
);
|
|
113
|
+
config.commitKeepRecentCount = clampInteger(
|
|
114
|
+
config.commitKeepRecentCount,
|
|
115
|
+
0,
|
|
116
|
+
1000,
|
|
117
|
+
DEFAULT_CONFIG.commitKeepRecentCount,
|
|
118
|
+
);
|
|
119
|
+
config.captureMaxLength = clampInteger(
|
|
120
|
+
config.captureMaxLength,
|
|
121
|
+
200,
|
|
122
|
+
100000,
|
|
123
|
+
DEFAULT_CONFIG.captureMaxLength,
|
|
124
|
+
);
|
|
125
|
+
config.captureToolMaxChars = clampInteger(
|
|
126
|
+
config.captureToolMaxChars,
|
|
127
|
+
200,
|
|
128
|
+
1000000,
|
|
129
|
+
DEFAULT_CONFIG.captureToolMaxChars,
|
|
130
|
+
);
|
|
131
|
+
config.requestTimeoutMs = clampInteger(
|
|
132
|
+
config.requestTimeoutMs,
|
|
133
|
+
1000,
|
|
134
|
+
120000,
|
|
135
|
+
DEFAULT_CONFIG.requestTimeoutMs,
|
|
136
|
+
);
|
|
137
|
+
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic";
|
|
138
|
+
config.syncTurns = config.syncTurns !== false;
|
|
139
|
+
config.captureAssistantTurns = config.captureAssistantTurns !== false;
|
|
140
|
+
config.captureToolResults = config.captureToolResults === true;
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function environmentBoolean(value, fallback) {
|
|
145
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
146
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
147
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function clampInteger(value, minimum, maximum, fallback) {
|
|
152
|
+
const number = Math.round(Number(value));
|
|
153
|
+
if (!Number.isFinite(number)) return fallback;
|
|
154
|
+
return Math.max(minimum, Math.min(maximum, number));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function clampNumber(value, minimum, maximum, fallback) {
|
|
158
|
+
const number = Number(value);
|
|
159
|
+
if (!Number.isFinite(number)) return fallback;
|
|
160
|
+
return Math.max(minimum, Math.min(maximum, number));
|
|
161
|
+
}
|
package/cordis.patch.yml
ADDED
package/index.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { OpenVikingClient } from "./client.mjs";
|
|
2
|
+
import { resolveConfig } from "./config.mjs";
|
|
3
|
+
import { injectStartupProfile } from "./lifecycle.mjs";
|
|
4
|
+
import { OpenVikingRuntime } from "./runtime.mjs";
|
|
5
|
+
import { registerOpenVikingTools } from "./tools.mjs";
|
|
6
|
+
import { guardVikingUri } from "./uri-guard.mjs";
|
|
7
|
+
|
|
8
|
+
export const name = "openviking-memory";
|
|
9
|
+
export const inject = ["agents", "sessions", "tools"];
|
|
10
|
+
|
|
11
|
+
export function apply(ctx, input = {}) {
|
|
12
|
+
const config = resolveConfig(input);
|
|
13
|
+
const client = new OpenVikingClient(config);
|
|
14
|
+
const runtime = new OpenVikingRuntime(client, config, ctx.logger);
|
|
15
|
+
ctx.provide("openvikingMemory", runtime);
|
|
16
|
+
ctx.effect(
|
|
17
|
+
() => () => runtime.disposeAll(),
|
|
18
|
+
"openvikingMemory.disposeAll()",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
registerOpenVikingTools(ctx, client, runtime);
|
|
22
|
+
|
|
23
|
+
ctx.on("agent/session-start", ({ agent }) => {
|
|
24
|
+
agent.ctx.effect(
|
|
25
|
+
() => () => runtime.dispose(agent.session),
|
|
26
|
+
"openvikingMemory.disposeSession()",
|
|
27
|
+
);
|
|
28
|
+
return injectStartupProfile(agent, runtime);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// prepend: downstream waterfall listeners run first, so this plugin sees
|
|
32
|
+
// the final claimed batch and appends after every other contributor.
|
|
33
|
+
ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
34
|
+
const decision = await next();
|
|
35
|
+
if (decision.kind !== "enter" || signal.aborted) return decision;
|
|
36
|
+
const profile = await runtime.profileMessage(agent);
|
|
37
|
+
if (signal.aborted) return decision;
|
|
38
|
+
const recall = await runtime.recallMessage(agent, decision.messages);
|
|
39
|
+
if (signal.aborted) return decision;
|
|
40
|
+
const additions = [profile, recall].filter(Boolean);
|
|
41
|
+
return additions.length > 0
|
|
42
|
+
? { kind: "enter", messages: [...decision.messages, ...additions] }
|
|
43
|
+
: decision;
|
|
44
|
+
}, { prepend: true });
|
|
45
|
+
|
|
46
|
+
ctx.on("session/event", (session, event) => {
|
|
47
|
+
runtime.capture(session, event);
|
|
48
|
+
runtime.maybeCommit(session, event);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
ctx.on("session/flush", async session => {
|
|
52
|
+
await runtime.flush(session);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
ctx.on("tools/pre-execute", guardVikingUri);
|
|
56
|
+
}
|
package/lifecycle.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function injectStartupProfile(agent, runtime) {
|
|
2
|
+
await runtime.initialize(agent);
|
|
3
|
+
if (agent.status !== "idle") return false;
|
|
4
|
+
const profile = await runtime.profileMessage(agent);
|
|
5
|
+
if (!profile) return false;
|
|
6
|
+
agent.inject(profile);
|
|
7
|
+
return true;
|
|
8
|
+
}
|