@aexol/spectral 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +12 -1
- package/dist/memory/branch.js +409 -0
- package/dist/memory/commands/status.js +91 -0
- package/dist/memory/commands/view.js +61 -0
- package/dist/memory/compaction.js +813 -0
- package/dist/memory/config.js +79 -0
- package/dist/memory/debug-log.js +45 -0
- package/dist/memory/hooks/compaction-hook.js +319 -0
- package/dist/memory/hooks/compaction-trigger.js +87 -0
- package/dist/memory/hooks/observer-trigger.js +108 -0
- package/dist/memory/ids.js +4 -0
- package/dist/memory/index.js +16 -0
- package/dist/memory/model-budget.js +6 -0
- package/dist/memory/observer.js +157 -0
- package/dist/memory/progress.js +134 -0
- package/dist/memory/prompts.js +287 -0
- package/dist/memory/relevance.js +14 -0
- package/dist/memory/runtime.js +62 -0
- package/dist/memory/serialize.js +199 -0
- package/dist/memory/tokens.js +26 -0
- package/dist/memory/tools/recall-observation.js +508 -0
- package/dist/memory/types.js +95 -0
- package/dist/relay/dispatcher.js +2 -0
- package/dist/server/session-stream.js +1 -0
- package/dist/server/storage.js +7 -1
- package/package.json +2 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DEFAULTS, loadConfig } from "./config.js";
|
|
2
|
+
export class Runtime {
|
|
3
|
+
config = { ...DEFAULTS };
|
|
4
|
+
configLoaded = false;
|
|
5
|
+
observerInFlight = false;
|
|
6
|
+
observerPromise = null;
|
|
7
|
+
compactInFlight = false;
|
|
8
|
+
compactHookInFlight = false;
|
|
9
|
+
resolveFailureNotified = false;
|
|
10
|
+
ensureConfig(cwd) {
|
|
11
|
+
if (this.configLoaded)
|
|
12
|
+
return;
|
|
13
|
+
this.config = loadConfig(cwd);
|
|
14
|
+
this.configLoaded = true;
|
|
15
|
+
}
|
|
16
|
+
async resolveModel(ctx) {
|
|
17
|
+
let model = ctx.model;
|
|
18
|
+
if (this.config.compactionModel) {
|
|
19
|
+
const configured = ctx.modelRegistry.find(this.config.compactionModel.provider, this.config.compactionModel.id);
|
|
20
|
+
if (configured) {
|
|
21
|
+
model = configured;
|
|
22
|
+
}
|
|
23
|
+
else if (ctx.hasUI && ctx.ui) {
|
|
24
|
+
ctx.ui.notify(`Observational memory: configured model ${this.config.compactionModel.provider}/${this.config.compactionModel.id} not found, using session model`, "warning");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!model)
|
|
28
|
+
return { ok: false, reason: "no model available (session has no model and no compactionModel configured)" };
|
|
29
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
30
|
+
if (!auth.ok || !auth.apiKey) {
|
|
31
|
+
const provider = model.provider ?? "unknown";
|
|
32
|
+
return { ok: false, reason: `no API key for provider "${provider}"` };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, model, apiKey: auth.apiKey, headers: auth.headers };
|
|
35
|
+
}
|
|
36
|
+
launchObserverTask(ctx, label, work) {
|
|
37
|
+
this.observerInFlight = true;
|
|
38
|
+
// Capture ctx properties synchronously — after `await work()` the extension ctx
|
|
39
|
+
// may be stale (e.g. after ctx.newSession/fork/switchSession/reload), and accessing
|
|
40
|
+
// ctx.hasUI or ctx.ui on a stale proxy throws.
|
|
41
|
+
const hasUI = ctx.hasUI;
|
|
42
|
+
const ui = ctx.ui;
|
|
43
|
+
let promise;
|
|
44
|
+
promise = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
await work();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
50
|
+
if (hasUI && ui)
|
|
51
|
+
ui.notify(`Observational memory: ${label} failed: ${msg}`, "warning");
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
this.observerInFlight = false;
|
|
55
|
+
if (this.observerPromise === promise)
|
|
56
|
+
this.observerPromise = null;
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
this.observerPromise = promise;
|
|
60
|
+
return promise;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
function pad(n) {
|
|
2
|
+
return n.toString().padStart(2, "0");
|
|
3
|
+
}
|
|
4
|
+
function fmtLocal(d) {
|
|
5
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
6
|
+
}
|
|
7
|
+
function formatTimestamp(v) {
|
|
8
|
+
if (v === undefined)
|
|
9
|
+
return "????-??-?? ??:??";
|
|
10
|
+
const d = new Date(v);
|
|
11
|
+
return Number.isNaN(d.getTime()) ? "????-??-?? ??:??" : fmtLocal(d);
|
|
12
|
+
}
|
|
13
|
+
function formatRecallTimestamp(...values) {
|
|
14
|
+
for (const v of values) {
|
|
15
|
+
if (v === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
const d = new Date(v);
|
|
18
|
+
if (!Number.isNaN(d.getTime()))
|
|
19
|
+
return fmtLocal(d);
|
|
20
|
+
}
|
|
21
|
+
return "Unknown time";
|
|
22
|
+
}
|
|
23
|
+
function textAndPlaceholders(content, options = {}) {
|
|
24
|
+
if (typeof content === "string")
|
|
25
|
+
return content;
|
|
26
|
+
if (!Array.isArray(content))
|
|
27
|
+
return "[non-text content omitted]";
|
|
28
|
+
const parts = [];
|
|
29
|
+
for (const block of content) {
|
|
30
|
+
if (!block || typeof block !== "object") {
|
|
31
|
+
parts.push("[non-text content omitted]");
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
35
|
+
parts.push(block.text);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (block.type === "thinking") {
|
|
39
|
+
if (options.omitRedactedThinking && block.redacted === true)
|
|
40
|
+
continue;
|
|
41
|
+
if (options.includeThinking && typeof block.thinking === "string") {
|
|
42
|
+
parts.push(`[thinking: ${block.thinking}]`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
parts.push("[non-text content omitted]");
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (block.type === "toolCall" && typeof block.name === "string") {
|
|
49
|
+
parts.push(`[${block.name}(${JSON.stringify(block.arguments ?? {})})]`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
parts.push("[non-text content omitted]");
|
|
53
|
+
}
|
|
54
|
+
return parts.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function textOnly(content) {
|
|
57
|
+
if (content == null)
|
|
58
|
+
return "";
|
|
59
|
+
if (typeof content === "string")
|
|
60
|
+
return content;
|
|
61
|
+
if (!Array.isArray(content))
|
|
62
|
+
return "";
|
|
63
|
+
return content
|
|
64
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
65
|
+
.map((b) => b.text)
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
export function serializeConversation(messages) {
|
|
69
|
+
return messages
|
|
70
|
+
.map((msg) => {
|
|
71
|
+
const time = formatTimestamp(msg.timestamp);
|
|
72
|
+
if (msg.role === "user") {
|
|
73
|
+
const text = textOnly(msg.content);
|
|
74
|
+
return `[User @ ${time}]: ${text}`;
|
|
75
|
+
}
|
|
76
|
+
if (msg.role === "assistant") {
|
|
77
|
+
const body = textAndPlaceholders(msg.content, {
|
|
78
|
+
includeThinking: true,
|
|
79
|
+
omitRedactedThinking: true,
|
|
80
|
+
})
|
|
81
|
+
.split("\n")
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join("\n");
|
|
84
|
+
if (!body)
|
|
85
|
+
return null;
|
|
86
|
+
return `[Assistant @ ${time}]: ${body}`;
|
|
87
|
+
}
|
|
88
|
+
const text = textOnly(msg.content);
|
|
89
|
+
return `[Tool result for ${msg.toolName} @ ${time}]: ${text}`;
|
|
90
|
+
})
|
|
91
|
+
.filter((line) => line !== null)
|
|
92
|
+
.join("\n\n");
|
|
93
|
+
}
|
|
94
|
+
export function nowTimestamp() {
|
|
95
|
+
return fmtLocal(new Date());
|
|
96
|
+
}
|
|
97
|
+
export const MAX_RECORD_CONTENT_CHARS = 10_000;
|
|
98
|
+
export function truncateRecordContent(content) {
|
|
99
|
+
if (content.length <= MAX_RECORD_CONTENT_CHARS)
|
|
100
|
+
return content;
|
|
101
|
+
const head = content.slice(0, MAX_RECORD_CONTENT_CHARS);
|
|
102
|
+
const dropped = content.length - MAX_RECORD_CONTENT_CHARS;
|
|
103
|
+
return `${head} … [truncated ${dropped} chars]`;
|
|
104
|
+
}
|
|
105
|
+
function renderCustomMessage(entry, options) {
|
|
106
|
+
const time = options.recallFormat ? formatRecallTimestamp(entry.timestamp) : formatTimestamp(entry.timestamp);
|
|
107
|
+
const text = options.recallFormat
|
|
108
|
+
? textAndPlaceholders(entry.content)
|
|
109
|
+
: typeof entry.content === "string"
|
|
110
|
+
? entry.content
|
|
111
|
+
: Array.isArray(entry.content)
|
|
112
|
+
? entry.content
|
|
113
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
114
|
+
.map((b) => b.text)
|
|
115
|
+
.join("\n")
|
|
116
|
+
: "";
|
|
117
|
+
if (options.recallFormat) {
|
|
118
|
+
const origin = entry.customType ? `Custom message (${entry.customType})` : "Custom message";
|
|
119
|
+
return `[${origin} @ ${time}]: ${text}`;
|
|
120
|
+
}
|
|
121
|
+
const tag = entry.customType ? `Custom (${entry.customType})` : "Custom";
|
|
122
|
+
return `[${tag} @ ${time}]: ${text}`;
|
|
123
|
+
}
|
|
124
|
+
export function serializeBranchEntries(entries) {
|
|
125
|
+
const blocks = [];
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (entry.type === "message" && entry.message) {
|
|
128
|
+
const part = serializeConversation([entry.message]);
|
|
129
|
+
if (part)
|
|
130
|
+
blocks.push(part);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (entry.type === "custom_message") {
|
|
134
|
+
blocks.push(renderCustomMessage(entry, { recallFormat: false }));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") {
|
|
138
|
+
const time = formatTimestamp(entry.timestamp);
|
|
139
|
+
blocks.push(`[Branch summary @ ${time}]: ${entry.summary}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return blocks.join("\n\n");
|
|
143
|
+
}
|
|
144
|
+
function isSourceRenderableEntry(entry) {
|
|
145
|
+
return entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary";
|
|
146
|
+
}
|
|
147
|
+
export function serializeSourceAddressedBranchEntries(entries) {
|
|
148
|
+
const blocks = [];
|
|
149
|
+
const sourceEntryIds = [];
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (!entry.id || !isSourceRenderableEntry(entry))
|
|
152
|
+
continue;
|
|
153
|
+
const rendered = serializeBranchEntries([entry]);
|
|
154
|
+
if (!rendered.trim())
|
|
155
|
+
continue;
|
|
156
|
+
sourceEntryIds.push(entry.id);
|
|
157
|
+
blocks.push(`[Source entry id: ${entry.id}]\n${rendered}`);
|
|
158
|
+
}
|
|
159
|
+
return { text: blocks.join("\n\n"), sourceEntryIds };
|
|
160
|
+
}
|
|
161
|
+
function renderRecallMessage(entry) {
|
|
162
|
+
if (!entry.message || typeof entry.message !== "object")
|
|
163
|
+
return null;
|
|
164
|
+
const msg = entry.message;
|
|
165
|
+
const time = formatRecallTimestamp(msg.timestamp, entry.timestamp);
|
|
166
|
+
if (msg.role === "user") {
|
|
167
|
+
return `[User @ ${time}]: ${textAndPlaceholders(msg.content)}`;
|
|
168
|
+
}
|
|
169
|
+
if (msg.role === "assistant") {
|
|
170
|
+
const body = textAndPlaceholders(msg.content, {
|
|
171
|
+
includeThinking: true,
|
|
172
|
+
omitRedactedThinking: true,
|
|
173
|
+
})
|
|
174
|
+
.split("\n")
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join("\n");
|
|
177
|
+
if (!body)
|
|
178
|
+
return null;
|
|
179
|
+
return `[Assistant @ ${time}]: ${body}`;
|
|
180
|
+
}
|
|
181
|
+
return `[Tool result: ${msg.toolName} @ ${time}]: ${textAndPlaceholders(msg.content)}`;
|
|
182
|
+
}
|
|
183
|
+
export function renderRecallSourceEntry(entry) {
|
|
184
|
+
if (entry.type === "message")
|
|
185
|
+
return renderRecallMessage(entry);
|
|
186
|
+
if (entry.type === "custom_message")
|
|
187
|
+
return renderCustomMessage(entry, { recallFormat: true });
|
|
188
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") {
|
|
189
|
+
const time = formatRecallTimestamp(entry.timestamp);
|
|
190
|
+
return `[Branch summary @ ${time}]: ${entry.summary}`;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
export function renderRecallSourceEntries(entries) {
|
|
195
|
+
return entries
|
|
196
|
+
.map(renderRecallSourceEntry)
|
|
197
|
+
.filter((block) => block !== null && block.trim().length > 0)
|
|
198
|
+
.join("\n\n");
|
|
199
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { estimateTokens as estimateMessageTokens } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
export function estimateStringTokens(text) {
|
|
3
|
+
return Math.ceil(text.length / 4);
|
|
4
|
+
}
|
|
5
|
+
export function estimateEntryTokens(entry) {
|
|
6
|
+
if (entry.type === "message" && entry.message) {
|
|
7
|
+
return estimateMessageTokens(entry.message);
|
|
8
|
+
}
|
|
9
|
+
if (entry.type === "custom_message" && entry.content) {
|
|
10
|
+
const content = entry.content;
|
|
11
|
+
if (typeof content === "string")
|
|
12
|
+
return estimateStringTokens(content);
|
|
13
|
+
if (Array.isArray(content)) {
|
|
14
|
+
let total = 0;
|
|
15
|
+
for (const block of content) {
|
|
16
|
+
if (block.type === "text" && block.text)
|
|
17
|
+
total += estimateStringTokens(block.text);
|
|
18
|
+
}
|
|
19
|
+
return total;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") {
|
|
23
|
+
return estimateStringTokens(entry.summary);
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|