fix(FN-000): search agent memory through memory tools
This commit is contained in:
@@ -2054,6 +2054,36 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(toolNames).not.toContain("memory_append");
|
||||
});
|
||||
|
||||
it("wires user-created agent memory into the memory_search tool", async () => {
|
||||
const store = createStoreWithAgentForExec({
|
||||
name: "CEO",
|
||||
memory: "Prioritize roadmap sequencing and delegate implementation follow-ups.",
|
||||
});
|
||||
const taskStore = createMockTaskStore({
|
||||
getSettings: vi.fn().mockResolvedValue({ memoryBackendType: "file" }),
|
||||
} as Partial<TaskStore>);
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore, rootDir: "/tmp/test" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const memorySearch = callArgs.customTools!.find((tool: any) => tool.name === "memory_search") as any;
|
||||
expect(memorySearch).toBeDefined();
|
||||
const result = await memorySearch.execute("call-1", {
|
||||
query: "roadmap delegate",
|
||||
limit: 5,
|
||||
}, undefined, undefined, undefined);
|
||||
|
||||
expect(result.content[0].text).toContain(".fusion/agent-memory/agent-001/MEMORY.md");
|
||||
expect(result.content[0].text).toContain("roadmap sequencing");
|
||||
expect(result.details.results[0].backend).toBe("agent-memory");
|
||||
});
|
||||
|
||||
it("includes document tools in heartbeat session", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
|
||||
@@ -945,7 +945,13 @@ export class HeartbeatMonitor {
|
||||
let memorySettings: Settings | undefined;
|
||||
try {
|
||||
memorySettings = await getHeartbeatMemorySettings(taskStore);
|
||||
heartbeatTools.push(...createMemoryTools(rootDir, memorySettings));
|
||||
heartbeatTools.push(...createMemoryTools(rootDir, memorySettings, {
|
||||
agentMemory: {
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
memory: agent.memory,
|
||||
},
|
||||
}));
|
||||
} catch (memorySettingsError) {
|
||||
const message = memorySettingsError instanceof Error ? memorySettingsError.message : String(memorySettingsError);
|
||||
heartbeatLog.warn(`Failed to configure heartbeat memory tools for ${agentId}: ${message}`);
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { createMemoryTools, createSendMessageTool, createReadMessagesTool, sendMessageParams, readMessagesParams } from "./agent-tools.js";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
buildQmdAgentMemoryCollectionAddArgs,
|
||||
buildQmdAgentMemorySearchArgs,
|
||||
createMemoryTools,
|
||||
createSendMessageTool,
|
||||
createReadMessagesTool,
|
||||
qmdAgentMemoryCollectionName,
|
||||
sendMessageParams,
|
||||
readMessagesParams,
|
||||
} from "./agent-tools.js";
|
||||
import type { MessageStore, Message } from "@fusion/core";
|
||||
|
||||
// Mock logger
|
||||
@@ -16,6 +28,16 @@ vi.mock("./logger.js", () => {
|
||||
});
|
||||
|
||||
describe("createMemoryTools", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agent-memory-tools-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("omits memory tools when memory is disabled", () => {
|
||||
expect(createMemoryTools("/repo", { memoryEnabled: false }).map((tool) => tool.name)).toEqual([]);
|
||||
});
|
||||
@@ -34,6 +56,54 @@ describe("createMemoryTools", () => {
|
||||
"memory_append",
|
||||
]);
|
||||
});
|
||||
|
||||
it("searches per-agent memory through the memory_search tool", async () => {
|
||||
const [searchTool, getTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
|
||||
agentMemory: {
|
||||
agentId: "ceo-agent",
|
||||
agentName: "CEO",
|
||||
memory: "The CEO agent should prioritize roadmap sequencing and delegation.",
|
||||
},
|
||||
});
|
||||
|
||||
const searchResult = await (searchTool as any).execute("call-1", {
|
||||
query: "roadmap delegation",
|
||||
limit: 5,
|
||||
}, undefined, undefined, undefined);
|
||||
|
||||
expect(searchResult.content[0]!.text).toContain(".fusion/agent-memory/ceo-agent/MEMORY.md");
|
||||
expect(searchResult.details.results[0].backend).toBe("agent-memory");
|
||||
|
||||
const getResult = await (getTool as any).execute("call-2", {
|
||||
path: ".fusion/agent-memory/ceo-agent/MEMORY.md",
|
||||
startLine: 1,
|
||||
lineCount: 20,
|
||||
}, undefined, undefined, undefined);
|
||||
|
||||
expect(getResult.content[0]!.text).toContain("Agent Memory: CEO");
|
||||
expect(getResult.content[0]!.text).toContain("roadmap sequencing");
|
||||
});
|
||||
|
||||
it("builds qmd collection and search args for separate agent memory", () => {
|
||||
expect(buildQmdAgentMemoryCollectionAddArgs(tempDir, "ceo-agent")).toEqual([
|
||||
"collection",
|
||||
"add",
|
||||
join(tempDir, ".fusion", "agent-memory", "ceo-agent"),
|
||||
"--name",
|
||||
qmdAgentMemoryCollectionName(tempDir, "ceo-agent"),
|
||||
"--mask",
|
||||
"**/*.md",
|
||||
]);
|
||||
expect(buildQmdAgentMemorySearchArgs(tempDir, "ceo-agent", "delegation", 7)).toEqual([
|
||||
"search",
|
||||
"delegation",
|
||||
"--json",
|
||||
"--collection",
|
||||
qmdAgentMemoryCollectionName(tempDir, "ceo-agent"),
|
||||
"-n",
|
||||
"7",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function createMessage(overrides: Partial<Message> = {}): Message {
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
* The parameter schemas are canonical here — executor.ts imports and reuses them.
|
||||
*/
|
||||
|
||||
import { appendFile } from "node:fs/promises";
|
||||
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message } from "@fusion/core";
|
||||
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, scheduleQmdProjectMemoryRefresh, searchProjectMemory } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
@@ -106,6 +108,217 @@ type MemoryToolSettings = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type AgentMemoryContext = {
|
||||
agentId: string;
|
||||
agentName?: string;
|
||||
memory?: string | null;
|
||||
};
|
||||
|
||||
type MemoryToolOptions = {
|
||||
agentMemory?: AgentMemoryContext;
|
||||
};
|
||||
|
||||
type MemorySearchHit = {
|
||||
path: string;
|
||||
lineStart: number;
|
||||
lineEnd: number;
|
||||
snippet: string;
|
||||
score: number;
|
||||
backend: string;
|
||||
};
|
||||
|
||||
const AGENT_MEMORY_ROOT = ".fusion/agent-memory";
|
||||
const AGENT_MEMORY_FILENAME = "MEMORY.md";
|
||||
const agentQmdRefreshState = new Map<string, { lastStartedAt: number; inFlight?: Promise<void> }>();
|
||||
const AGENT_QMD_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
function sanitizeAgentMemoryId(agentId: string): string {
|
||||
return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
||||
}
|
||||
|
||||
function agentMemoryDisplayPath(agentId: string): string {
|
||||
return `${AGENT_MEMORY_ROOT}/${sanitizeAgentMemoryId(agentId)}/${AGENT_MEMORY_FILENAME}`;
|
||||
}
|
||||
|
||||
function agentMemoryDirectory(rootDir: string, agentId: string): string {
|
||||
return join(rootDir, AGENT_MEMORY_ROOT, sanitizeAgentMemoryId(agentId));
|
||||
}
|
||||
|
||||
function agentMemoryFilePath(rootDir: string, agentId: string): string {
|
||||
return join(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME);
|
||||
}
|
||||
|
||||
export function qmdAgentMemoryCollectionName(rootDir: string, agentId: string): string {
|
||||
const hash = createHash("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
|
||||
return `fusion-agent-memory-${sanitizeAgentMemoryId(agentId).toLowerCase()}-${hash}`;
|
||||
}
|
||||
|
||||
export function buildQmdAgentMemoryCollectionAddArgs(rootDir: string, agentId: string): string[] {
|
||||
return [
|
||||
"collection",
|
||||
"add",
|
||||
agentMemoryDirectory(rootDir, agentId),
|
||||
"--name",
|
||||
qmdAgentMemoryCollectionName(rootDir, agentId),
|
||||
"--mask",
|
||||
"**/*.md",
|
||||
];
|
||||
}
|
||||
|
||||
export function buildQmdAgentMemorySearchArgs(rootDir: string, agentId: string, query: string, limit = 5): string[] {
|
||||
return [
|
||||
"search",
|
||||
query,
|
||||
"--json",
|
||||
"--collection",
|
||||
qmdAgentMemoryCollectionName(rootDir, agentId),
|
||||
"-n",
|
||||
String(Math.max(1, Math.min(limit, 20))),
|
||||
];
|
||||
}
|
||||
|
||||
async function syncAgentMemoryFile(rootDir: string, agentMemory?: AgentMemoryContext): Promise<string | null> {
|
||||
const content = agentMemory?.memory?.trim();
|
||||
if (!agentMemory?.agentId || !content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
|
||||
await mkdir(dir, { recursive: true });
|
||||
const title = agentMemory.agentName?.trim()
|
||||
? `# Agent Memory: ${agentMemory.agentName.trim()}`
|
||||
: "# Agent Memory";
|
||||
const fileContent = `${title}\n\n<!-- Per-agent memory. Keep separate from workspace Project Memory. -->\n\n${content}\n`;
|
||||
await writeFile(agentMemoryFilePath(rootDir, agentMemory.agentId), fileContent, "utf-8");
|
||||
return agentMemoryDisplayPath(agentMemory.agentId);
|
||||
}
|
||||
|
||||
function scoreAgentMemorySnippet(snippet: string, query: string): number {
|
||||
const terms = query.toLowerCase().split(/[^a-z0-9_-]+/i).filter((term) => term.length >= 2);
|
||||
const normalized = snippet.toLowerCase();
|
||||
return terms.reduce((score, term) => score + (normalized.includes(term) ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
async function searchAgentMemoryFile(rootDir: string, agentMemory: AgentMemoryContext, query: string, limit: number): Promise<MemorySearchHit[]> {
|
||||
const displayPath = await syncAgentMemoryFile(rootDir, agentMemory);
|
||||
if (!displayPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = await readFile(agentMemoryFilePath(rootDir, agentMemory.agentId), "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const results: MemorySearchHit[] = [];
|
||||
for (let index = 0; index < lines.length; index += 8) {
|
||||
const chunk = lines.slice(index, index + 12).join("\n").trim();
|
||||
if (!chunk) continue;
|
||||
const score = scoreAgentMemorySnippet(chunk, query);
|
||||
if (score === 0) continue;
|
||||
results.push({
|
||||
path: displayPath,
|
||||
lineStart: index + 1,
|
||||
lineEnd: Math.min(index + 12, lines.length),
|
||||
snippet: chunk.slice(0, 1200),
|
||||
score: score + 1000,
|
||||
backend: "agent-memory",
|
||||
});
|
||||
}
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
async function refreshAgentMemoryQmdIndex(rootDir: string, agentMemory: AgentMemoryContext): Promise<void> {
|
||||
await syncAgentMemoryFile(rootDir, agentMemory);
|
||||
const key = `${rootDir}:${agentMemory.agentId}`;
|
||||
const now = Date.now();
|
||||
const current = agentQmdRefreshState.get(key);
|
||||
if (current?.inFlight) {
|
||||
return current.inFlight;
|
||||
}
|
||||
if (current && now - current.lastStartedAt < AGENT_QMD_REFRESH_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
const { execFile } = await import("node:child_process");
|
||||
const { promisify } = await import("node:util");
|
||||
const execFileAsync = promisify(execFile);
|
||||
try {
|
||||
await execFileAsync("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
|
||||
cwd: rootDir,
|
||||
timeout: 4000,
|
||||
maxBuffer: 512 * 1024,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const stderr = typeof error === "object" && error && "stderr" in error ? String((error as { stderr?: unknown }).stderr ?? "") : "";
|
||||
if (!/already exists|exists/i.test(`${message}\n${stderr}`)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await execFileAsync("qmd", ["update"], { cwd: rootDir, timeout: 30_000, maxBuffer: 1024 * 1024 });
|
||||
await execFileAsync("qmd", ["embed"], { cwd: rootDir, timeout: 120_000, maxBuffer: 1024 * 1024 });
|
||||
})();
|
||||
|
||||
agentQmdRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
|
||||
try {
|
||||
await promise;
|
||||
} finally {
|
||||
const latest = agentQmdRefreshState.get(key);
|
||||
if (latest?.inFlight === promise) {
|
||||
agentQmdRefreshState.set(key, { lastStartedAt: latest.lastStartedAt });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function searchAgentMemoryWithQmd(rootDir: string, agentMemory: AgentMemoryContext, query: string, limit: number): Promise<MemorySearchHit[]> {
|
||||
if (!agentMemory.memory?.trim()) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await refreshAgentMemoryQmdIndex(rootDir, agentMemory);
|
||||
const { execFile } = await import("node:child_process");
|
||||
const { promisify } = await import("node:util");
|
||||
const execFileAsync = promisify(execFile);
|
||||
const { stdout } = await execFileAsync("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
|
||||
cwd: rootDir,
|
||||
timeout: 4000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const parsed = JSON.parse(stdout);
|
||||
const rawResults = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.results) ? parsed.results : [];
|
||||
return rawResults.slice(0, limit).map((result: Record<string, unknown>) => ({
|
||||
path: agentMemoryDisplayPath(agentMemory.agentId),
|
||||
lineStart: Number(result.lineStart ?? result.startLine ?? 1),
|
||||
lineEnd: Number(result.lineEnd ?? result.endLine ?? result.startLine ?? 1),
|
||||
snippet: String(result.snippet ?? result.text ?? result.content ?? "").slice(0, 1200),
|
||||
score: Number(result.score ?? 1) + 1000,
|
||||
backend: "qmd-agent-memory",
|
||||
})).filter((result: MemorySearchHit) => result.snippet.trim().length > 0);
|
||||
} catch {
|
||||
return searchAgentMemoryFile(rootDir, agentMemory, query, limit);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryContext, path: string, startLine = 1, lineCount = 40) {
|
||||
if (path !== agentMemoryDisplayPath(agentMemory.agentId) || !agentMemory.memory?.trim()) {
|
||||
return null;
|
||||
}
|
||||
await syncAgentMemoryFile(rootDir, agentMemory);
|
||||
const content = await readFile(agentMemoryFilePath(rootDir, agentMemory.agentId), "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const start = Math.max(1, Math.floor(startLine));
|
||||
const count = Math.max(1, Math.min(Math.floor(lineCount), 200));
|
||||
const startIndex = Math.min(start - 1, lines.length);
|
||||
const endIndex = Math.min(startIndex + count, lines.length);
|
||||
return {
|
||||
path: agentMemoryDisplayPath(agentMemory.agentId),
|
||||
content: lines.slice(startIndex, endIndex).join("\n"),
|
||||
startLine: start,
|
||||
endLine: endIndex,
|
||||
totalLines: lines.length,
|
||||
backend: "agent-memory",
|
||||
};
|
||||
}
|
||||
|
||||
// ── Tool factory functions ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -306,19 +519,28 @@ export function createTaskDocumentReadTool(store: TaskStore, taskId: string): To
|
||||
};
|
||||
}
|
||||
|
||||
export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSettings): ToolDefinition {
|
||||
export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
|
||||
return {
|
||||
name: "memory_search",
|
||||
label: "Search Memory",
|
||||
description:
|
||||
"Search durable project memory and return small snippets with file paths and line ranges. " +
|
||||
"Search durable project memory and this agent's own memory, returning small snippets with file paths and line ranges. " +
|
||||
"Use this before memory_get; do not read all memory by default.",
|
||||
parameters: memorySearchParams,
|
||||
execute: async (_id: string, params: Static<typeof memorySearchParams>) => {
|
||||
const results = await searchProjectMemory(rootDir, {
|
||||
const limit = params.limit ?? 5;
|
||||
const agentResults = options?.agentMemory
|
||||
? resolveMemoryBackend(settings).type === "qmd"
|
||||
? await searchAgentMemoryWithQmd(rootDir, options.agentMemory, params.query, limit)
|
||||
: await searchAgentMemoryFile(rootDir, options.agentMemory, params.query, limit)
|
||||
: [];
|
||||
const projectResults = await searchProjectMemory(rootDir, {
|
||||
query: params.query,
|
||||
limit: params.limit,
|
||||
limit,
|
||||
}, settings);
|
||||
const results = [...agentResults, ...projectResults]
|
||||
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
||||
.slice(0, limit);
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
@@ -336,15 +558,27 @@ export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSet
|
||||
};
|
||||
}
|
||||
|
||||
export function createMemoryGetTool(rootDir: string, settings?: MemoryToolSettings): ToolDefinition {
|
||||
export function createMemoryGetTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
|
||||
return {
|
||||
name: "memory_get",
|
||||
label: "Get Memory",
|
||||
description:
|
||||
"Read a bounded line window from a memory file returned by memory_search. " +
|
||||
"Allowed files are .fusion/memory/MEMORY.md, .fusion/memory/YYYY-MM-DD.md, and legacy .fusion/memory.md.",
|
||||
"Allowed files include project memory under .fusion/memory/ and this agent's own .fusion/agent-memory/{agentId}/MEMORY.md file.",
|
||||
parameters: memoryGetParams,
|
||||
execute: async (_id: string, params: Static<typeof memoryGetParams>) => {
|
||||
const agentResult = options?.agentMemory
|
||||
? await getAgentMemoryWindow(rootDir, options.agentMemory, params.path, params.startLine, params.lineCount)
|
||||
: null;
|
||||
if (agentResult) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `${agentResult.path}:${agentResult.startLine}-${agentResult.endLine} (${agentResult.totalLines} total lines, ${agentResult.backend})\n\n${agentResult.content}`,
|
||||
}],
|
||||
details: agentResult,
|
||||
};
|
||||
}
|
||||
const result = await getProjectMemory(rootDir, {
|
||||
path: params.path,
|
||||
startLine: params.startLine,
|
||||
@@ -389,13 +623,13 @@ export function createMemoryAppendTool(rootDir: string, settings?: MemoryToolSet
|
||||
};
|
||||
}
|
||||
|
||||
export function createMemoryTools(rootDir: string, settings?: MemoryToolSettings): ToolDefinition[] {
|
||||
export function createMemoryTools(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition[] {
|
||||
if (settings?.memoryEnabled === false) {
|
||||
return [];
|
||||
}
|
||||
const tools = [
|
||||
createMemorySearchTool(rootDir, settings),
|
||||
createMemoryGetTool(rootDir, settings),
|
||||
createMemorySearchTool(rootDir, settings, options),
|
||||
createMemoryGetTool(rootDir, settings, options),
|
||||
];
|
||||
if (getMemoryBackendCapabilities(settings).writable) {
|
||||
tools.push(createMemoryAppendTool(rootDir, settings));
|
||||
|
||||
@@ -1409,6 +1409,9 @@ export class TaskExecutor {
|
||||
const reflectionTools = this.options.reflectionService && settings.reflectionEnabled && assignedAgentId
|
||||
? [createReflectOnPerformanceTool(this.options.reflectionService, assignedAgentId)]
|
||||
: [];
|
||||
const assignedAgent = assignedAgentId && this.options.agentStore
|
||||
? await this.options.agentStore.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
|
||||
const customTools = [
|
||||
this.createTaskUpdateTool(task.id, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector),
|
||||
@@ -1420,7 +1423,13 @@ export class TaskExecutor {
|
||||
this.createSpawnAgentTool(task.id, worktreePath, settings),
|
||||
this.createTaskDocumentWriteTool(task.id),
|
||||
this.createTaskDocumentReadTool(task.id),
|
||||
...createMemoryTools(this.rootDir, settings),
|
||||
...createMemoryTools(this.rootDir, settings, assignedAgent ? {
|
||||
agentMemory: {
|
||||
agentId: assignedAgent.id,
|
||||
agentName: assignedAgent.name,
|
||||
memory: assignedAgent.memory,
|
||||
},
|
||||
} : undefined),
|
||||
// Conditionally add agent self-reflection when enabled and task has an assigned agent.
|
||||
...reflectionTools,
|
||||
// Agent delegation tools — discover and delegate work to other agents.
|
||||
|
||||
@@ -332,10 +332,25 @@ export async function reviewStep(
|
||||
}
|
||||
|
||||
// Spawn a reviewer agent with read-only tools
|
||||
const memoryAgent = options.rootDir && options.agentStore && options.task?.assignedAgentId
|
||||
? await options.agentStore.getAgent(options.task.assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const memoryTools = options.rootDir && options.settings?.memoryEnabled !== false
|
||||
? [
|
||||
createMemorySearchTool(options.rootDir, options.settings),
|
||||
createMemoryGetTool(options.rootDir, options.settings),
|
||||
createMemorySearchTool(options.rootDir, options.settings, memoryAgent ? {
|
||||
agentMemory: {
|
||||
agentId: memoryAgent.id,
|
||||
agentName: memoryAgent.name,
|
||||
memory: memoryAgent.memory,
|
||||
},
|
||||
} : undefined),
|
||||
createMemoryGetTool(options.rootDir, options.settings, memoryAgent ? {
|
||||
agentMemory: {
|
||||
agentId: memoryAgent.id,
|
||||
agentName: memoryAgent.name,
|
||||
memory: memoryAgent.memory,
|
||||
},
|
||||
} : undefined),
|
||||
]
|
||||
: undefined;
|
||||
const { session } = await createKbAgent({
|
||||
|
||||
Reference in New Issue
Block a user