Skip to content

feat(examples): add pi coding agent memory extension#2314

Open
hammerhoundai wants to merge 2 commits into
volcengine:mainfrom
hammerhoundai:feature/pi-coding-agent-extension
Open

feat(examples): add pi coding agent memory extension#2314
hammerhoundai wants to merge 2 commits into
volcengine:mainfrom
hammerhoundai:feature/pi-coding-agent-extension

Conversation

@hammerhoundai
Copy link
Copy Markdown

Summary

Adds an OpenViking memory extension for the Pi coding agent (@mariozechner/pi-coding-agent).

Pi is a terminal-based LLM coding agent harness (similar to Claude Code, built in TypeScript) with a native extension API — event-driven lifecycle hooks, tool registration, system prompt injection, and session management. This extension brings OpenViking's long-term semantic memory to pi sessions: synchronous recall, automatic turn capture, 7 LLM tools, session commit with memory extraction, and a knowledge index so the model knows what OV contains.

Design lineage: Informed by deep study of all three existing OV agent plugins:

  • OpenClaw → synchronous recall (query the current prompt, not the previous one)
  • Claude Code → production-hardened capture filtering, multi-scope parallel search, token-budgeted ranking, pollution prevention, bypass patterns
  • Hermes → studied as an anti-pattern (stale prefetch, session-end-only commit)

The extension is 8 TypeScript files (~1692 LOC) + a JSON config, loaded by pi's built-in jiti transpiler — zero npm dependencies beyond Node.js. All communication uses the OpenViking REST API (same endpoints as the CC plugin's ov-session.mjs client).

Type of Change

  • New feature (feat)
  • Bug fix (fix)
  • Documentation (docs)
  • Refactoring (refactor)
  • Other — new agent integration example

Key Features

Feature Implementation
Synchronous recall Pi context event searches OV with current prompt, injects <relevant-memories> block in same turn
7 LLM tools Native pi.registerTool() — search, read, browse, remember, forget, add_resource, archive_expand
Auto-capture turn_end event extracts user + assistant turns (tool USE preserved, tool RESULTS dropped)
Client-driven commit Checks pending_tokens from OV session, commits at threshold (20k default)
Pre-compact commit session_before_compact event commits before pi rewrites the transcript
Session resume rehydration Fetches archive overview on resume, injects via before_agent_start
Memory index Builds a map of what OV knows (viking:// directories + entry counts) — model sees the map before searching
Token-budgeted ranking Items within budget get full content; items beyond degrade to URI hints (never dropped)
Pollution prevention Strips <openviking-context>, <relevant-memories>, <system-reminder> before pushing to OV
Write queue Batches turn writes, flushes at threshold (5 turns) or interval (5s)
Bypass patterns Glob patterns matched against session_id/cwd to skip processing
Env var overrides OPENVIKING_URL, OPENVIKING_API_KEY, OPENVIKING_ACCOUNT, OPENVIKING_USER, OPENVIKING_AGENT_ID
Pi -c resume fix Workaround for pi's tool re-registration quirk on session continuations

Architecture

The extension wires into pi's event lifecycle:

session_start        → health check, create OV session, build profile + index, register tools
before_agent_start   → fallback tool registration, inject archive overview
context              → search OV with current prompt → inject <relevant-memories>
turn_end             → extract turns → write queue → OV session
session_before_compact → commit pending messages
session_shutdown     → final commit

Testing

  • All 7 integration tests pass against local OpenViking v0.3.20
    • Extension loads without errors
    • Turn capture → OV session messages verified
    • Manual commit → memory extraction confirmed (cross-session recall)
    • All 7 LLM tools verified via model tool calls
    • /viking command (status + manual commit)
    • pi -c session continuation with tools available
  • Cross-session memory persistence confirmed ("I prefer dark themes" stored in session A, recalled in session B)
  • viking_search output trimmed to recallMaxContentChars (500 default)
  • Tools remain registered on pi -c resume (workaround for pi internal quirk)
  • Untested: pre-compact commit, MEMORY.md mirroring, bypass patterns, write queue batching (require long-running interactive sessions)

Related Issues

N/A — new contribution.

Checklist

  • Code follows project style guidelines
  • Tests added for new functionality — N/A (extension is TypeScript, tested manually against live OV server)
  • Documentation updated (README.md with setup, config reference, architecture, troubleshooting)
  • All manual tests pass

Maintainer Routing

This is a new agent integration example in examples/. It doesn't modify any Python/Go/Rust code.

The existing examples/claude-code-memory-plugin is the closest sibling. This extension is the pi equivalent — same REST API surface, complementary design choices (native tools vs MCP delegation, map index vs flashlight search).

Possible reviewers: @qin-ctx (platform/integration) or any cross-module maintainer.

Add OpenViking extension for the pi coding agent (github.com/mariozechner/pi-coding-agent).
Provides synchronous recall, auto-capture, 7 LLM tools, session commit, and memory indexing.

Architecture: 8 TypeScript files + config, loaded by pi's jiti transpiler — zero dependencies
beyond Node.js. Design informed by all three existing OV plugins (OpenClaw for synchronous recall,
Claude Code for production-hardened capture/ranking, Hermes for anti-patterns).

Key features:
- Synchronous recall via pi's context event (not stale prefetch)
- 7 native LLM tools (search, read, browse, remember, forget, add_resource, archive_expand)
- Tool USE preservation, tool result dropping
- Token-budgeted content resolution with graceful degradation
- Memory pollution prevention (context block stripping)
- Client-driven commit threshold
- Pre-compact commit, session resume rehydration
- Memory index (map model vs flashlight)
- Write queue with batching
- Globs-based bypass patterns
- Env var config overrides (OPENVIKING_URL, OPENVIKING_API_KEY, etc.)

~1692 lines TypeScript + 29 lines config. Tested with local OpenViking server:
7/7 tests passing (load, sync, commit, recall, 7 tools, /viking command, continuation).
Adds the full design specification (901 lines) documenting:
- Cross-plugin comparison table (Hermes vs OpenClaw vs Claude Code vs Pi)
- Architectural decisions and their provenance
- Detailed event flow diagrams
- Capture filtering, memory stripping, token estimation
- Write queue design and async patterns
- Why a memory index (map vs flashlight model)

Useful as a reference for building OV extensions for any agent harness,
not just pi.
@github-actions
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🏅 Score: 85
🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Incorrect parameter passed to client.commitSession()

sync.commit() passes an extra 'wait' argument to client.commitSession(), which does not accept a second parameter. This is a bug that could cause unexpected behavior.

const result = await this.client.commitSession(this.ovSessionId, wait);
Incorrect return type for sync.commit()

sync.commit() declares it returns string | null but actually returns { task_id: string; archive_uri: string } | null (the result from client.commitSession()). This is a type mismatch.

async commit(wait: boolean = false): Promise<string | null> {
  if (!this.ovSessionId) return null;
  const result = await this.client.commitSession(this.ovSessionId, wait);
  if (result) this.pendingTokens = 0;
  return result;
}
Hardcoded max length in shouldCapture()

shouldCapture() uses a hardcoded max length of 24000 instead of the config.captureMaxLength value. This ignores user configuration for capture max length.

if (normalized.length > 24000) return { capture: false, reason: "too_long" };

@hammerhoundai
Copy link
Copy Markdown
Author

Been using this extension personally for the last 2 days and been very satisfied.

@github-actions
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Replace blocking file operations with async equivalents

Replace blocking existsSync and readFileSync with async fs/promises methods to avoid
blocking the event loop in the async session_shutdown handler.

examples/pi-coding-agent-extension/index.ts [213-227]

 import { dirname } from "node:path";
 import { readFileSync, existsSync } from "node:fs";
+import { readFile, access } from "node:fs/promises";
+import { constants } from "node:fs";
 ...
     // Mirror MEMORY.md
     if (config.mirrorMemoryWrites && sync.sessionId) {
       const memoryPath = `${ctx.cwd}/.memory/MEMORY.md`;
-      if (existsSync(memoryPath)) {
-        try {
-          const content = readFileSync(memoryPath, "utf8");
-          if (content.trim()) {
-            await client.addMessage(
-              sync.sessionId, "user",
-              `[Memory mirror]\n${content.slice(0, 50000)}`,
-            );
-          }
-        } catch {
-          // Best effort
+      try {
+        await access(memoryPath, constants.R_OK);
+        const content = await readFile(memoryPath, "utf8");
+        if (content.trim()) {
+          await client.addMessage(
+            sync.sessionId, "user",
+            `[Memory mirror]\n${content.slice(0, 50000)}`,
+          );
         }
+      } catch {
+        // Best effort
       }
     }
Suggestion importance[1-10]: 5

__

Why: Uses async file system methods in an async handler to avoid blocking the event loop, a valid moderate-improvement change.

Low
General
Parallelize abstract fetching for better performance

Parallelize sequential abstract fetching using Promise.all to reduce index build
latency.

examples/pi-coding-agent-extension/index-builder.ts [24-31]

-const memAbstracts: string[] = [];
-for (const entry of memEntries.slice(0, 20)) {
-  if (!entry.isDir) {
-    const abs = await this.client.abstract(
-      `${memUri}/${entry.name}`,
-    );
-    if (abs) memAbstracts.push(abs);
-  }
-}
+const memAbstractPromises = memEntries.slice(0, 20)
+  .filter(entry => !entry.isDir)
+  .map(async entry => this.client.abstract(`${memUri}/${entry.name}`));
+const memAbstracts = (await Promise.all(memAbstractPromises)).filter(abs => abs !== null) as string[];
Suggestion importance[1-10]: 5

__

Why: Parallelizes sequential network calls to reduce index build latency, a valid moderate-improvement change.

Low

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant