mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor official hippo memory extension
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Keith So
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Hippo Memory - Pi Extension
|
||||
|
||||
> **my-pi packaging note:** this official source is bundled and loaded directly by the root package; do not copy it to another Pi extension directory. Use the root `install.sh`, `update.sh`, and `uninstall.sh` prompts to manage the matching global CLI. Upstream's original manual instructions are retained below for reference.
|
||||
|
||||
Biologically-inspired memory for [Pi coding agent](https://github.com/badlogic/pi-mono). Memories decay by default, retrieval strengthens them, errors stick longer, and sleep consolidation compresses episodes into patterns.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Install hippo CLI
|
||||
npm install -g hippo-memory
|
||||
|
||||
# Init hippo in your project (or scan all repos)
|
||||
hippo init
|
||||
# or: hippo init --scan ~
|
||||
|
||||
# Copy extension to Pi's extensions directory
|
||||
cp -r extensions/pi-extension ~/.pi/agent/extensions/hippo-memory
|
||||
```
|
||||
|
||||
Or for project-level:
|
||||
```bash
|
||||
cp -r extensions/pi-extension .pi/extensions/hippo-memory
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
| Automatic | When |
|
||||
|---|---|
|
||||
| Injects memory context | Session start |
|
||||
| Captures tool errors | After any failed tool call |
|
||||
| Learns from git | On `hippo sleep` (session end) |
|
||||
| Consolidates memories | On `hippo sleep` (session end) |
|
||||
| Shares to global store | On `hippo sleep` (session end) |
|
||||
|
||||
Error capture has three filters to prevent memory pollution:
|
||||
1. Noise pattern filter (timeouts, ECONNREFUSED, etc.)
|
||||
2. Per-session rate limit (max 5 error memories)
|
||||
3. Per-session deduplication
|
||||
|
||||
## Tools
|
||||
|
||||
The extension registers 5 tools for the LLM:
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `hippo_recall` | Search memories by topic |
|
||||
| `hippo_remember` | Store a new memory |
|
||||
| `hippo_outcome` | Report if memories were helpful |
|
||||
| `hippo_status` | Check memory health |
|
||||
| `hippo_context` | Smart context from git state |
|
||||
|
||||
## Config
|
||||
|
||||
Edit the `DEFAULT_CONFIG` object at the top of `index.ts`:
|
||||
|
||||
```typescript
|
||||
const DEFAULT_CONFIG = {
|
||||
budget: 4000, // Token budget for recall
|
||||
contextBudget: 1500, // Token budget for auto-context
|
||||
framing: 'observe', // observe | suggest | assert
|
||||
autoContext: true, // Inject context at session start
|
||||
autoLearn: true, // Capture tool errors
|
||||
autoSleep: true, // Run consolidation on exit
|
||||
maxErrorsPerSession: 5, // Error capture rate limit
|
||||
};
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- `hippo-memory` CLI installed globally: `npm install -g hippo-memory`
|
||||
- `.hippo/` initialized in your project: `hippo init`
|
||||
- Pi coding agent
|
||||
|
||||
## How it differs from the OpenClaw plugin
|
||||
|
||||
Same core behavior. The Pi extension uses Pi's event system (`session_start`, `tool_result`, `session_shutdown`) instead of OpenClaw's hook system. Both use `execFileSync` with args arrays (no shell injection).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Upstream provenance
|
||||
|
||||
- Upstream repository: <https://github.com/kitfunso/hippo-memory>
|
||||
- Imported release: `v1.33.0`
|
||||
- Imported commit: `e928179a3b35e8fe5837878aed071d6025ced45c`
|
||||
- Imported source: `extensions/pi-extension/`
|
||||
- Initial import date: 2026-08-19
|
||||
|
||||
This directory contains the official Hippo Memory Pi extension source, which is
|
||||
present in the upstream Git repository but excluded from the
|
||||
`hippo-memory@1.33.0` npm tarball. The extension is maintained directly in
|
||||
`my-pi`, is not a submodule, and does not retain a nested `.git` directory.
|
||||
|
||||
The matching CLI version is declared in `config/hippo-memory-version` and is
|
||||
managed as an optional machine-level dependency by the bundle helper scripts.
|
||||
Future upstream updates must be ported from an exact tag/commit and reviewed
|
||||
against local changes rather than overwriting this directory wholesale.
|
||||
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* Hippo Memory - Pi Coding Agent Extension
|
||||
*
|
||||
* Auto-injects memory context at session start, captures tool errors,
|
||||
* and runs consolidation on shutdown. Registers hippo tools for the LLM.
|
||||
*
|
||||
* Install: copy to ~/.pi/agent/extensions/hippo-memory/
|
||||
* Or add to .pi/extensions/ in your project.
|
||||
*
|
||||
* Requires: hippo-memory CLI installed globally (npm install -g hippo-memory)
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface HippoConfig {
|
||||
budget: number;
|
||||
contextBudget: number;
|
||||
framing: 'observe' | 'suggest' | 'assert';
|
||||
autoContext: boolean;
|
||||
autoLearn: boolean;
|
||||
autoSleep: boolean;
|
||||
maxErrorsPerSession: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: HippoConfig = {
|
||||
budget: 4000,
|
||||
contextBudget: 1500,
|
||||
framing: 'observe',
|
||||
autoContext: true,
|
||||
autoLearn: true,
|
||||
autoSleep: true,
|
||||
maxErrorsPerSession: 5,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Noise filtering (matches openclaw-plugin patterns)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NOISE_ERROR_PATTERNS: RegExp[] = [
|
||||
/Local media path is not under an allowed directory/i,
|
||||
/timed out\.?\s*Restart/i,
|
||||
/EISDIR:\s*illegal operation on a directory/i,
|
||||
/Missing required parameter:\s*path/i,
|
||||
/ENOENT:\s*no such file or directory/i,
|
||||
/EACCES:\s*permission denied/i,
|
||||
/EPERM:\s*operation not permitted/i,
|
||||
/socket hang up/i,
|
||||
/ECONNREFUSED/i,
|
||||
/ECONNRESET/i,
|
||||
/ERR_SOCKET_CONNECTION_TIMEOUT/i,
|
||||
/net::ERR_/i,
|
||||
/Navigation timeout/i,
|
||||
];
|
||||
|
||||
function isNoiseError(error: string): boolean {
|
||||
return NOISE_ERROR_PATTERNS.some((p) => p.test(error));
|
||||
}
|
||||
|
||||
/** True for a string value, without assuming the input's shape — used on
|
||||
* fields read off the untyped Pi extension event/plugin surface. */
|
||||
function isString<T>(value: T): value is T & string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
function hashError(toolName: string, error: string): string {
|
||||
return `${toolName}::${error.replace(/\s+/g, ' ').trim().slice(0, 80).toLowerCase()}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let sessionErrorCount = 0;
|
||||
const sessionErrorHashes = new Set<string>();
|
||||
let sessionMemoryCount = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hippo CLI wrapper (no shell, args as array)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function runHippo(args: readonly string[], cwd?: string): string {
|
||||
try {
|
||||
const result = execFileSync('hippo', args, {
|
||||
cwd: cwd || process.cwd(),
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
// `encoding: 'utf8'` above selects the ExecFileSyncOptionsWithStringEncoding
|
||||
// overload, so `result` is always a `string` here — no runtime check needed.
|
||||
return result.trim();
|
||||
} catch (err: any) {
|
||||
return err.stdout?.trim() || err.message || 'hippo command failed';
|
||||
}
|
||||
}
|
||||
|
||||
function hippoAvailable(): boolean {
|
||||
try {
|
||||
execFileSync('hippo', ['status'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hippoInitialized(cwd: string): boolean {
|
||||
return existsSync(join(cwd, '.hippo'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function (pi: any) {
|
||||
// Check hippo is available
|
||||
if (!hippoAvailable()) {
|
||||
console.error('[hippo] hippo-memory CLI not found. Install: npm install -g hippo-memory');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { ...DEFAULT_CONFIG };
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Session start: inject memory context
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.on('session_start', async (_event: any, ctx: any) => {
|
||||
sessionErrorCount = 0;
|
||||
sessionErrorHashes.clear();
|
||||
sessionMemoryCount = 0;
|
||||
|
||||
if (!config.autoContext) return;
|
||||
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
if (!hippoInitialized(cwd)) return;
|
||||
|
||||
const context = runHippo(
|
||||
['context', '--auto', '--budget', String(config.contextBudget), '--framing', config.framing],
|
||||
cwd,
|
||||
);
|
||||
|
||||
if (context && context.length > 10 && !context.includes('No hippo store')) {
|
||||
return {
|
||||
systemPromptAppend: `\n\n## Project Memory (Hippo)\n\n${context}\n\nWhen you learn something important, tell the user to run: hippo remember "<lesson>"\nWhen you encounter errors, suggest: hippo remember "<error description>" --error`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tool errors: auto-capture (with noise filtering + rate limiting + dedup)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (config.autoLearn) {
|
||||
pi.on('tool_result', async (event: any, ctx: any) => {
|
||||
if (!event.isError) return;
|
||||
|
||||
const error = isString(event.content)
|
||||
? event.content
|
||||
: Array.isArray(event.content)
|
||||
? event.content.map((c: any) => c.text || '').join(' ')
|
||||
: '';
|
||||
|
||||
if (!error || error.length < 10) return;
|
||||
|
||||
// Filter 1: noise
|
||||
if (isNoiseError(error)) return;
|
||||
|
||||
// Filter 2: rate limit
|
||||
if (sessionErrorCount >= config.maxErrorsPerSession) return;
|
||||
|
||||
// Filter 3: dedup
|
||||
const hash = hashError(event.toolName || 'unknown', error);
|
||||
if (sessionErrorHashes.has(hash)) return;
|
||||
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
if (!hippoInitialized(cwd)) return;
|
||||
|
||||
const toolTag = (event.toolName || 'unknown')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 30);
|
||||
|
||||
const truncated = error.replace(/\s+/g, ' ').trim().slice(0, 500);
|
||||
const text = `Tool '${event.toolName || 'unknown'}' failed: ${truncated}`;
|
||||
|
||||
const args: string[] = ['remember', text, '--error', '--observed', '--tag', 'pi-agent'];
|
||||
if (toolTag) args.push('--tag', toolTag);
|
||||
|
||||
const result = runHippo(args, cwd);
|
||||
if (result.includes('Remembered [')) {
|
||||
sessionErrorHashes.add(hash);
|
||||
sessionErrorCount++;
|
||||
sessionMemoryCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Session shutdown: run consolidation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.on('session_shutdown', async (_event: any, ctx: any) => {
|
||||
if (!config.autoSleep) return;
|
||||
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
if (!hippoInitialized(cwd)) return;
|
||||
|
||||
// Only sleep if we captured some memories this session
|
||||
if (sessionMemoryCount < 1) {
|
||||
// Still run sleep for the auto-learn-from-git + auto-share features
|
||||
try { runHippo(['sleep'], cwd); } catch { /* best effort */ }
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
runHippo(['sleep'], cwd);
|
||||
} catch {
|
||||
// Best effort — don't block shutdown
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tools: hippo_recall
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.registerTool({
|
||||
name: 'hippo_recall',
|
||||
description: 'Search project memory for relevant context. Returns memories ranked by relevance and strength.',
|
||||
promptSnippet: 'hippo_recall: Search project memory by topic',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'What to search for (natural language)' },
|
||||
budget: { type: 'number', description: 'Max tokens to return (default: 4000)' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
const budget = params.budget || config.budget;
|
||||
const result = runHippo(
|
||||
['recall', params.query, '--budget', String(budget), '--framing', config.framing],
|
||||
cwd,
|
||||
);
|
||||
return { content: [{ type: 'text', text: result || 'No relevant memories found.' }] };
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tools: hippo_remember
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.registerTool({
|
||||
name: 'hippo_remember',
|
||||
description: 'Store a lesson, insight, or error in project memory. Memories decay over time unless retrieved.',
|
||||
promptSnippet: 'hippo_remember: Store a memory (lessons, errors, decisions)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'The memory content' },
|
||||
error: { type: 'boolean', description: 'Tag as error (doubles retention)' },
|
||||
tag: { type: 'string', description: 'Optional tag for categorization' },
|
||||
pin: { type: 'boolean', description: 'Pin memory (never decays)' },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
const args: string[] = ['remember', params.text];
|
||||
if (params.error) args.push('--error');
|
||||
if (params.pin) args.push('--pin');
|
||||
if (params.tag) {
|
||||
const safe = params.tag.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 30);
|
||||
if (safe) args.push('--tag', safe);
|
||||
}
|
||||
const result = runHippo(args, cwd);
|
||||
if (result.includes('Remembered [')) sessionMemoryCount++;
|
||||
return { content: [{ type: 'text', text: result || 'Memory stored.' }] };
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tools: hippo_outcome
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.registerTool({
|
||||
name: 'hippo_outcome',
|
||||
description: 'Report whether recalled memories were helpful. Strengthens or weakens them.',
|
||||
promptSnippet: 'hippo_outcome: Rate recalled memories as good or bad',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
good: { type: 'boolean', description: 'true = helpful, false = not helpful' },
|
||||
},
|
||||
required: ['good'],
|
||||
},
|
||||
async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
const flag = params.good ? '--good' : '--bad';
|
||||
const result = runHippo(['outcome', flag], cwd);
|
||||
return { content: [{ type: 'text', text: result || 'Outcome recorded.' }] };
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tools: hippo_status
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.registerTool({
|
||||
name: 'hippo_status',
|
||||
description: 'Show memory health: counts, strength distribution, conflicts.',
|
||||
promptSnippet: 'hippo_status: Check memory store health',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
async execute(_toolCallId: string, _params: any, _signal: any, _onUpdate: any, ctx: any) {
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
const result = runHippo(['status'], cwd);
|
||||
return { content: [{ type: 'text', text: result || 'No hippo store found.' }] };
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tools: hippo_context
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pi.registerTool({
|
||||
name: 'hippo_context',
|
||||
description: 'Get smart memory context based on current git state and task.',
|
||||
promptSnippet: 'hippo_context: Smart context injection from git state',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
budget: { type: 'number', description: 'Token budget (default: 1500)' },
|
||||
},
|
||||
},
|
||||
async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
||||
const cwd = ctx.cwd || process.cwd();
|
||||
const budget = params.budget || config.contextBudget;
|
||||
const result = runHippo(
|
||||
['context', '--auto', '--budget', String(budget), '--framing', config.framing],
|
||||
cwd,
|
||||
);
|
||||
return { content: [{ type: 'text', text: result || 'No context available.' }] };
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user