feat(plugins): share native session handoff and soften memory search
This commit is contained in:
@@ -4,6 +4,7 @@ import { loadConfig } from "./config/index.ts";
|
||||
import { detectAppId, detectRunId, resolveSearchFilters } from "./memory/scoping.ts";
|
||||
import { registerMemoryTool } from "./memory/tools.ts";
|
||||
import { registerCommands } from "./commands.ts";
|
||||
import { registerHandoffCommand } from "./handoff.ts";
|
||||
import { setupAutoCapture } from "./capture/index.ts";
|
||||
import { MEMORY_POLICY } from "./prompt.ts";
|
||||
import { captureEvent } from "./telemetry.ts";
|
||||
@@ -21,10 +22,11 @@ export function resolveUserId(configUserId: string): string {
|
||||
}
|
||||
|
||||
export default function mem0Extension(pi: ExtensionAPI): void {
|
||||
registerHandoffCommand(pi);
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config.apiKey) {
|
||||
console.warn("[mem0] No API key found. Set MEM0_API_KEY or add apiKey to ~/.pi/agent/mem0-config.json. Extension disabled.");
|
||||
console.warn("[mem0] No API key found. Set MEM0_API_KEY or add apiKey to ~/.pi/agent/mem0-config.json. Memory features disabled; /mem0-handoff remains available.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import mem0Extension from "./entry.ts";
|
||||
import {runHandoff} from "../../agent-plugin-core/typescript/src/handoff.ts";
|
||||
vi.mock("../../agent-plugin-core/typescript/src/handoff.ts", async (original) => ({...await original<typeof import("../../agent-plugin-core/typescript/src/handoff.ts")>(), runHandoff: vi.fn()}));
|
||||
vi.mock("./config/index.ts", () => ({loadConfig: () => ({apiKey: ""}), CONFIG_DIR: "/tmp/mem0-pi-handoff-tests"}));
|
||||
beforeEach(() => { vi.clearAllMocks(); });
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
function setup() {
|
||||
const commands = new Map<string, any>();
|
||||
const pi = {registerCommand: vi.fn((name, command) => commands.set(name, command)), sendMessage: vi.fn()};
|
||||
const entries = [
|
||||
{type: "message", id: "old", parentId: null, message: {role: "user", content: "old context"}},
|
||||
{type: "message", id: "keep", parentId: "old", message: {role: "user", content: "retained tail"}},
|
||||
{type: "compaction", id: "compact", parentId: "keep", summary: "Native summary", firstKeptEntryId: "keep", tokensBefore: 100},
|
||||
{type: "message", id: "new", parentId: "compact", message: {role: "user", content: "Continue this task"}},
|
||||
];
|
||||
const ctx = {ui: {notify: vi.fn()}, isIdle: () => true, sessionManager: {
|
||||
getEntries: () => entries, getLeafId: () => "new", getSessionId: () => "pi-native",
|
||||
getSessionName: () => "My Pi task", getCwd: () => "/tmp", getSessionFile: () => "/tmp/pi.jsonl",
|
||||
}};
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
mem0Extension(pi as any);
|
||||
return {pi, ctx, handler: commands.get("mem0-handoff").handler};
|
||||
}
|
||||
describe("native Pi handoff", () => {
|
||||
it("uses host compaction/branch selection and the current session without a Mem0 key", async () => {
|
||||
const {pi, ctx, handler} = setup();
|
||||
vi.mocked(runHandoff).mockResolvedValue("Created Codex task");
|
||||
await handler("codex", ctx);
|
||||
const bundle = vi.mocked(runHandoff).mock.calls[0][1];
|
||||
expect(bundle.source).toMatchObject({host: "pi-agent", session_id: "pi-native", title: "My Pi task"});
|
||||
expect(JSON.stringify(bundle.items)).toContain("Native summary");
|
||||
expect(JSON.stringify(bundle.items)).toContain("retained tail");
|
||||
expect(JSON.stringify(bundle.items)).not.toContain("old context");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith({customType: "mem0-handoff", content: "Created Codex task", display: true});
|
||||
});
|
||||
it.each(["", "codex session-id", "claude"])("rejects unsupported arguments %s", async (args) => {
|
||||
const {ctx, handler} = setup();
|
||||
await handler(args, ctx);
|
||||
expect(runHandoff).not.toHaveBeenCalled();
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Usage:"), "warning");
|
||||
});
|
||||
it("reports an active response or importer failure", async () => {
|
||||
const {pi, ctx, handler} = setup();
|
||||
await handler("codex", {...ctx, isIdle: () => false});
|
||||
expect(runHandoff).not.toHaveBeenCalled();
|
||||
vi.mocked(runHandoff).mockRejectedValue(new Error("saved at /tmp/retry.json"));
|
||||
await handler("codex", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("saved at /tmp/retry.json", "error");
|
||||
expect(pi.sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { buildSessionContext, convertToLlm, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { buildHandoffBundle, runHandoff } from "../../agent-plugin-core/typescript/src/handoff.ts";
|
||||
|
||||
export function registerHandoffCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("mem0-handoff", {
|
||||
description: "Continue this Pi session in a new Codex task",
|
||||
handler: async (args, ctx) => {
|
||||
if (args.trim() !== "codex") {
|
||||
ctx.ui.notify("Usage: /mem0-handoff codex", "warning");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!ctx.isIdle()) throw new Error("Finish the current response before handing off this session.");
|
||||
const session = ctx.sessionManager;
|
||||
const context = buildSessionContext(session.getEntries(), session.getLeafId());
|
||||
const bundle = await buildHandoffBundle({
|
||||
host: "pi-agent", session_id: session.getSessionId(),
|
||||
title: session.getSessionName() || `Pi session ${session.getSessionId()}`,
|
||||
cwd: session.getCwd(), path: session.getSessionFile(),
|
||||
}, convertToLlm(context.messages));
|
||||
const content = await runHandoff(new URL("./session_handoff.py", import.meta.url), bundle);
|
||||
pi.sendMessage({ customType: "mem0-handoff", content, display: true });
|
||||
} catch (error) {
|
||||
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SEARCH_GUIDANCE } from "../../../agent-plugin-core/typescript/src/search_guidance.ts";
|
||||
import { resolveToolScope } from "../../../agent-plugin-core/typescript/src/scoping.ts";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
@@ -144,11 +145,10 @@ export function registerMemoryTool(
|
||||
name: "mem0_memory",
|
||||
label: "Mem0 Memory",
|
||||
description:
|
||||
"Search, add, update, and manage persistent semantic memories powered by Mem0. Memories persist across sessions and devices. Use action \"search\" proactively -- before answering anything that may depend on what the user told you earlier -- and run multiple searches with different phrasings for multi-part questions. Output is truncated to 200 lines / 50KB.",
|
||||
"Search, add, update, and manage persistent semantic memories powered by Mem0. Memories persist across sessions and devices. Output is truncated to 200 lines / 50KB.",
|
||||
promptSnippet: "Semantic memory search and storage via Mem0",
|
||||
promptGuidelines: [
|
||||
'Use mem0_memory with action "search" proactively whenever the request may depend on the user\'s past work, preferences, decisions, or environment -- not only when they explicitly mention the past',
|
||||
'For multi-part or comparative questions, run several searches with different phrasings and combine the results before answering -- one search is rarely enough',
|
||||
SEARCH_GUIDANCE,
|
||||
'Use mem0_memory with action "add" to save important facts, preferences, goals, decisions, or lessons the user shares',
|
||||
'Use mem0_memory with action "update" to modify an existing memory — requires memory_id and content. Preserves the memory ID',
|
||||
"Always use the default project scope unless the user EXPLICITLY asks to search across all projects — only after the user selects /mem0-scope global use scope \"global\"",
|
||||
@@ -166,13 +166,13 @@ export function registerMemoryTool(
|
||||
] as const,
|
||||
{
|
||||
description:
|
||||
"Memory operation to run: \"search\" (semantic recall -- use proactively before answering; run several with different phrasings for multi-part questions), \"add\" (save a new fact/preference/decision), \"get_all\" (list everything in scope, no query needed), \"update\" (replace an existing memory's text by id), \"delete\" (remove one memory by id), \"delete_all\" (wipe every memory in the scope -- destructive, only on explicit request).",
|
||||
"Memory operation to run: \"search\" (semantic recall for a focused question), \"add\" (save a new fact/preference/decision), \"get_all\" (list everything in scope, no query needed), \"update\" (replace an existing memory's text by id), \"delete\" (remove one memory by id), \"delete_all\" (wipe every memory in the scope -- destructive, only on explicit request).",
|
||||
},
|
||||
),
|
||||
query: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Search text -- required for action \"search\". Use a focused noun-phrase; for multi-part questions run several searches with different phrasings.",
|
||||
"Search text -- required for action \"search\". Use a focused question about the missing context.",
|
||||
}),
|
||||
),
|
||||
content: Type.Optional(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
export const MEMORY_POLICY = `<mem0-memory-policy>
|
||||
You have persistent semantic memory via the mem0_memory tool, powered by Mem0. Relevant memories may be auto-injected under <mem0-relevant-memories>, but that retrieval is shallow — treat it as a starting point, not the full picture.
|
||||
import { SEARCH_GUIDANCE } from "../../agent-plugin-core/typescript/src/search_guidance.ts";
|
||||
|
||||
Be proactive about retrieval:
|
||||
- Search memory BEFORE answering whenever the request could depend on the user's past work, preferences, decisions, environment, or anything they told you earlier — don't wait to be asked.
|
||||
- Check memory before asking the user something they may have already told you.
|
||||
- For multi-part, comparative, or "how did we…" questions, run SEVERAL searches with different phrasings and combine the results. One search is rarely enough — keep going until you have what you need (multi-hop).
|
||||
export const MEMORY_POLICY = `<mem0-memory-policy>
|
||||
You have persistent semantic memory via the mem0_memory tool, powered by Mem0. Relevant memories may be auto-injected under <mem0-relevant-memories>.
|
||||
|
||||
${SEARCH_GUIDANCE}
|
||||
|
||||
Be proactive about saving:
|
||||
- Save important facts, preferences, goals, decisions, lessons learned, identity, relationships, and routines the user shares.
|
||||
|
||||
Reference in New Issue
Block a user