Skip to content

Commit df3905f

Browse files
committed
feat(context-engine): add bounded assemble-time BM recall
1 parent 230e8c6 commit df3905f

File tree

2 files changed

+107
-2
lines changed

2 files changed

+107
-2
lines changed

context-engine/basic-memory-context-engine.test.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { beforeEach, describe, expect, it, jest } from "bun:test"
22
import type { AgentMessage } from "@mariozechner/pi-agent-core"
33
import type { BmClient } from "../bm-client.ts"
44
import type { BasicMemoryConfig } from "../config.ts"
5-
import { BasicMemoryContextEngine } from "./basic-memory-context-engine.ts"
5+
import {
6+
BasicMemoryContextEngine,
7+
MAX_ASSEMBLE_RECALL_CHARS,
8+
} from "./basic-memory-context-engine.ts"
69

710
function makeConfig(
811
overrides?: Partial<BasicMemoryConfig>,
@@ -102,6 +105,31 @@ describe("BasicMemoryContextEngine", () => {
102105
})
103106
})
104107

108+
it("injects bounded BM recall during assemble when bootstrap found context", async () => {
109+
const engine = new BasicMemoryContextEngine(
110+
mockClient as unknown as BmClient,
111+
makeConfig(),
112+
)
113+
114+
await engine.bootstrap({
115+
sessionId: "session-assemble",
116+
sessionFile: "/tmp/session-assemble.jsonl",
117+
})
118+
119+
const result = await engine.assemble({
120+
sessionId: "session-assemble",
121+
messages: makeMessages([{ role: "user", content: "hello" }]),
122+
})
123+
124+
expect(result.messages).toEqual(
125+
makeMessages([{ role: "user", content: "hello" }]),
126+
)
127+
expect(result.systemPromptAddition).toContain("## Active Tasks")
128+
expect(result.systemPromptAddition).toContain("Fix auth rollout")
129+
expect(result.systemPromptAddition).toContain("## Recent Activity")
130+
expect(result.systemPromptAddition).toContain("API review")
131+
})
132+
105133
it("returns a no-op bootstrap result when there is no recall context", async () => {
106134
mockClient.search.mockResolvedValue([])
107135
mockClient.recentActivity.mockResolvedValue([])
@@ -120,6 +148,63 @@ describe("BasicMemoryContextEngine", () => {
120148
bootstrapped: false,
121149
reason: "no recall context found",
122150
})
151+
152+
const result = await engine.assemble({
153+
sessionId: "session-3",
154+
messages: makeMessages([{ role: "user", content: "hello" }]),
155+
})
156+
157+
expect(result).toEqual({
158+
messages: makeMessages([{ role: "user", content: "hello" }]),
159+
estimatedTokens: 0,
160+
})
161+
})
162+
163+
it("keeps assemble recall stable and within the hard bound", async () => {
164+
mockClient.search.mockResolvedValue([
165+
{
166+
title: "Long task",
167+
permalink: "long-task",
168+
content: "A".repeat(4000),
169+
file_path: "memory/tasks/long-task.md",
170+
},
171+
])
172+
mockClient.recentActivity.mockResolvedValue([
173+
{
174+
title: "Long recent item",
175+
permalink: "long-recent-item",
176+
file_path: "memory/long-recent-item.md",
177+
created_at: "2026-03-09T12:00:00Z",
178+
},
179+
])
180+
181+
const engine = new BasicMemoryContextEngine(
182+
mockClient as unknown as BmClient,
183+
makeConfig({
184+
recallPrompt: "P".repeat(4000),
185+
}),
186+
)
187+
188+
await engine.bootstrap({
189+
sessionId: "session-bounded",
190+
sessionFile: "/tmp/session-bounded.jsonl",
191+
})
192+
193+
const first = await engine.assemble({
194+
sessionId: "session-bounded",
195+
messages: makeMessages([{ role: "user", content: "hello" }]),
196+
})
197+
const second = await engine.assemble({
198+
sessionId: "session-bounded",
199+
messages: makeMessages([{ role: "user", content: "hello" }]),
200+
})
201+
202+
expect(first.systemPromptAddition).toBeDefined()
203+
expect(first.systemPromptAddition?.length).toBeLessThanOrEqual(
204+
MAX_ASSEMBLE_RECALL_CHARS,
205+
)
206+
expect(first.systemPromptAddition).toContain("[Basic Memory recall truncated]")
207+
expect(second.systemPromptAddition).toBe(first.systemPromptAddition)
123208
})
124209

125210
it("captures only the current turn after prePromptMessageCount", async () => {

context-engine/basic-memory-context-engine.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,28 @@ import { loadRecallState } from "../hooks/recall.ts"
1515
import { log } from "../logger.ts"
1616

1717
const require = createRequire(import.meta.url)
18+
export const MAX_ASSEMBLE_RECALL_CHARS = 1200
19+
const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]"
1820

1921
interface SessionMemoryState {
2022
recallContext: string
2123
}
2224

25+
function boundRecallContext(context: string): string {
26+
if (context.length <= MAX_ASSEMBLE_RECALL_CHARS) {
27+
return context
28+
}
29+
30+
const trimmed = context
31+
.slice(
32+
0,
33+
Math.max(0, MAX_ASSEMBLE_RECALL_CHARS - TRUNCATED_RECALL_SUFFIX.length),
34+
)
35+
.trimEnd()
36+
37+
return `${trimmed}${TRUNCATED_RECALL_SUFFIX}`
38+
}
39+
2340
type LegacyContextEngineModule = {
2441
LegacyContextEngine: new () => {
2542
compact(params: {
@@ -85,7 +102,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
85102
}
86103

87104
this.sessionState.set(params.sessionId, {
88-
recallContext: recall.context,
105+
recallContext: boundRecallContext(recall.context),
89106
})
90107

91108
log.debug(
@@ -109,9 +126,12 @@ export class BasicMemoryContextEngine implements ContextEngine {
109126
messages: AgentMessage[]
110127
tokenBudget?: number
111128
}): Promise<AssembleResult> {
129+
const state = this.sessionState.get(params.sessionId)
130+
112131
return {
113132
messages: params.messages,
114133
estimatedTokens: 0,
134+
systemPromptAddition: state?.recallContext,
115135
}
116136
}
117137

0 commit comments

Comments
 (0)