peon-mem 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/bin/peon-mem.mjs +273 -0
- package/dist/brain.d.ts +72 -0
- package/dist/brain.js +224 -0
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +37 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +99 -0
- package/dist/daemon-cli.d.ts +2 -0
- package/dist/daemon-cli.js +54 -0
- package/dist/daemon.d.ts +23 -0
- package/dist/daemon.js +1078 -0
- package/dist/embedding-store.d.ts +43 -0
- package/dist/embedding-store.js +169 -0
- package/dist/embeddings.d.ts +93 -0
- package/dist/embeddings.js +345 -0
- package/dist/entities.d.ts +61 -0
- package/dist/entities.js +191 -0
- package/dist/entity-extraction.d.ts +33 -0
- package/dist/entity-extraction.js +75 -0
- package/dist/eval-metrics.d.ts +27 -0
- package/dist/eval-metrics.js +50 -0
- package/dist/evaluation.d.ts +58 -0
- package/dist/evaluation.js +244 -0
- package/dist/global-extraction.d.ts +15 -0
- package/dist/global-extraction.js +61 -0
- package/dist/global-memory.d.ts +43 -0
- package/dist/global-memory.js +306 -0
- package/dist/global-promotion.d.ts +25 -0
- package/dist/global-promotion.js +29 -0
- package/dist/hyde.d.ts +31 -0
- package/dist/hyde.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +246 -0
- package/dist/injection.d.ts +38 -0
- package/dist/injection.js +133 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.js +63 -0
- package/dist/memory-mutations.d.ts +24 -0
- package/dist/memory-mutations.js +57 -0
- package/dist/memory-store.d.ts +194 -0
- package/dist/memory-store.js +1205 -0
- package/dist/monitor.d.ts +13 -0
- package/dist/monitor.js +977 -0
- package/dist/overview.d.ts +73 -0
- package/dist/overview.js +104 -0
- package/dist/processor.d.ts +90 -0
- package/dist/processor.js +450 -0
- package/dist/quality.d.ts +86 -0
- package/dist/quality.js +338 -0
- package/dist/recuration.d.ts +13 -0
- package/dist/recuration.js +65 -0
- package/dist/reranker.d.ts +34 -0
- package/dist/reranker.js +89 -0
- package/dist/retrieval.d.ts +106 -0
- package/dist/retrieval.js +392 -0
- package/dist/session-index.d.ts +34 -0
- package/dist/session-index.js +87 -0
- package/dist/temporal.d.ts +20 -0
- package/dist/temporal.js +62 -0
- package/dist/token-ab-monitor.d.ts +1 -0
- package/dist/token-ab-monitor.js +7 -0
- package/dist/tools.d.ts +232 -0
- package/dist/tools.js +546 -0
- package/dist/types.d.ts +169 -0
- package/dist/types.js +1 -0
- package/docs/assets/neural-universe.png +0 -0
- package/package.json +57 -0
- package/scripts/claude-peon-hook.mjs +522 -0
- package/scripts/codex-peon-hook.mjs +4 -0
- package/scripts/eval-retrieval-labeled.mjs +135 -0
- package/scripts/eval-retrieval.mjs +96 -0
- package/scripts/evaluate-peon.mjs +47 -0
- package/scripts/install-peon-stl.mjs +82 -0
- package/scripts/install-peon.mjs +318 -0
- package/scripts/lib/eval-ledger.mjs +104 -0
- package/scripts/lib/stl-classify.mjs +44 -0
- package/scripts/longmemeval-eval.mjs +144 -0
- package/scripts/peon-report.mjs +155 -0
- package/scripts/peon-stl.mjs +506 -0
- package/scripts/token-ab-monitor.html +235 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MemoryRecord, MemoryType } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-promotion policy: which project beliefs should also live in GLOBAL memory
|
|
4
|
+
* so any project can recall them, not just the one that learned them.
|
|
5
|
+
*
|
|
6
|
+
* The bar is deliberately high to keep project isolation the default (a locked
|
|
7
|
+
* product decision). Only two things cross the line:
|
|
8
|
+
* 1. Records the processor (or a human) explicitly scoped "global" — the
|
|
9
|
+
* AI's own judgment that knowledge is cross-cutting (infra, environment,
|
|
10
|
+
* external services, reusable references like cluster docs).
|
|
11
|
+
* 2. `fact` records — facts about the user/environment/tooling are
|
|
12
|
+
* cross-cutting by nature.
|
|
13
|
+
*
|
|
14
|
+
* Project-internal noise (decisions about THIS repo, file artifacts, timeline
|
|
15
|
+
* events, project-specific preferences) intentionally stays project-scoped.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_PROMOTABLE_TYPES: readonly MemoryType[];
|
|
18
|
+
export interface PromotionPolicy {
|
|
19
|
+
/** Types always promoted regardless of explicit scope. */
|
|
20
|
+
promotableTypes?: readonly MemoryType[];
|
|
21
|
+
}
|
|
22
|
+
/** A pure predicate — true if this record should be copied into global memory. */
|
|
23
|
+
export declare function isGloballyPromotable(record: MemoryRecord, policy?: PromotionPolicy): boolean;
|
|
24
|
+
/** Select the subset of project records that should be promoted to global memory. */
|
|
25
|
+
export declare function selectGloballyPromotable(records: readonly MemoryRecord[], policy?: PromotionPolicy): MemoryRecord[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-promotion policy: which project beliefs should also live in GLOBAL memory
|
|
3
|
+
* so any project can recall them, not just the one that learned them.
|
|
4
|
+
*
|
|
5
|
+
* The bar is deliberately high to keep project isolation the default (a locked
|
|
6
|
+
* product decision). Only two things cross the line:
|
|
7
|
+
* 1. Records the processor (or a human) explicitly scoped "global" — the
|
|
8
|
+
* AI's own judgment that knowledge is cross-cutting (infra, environment,
|
|
9
|
+
* external services, reusable references like cluster docs).
|
|
10
|
+
* 2. `fact` records — facts about the user/environment/tooling are
|
|
11
|
+
* cross-cutting by nature.
|
|
12
|
+
*
|
|
13
|
+
* Project-internal noise (decisions about THIS repo, file artifacts, timeline
|
|
14
|
+
* events, project-specific preferences) intentionally stays project-scoped.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_PROMOTABLE_TYPES = ["fact"];
|
|
17
|
+
/** A pure predicate — true if this record should be copied into global memory. */
|
|
18
|
+
export function isGloballyPromotable(record, policy = {}) {
|
|
19
|
+
if (record.status !== "active")
|
|
20
|
+
return false;
|
|
21
|
+
if (record.scope === "global")
|
|
22
|
+
return true;
|
|
23
|
+
const types = policy.promotableTypes ?? DEFAULT_PROMOTABLE_TYPES;
|
|
24
|
+
return types.includes(record.type);
|
|
25
|
+
}
|
|
26
|
+
/** Select the subset of project records that should be promoted to global memory. */
|
|
27
|
+
export function selectGloballyPromotable(records, policy = {}) {
|
|
28
|
+
return records.filter((record) => isGloballyPromotable(record, policy));
|
|
29
|
+
}
|
package/dist/hyde.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PeonConfig } from "./config.js";
|
|
2
|
+
import type { FetchLike } from "./reranker.js";
|
|
3
|
+
/**
|
|
4
|
+
* HyDE — Hypothetical Document Embeddings (Gao et al., 2022).
|
|
5
|
+
*
|
|
6
|
+
* A short, vague query ("how do we handle retries?") often shares few tokens — and little
|
|
7
|
+
* embedding mass — with the belief that actually answers it ("Route webhook retries through
|
|
8
|
+
* the durable queue with exponential backoff"). HyDE closes that gap: a small LLM writes a
|
|
9
|
+
* HYPOTHETICAL answer to the query, and we retrieve against THAT richer text instead of the
|
|
10
|
+
* bare query. The hypothetical may be factually wrong — it doesn't matter; its job is to land
|
|
11
|
+
* in the right neighborhood of the embedding/lexical space so the real, stored answer ranks up.
|
|
12
|
+
*
|
|
13
|
+
* We return an EXPANDED query = original query + hypothetical. Keeping the original terms means
|
|
14
|
+
* lexical matching stays anchored on what the user literally asked, while the hypothetical adds
|
|
15
|
+
* the vocabulary and semantic signal that pure query embedding lacks. Opt-in and fail-safe:
|
|
16
|
+
* with no API key, AI disabled, or any error, it returns the original query untouched.
|
|
17
|
+
*/
|
|
18
|
+
export interface HydeOptions {
|
|
19
|
+
config: PeonConfig;
|
|
20
|
+
model?: string;
|
|
21
|
+
fetchImpl?: FetchLike;
|
|
22
|
+
/** Cap on the generated hypothetical (keeps prompt cost and noise down). Default 320. */
|
|
23
|
+
maxChars?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface HydeResult {
|
|
26
|
+
/** original + hypothetical, for use as the retrieval query. Equals original on any failure. */
|
|
27
|
+
expanded: string;
|
|
28
|
+
/** the generated hypothetical answer alone (empty if generation was skipped/failed). */
|
|
29
|
+
hypothetical: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function expandQuery(query: string | undefined, options: HydeOptions): Promise<HydeResult>;
|
package/dist/hyde.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const DEFAULT_MAX_CHARS = 320;
|
|
2
|
+
export async function expandQuery(query, options) {
|
|
3
|
+
const q = (query ?? "").trim();
|
|
4
|
+
if (!q)
|
|
5
|
+
return { expanded: "", hypothetical: "" };
|
|
6
|
+
const { config } = options;
|
|
7
|
+
if (config.aiMode === "off" || !config.openRouterApiKey)
|
|
8
|
+
return { expanded: q, hypothetical: "" };
|
|
9
|
+
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
10
|
+
if (!doFetch)
|
|
11
|
+
return { expanded: q, hypothetical: "" };
|
|
12
|
+
const maxChars = Math.max(80, Math.trunc(options.maxChars ?? DEFAULT_MAX_CHARS));
|
|
13
|
+
const system = "You write a brief HYPOTHETICAL answer used only to improve memory retrieval. " +
|
|
14
|
+
"Given a question, write 1-3 plausible sentences that such an answer might contain, " +
|
|
15
|
+
"using concrete, specific vocabulary (entities, file names, decisions, values). " +
|
|
16
|
+
"Do not hedge, do not say you lack context, do not ask questions. Output the sentences only.";
|
|
17
|
+
const user = `Question: ${q}\n\nHypothetical answer:`;
|
|
18
|
+
try {
|
|
19
|
+
const response = await doFetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${config.openRouterApiKey}`,
|
|
23
|
+
"Content-Type": "application/json"
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
model: options.model ?? config.processingModel,
|
|
27
|
+
messages: [
|
|
28
|
+
{ role: "system", content: system },
|
|
29
|
+
{ role: "user", content: user }
|
|
30
|
+
],
|
|
31
|
+
temperature: 0.3
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
return { expanded: q, hypothetical: "" };
|
|
36
|
+
const json = (await response.json());
|
|
37
|
+
const raw = (json.choices?.[0]?.message?.content ?? "").replace(/\s+/g, " ").trim();
|
|
38
|
+
if (!raw)
|
|
39
|
+
return { expanded: q, hypothetical: "" };
|
|
40
|
+
const hypothetical = raw.length > maxChars ? `${raw.slice(0, maxChars - 1)}…` : raw;
|
|
41
|
+
return { expanded: `${q}\n${hypothetical}`, hypothetical };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { expanded: q, hypothetical: "" };
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createPeonTools } from "./tools.js";
|
|
6
|
+
const tools = createPeonTools({ daemonUrl: process.env.PEON_DAEMON_URL });
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "peon-mcp",
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
});
|
|
11
|
+
const projectName = (path) => path.split("/").filter(Boolean).pop() ?? "project";
|
|
12
|
+
/**
|
|
13
|
+
* Compact text for the model. The internal tool results carry redundant fields
|
|
14
|
+
* (normalized index text, duplicated record arrays, per-reason breakdowns) that
|
|
15
|
+
* would otherwise flood the calling model's context — a single recall could cost
|
|
16
|
+
* thousands of tokens. These formatters return only what an AI needs to read.
|
|
17
|
+
*/
|
|
18
|
+
function compactContext(ctx) {
|
|
19
|
+
const sections = [
|
|
20
|
+
["Summary", ctx.summary],
|
|
21
|
+
["Memory", ctx.memories],
|
|
22
|
+
["Decisions", ctx.decisions],
|
|
23
|
+
["Preferences", ctx.preferences],
|
|
24
|
+
["Open questions", ctx.openQuestions],
|
|
25
|
+
["Artifacts", ctx.artifacts],
|
|
26
|
+
["Timeline", ctx.timeline]
|
|
27
|
+
];
|
|
28
|
+
const body = sections
|
|
29
|
+
.filter(([, value]) => value && value.trim())
|
|
30
|
+
.map(([label, value]) => `## ${label}\n${value.trim()}`)
|
|
31
|
+
.join("\n\n");
|
|
32
|
+
return body || "No memory recorded for this project yet.";
|
|
33
|
+
}
|
|
34
|
+
function compactSearch(result) {
|
|
35
|
+
return result.injectionPreview?.trim() || `No memory matched "${result.query}".`;
|
|
36
|
+
}
|
|
37
|
+
function compactBrain(result) {
|
|
38
|
+
const counts = {};
|
|
39
|
+
for (const record of result.records)
|
|
40
|
+
counts[record.status] = (counts[record.status] ?? 0) + 1;
|
|
41
|
+
const status = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ") || "no records";
|
|
42
|
+
const head = `Peon brain — ${projectName(result.projectPath)} (${status}; ${result.graph.nodes.length} graph nodes)`;
|
|
43
|
+
return `${head}\n\n${result.injectionPreview?.trim() || compactContext(result.context)}`;
|
|
44
|
+
}
|
|
45
|
+
function compactCrossProject(result) {
|
|
46
|
+
if (result.results.length === 0) {
|
|
47
|
+
return `No relevant beliefs found in other projects for "${result.query}" (searched ${result.projectsSearched.length}).`;
|
|
48
|
+
}
|
|
49
|
+
const lines = result.results.map((hit) => `- [${hit.projectName} · ${hit.record.type}] ${hit.record.content}`);
|
|
50
|
+
return `Recall across projects for "${result.query}" (searched ${result.projectsSearched.length}):\n${lines.join("\n")}`;
|
|
51
|
+
}
|
|
52
|
+
server.registerTool("start_session", {
|
|
53
|
+
title: "Start Peon Session",
|
|
54
|
+
description: "Start a Peon memory session for a project.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
projectPath: z.string(),
|
|
57
|
+
client: z.string(),
|
|
58
|
+
cwd: z.string().optional()
|
|
59
|
+
}
|
|
60
|
+
}, async (input) => ({
|
|
61
|
+
content: [{ type: "text", text: JSON.stringify(await tools.startSession(input), null, 2) }]
|
|
62
|
+
}));
|
|
63
|
+
server.registerTool("record_message", {
|
|
64
|
+
title: "Record Message",
|
|
65
|
+
description: "Record a user, assistant, or system message into Peon memory.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
sessionId: z.string(),
|
|
68
|
+
role: z.enum(["user", "assistant", "system"]),
|
|
69
|
+
content: z.string()
|
|
70
|
+
}
|
|
71
|
+
}, async (input) => ({
|
|
72
|
+
content: [{ type: "text", text: JSON.stringify(await tools.recordMessage(input), null, 2) }]
|
|
73
|
+
}));
|
|
74
|
+
server.registerTool("record_event", {
|
|
75
|
+
title: "Record Event",
|
|
76
|
+
description: "Record a structured Peon memory event such as a decision or preference.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
sessionId: z.string(),
|
|
79
|
+
type: z.string(),
|
|
80
|
+
content: z.string()
|
|
81
|
+
}
|
|
82
|
+
}, async (input) => ({
|
|
83
|
+
content: [{ type: "text", text: JSON.stringify(await tools.recordEvent(input), null, 2) }]
|
|
84
|
+
}));
|
|
85
|
+
server.registerTool("end_session", {
|
|
86
|
+
title: "End Peon Session",
|
|
87
|
+
description: "End a Peon session and update brain files.",
|
|
88
|
+
inputSchema: { sessionId: z.string() }
|
|
89
|
+
}, async (input) => ({
|
|
90
|
+
content: [{ type: "text", text: JSON.stringify(await tools.endSession(input), null, 2) }]
|
|
91
|
+
}));
|
|
92
|
+
server.registerTool("get_context", {
|
|
93
|
+
title: "Get Peon Context",
|
|
94
|
+
description: "Return project brain context from Peon memory.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
projectPath: z.string(),
|
|
97
|
+
query: z.string().optional(),
|
|
98
|
+
maxChars: z.number().optional()
|
|
99
|
+
}
|
|
100
|
+
}, async (input) => ({
|
|
101
|
+
content: [{ type: "text", text: compactContext(await tools.getContext(input)) }]
|
|
102
|
+
}));
|
|
103
|
+
server.registerTool("inspect_brain", {
|
|
104
|
+
title: "Inspect Peon Brain",
|
|
105
|
+
description: "Return a compact summary of a project's brain: record counts by status and the prompt injection preview.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
projectPath: z.string(),
|
|
108
|
+
query: z.string().optional(),
|
|
109
|
+
maxChars: z.number().optional()
|
|
110
|
+
}
|
|
111
|
+
}, async (input) => ({
|
|
112
|
+
content: [{ type: "text", text: compactBrain(await tools.inspectBrain(input)) }]
|
|
113
|
+
}));
|
|
114
|
+
server.registerTool("search_memory", {
|
|
115
|
+
title: "Search Peon Memory",
|
|
116
|
+
description: "Search structured Peon memory; returns a compact ranked list with one-line reasons.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
projectPath: z.string(),
|
|
119
|
+
query: z.string(),
|
|
120
|
+
limit: z.number().optional(),
|
|
121
|
+
maxChars: z.number().optional()
|
|
122
|
+
}
|
|
123
|
+
}, async (input) => ({
|
|
124
|
+
content: [{ type: "text", text: compactSearch(await tools.searchMemory(input)) }]
|
|
125
|
+
}));
|
|
126
|
+
server.registerTool("quality_report", {
|
|
127
|
+
title: "Peon Quality Report",
|
|
128
|
+
description: "Inspect Peon memory quality signals: duplicates, conflicts, stale records, and promotions.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
projectPath: z.string(),
|
|
131
|
+
staleAfterDays: z.number().optional()
|
|
132
|
+
}
|
|
133
|
+
}, async (input) => ({
|
|
134
|
+
content: [{ type: "text", text: JSON.stringify(await tools.qualityReport(input), null, 2) }]
|
|
135
|
+
}));
|
|
136
|
+
server.registerTool("build_injection", {
|
|
137
|
+
title: "Build Peon Context Injection",
|
|
138
|
+
description: "Build a redacted context injection from project and global memory with selection explanations.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
projectPath: z.string(),
|
|
141
|
+
query: z.string().optional(),
|
|
142
|
+
maxChars: z.number().optional(),
|
|
143
|
+
includeInactive: z.boolean().optional()
|
|
144
|
+
}
|
|
145
|
+
}, async (input) => ({
|
|
146
|
+
content: [{ type: "text", text: JSON.stringify(await tools.buildInjection(input), null, 2) }]
|
|
147
|
+
}));
|
|
148
|
+
server.registerTool("query_projects", {
|
|
149
|
+
title: "Recall Across Projects",
|
|
150
|
+
description: "Recall relevant beliefs from OTHER Peon projects on demand. Peon memory is isolated per project by default; call this when the user explicitly asks about another project (e.g. 'what did we decide in project Y about auth?'). Omit projectPath to search every known project; pass it to target one. Set excludeProjectPath to the current project so you only get other projects' beliefs.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
query: z.string(),
|
|
153
|
+
projectPath: z.string().optional(),
|
|
154
|
+
excludeProjectPath: z.string().optional(),
|
|
155
|
+
limit: z.number().optional()
|
|
156
|
+
}
|
|
157
|
+
}, async (input) => ({
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: compactCrossProject(await tools.crossProjectSearch({
|
|
162
|
+
query: input.query,
|
|
163
|
+
projectPaths: input.projectPath ? [input.projectPath] : undefined,
|
|
164
|
+
excludeProjectPath: input.excludeProjectPath,
|
|
165
|
+
limit: input.limit
|
|
166
|
+
}))
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}));
|
|
170
|
+
server.registerTool("remember_global", {
|
|
171
|
+
title: "Remember Global Memory",
|
|
172
|
+
description: "Upsert a global Peon memory record for cross-project reuse.",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
memory: z.object({
|
|
175
|
+
type: z.enum(["summary", "decision", "preference", "open_question", "artifact", "timeline", "fact"]),
|
|
176
|
+
content: z.string(),
|
|
177
|
+
scope: z.enum(["project", "global", "session"]).optional(),
|
|
178
|
+
importance: z.number().optional(),
|
|
179
|
+
confidence: z.number().optional(),
|
|
180
|
+
entities: z.array(z.string()).optional(),
|
|
181
|
+
status: z.enum(["active", "stale", "conflicted"]).optional()
|
|
182
|
+
}),
|
|
183
|
+
source: z
|
|
184
|
+
.object({
|
|
185
|
+
kind: z.enum(["ai_processing", "manual", "hook"]).optional(),
|
|
186
|
+
reason: z.string().optional()
|
|
187
|
+
})
|
|
188
|
+
.optional()
|
|
189
|
+
}
|
|
190
|
+
}, async (input) => ({
|
|
191
|
+
content: [{ type: "text", text: JSON.stringify(await tools.rememberGlobal(input), null, 2) }]
|
|
192
|
+
}));
|
|
193
|
+
server.registerTool("search_global_memory", {
|
|
194
|
+
title: "Search Global Peon Memory",
|
|
195
|
+
description: "Search cross-project global Peon memory.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
query: z.string().optional(),
|
|
198
|
+
type: z.enum(["summary", "decision", "preference", "open_question", "artifact", "timeline", "fact"]).optional(),
|
|
199
|
+
status: z.enum(["active", "stale", "conflicted"]).optional()
|
|
200
|
+
}
|
|
201
|
+
}, async (input) => ({
|
|
202
|
+
content: [{ type: "text", text: JSON.stringify(await tools.searchGlobalMemory(input), null, 2) }]
|
|
203
|
+
}));
|
|
204
|
+
server.registerTool("import_global_memory", {
|
|
205
|
+
title: "Import Global Peon Memory",
|
|
206
|
+
description: "Import global-scoped memory records from a project brain into the global memory store.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
projectPath: z.string()
|
|
209
|
+
}
|
|
210
|
+
}, async (input) => ({
|
|
211
|
+
content: [{ type: "text", text: JSON.stringify(await tools.importGlobalMemory(input), null, 2) }]
|
|
212
|
+
}));
|
|
213
|
+
server.registerTool("evaluate_project", {
|
|
214
|
+
title: "Evaluate Peon Project Memory",
|
|
215
|
+
description: "Evaluate recall, coverage, noise, and cost for a Peon project memory folder.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
projectPath: z.string(),
|
|
218
|
+
expectedMemories: z
|
|
219
|
+
.array(z.union([z.string(), z.object({ id: z.string().optional(), content: z.string() })]))
|
|
220
|
+
.optional()
|
|
221
|
+
}
|
|
222
|
+
}, async (input) => ({
|
|
223
|
+
content: [{ type: "text", text: JSON.stringify(await tools.evaluateProject(input), null, 2) }]
|
|
224
|
+
}));
|
|
225
|
+
server.registerTool("process_memory", {
|
|
226
|
+
title: "Process Peon Memory",
|
|
227
|
+
description: "Run gated Peon AI processing for a project and update structured brain files.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
projectPath: z.string(),
|
|
230
|
+
reason: z.string().optional()
|
|
231
|
+
}
|
|
232
|
+
}, async (input) => ({
|
|
233
|
+
content: [{ type: "text", text: JSON.stringify(await tools.processMemory(input), null, 2) }]
|
|
234
|
+
}));
|
|
235
|
+
server.registerTool("maybe_process_memory", {
|
|
236
|
+
title: "Maybe Process Peon Memory",
|
|
237
|
+
description: "Apply Peon's automatic cost-aware processing policy and process only when the gate says to run.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
projectPath: z.string(),
|
|
240
|
+
trigger: z.string(),
|
|
241
|
+
force: z.boolean().optional()
|
|
242
|
+
}
|
|
243
|
+
}, async (input) => ({
|
|
244
|
+
content: [{ type: "text", text: JSON.stringify(await tools.maybeProcessMemory(input), null, 2) }]
|
|
245
|
+
}));
|
|
246
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type RankedMemoryRecord } from "./retrieval.js";
|
|
2
|
+
import type { MemoryRecord, MemoryScope, MemoryStatus, MemoryType } from "./types.js";
|
|
3
|
+
export interface BuildContextInjectionOptions {
|
|
4
|
+
projectResults: RankedMemoryRecord[];
|
|
5
|
+
globalRecords: MemoryRecord[];
|
|
6
|
+
query?: string;
|
|
7
|
+
maxChars: number;
|
|
8
|
+
includeInactive?: boolean;
|
|
9
|
+
now?: Date | string | number;
|
|
10
|
+
}
|
|
11
|
+
export interface SelectedInjectionMetadata {
|
|
12
|
+
id: string;
|
|
13
|
+
scope: MemoryScope;
|
|
14
|
+
type: MemoryType;
|
|
15
|
+
status: MemoryStatus;
|
|
16
|
+
score: number;
|
|
17
|
+
whySelected: string;
|
|
18
|
+
source: MemoryRecord["source"];
|
|
19
|
+
chars: number;
|
|
20
|
+
}
|
|
21
|
+
export type OmittedInjectionReason = "suppressed_status" | "max_chars";
|
|
22
|
+
export interface OmittedInjectionMetadata {
|
|
23
|
+
id: string;
|
|
24
|
+
scope: MemoryScope;
|
|
25
|
+
type: MemoryType;
|
|
26
|
+
status: MemoryStatus;
|
|
27
|
+
reason: OmittedInjectionReason;
|
|
28
|
+
score: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ContextInjection {
|
|
31
|
+
preview: string;
|
|
32
|
+
selected: SelectedInjectionMetadata[];
|
|
33
|
+
omitted: OmittedInjectionMetadata[];
|
|
34
|
+
totalChars: number;
|
|
35
|
+
maxChars: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function buildContextInjection(options: BuildContextInjectionOptions): ContextInjection;
|
|
38
|
+
export declare function redactSecrets(value: string): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { diversifyByMMR, rankMemoryRecords } from "./retrieval.js";
|
|
2
|
+
const title = "Peon Context Injection v2";
|
|
3
|
+
const emptyPreview = `${title}\nNo memory selected.`;
|
|
4
|
+
export function buildContextInjection(options) {
|
|
5
|
+
const maxChars = Math.max(0, Math.trunc(options.maxChars));
|
|
6
|
+
const includeInactive = options.includeInactive === true;
|
|
7
|
+
const projectResults = options.projectResults.map((item, originIndex) => ({ item, originIndex }));
|
|
8
|
+
const globalResults = rankMemoryRecords(options.globalRecords, options.query, { now: options.now }).map((item, originIndex) => ({
|
|
9
|
+
item,
|
|
10
|
+
originIndex: projectResults.length + originIndex
|
|
11
|
+
}));
|
|
12
|
+
const omitted = [];
|
|
13
|
+
const relevant = [...projectResults, ...globalResults]
|
|
14
|
+
.filter((candidate) => {
|
|
15
|
+
if (includeInactive || candidate.item.record.status === "active")
|
|
16
|
+
return true;
|
|
17
|
+
omitted.push(omittedMetadata(candidate.item, "suppressed_status"));
|
|
18
|
+
return false;
|
|
19
|
+
})
|
|
20
|
+
.sort(compareCandidates);
|
|
21
|
+
// Diversify (MMR) so the block has coverage, not N paraphrases of the top hit.
|
|
22
|
+
// We reorder the relevance-sorted candidates by the diversified order of their records.
|
|
23
|
+
const diversified = diversifyByMMR(relevant.map((c) => c.item));
|
|
24
|
+
const orderOf = new Map(diversified.map((item, i) => [item, i]));
|
|
25
|
+
const candidates = relevant.slice().sort((a, b) => (orderOf.get(a.item) ?? 0) - (orderOf.get(b.item) ?? 0));
|
|
26
|
+
const selected = [];
|
|
27
|
+
const lines = [title];
|
|
28
|
+
let preview = lines.join("\n");
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
const block = formatCandidate(candidate.item);
|
|
31
|
+
const nextPreview = `${preview}\n${block}`;
|
|
32
|
+
if (nextPreview.length > maxChars) {
|
|
33
|
+
omitted.push(omittedMetadata(candidate.item, "max_chars"));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
preview = nextPreview;
|
|
37
|
+
selected.push(selectedMetadata(candidate.item, block.length));
|
|
38
|
+
}
|
|
39
|
+
if (selected.length === 0 && emptyPreview.length <= maxChars) {
|
|
40
|
+
preview = emptyPreview;
|
|
41
|
+
}
|
|
42
|
+
if (preview.length > maxChars) {
|
|
43
|
+
preview = preview.slice(0, maxChars);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
preview,
|
|
47
|
+
selected,
|
|
48
|
+
omitted,
|
|
49
|
+
totalChars: preview.length,
|
|
50
|
+
maxChars
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function compareCandidates(left, right) {
|
|
54
|
+
const scoreDelta = injectionScore(right.item) - injectionScore(left.item);
|
|
55
|
+
if (scoreDelta !== 0)
|
|
56
|
+
return scoreDelta;
|
|
57
|
+
const updatedDelta = timestamp(right.item.record.updatedAt) - timestamp(left.item.record.updatedAt);
|
|
58
|
+
if (updatedDelta !== 0)
|
|
59
|
+
return updatedDelta;
|
|
60
|
+
return left.originIndex - right.originIndex;
|
|
61
|
+
}
|
|
62
|
+
function injectionScore(item) {
|
|
63
|
+
if (item.record.status === "stale")
|
|
64
|
+
return item.score - 4;
|
|
65
|
+
if (item.record.status === "conflicted")
|
|
66
|
+
return item.score - 1;
|
|
67
|
+
return item.score;
|
|
68
|
+
}
|
|
69
|
+
function formatCandidate(item) {
|
|
70
|
+
const record = item.record;
|
|
71
|
+
const statusSuffix = record.status === "active" ? "" : ` status: ${record.status}`;
|
|
72
|
+
return [
|
|
73
|
+
`- [${record.scope}:${record.type}] ${redactSecrets(record.content)}${statusSuffix}`,
|
|
74
|
+
` why: ${redactSecrets(whySelected(item))}`
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
function selectedMetadata(item, chars) {
|
|
78
|
+
return {
|
|
79
|
+
id: item.record.id,
|
|
80
|
+
scope: item.record.scope,
|
|
81
|
+
type: item.record.type,
|
|
82
|
+
status: item.record.status,
|
|
83
|
+
score: item.score,
|
|
84
|
+
whySelected: redactSecrets(whySelected(item)),
|
|
85
|
+
source: item.record.source,
|
|
86
|
+
chars
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function omittedMetadata(item, reason) {
|
|
90
|
+
return {
|
|
91
|
+
id: item.record.id,
|
|
92
|
+
scope: item.record.scope,
|
|
93
|
+
type: item.record.type,
|
|
94
|
+
status: item.record.status,
|
|
95
|
+
reason,
|
|
96
|
+
score: item.score
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function whySelected(item) {
|
|
100
|
+
if (item.explanation.trim().length > 0)
|
|
101
|
+
return item.explanation.trim();
|
|
102
|
+
const positiveReasons = item.reasons.filter((reason) => reason.score > 0);
|
|
103
|
+
if (positiveReasons.length > 0)
|
|
104
|
+
return positiveReasons.map(formatReason).join("; ");
|
|
105
|
+
return `score ${item.score.toFixed(3)}`;
|
|
106
|
+
}
|
|
107
|
+
function formatReason(reason) {
|
|
108
|
+
return reason.label;
|
|
109
|
+
}
|
|
110
|
+
export function redactSecrets(value) {
|
|
111
|
+
return value
|
|
112
|
+
// Anthropic (must run before the generic sk- rule)
|
|
113
|
+
.replace(/\bsk-ant-[A-Za-z0-9_-]{16,}/g, "sk-ant-[REDACTED]")
|
|
114
|
+
// OpenAI (sk-, sk-proj-, sk-live-, …)
|
|
115
|
+
.replace(/\bsk-(?:proj-|live-|test-)?[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]")
|
|
116
|
+
// GitHub tokens
|
|
117
|
+
.replace(/\b(gh[pousr]_[A-Za-z0-9_]{8,})\b/g, "[REDACTED]")
|
|
118
|
+
// AWS access key id
|
|
119
|
+
.replace(/\bAKIA[0-9A-Z]{16}\b/g, "AKIA[REDACTED]")
|
|
120
|
+
// Google API key
|
|
121
|
+
.replace(/\bAIza[0-9A-Za-z_-]{20,}\b/g, "AIza[REDACTED]")
|
|
122
|
+
// JWTs (header.payload.signature)
|
|
123
|
+
.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g, "[REDACTED_JWT]")
|
|
124
|
+
// Bearer tokens
|
|
125
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._-]{12,}/gi, "$1[REDACTED]")
|
|
126
|
+
// Generic NAME=secret / NAME: secret for *_KEY/*_TOKEN/*_SECRET/PASSWORD/API_KEY (the \b before
|
|
127
|
+
// the name is dropped so FOO_API_KEY=… with a leading underscore still matches)
|
|
128
|
+
.replace(/((?:[A-Za-z0-9_]*(?:api[_-]?key|token|secret|password))\s*[:=]\s*)[^\n]+/gi, "$1[REDACTED]");
|
|
129
|
+
}
|
|
130
|
+
function timestamp(value) {
|
|
131
|
+
const time = new Date(value).getTime();
|
|
132
|
+
return Number.isFinite(time) ? time : 0;
|
|
133
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PeonLoggerOptions {
|
|
2
|
+
logDir?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface PeonLogEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
type: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare class PeonLogger {
|
|
11
|
+
private readonly logFile;
|
|
12
|
+
private writeQueue;
|
|
13
|
+
constructor(options?: PeonLoggerOptions);
|
|
14
|
+
log(type: string, fields?: Record<string, unknown>): Promise<PeonLogEntry>;
|
|
15
|
+
recent(limit?: number): Promise<PeonLogEntry[]>;
|
|
16
|
+
private enqueueWrite;
|
|
17
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DEFAULT_LOG_DIR = join(homedir(), "Library", "Logs", "Peon");
|
|
5
|
+
export class PeonLogger {
|
|
6
|
+
logFile;
|
|
7
|
+
writeQueue = Promise.resolve();
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.logFile = join(options.logDir ?? DEFAULT_LOG_DIR, "daemon.jsonl");
|
|
10
|
+
}
|
|
11
|
+
async log(type, fields = {}) {
|
|
12
|
+
const entry = {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
type,
|
|
15
|
+
createdAt: new Date().toISOString(),
|
|
16
|
+
...sanitize(fields)
|
|
17
|
+
};
|
|
18
|
+
await this.enqueueWrite(`${JSON.stringify(entry)}\n`);
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
async recent(limit = 100) {
|
|
22
|
+
const raw = await readFile(this.logFile, "utf8").catch(() => "");
|
|
23
|
+
return raw
|
|
24
|
+
.trim()
|
|
25
|
+
.split("\n")
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.slice(-limit)
|
|
28
|
+
.reverse()
|
|
29
|
+
.flatMap((line) => {
|
|
30
|
+
try {
|
|
31
|
+
return [JSON.parse(line)];
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async enqueueWrite(line) {
|
|
39
|
+
const write = async () => {
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(this.logFile.slice(0, this.logFile.lastIndexOf("/")), { recursive: true });
|
|
42
|
+
await appendFile(this.logFile, line, "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Best-effort logging: a log write failure must never crash the daemon
|
|
46
|
+
// or reject a request handler.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
this.writeQueue = this.writeQueue.then(write);
|
|
50
|
+
return this.writeQueue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function sanitize(fields) {
|
|
54
|
+
return Object.fromEntries(Object.entries(fields).map(([key, value]) => {
|
|
55
|
+
if (key.toLowerCase().includes("key") || key.toLowerCase().includes("authorization")) {
|
|
56
|
+
return [key, "[redacted]"];
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string" && value.length > 1200) {
|
|
59
|
+
return [key, `${value.slice(0, 1200)}...`];
|
|
60
|
+
}
|
|
61
|
+
return [key, value];
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MemoryRecord, MemoryStatus } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure, in-memory transforms over a belief set. The store wraps these with
|
|
4
|
+
* read + replaceMemoryRecords so the curation logic stays testable in isolation.
|
|
5
|
+
*/
|
|
6
|
+
export interface MemoryPatch {
|
|
7
|
+
content?: string;
|
|
8
|
+
importance?: number;
|
|
9
|
+
confidence?: number;
|
|
10
|
+
status?: MemoryStatus;
|
|
11
|
+
pinned?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Edit a belief's content/scores/status/pin. Unknown id → unchanged array. */
|
|
14
|
+
export declare function applyUpdate(records: readonly MemoryRecord[], id: string, patch: MemoryPatch, now: string): MemoryRecord[];
|
|
15
|
+
/** Remove a belief outright. */
|
|
16
|
+
export declare function applyDelete(records: readonly MemoryRecord[], id: string): MemoryRecord[];
|
|
17
|
+
/** Pin/unpin a belief — pinned beliefs are protected and rank first. */
|
|
18
|
+
export declare function applyPin(records: readonly MemoryRecord[], id: string, pinned: boolean, now: string): MemoryRecord[];
|
|
19
|
+
/**
|
|
20
|
+
* Fold `dropId` into `keepId`: union the entities, take the higher importance and
|
|
21
|
+
* confidence, OR the pin flag, then remove the dropped record. Either id missing
|
|
22
|
+
* → unchanged array (no partial merge).
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyMerge(records: readonly MemoryRecord[], keepId: string, dropId: string, now: string): MemoryRecord[];
|