feat(plugins): share native session handoff and soften memory search
This commit is contained in:
@@ -4,7 +4,7 @@ Persistent semantic memory for [Pi Agent](https://pi.dev), powered by [Mem0](htt
|
||||
|
||||
This extension gives Pi Agent long-term memory that persists across sessions, projects, and devices. Memories are automatically captured from conversations and can be searched and managed through slash commands and an agent-accessible tool.
|
||||
|
||||
Current package version: `0.3.0`. Shared redaction and lifecycle utilities come from [agent-plugin-core](../agent-plugin-core/README.md); Pi keeps its own tools and scopes.
|
||||
Current package version: `0.4.0`. Shared redaction and lifecycle utilities come from [agent-plugin-core](../agent-plugin-core/README.md); Pi keeps its own tools and scopes.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -13,7 +13,7 @@ Current package version: `0.3.0`. Shared redaction and lifecycle utilities come
|
||||
- **Scoped memory** — project, session, or global scope
|
||||
- **Monorepo-aware** — uses git root for project detection, consistent app_id across subdirectories
|
||||
- **Confirmation dialogs** — destructive commands ask before acting
|
||||
- **6 slash commands** — essential memory management from the command line
|
||||
- **7 slash commands** — essential memory management from the command line
|
||||
- **Agent tool** — `mem0_memory` tool lets the agent search and store memories autonomously
|
||||
|
||||
## Setup
|
||||
@@ -52,6 +52,14 @@ Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file.
|
||||
|
||||
`searchThreshold` (default `0.3`) is the minimum similarity score (0–1) a memory must reach to count as a match for `/mem0-search` and `/mem0-forget`. It is passed to the mem0 search API (along with reranking for higher-precision ordering), so a query with no sufficiently similar memory reports no match instead of returning the closest unrelated memories. Raise it to be stricter; lower it if relevant results are missed.
|
||||
|
||||
## Session handoff
|
||||
|
||||
Run `/mem0-handoff codex` to continue the current Pi session in Codex. The plugin reads native session context, including readable compaction summaries and completed tool outcomes.
|
||||
|
||||
Requires **Python 3.11+** available as `python3` and an installed, signed-in Codex CLI with native session import support. Handoff works independently of Mem0 credentials and never sends the transcript through the Mem0 API. Unsupported content, missing results, and unrelated unfinished calls fail explicitly.
|
||||
|
||||
The shared engine is packaged in `dist/` by [agent-plugin-core](../agent-plugin-core/README.md); it is not maintained separately in this plugin. Failed imports save a private recovery bundle under `~/.mem0/handoffs/`. See the [session handoff guide](../../docs/integrations/session-handoff.mdx) for supported formats, privacy, and recovery.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
@@ -62,6 +70,7 @@ Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file.
|
||||
| `/mem0-tour [scope]` | Browse all memories grouped by category |
|
||||
| `/mem0-scope <scope>` | Change default scope for this session |
|
||||
| `/mem0-status` | Connection health, identity, and memory count |
|
||||
| `/mem0-handoff codex` | Continue the current Pi session in Codex |
|
||||
|
||||
## Skills
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/pi-agent-plugin",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"type": "module",
|
||||
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: context-loader
|
||||
description: Searches and injects relevant memories into context before starting work on a task or topic. Use when beginning a new task, switching context, or when past decisions, preferences, or knowledge need to be loaded.
|
||||
description: Search earlier context when project history, decisions, or preferences could help with a specific question.
|
||||
---
|
||||
|
||||
# Context Loader
|
||||
@@ -9,22 +9,14 @@ Pre-fetches relevant memories to prime context before working on a task or topic
|
||||
|
||||
## When to use
|
||||
|
||||
- Session start (auto-triggered by the extension's `before_agent_start` event)
|
||||
- User starts work on a specific topic or area
|
||||
- User says "what do we know about X" or "context for X"
|
||||
Use when earlier work could explain a decision, fix, command, or preference you
|
||||
need. Skip this skill when the context already answers the question.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Extract topics** from current message/task. Identify: subject areas, people mentioned, project names, goal references.
|
||||
|
||||
2. **Run 2-4 parallel searches** using `mem0_memory` tool with `action="search"` and different query angles:
|
||||
|
||||
| Query angle | Purpose |
|
||||
|---|---|
|
||||
| Topic/subject name | Relevant decisions and preferences |
|
||||
| People mentioned | Relationship context |
|
||||
| Project/goal references | Progress and background |
|
||||
| Broad context | Catch-all for anything relevant |
|
||||
2. **Search one focused question** using `mem0_memory` with `action="search"`. Keep the default project scope. Search again only if a specific gap remains.
|
||||
|
||||
3. **Deduplicate** results by memory ID across all search responses.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from "tsup";
|
||||
import { packageHandoff } from "../agent-plugin-core/build/package_handoff.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts", "src/entry.ts"],
|
||||
@@ -7,6 +8,7 @@ export default defineConfig({
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
onSuccess: () => packageHandoff(),
|
||||
external: [
|
||||
/^node:/,
|
||||
/^@earendil-works\//,
|
||||
|
||||
Reference in New Issue
Block a user