@aexol/spectral 0.9.12 → 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.
Files changed (40) hide show
  1. package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
  2. package/dist/memory/hooks/compaction-hook.js +69 -3
  3. package/dist/memory/observer.d.ts +2 -1
  4. package/dist/memory/observer.d.ts.map +1 -1
  5. package/dist/memory/observer.js +2 -1
  6. package/dist/memory/testing/index.d.ts +12 -0
  7. package/dist/memory/testing/index.d.ts.map +1 -0
  8. package/dist/memory/testing/index.js +11 -0
  9. package/dist/memory/testing/populate.d.ts +93 -0
  10. package/dist/memory/testing/populate.d.ts.map +1 -0
  11. package/dist/memory/testing/populate.js +177 -0
  12. package/dist/memory/testing/random-model.d.ts +75 -0
  13. package/dist/memory/testing/random-model.d.ts.map +1 -0
  14. package/dist/memory/testing/random-model.js +237 -0
  15. package/dist/memory/testing/random-text.d.ts +27 -0
  16. package/dist/memory/testing/random-text.d.ts.map +1 -0
  17. package/dist/memory/testing/random-text.js +216 -0
  18. package/dist/memory/testing/seeded-random.d.ts +26 -0
  19. package/dist/memory/testing/seeded-random.d.ts.map +1 -0
  20. package/dist/memory/testing/seeded-random.js +57 -0
  21. package/dist/relay/dispatcher.d.ts.map +1 -1
  22. package/dist/relay/dispatcher.js +2 -0
  23. package/dist/sdk/agent-core/harness/types.d.ts +2 -0
  24. package/dist/sdk/agent-core/harness/types.d.ts.map +1 -1
  25. package/dist/sdk/coding-agent/core/agent-session.d.ts +4 -0
  26. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  27. package/dist/sdk/coding-agent/core/agent-session.js +74 -1
  28. package/dist/sdk/coding-agent/core/extensions/types.d.ts +6 -0
  29. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  30. package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
  31. package/dist/sdk/coding-agent/core/system-prompt.js +1 -0
  32. package/dist/server/agent-bridge.d.ts +0 -1
  33. package/dist/server/agent-bridge.d.ts.map +1 -1
  34. package/dist/server/agent-bridge.js +13 -8
  35. package/dist/server/session-stream.d.ts +2 -0
  36. package/dist/server/session-stream.d.ts.map +1 -1
  37. package/dist/server/session-stream.js +6 -0
  38. package/dist/server/wire.d.ts +15 -0
  39. package/dist/server/wire.d.ts.map +1 -1
  40. 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,EAAoB,MAAM,iCAAiC,CAAC;AAsBtF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AA6B7C,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CA2VhF"}
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: deltaObservationData.reduce((sum, data) => sum + data.records.length, 0),
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({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal, maxTurns: turnLimits.reflectorMaxTurnsPerPass }, workingReflections, workingObservations);
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({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal, maxTurns: turnLimits.prunerMaxTurnsPerPass }, finalReflections, workingObservations, runtime.config.reflectionThresholdTokens);
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,EAA2D,MAAM,4BAA4B,CAAC;AAChH,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,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,CA+GjG;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,MAAM,EAAE,CAEhF"}
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"}
@@ -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 _event of stream) {
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"}