omp-memory-atlas 0.1.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/package.json +18 -0
- package/src/index.ts +178 -0
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omp-memory-atlas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "On-demand project knowledge maps for Oh My Pi, backed by automatically curated mental models.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"omp": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./src/index.ts"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"@oh-my-pi/pi-coding-agent": ">=17.2.2"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
3
|
+
import { computeBankScope } from "@oh-my-pi/pi-coding-agent/hindsight/bank";
|
|
4
|
+
import {
|
|
5
|
+
createHindsightClient,
|
|
6
|
+
type HindsightApi,
|
|
7
|
+
type MentalModelSummary,
|
|
8
|
+
} from "@oh-my-pi/pi-coding-agent/hindsight/client";
|
|
9
|
+
import { isHindsightConfigured, loadHindsightConfig } from "@oh-my-pi/pi-coding-agent/hindsight/config";
|
|
10
|
+
import { resolveSeedsForScope } from "@oh-my-pi/pi-coding-agent/hindsight/mental-models";
|
|
11
|
+
|
|
12
|
+
const CATALOGUE_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
type ProjectMemory = {
|
|
15
|
+
client: HindsightApi;
|
|
16
|
+
bankId: string;
|
|
17
|
+
projectTags: string[];
|
|
18
|
+
coreModelIds: Set<string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CachedCatalogue = {
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
models: MentalModelSummary[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function textResult(text: string, details: Record<string, unknown> = {}) {
|
|
27
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function projectMemory(cwd: string): ProjectMemory {
|
|
31
|
+
const config = loadHindsightConfig(settings);
|
|
32
|
+
if (!isHindsightConfigured(config)) {
|
|
33
|
+
throw new Error("Hindsight is not configured.");
|
|
34
|
+
}
|
|
35
|
+
const scope = computeBankScope(config, cwd);
|
|
36
|
+
return {
|
|
37
|
+
client: createHindsightClient(config),
|
|
38
|
+
bankId: scope.bankId,
|
|
39
|
+
projectTags: scope.retainTags ?? [],
|
|
40
|
+
coreModelIds: new Set(
|
|
41
|
+
resolveSeedsForScope(scope, config.scoping).flatMap((seed) => [seed.id, ...(seed.legacyIds ?? [])]),
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isProjectModel(model: MentalModelSummary, projectTags: string[]): boolean {
|
|
47
|
+
if (projectTags.length === 0) return true;
|
|
48
|
+
if (!model.tags || model.tags.length === 0) return true;
|
|
49
|
+
return model.tags.some((tag) => projectTags.includes(tag));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function areaModels(models: MentalModelSummary[], projectTags: string[], coreModelIds: Set<string>): MentalModelSummary[] {
|
|
53
|
+
return models
|
|
54
|
+
.filter((model) => !coreModelIds.has(model.id))
|
|
55
|
+
.filter((model) => isProjectModel(model, projectTags))
|
|
56
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function modelDescription(model: MentalModelSummary): string {
|
|
60
|
+
const heading = model.content?.match(/^#{1,6}\s+(.+)$/m)?.[1]?.trim();
|
|
61
|
+
if (heading) return heading;
|
|
62
|
+
const sourceQuery = model.source_query?.replace(/\s+/g, " ").trim();
|
|
63
|
+
if (sourceQuery) return sourceQuery;
|
|
64
|
+
const opening = model.content?.replace(/\s+/g, " ").trim();
|
|
65
|
+
return opening || "No description available";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function catalogueLine(model: MentalModelSummary): string {
|
|
69
|
+
const scope = modelDescription(model);
|
|
70
|
+
const description = scope.length > 180 ? `${scope.slice(0, 177)}...` : scope;
|
|
71
|
+
return `- ${model.name} [${model.id}]: ${description}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function catalogueBlock(models: MentalModelSummary[]): string {
|
|
75
|
+
const entries = models.length === 0 ? "- No project memory areas exist yet." : models.map(catalogueLine).join("\n");
|
|
76
|
+
return `## Project memory catalogue\n\nThis catalogue contains historical knowledge areas for this project. Load a relevant area with \`get_project_mental_model\` before relying on historical assumptions. Create an area with \`create_project_mental_model\` when durable project history does not fit an existing area.\n\n${entries}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function memoryAtlas(pi: ExtensionAPI) {
|
|
80
|
+
const { z } = pi.zod;
|
|
81
|
+
let catalogue: CachedCatalogue | undefined;
|
|
82
|
+
let sessionCatalogueBlock: string | undefined;
|
|
83
|
+
|
|
84
|
+
async function loadCatalogue(cwd: string, force = false): Promise<MentalModelSummary[]> {
|
|
85
|
+
if (!force && catalogue && catalogue.expiresAt > Date.now()) return catalogue.models;
|
|
86
|
+
const memory = projectMemory(cwd);
|
|
87
|
+
const response = await memory.client.listMentalModels(memory.bankId, { detail: "content" });
|
|
88
|
+
const models = areaModels(response.items, memory.projectTags, memory.coreModelIds);
|
|
89
|
+
catalogue = { expiresAt: Date.now() + CATALOGUE_TTL_MS, models };
|
|
90
|
+
return models;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveModel(cwd: string, nameOrId: string): Promise<{ memory: ProjectMemory; model: MentalModelSummary }> {
|
|
94
|
+
const memory = projectMemory(cwd);
|
|
95
|
+
const models = await loadCatalogue(cwd);
|
|
96
|
+
const model = models.find((candidate) => candidate.id === nameOrId || candidate.name === nameOrId);
|
|
97
|
+
if (!model) throw new Error(`Unknown project mental model: ${nameOrId}`);
|
|
98
|
+
return { memory, model };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const event of ["session_start", "session_switch", "session_branch", "session_tree"] as const) {
|
|
102
|
+
pi.on(event, () => {
|
|
103
|
+
sessionCatalogueBlock = undefined;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
108
|
+
if (!sessionCatalogueBlock) {
|
|
109
|
+
sessionCatalogueBlock = catalogueBlock(await loadCatalogue(ctx.cwd));
|
|
110
|
+
}
|
|
111
|
+
return { systemPrompt: [...event.systemPrompt, sessionCatalogueBlock] };
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
pi.registerTool({
|
|
115
|
+
name: "get_project_mental_model",
|
|
116
|
+
label: "Get Project Mental Model",
|
|
117
|
+
description: "Load the full Hindsight mental model for an area shown in the project memory catalogue.",
|
|
118
|
+
parameters: z.object({ area: z.string().min(1) }),
|
|
119
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
120
|
+
const { memory, model } = await resolveModel(ctx.cwd, params.area);
|
|
121
|
+
const full = await memory.client.getMentalModel(memory.bankId, model.id, { detail: "content", signal });
|
|
122
|
+
if (!full) throw new Error(`Project mental model disappeared: ${model.id}`);
|
|
123
|
+
return textResult(full.content?.trim() || "Mental model exists but has no generated content yet.", {
|
|
124
|
+
id: full.id,
|
|
125
|
+
name: full.name,
|
|
126
|
+
lastRefreshedAt: full.last_refreshed_at,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
pi.registerTool({
|
|
132
|
+
name: "create_project_mental_model",
|
|
133
|
+
label: "Create Project Mental Model",
|
|
134
|
+
description: "Create a project-scoped Hindsight mental model that refreshes automatically after memory consolidation.",
|
|
135
|
+
parameters: z.object({
|
|
136
|
+
name: z.string().min(1),
|
|
137
|
+
sourceQuery: z.string().min(1),
|
|
138
|
+
maxTokens: z.number().int().min(256).max(8000).default(2000),
|
|
139
|
+
}),
|
|
140
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
141
|
+
const memory = projectMemory(ctx.cwd);
|
|
142
|
+
const existing = await loadCatalogue(ctx.cwd, true);
|
|
143
|
+
if (existing.some((model) => model.name === params.name || model.id === params.name)) {
|
|
144
|
+
throw new Error(`Project mental model already exists: ${params.name}`);
|
|
145
|
+
}
|
|
146
|
+
const result = await memory.client.createMentalModel(memory.bankId, params.name, params.sourceQuery, {
|
|
147
|
+
id: params.name,
|
|
148
|
+
tags: memory.projectTags,
|
|
149
|
+
maxTokens: params.maxTokens,
|
|
150
|
+
trigger: { mode: "delta", refresh_after_consolidation: true },
|
|
151
|
+
signal,
|
|
152
|
+
});
|
|
153
|
+
const updatedModels = [
|
|
154
|
+
...existing,
|
|
155
|
+
{
|
|
156
|
+
id: params.name,
|
|
157
|
+
bank_id: memory.bankId,
|
|
158
|
+
name: params.name,
|
|
159
|
+
tags: memory.projectTags,
|
|
160
|
+
source_query: params.sourceQuery,
|
|
161
|
+
},
|
|
162
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
163
|
+
pi.sendMessage(
|
|
164
|
+
{
|
|
165
|
+
customType: "project-memory-catalogue-update",
|
|
166
|
+
content: `## Updated project memory catalogue\n\n${catalogueBlock(updatedModels)}`,
|
|
167
|
+
display: false,
|
|
168
|
+
},
|
|
169
|
+
{ deliverAs: "nextTurn" },
|
|
170
|
+
);
|
|
171
|
+
catalogue = undefined;
|
|
172
|
+
return textResult(`Queued project mental model creation: ${params.name}`, {
|
|
173
|
+
name: params.name,
|
|
174
|
+
operationId: result.operation_id,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|