@aexol/spectral 0.9.10 → 0.9.13
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/memory/hooks/compaction-hook.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-hook.js +69 -3
- package/dist/memory/observer.d.ts +2 -1
- package/dist/memory/observer.d.ts.map +1 -1
- package/dist/memory/observer.js +2 -1
- package/dist/memory/testing/index.d.ts +12 -0
- package/dist/memory/testing/index.d.ts.map +1 -0
- package/dist/memory/testing/index.js +11 -0
- package/dist/memory/testing/populate.d.ts +93 -0
- package/dist/memory/testing/populate.d.ts.map +1 -0
- package/dist/memory/testing/populate.js +177 -0
- package/dist/memory/testing/random-model.d.ts +75 -0
- package/dist/memory/testing/random-model.d.ts.map +1 -0
- package/dist/memory/testing/random-model.js +237 -0
- package/dist/memory/testing/random-text.d.ts +27 -0
- package/dist/memory/testing/random-text.d.ts.map +1 -0
- package/dist/memory/testing/random-text.js +216 -0
- package/dist/memory/testing/seeded-random.d.ts +26 -0
- package/dist/memory/testing/seeded-random.d.ts.map +1 -0
- package/dist/memory/testing/seeded-random.js +57 -0
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +2 -0
- package/dist/sdk/agent-core/harness/types.d.ts +2 -0
- package/dist/sdk/agent-core/harness/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts +4 -0
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +74 -1
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +6 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +1 -0
- package/dist/server/agent-bridge.d.ts +0 -1
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +13 -8
- package/dist/server/session-stream.d.ts +2 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +6 -0
- package/dist/server/wire.d.ts +15 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction-hook.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/compaction-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"compaction-hook.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/compaction-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+C,MAAM,iCAAiC,CAAC;AAsBjH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAmE7C,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CA8XhF"}
|
|
@@ -20,6 +20,43 @@ function formatReflectorStats(stats) {
|
|
|
20
20
|
function formatPrunerStats(result) {
|
|
21
21
|
return `pruner dropped ${plural(result.droppedIds.length, "observation")} in ${plural(result.passes.length, "pass", "passes")}, stop: ${result.stopReason}`;
|
|
22
22
|
}
|
|
23
|
+
function emitProgressLine(emitProgress, line) {
|
|
24
|
+
emitProgress?.(`${line}\n`);
|
|
25
|
+
}
|
|
26
|
+
function compactToolDetails(details) {
|
|
27
|
+
if (!details || typeof details !== "object")
|
|
28
|
+
return "";
|
|
29
|
+
const r = details;
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (const key of ["added", "accepted", "dropped", "duplicates", "rejected", "remaining", "total"]) {
|
|
32
|
+
const value = r[key];
|
|
33
|
+
if (typeof value === "number")
|
|
34
|
+
parts.push(`${key}=${value}`);
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
parts.push(`${key}=${value.length}`);
|
|
37
|
+
}
|
|
38
|
+
return parts.length ? ` (${parts.join(", ")})` : "";
|
|
39
|
+
}
|
|
40
|
+
function emitAgentLoopProgress(emitProgress, phase, event) {
|
|
41
|
+
if (!emitProgress || !event || typeof event !== "object")
|
|
42
|
+
return;
|
|
43
|
+
const e = event;
|
|
44
|
+
if (e.type === "message_update") {
|
|
45
|
+
const assistantMessageEvent = e.assistantMessageEvent;
|
|
46
|
+
if (assistantMessageEvent?.type === "text_delta" && typeof assistantMessageEvent.delta === "string") {
|
|
47
|
+
emitProgress(assistantMessageEvent.delta);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (e.type === "tool_execution_start" && typeof e.toolName === "string") {
|
|
52
|
+
emitProgressLine(emitProgress, `\n[${phase}] ${e.toolName} started`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (e.type === "tool_execution_end" && typeof e.toolName === "string") {
|
|
56
|
+
const result = e.result && typeof e.result === "object" ? e.result : undefined;
|
|
57
|
+
emitProgressLine(emitProgress, `[${phase}] ${e.toolName} complete${compactToolDetails(result?.details ?? result)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
23
60
|
export function registerCompactionHook(ext, runtime) {
|
|
24
61
|
ext.on("session_before_compact", async (event, ctx) => {
|
|
25
62
|
if (runtime.compactHookInFlight) {
|
|
@@ -34,6 +71,8 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
34
71
|
return await withDebugLogContext({ enabled: runtime.config.debugLog === true, cwd: ctx.cwd, runId }, async () => {
|
|
35
72
|
const { preparation, branchEntries, signal } = event;
|
|
36
73
|
const { firstKeptEntryId, tokensBefore } = preparation;
|
|
74
|
+
const emitProgress = event.emitProgress;
|
|
75
|
+
emitProgressLine(emitProgress, `Observational memory compaction started on ~${tokensBefore.toLocaleString()} tokens.`);
|
|
37
76
|
// Capture ctx properties synchronously — after multiple awaits below,
|
|
38
77
|
// the extension ctx may become stale (e.g. after session replacement/reload).
|
|
39
78
|
const hasUI = ctx.hasUI;
|
|
@@ -47,6 +86,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
47
86
|
turnLimits,
|
|
48
87
|
legacyCompactionMaxToolCalls: runtime.config.compactionMaxToolCalls,
|
|
49
88
|
});
|
|
89
|
+
emitProgressLine(emitProgress, "Resolving memory model…");
|
|
50
90
|
const resolved = await runtime.resolveModel(ctx);
|
|
51
91
|
if (!resolved.ok) {
|
|
52
92
|
debugLog("compaction.model_unavailable", { reason: resolved.reason });
|
|
@@ -56,8 +96,10 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
56
96
|
return { cancel: true };
|
|
57
97
|
}
|
|
58
98
|
runtime.resolveFailureNotified = false;
|
|
99
|
+
emitProgressLine(emitProgress, "Memory model ready.");
|
|
59
100
|
let entries = branchEntries;
|
|
60
101
|
if (runtime.observerPromise) {
|
|
102
|
+
emitProgressLine(emitProgress, "Waiting for in-flight observer…");
|
|
61
103
|
try {
|
|
62
104
|
await runtime.observerPromise;
|
|
63
105
|
}
|
|
@@ -72,6 +114,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
72
114
|
pendingObservations: memoryState.pendingObs.length,
|
|
73
115
|
reflections: memoryState.reflections.length,
|
|
74
116
|
});
|
|
117
|
+
emitProgressLine(emitProgress, `Loaded memory state: ${memoryState.committedObs.length} committed observations, ${memoryState.pendingObs.length} pending observations, ${memoryState.reflections.length} reflections.`);
|
|
75
118
|
let gapObservationData = null;
|
|
76
119
|
const gap = gapRawEntries(entries, firstKeptEntryId);
|
|
77
120
|
if (gap.length > 0) {
|
|
@@ -103,6 +146,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
103
146
|
chunk: gapChunk,
|
|
104
147
|
allowedSourceEntryIds: sourceEntryIds,
|
|
105
148
|
signal,
|
|
149
|
+
onEvent: (agentEvent) => emitAgentLoopProgress(emitProgress, "observer", agentEvent),
|
|
106
150
|
maxTurns: turnLimits.observerMaxTurnsPerRun,
|
|
107
151
|
});
|
|
108
152
|
const gapPromise = gapCall.then(() => undefined, () => undefined);
|
|
@@ -125,6 +169,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
125
169
|
records,
|
|
126
170
|
});
|
|
127
171
|
ext.appendEntry(OBSERVATION_CUSTOM_TYPE, gapObservationData);
|
|
172
|
+
emitProgressLine(emitProgress, `Sync catch-up recorded ${records.length} observation${records.length === 1 ? "" : "s"} (~${observationTokens.toLocaleString()} tokens).`);
|
|
128
173
|
if (hasUI && ui)
|
|
129
174
|
ui.notify(`Observational memory: sync catch-up recorded ${records.length} observation${records.length === 1 ? "" : "s"} (~${observationTokens.toLocaleString()} tokens)`, "info");
|
|
130
175
|
}
|
|
@@ -152,13 +197,15 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
152
197
|
const deltaObservationData = collectObservationsByCoverage(entries, priorFirstKeptEntryId, firstKeptEntryId);
|
|
153
198
|
if (gapObservationData)
|
|
154
199
|
deltaObservationData.push(gapObservationData);
|
|
200
|
+
const deltaObservationRecords = deltaObservationData.reduce((sum, data) => sum + data.records.length, 0);
|
|
155
201
|
debugLog("compaction.delta", {
|
|
156
202
|
priorFirstKeptEntryId,
|
|
157
203
|
firstKeptEntryId,
|
|
158
204
|
deltaObservationEntries: deltaObservationData.length,
|
|
159
|
-
deltaObservationRecords
|
|
205
|
+
deltaObservationRecords,
|
|
160
206
|
gapObservationRecords: gapObservationData?.records.length ?? 0,
|
|
161
207
|
});
|
|
208
|
+
emitProgressLine(emitProgress, `Compaction delta contains ${deltaObservationRecords} observation${deltaObservationRecords === 1 ? "" : "s"}.`);
|
|
162
209
|
if (deltaObservationData.length === 0) {
|
|
163
210
|
// No new observations since last compaction. If we have existing memory,
|
|
164
211
|
// carry it forward in a no-op compaction so it survives spectral's compaction.
|
|
@@ -175,6 +222,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
175
222
|
return { cancel: true };
|
|
176
223
|
}
|
|
177
224
|
// Carry forward existing memory without running reflector/pruner
|
|
225
|
+
emitProgressLine(emitProgress, "No new observations; carrying forward existing memory.");
|
|
178
226
|
const workingReflections = migrateLegacyReflections(memoryState.reflections);
|
|
179
227
|
debugLog("compaction.no_delta_carry_forward", {
|
|
180
228
|
observations: memoryState.committedObs.length,
|
|
@@ -206,6 +254,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
206
254
|
...deltaObservationData.flatMap((d) => d.records),
|
|
207
255
|
];
|
|
208
256
|
const observationTokens = observationPoolTokens(workingObservations);
|
|
257
|
+
emitProgressLine(emitProgress, `Working set: ${workingObservations.length} observations, ${workingReflections.length} reflections, ~${observationTokens.toLocaleString()} observation tokens.`);
|
|
209
258
|
debugLog("compaction.reflect_prune.gate", {
|
|
210
259
|
observationTokens,
|
|
211
260
|
reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
|
|
@@ -224,8 +273,16 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
224
273
|
});
|
|
225
274
|
if (hasUI)
|
|
226
275
|
ui?.notify("Observational memory: running reflector + pruner...", "info");
|
|
276
|
+
emitProgressLine(emitProgress, "Running reflector + pruner…");
|
|
227
277
|
const coverageBefore = coverageTagCounts(workingReflections, workingObservations);
|
|
228
|
-
const reflectorResult = await runReflector({
|
|
278
|
+
const reflectorResult = await runReflector({
|
|
279
|
+
model: resolved.model,
|
|
280
|
+
apiKey: resolved.apiKey,
|
|
281
|
+
headers: resolved.headers,
|
|
282
|
+
signal,
|
|
283
|
+
onEvent: (agentEvent) => emitAgentLoopProgress(emitProgress, "reflector", agentEvent),
|
|
284
|
+
maxTurns: turnLimits.reflectorMaxTurnsPerPass,
|
|
285
|
+
}, workingReflections, workingObservations, (pass, maxPasses) => emitProgressLine(emitProgress, `Reflector pass ${pass}/${maxPasses} started.`));
|
|
229
286
|
finalReflections = reflectorResult.reflections;
|
|
230
287
|
const coverageAfter = coverageTagCounts(finalReflections, workingObservations);
|
|
231
288
|
debugLog("compaction.reflector.result", {
|
|
@@ -235,7 +292,14 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
235
292
|
beforeReflections: workingReflections.length,
|
|
236
293
|
afterReflections: finalReflections.length,
|
|
237
294
|
});
|
|
238
|
-
const prunerResult = await runPruner({
|
|
295
|
+
const prunerResult = await runPruner({
|
|
296
|
+
model: resolved.model,
|
|
297
|
+
apiKey: resolved.apiKey,
|
|
298
|
+
headers: resolved.headers,
|
|
299
|
+
signal,
|
|
300
|
+
onEvent: (agentEvent) => emitAgentLoopProgress(emitProgress, "pruner", agentEvent),
|
|
301
|
+
maxTurns: turnLimits.prunerMaxTurnsPerPass,
|
|
302
|
+
}, finalReflections, workingObservations, runtime.config.reflectionThresholdTokens, (pass, maxPasses) => emitProgressLine(emitProgress, `Pruner pass ${pass}/${maxPasses} started.`));
|
|
239
303
|
finalObservations = prunerResult.observations;
|
|
240
304
|
debugLog("compaction.pruner.result", {
|
|
241
305
|
stopReason: prunerResult.stopReason,
|
|
@@ -259,6 +323,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
259
323
|
ui?.notify(`Observational memory: reflect/prune failed: ${msg}`, "warning");
|
|
260
324
|
}
|
|
261
325
|
}
|
|
326
|
+
emitProgressLine(emitProgress, `Rendering final memory summary from ${finalObservations.length} observations and ${finalReflections.length} reflections…`);
|
|
262
327
|
const summary = renderSummary(finalReflections, finalObservations);
|
|
263
328
|
if (finalObservations.length === 0) {
|
|
264
329
|
throw new Error("invariant violated: finalObservations empty after delta guard");
|
|
@@ -275,6 +340,7 @@ export function registerCompactionHook(ext, runtime) {
|
|
|
275
340
|
firstKeptEntryId,
|
|
276
341
|
tokensBefore,
|
|
277
342
|
});
|
|
343
|
+
emitProgressLine(emitProgress, `Compaction assembled — ${finalObservations.length} observation${finalObservations.length === 1 ? "" : "s"}, ${finalReflections.length} reflection${finalReflections.length === 1 ? "" : "s"}.`);
|
|
278
344
|
if (hasUI)
|
|
279
345
|
ui?.notify(`Observational memory: compaction assembled — ${finalObservations.length} observation${finalObservations.length === 1 ? "" : "s"}, ${finalReflections.length} reflection${finalReflections.length === 1 ? "" : "s"}`, "info");
|
|
280
346
|
// Promote reflections as project observations (cross-session durable memory).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentLoop } from "../sdk/agent-core/index.js";
|
|
1
|
+
import { agentLoop, type AgentEvent } from "../sdk/agent-core/index.js";
|
|
2
2
|
import type { Model } from "../sdk/ai/index.js";
|
|
3
3
|
import type { ObservationRecord } from "./types.js";
|
|
4
4
|
interface RunObserverArgs {
|
|
@@ -11,6 +11,7 @@ interface RunObserverArgs {
|
|
|
11
11
|
allowedSourceEntryIds: string[];
|
|
12
12
|
signal?: AbortSignal;
|
|
13
13
|
agentLoop?: typeof agentLoop;
|
|
14
|
+
onEvent?: (event: AgentEvent) => void;
|
|
14
15
|
maxTurns?: number;
|
|
15
16
|
}
|
|
16
17
|
export declare const OBSERVATION_TIMESTAMP_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observer.d.ts","sourceRoot":"","sources":["../../src/memory/observer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"observer.d.ts","sourceRoot":"","sources":["../../src/memory/observer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAqB,KAAK,UAAU,EAAwC,MAAM,4BAA4B,CAAC;AACjI,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAOzD,OAAO,KAAK,EAAE,iBAAiB,EAAa,MAAM,YAAY,CAAC;AAE/D,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AASD,eAAO,MAAM,6BAA6B,mDAAmD,CAAC;AAkC9F,wBAAgB,uBAAuB,CACtC,cAAc,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAC7C,qBAAqB,EAAE,SAAS,MAAM,EAAE,GACtC,MAAM,EAAE,GAAG,SAAS,CAYtB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,EAAE,GAAG,SAAS,CAAC,CAgHjG;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,MAAM,EAAE,CAEhF"}
|
package/dist/memory/observer.js
CHANGED
|
@@ -144,8 +144,9 @@ ${conversation}`;
|
|
|
144
144
|
};
|
|
145
145
|
const loop = args.agentLoop ?? agentLoop;
|
|
146
146
|
const stream = loop(prompts, context, config, signal);
|
|
147
|
-
for await (const
|
|
147
|
+
for await (const event of stream) {
|
|
148
148
|
// Drain events; the tool's execute already collects records.
|
|
149
|
+
args.onEvent?.(event);
|
|
149
150
|
}
|
|
150
151
|
await stream.result();
|
|
151
152
|
if (accumulated.size === 0)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing utilities for the observational memory pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic, seeded random message generation and a faux LLM model
|
|
5
|
+
* that produces structured tool-call responses for observer, reflector, and pruner,
|
|
6
|
+
* enabling end-to-end pipeline testing without real API costs.
|
|
7
|
+
*/
|
|
8
|
+
export { SeededRandom } from "./seeded-random.js";
|
|
9
|
+
export { generateRandomText, generateObservationContent, generateReflectionContent, generateTimestamp, type RandomTextOptions, } from "./random-text.js";
|
|
10
|
+
export { createRandomModel, type RandomModelConfig, type RandomModelRegistration, } from "./random-model.js";
|
|
11
|
+
export { generateRandomMessages, populateSession, createInMemoryInjector, type PopulateSessionOptions, type PopulateResult, } from "./populate.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/memory/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACN,kBAAkB,EAClB,0BAA0B,EAC1B,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,iBAAiB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC5B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,sBAAsB,EACtB,eAAe,EACf,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,GACnB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing utilities for the observational memory pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic, seeded random message generation and a faux LLM model
|
|
5
|
+
* that produces structured tool-call responses for observer, reflector, and pruner,
|
|
6
|
+
* enabling end-to-end pipeline testing without real API costs.
|
|
7
|
+
*/
|
|
8
|
+
export { SeededRandom } from "./seeded-random.js";
|
|
9
|
+
export { generateRandomText, generateObservationContent, generateReflectionContent, generateTimestamp, } from "./random-text.js";
|
|
10
|
+
export { createRandomModel, } from "./random-model.js";
|
|
11
|
+
export { generateRandomMessages, populateSession, createInMemoryInjector, } from "./populate.js";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session population utilities for testing compaction, reflection, and observation.
|
|
3
|
+
*
|
|
4
|
+
* Populates sessions with deterministic random messages to reach target token
|
|
5
|
+
* counts, enabling efficient stress testing of the full memory pipeline.
|
|
6
|
+
*/
|
|
7
|
+
import type { Message } from "../../sdk/ai/types.js";
|
|
8
|
+
export interface PopulateSessionOptions {
|
|
9
|
+
/** Seed for reproducible sequences. Default: 42. */
|
|
10
|
+
seed?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Target total tokens in the session.
|
|
13
|
+
* Messages are generated until this approximate token count is reached.
|
|
14
|
+
* Token estimation: 4 chars ≈ 1 token.
|
|
15
|
+
* Default: 20000.
|
|
16
|
+
*/
|
|
17
|
+
targetTokens?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Average tokens per user message. Default: 200.
|
|
20
|
+
*/
|
|
21
|
+
avgUserTokens?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Average tokens per assistant message. Default: 400.
|
|
24
|
+
*/
|
|
25
|
+
avgAssistantTokens?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Ratio of user:assistant messages. Higher = more user messages per assistant.
|
|
28
|
+
* Default: 0.8 (80% user, 20% assistant).
|
|
29
|
+
*/
|
|
30
|
+
userMessageRatio?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Include tool result messages between assistant turns. Default: false.
|
|
33
|
+
*/
|
|
34
|
+
includeToolResults?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Callback for each generated message. Useful for progress reporting.
|
|
37
|
+
*/
|
|
38
|
+
onMessage?: (index: number, totalEstimated: number, role: string, charCount: number) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface PopulateResult {
|
|
41
|
+
/** Total messages generated. */
|
|
42
|
+
messageCount: number;
|
|
43
|
+
/** Estimated total tokens (chars / 4). */
|
|
44
|
+
estimatedTokens: number;
|
|
45
|
+
/** Total characters generated. */
|
|
46
|
+
totalChars: number;
|
|
47
|
+
/** Breakdown by role. */
|
|
48
|
+
breakdown: {
|
|
49
|
+
user: number;
|
|
50
|
+
assistant: number;
|
|
51
|
+
toolResult: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
interface SessionInjector {
|
|
55
|
+
appendMessage(message: Message): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate an array of random messages totaling approximately `targetTokens` tokens.
|
|
59
|
+
*
|
|
60
|
+
* Does NOT require a session — returns the message array for injection into
|
|
61
|
+
* any session or storage system.
|
|
62
|
+
*
|
|
63
|
+
* Token estimation: Math.ceil(text.length / 4).
|
|
64
|
+
*/
|
|
65
|
+
export declare function generateRandomMessages(options?: PopulateSessionOptions): {
|
|
66
|
+
messages: Message[];
|
|
67
|
+
result: PopulateResult;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Populate a session-like object with random messages.
|
|
71
|
+
*
|
|
72
|
+
* The injector must implement `appendMessage(message)`.
|
|
73
|
+
* This works with any session manager that exposes message-append capability.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const injector = {
|
|
78
|
+
* appendMessage(msg: Message) { sessionManager.appendMessage(msg); }
|
|
79
|
+
* };
|
|
80
|
+
* const result = await populateSession(injector, { targetTokens: 20000, seed: 42 });
|
|
81
|
+
* console.log(`Populated ${result.messageCount} messages, ~${result.estimatedTokens} tokens`);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function populateSession(injector: SessionInjector, options?: PopulateSessionOptions): Promise<PopulateResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Create an in-memory message array injector. Useful for testing without a real session.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createInMemoryInjector(): {
|
|
89
|
+
messages: Message[];
|
|
90
|
+
injector: SessionInjector;
|
|
91
|
+
};
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=populate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"populate.d.ts","sourceRoot":"","sources":["../../../src/memory/testing/populate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAW,OAAO,EAA4C,MAAM,uBAAuB,CAAC;AAMxG,MAAM,WAAW,sBAAsB;IACtC,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7F;AAED,MAAM,WAAW,cAAc;IAC9B,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,eAAe,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAcD,UAAU,eAAe;IACxB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD;AA0CD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,CA8C5H;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACpC,QAAQ,EAAE,eAAe,EACzB,OAAO,GAAE,sBAA2B,GAClC,OAAO,CAAC,cAAc,CAAC,CAMzB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAA;CAAE,CAU3F"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session population utilities for testing compaction, reflection, and observation.
|
|
3
|
+
*
|
|
4
|
+
* Populates sessions with deterministic random messages to reach target token
|
|
5
|
+
* counts, enabling efficient stress testing of the full memory pipeline.
|
|
6
|
+
*/
|
|
7
|
+
import { fauxAssistantMessage } from "../../sdk/ai/providers/faux.js";
|
|
8
|
+
import { SeededRandom } from "./seeded-random.js";
|
|
9
|
+
import { generateRandomText, generateObservationContent } from "./random-text.js";
|
|
10
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
11
|
+
const DEFAULT_POPULATE_OPTIONS = {
|
|
12
|
+
seed: 42,
|
|
13
|
+
targetTokens: 20000,
|
|
14
|
+
avgUserTokens: 200,
|
|
15
|
+
avgAssistantTokens: 400,
|
|
16
|
+
userMessageRatio: 0.8,
|
|
17
|
+
includeToolResults: false,
|
|
18
|
+
onMessage: () => { },
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Generate a single random user message.
|
|
22
|
+
*/
|
|
23
|
+
function generateUserMessage(rng, targetChars) {
|
|
24
|
+
const text = generateRandomText(rng, { targetChars });
|
|
25
|
+
return {
|
|
26
|
+
role: "user",
|
|
27
|
+
content: [{ type: "text", text }],
|
|
28
|
+
timestamp: Date.now() - rng.nextInt(0, 7 * 24 * 60 * 60 * 1000),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate a single random assistant message.
|
|
33
|
+
*/
|
|
34
|
+
function generateAssistantMessage(rng, targetChars) {
|
|
35
|
+
const text = generateRandomText(rng, { targetChars });
|
|
36
|
+
return fauxAssistantMessage(text, {
|
|
37
|
+
stopReason: rng.chance(0.9) ? "stop" : "length",
|
|
38
|
+
timestamp: Date.now() - rng.nextInt(0, 7 * 24 * 60 * 60 * 1000),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generate a single random tool result message.
|
|
43
|
+
*/
|
|
44
|
+
function generateToolResultMessage(rng) {
|
|
45
|
+
const text = generateObservationContent(rng);
|
|
46
|
+
return {
|
|
47
|
+
role: "toolResult",
|
|
48
|
+
toolCallId: `tool_${rng.hexId(8)}`,
|
|
49
|
+
toolName: rng.pick(["read", "bash", "grep", "write", "execute"]),
|
|
50
|
+
content: [{ type: "text", text }],
|
|
51
|
+
isError: rng.chance(0.1),
|
|
52
|
+
timestamp: Date.now() - rng.nextInt(0, 7 * 24 * 60 * 60 * 1000),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Generate an array of random messages totaling approximately `targetTokens` tokens.
|
|
58
|
+
*
|
|
59
|
+
* Does NOT require a session — returns the message array for injection into
|
|
60
|
+
* any session or storage system.
|
|
61
|
+
*
|
|
62
|
+
* Token estimation: Math.ceil(text.length / 4).
|
|
63
|
+
*/
|
|
64
|
+
export function generateRandomMessages(options = {}) {
|
|
65
|
+
const opts = { ...DEFAULT_POPULATE_OPTIONS, ...options };
|
|
66
|
+
const rng = new SeededRandom(opts.seed);
|
|
67
|
+
const targetChars = opts.targetTokens * 4;
|
|
68
|
+
const messages = [];
|
|
69
|
+
let totalChars = 0;
|
|
70
|
+
const breakdown = { user: 0, assistant: 0, toolResult: 0 };
|
|
71
|
+
let index = 0;
|
|
72
|
+
while (totalChars < targetChars) {
|
|
73
|
+
const remainingChars = targetChars - totalChars;
|
|
74
|
+
const isUser = rng.next() < opts.userMessageRatio;
|
|
75
|
+
let message;
|
|
76
|
+
if (isUser) {
|
|
77
|
+
const chars = Math.min(opts.avgUserTokens * 4, remainingChars);
|
|
78
|
+
const variedChars = Math.max(20, chars + rng.nextInt(-Math.floor(chars * 0.3), Math.floor(chars * 0.3)));
|
|
79
|
+
message = generateUserMessage(rng, variedChars);
|
|
80
|
+
breakdown.user++;
|
|
81
|
+
}
|
|
82
|
+
else if (opts.includeToolResults && rng.chance(0.15)) {
|
|
83
|
+
message = generateToolResultMessage(rng);
|
|
84
|
+
breakdown.toolResult++;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const chars = Math.min(opts.avgAssistantTokens * 4, remainingChars);
|
|
88
|
+
const variedChars = Math.max(20, chars + rng.nextInt(-Math.floor(chars * 0.3), Math.floor(chars * 0.3)));
|
|
89
|
+
message = generateAssistantMessage(rng, variedChars);
|
|
90
|
+
breakdown.assistant++;
|
|
91
|
+
}
|
|
92
|
+
const messageChars = estimateMessageChars(message);
|
|
93
|
+
totalChars += messageChars;
|
|
94
|
+
messages.push(message);
|
|
95
|
+
index++;
|
|
96
|
+
opts.onMessage(index, Math.ceil(totalChars / 4), message.role, messageChars);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
messages,
|
|
100
|
+
result: {
|
|
101
|
+
messageCount: messages.length,
|
|
102
|
+
estimatedTokens: Math.ceil(totalChars / 4),
|
|
103
|
+
totalChars,
|
|
104
|
+
breakdown,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Populate a session-like object with random messages.
|
|
110
|
+
*
|
|
111
|
+
* The injector must implement `appendMessage(message)`.
|
|
112
|
+
* This works with any session manager that exposes message-append capability.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const injector = {
|
|
117
|
+
* appendMessage(msg: Message) { sessionManager.appendMessage(msg); }
|
|
118
|
+
* };
|
|
119
|
+
* const result = await populateSession(injector, { targetTokens: 20000, seed: 42 });
|
|
120
|
+
* console.log(`Populated ${result.messageCount} messages, ~${result.estimatedTokens} tokens`);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export async function populateSession(injector, options = {}) {
|
|
124
|
+
const { messages, result } = generateRandomMessages(options);
|
|
125
|
+
for (const message of messages) {
|
|
126
|
+
await injector.appendMessage(message);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create an in-memory message array injector. Useful for testing without a real session.
|
|
132
|
+
*/
|
|
133
|
+
export function createInMemoryInjector() {
|
|
134
|
+
const messages = [];
|
|
135
|
+
return {
|
|
136
|
+
messages,
|
|
137
|
+
injector: {
|
|
138
|
+
appendMessage(message) {
|
|
139
|
+
messages.push(message);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
145
|
+
function estimateMessageChars(message) {
|
|
146
|
+
if (message.role === "user" || message.role === "toolResult") {
|
|
147
|
+
const content = message.content;
|
|
148
|
+
if (typeof content === "string")
|
|
149
|
+
return content.length;
|
|
150
|
+
if (Array.isArray(content)) {
|
|
151
|
+
return content.reduce((sum, block) => {
|
|
152
|
+
if (block.type === "text" && "text" in block) {
|
|
153
|
+
return sum + block.text.length;
|
|
154
|
+
}
|
|
155
|
+
return sum;
|
|
156
|
+
}, 0);
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
if (message.role === "assistant") {
|
|
161
|
+
const content = message.content;
|
|
162
|
+
if (typeof content === "string")
|
|
163
|
+
return content.length;
|
|
164
|
+
if (Array.isArray(content)) {
|
|
165
|
+
return content.reduce((sum, block) => {
|
|
166
|
+
if (block.type === "text" && block.text)
|
|
167
|
+
return sum + block.text.length;
|
|
168
|
+
if (block.type === "thinking" && block.thinking)
|
|
169
|
+
return sum + block.thinking.length;
|
|
170
|
+
if (block.type === "toolCall" && block.arguments)
|
|
171
|
+
return sum + JSON.stringify(block.arguments).length;
|
|
172
|
+
return sum;
|
|
173
|
+
}, 0);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random testing model built on the faux provider.
|
|
3
|
+
*
|
|
4
|
+
* Generates deterministic, structured LLM-like responses that exercise the
|
|
5
|
+
* full observational memory pipeline (observer → reflector → pruner → compaction)
|
|
6
|
+
* without real LLM API calls.
|
|
7
|
+
*
|
|
8
|
+
* The model inspects the tool definitions in the context to decide what kind
|
|
9
|
+
* of response to generate:
|
|
10
|
+
* - `record_observations` → generates observation batches with source entry IDs
|
|
11
|
+
* - `record_reflections` → generates reflection proposals with supporting IDs
|
|
12
|
+
* - `drop_observations` → generates drop-lists with observation IDs
|
|
13
|
+
* - No tool / unknown → generates plain text
|
|
14
|
+
*/
|
|
15
|
+
import { type FauxProviderRegistration } from "../../sdk/ai/providers/faux.js";
|
|
16
|
+
import { SeededRandom } from "./seeded-random.js";
|
|
17
|
+
export interface RandomModelConfig {
|
|
18
|
+
/** Seed for reproducible sequences. Default: 42. */
|
|
19
|
+
seed?: number;
|
|
20
|
+
/** Target character count per assistant text response. Default: 500. */
|
|
21
|
+
textTargetChars?: number;
|
|
22
|
+
/** Number of observation batches per observer run. Default: 2-4 (random). */
|
|
23
|
+
observationBatches?: {
|
|
24
|
+
min: number;
|
|
25
|
+
max: number;
|
|
26
|
+
};
|
|
27
|
+
/** Observations per batch. Default: 2-5 (random). */
|
|
28
|
+
observationsPerBatch?: {
|
|
29
|
+
min: number;
|
|
30
|
+
max: number;
|
|
31
|
+
};
|
|
32
|
+
/** Number of reflection batches per reflector pass. Default: 1-3. */
|
|
33
|
+
reflectionBatches?: {
|
|
34
|
+
min: number;
|
|
35
|
+
max: number;
|
|
36
|
+
};
|
|
37
|
+
/** Reflections per batch. Default: 1-3. */
|
|
38
|
+
reflectionsPerBatch?: {
|
|
39
|
+
min: number;
|
|
40
|
+
max: number;
|
|
41
|
+
};
|
|
42
|
+
/** Number of drop calls per pruner pass. Default: 1-3. */
|
|
43
|
+
prunerDropCalls?: {
|
|
44
|
+
min: number;
|
|
45
|
+
max: number;
|
|
46
|
+
};
|
|
47
|
+
/** IDs to drop per call. Default: 1-5. */
|
|
48
|
+
prunerIdsPerCall?: {
|
|
49
|
+
min: number;
|
|
50
|
+
max: number;
|
|
51
|
+
};
|
|
52
|
+
/** Tokens per second for streaming simulation. Default: 0 (instant). */
|
|
53
|
+
tokensPerSecond?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface RandomModelRegistration {
|
|
56
|
+
provider: FauxProviderRegistration;
|
|
57
|
+
config: Required<RandomModelConfig>;
|
|
58
|
+
/** The seeded RNG for direct use by tests. */
|
|
59
|
+
rng: SeededRandom;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Register a deterministic random model as a faux provider.
|
|
63
|
+
*
|
|
64
|
+
* The model automatically detects which tools are in the context and generates
|
|
65
|
+
* appropriate structured responses for the observer, reflector, and pruner.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const testModel = createRandomModel({ seed: 123 });
|
|
70
|
+
* // Use testModel.provider.getModel() in agentLoop calls
|
|
71
|
+
* // After testing: testModel.provider.unregister()
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function createRandomModel(config?: RandomModelConfig): RandomModelRegistration;
|
|
75
|
+
//# sourceMappingURL=random-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random-model.d.ts","sourceRoot":"","sources":["../../../src/memory/testing/random-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAON,KAAK,wBAAwB,EAC7B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD,MAAM,WAAW,iBAAiB;IACjC,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,qDAAqD;IACrD,oBAAoB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,qEAAqE;IACrE,iBAAiB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,0DAA0D;IAC1D,eAAe,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AA4MD,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,wBAAwB,CAAC;IACnC,MAAM,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IACpC,8CAA8C;IAC9C,GAAG,EAAE,YAAY,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,iBAAsB,GAAG,uBAAuB,CA6CzF"}
|