Files
oh-my-pi/packages/coding-agent/src/tools/memory-reflect.ts
T
roboomp 34005e6656 fix(coding-agent/hindsight): reacted to live bank scope changes and flushed before dispose
Mid-session edits to hindsight.bankId / bankIdPrefix / scoping kept the
active HindsightSessionState pinned to the bank selected at session
start, so retain/recall/reflect calls landed in the stale bank. Settings
hooks now fire onHindsightScopeChanged; the backend rebuilds the
primary state against the recomputed scope, disposing the previous one
after flushing its queue so queued tool-initiated retains still land in
the bank they were enqueued for.

Also:
- Renamed ensureBankMission to ensureBankExists. The old version
  skipped creation entirely when bankMission was blank, so the first
  mental-model POST (auto-seed) could land against a never-PUT bank.
  Bank creation is now idempotent and unconditional, and runs before
  mental-model bootstrap.
- Fixed AgentSession.dispose to flush the retain queue BEFORE clearing
  the session state pointer. Reversed, HindsightRetainQueue.#doFlush's
  identity guard would see the cleared pointer and drop the spliced
  batch with a 'session vanished' warning.
- Snapshotted hindsightScopeCallbacks before iterating because each
  rebuild subscribes a fresh callback inside the same fire; iterating
  the live Set would spin.

Fixes #1902
2026-06-05 05:25:45 +00:00

89 lines
3.2 KiB
TypeScript

import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
import * as z from "zod/v4";
import { ensureBankExists } from "../hindsight/bank";
import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
import type { ToolSession } from ".";
const memoryReflectSchema = z.object({
query: z.string().describe("question to answer"),
context: z.string().describe("optional context").optional(),
});
export type MemoryReflectParams = z.infer<typeof memoryReflectSchema>;
export class MemoryReflectTool implements AgentTool<typeof memoryReflectSchema> {
readonly name = "reflect";
readonly approval = "read" as const;
readonly label = "Reflect";
readonly description = reflectDescription;
readonly parameters = memoryReflectSchema;
readonly strict = true;
readonly loadMode = "discoverable";
readonly summary = "Synthesize an answer from long-term memory";
constructor(private readonly session: ToolSession) {}
static createIf(session: ToolSession): MemoryReflectTool | null {
const backend = session.settings.get("memory.backend");
if (backend !== "hindsight" && backend !== "mnemopi") return null;
return new MemoryReflectTool(session);
}
async execute(_id: string, params: MemoryReflectParams, signal?: AbortSignal): Promise<AgentToolResult> {
return untilAborted(signal, async () => {
const backend = this.session.settings.get("memory.backend");
if (backend === "mnemopi") {
const state = this.session.getMnemopiSessionState?.();
if (!state) {
throw new Error("Mnemopi backend is not initialised for this session.");
}
try {
const query = params.context?.trim()
? `${params.query.trim()}\n\nAdditional context:\n${params.context.trim()}`
: params.query;
const results = await state.recallResultsScoped(query);
if (results.length === 0) {
return {
content: [{ type: "text", text: "No relevant information found to reflect on." }],
details: {},
};
}
const summary = state.formatContextScoped(results);
return {
content: [{ type: "text", text: `Based on recalled memories:\n\n${summary}` }],
details: {},
};
} catch (err) {
logger.warn("reflect failed", { backend: "mnemopi", bank: state.config.bank, error: String(err) });
throw err instanceof Error ? err : new Error(String(err));
}
}
const state = this.session.getHindsightSessionState?.();
if (!state) {
throw new Error("Hindsight backend is not initialised for this session.");
}
try {
await ensureBankExists(state.client, state.bankId, state.config, state.banksSet);
const response = await state.client.reflect(state.bankId, params.query, {
context: params.context,
budget: state.config.recallBudget,
tags: state.recallTags,
tagsMatch: state.recallTagsMatch,
});
const text = response.text?.trim() || "No relevant information found to reflect on.";
return {
content: [{ type: "text", text }],
details: {},
};
} catch (err) {
logger.warn("reflect failed", { bankId: state.bankId, error: String(err) });
throw err instanceof Error ? err : new Error(String(err));
}
});
}
}