@zosmaai/pi-llm-wiki 0.10.9 → 0.11.1
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/CHANGELOG.md +6 -0
- package/README.de.md +35 -4
- package/README.es.md +260 -170
- package/README.fr.md +35 -4
- package/README.hi.md +35 -4
- package/README.ja.md +35 -4
- package/README.ko.md +35 -4
- package/README.md +41 -12
- package/README.pt.md +35 -4
- package/README.ru.md +35 -4
- package/README.zh.md +260 -170
- package/assets/demo.gif +0 -0
- package/dist/extensions/llm-wiki/lib/bootstrap.js +74 -0
- package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +410 -0
- package/dist/extensions/llm-wiki/lib/inject.js +65 -0
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
- package/dist/extensions/llm-wiki/lib/metadata.js +505 -0
- package/dist/extensions/llm-wiki/lib/model-command.js +85 -0
- package/dist/extensions/llm-wiki/lib/observation.js +283 -0
- package/dist/extensions/llm-wiki/lib/recall.js +875 -0
- package/dist/extensions/llm-wiki/lib/retro.js +158 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +187 -0
- package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
- package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
- package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +201 -0
- package/dist/extensions/llm-wiki/lib/tools.js +1199 -0
- package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
- package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
- package/dist/extensions/llm-wiki/lib/utils.js +353 -0
- package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
- package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
- package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
- package/dist/mcp/exec.js +121 -0
- package/dist/mcp/index.js +229 -0
- package/dist/mcp/operations.js +130 -0
- package/dist/package.json +1 -0
- package/docs/api.md +5 -2
- package/docs/architecture.md +5 -2
- package/docs/configuration.md +25 -0
- package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
- package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
- package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
- package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
- package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +593 -0
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +542 -0
- package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
- package/extensions/llm-wiki/index.ts +22 -36
- package/extensions/llm-wiki/lib/bootstrap.ts +87 -0
- package/extensions/llm-wiki/lib/embeddings.ts +9 -3
- package/extensions/llm-wiki/lib/guardrails.ts +26 -18
- package/extensions/llm-wiki/lib/indexing.ts +2 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +304 -28
- package/extensions/llm-wiki/lib/knowledge-document.ts +663 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
- package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
- package/extensions/llm-wiki/lib/metadata.ts +550 -128
- package/extensions/llm-wiki/lib/model-command.ts +0 -1
- package/extensions/llm-wiki/lib/observation.ts +37 -43
- package/extensions/llm-wiki/lib/recall.ts +61 -33
- package/extensions/llm-wiki/lib/retro.ts +65 -41
- package/extensions/llm-wiki/lib/runtime.ts +0 -3
- package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
- package/extensions/llm-wiki/lib/source-packet.ts +45 -32
- package/extensions/llm-wiki/lib/task-config.ts +36 -0
- package/extensions/llm-wiki/lib/tools.ts +413 -342
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +127 -131
- package/extensions/llm-wiki/lib/vault-format.ts +363 -0
- package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
- package/mcp/exec.ts +122 -0
- package/mcp/index.ts +60 -250
- package/mcp/operations.ts +176 -0
- package/package.json +8 -2
- package/scripts/migrate-llm-wiki.js +801 -0
- package/skills/llm-wiki/SKILL.md +12 -8
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { createKnowledgeDocument, patchKnowledgeDocument, readKnowledgeDocumentFile, serializeKnowledgeDocument, writeKnowledgeDocumentFile, } from "./knowledge-document.js";
|
|
5
|
+
import { appendEvent, rebuildMetadataLight } from "./metadata.js";
|
|
6
|
+
import { runSubAgent } from "./subagent.js";
|
|
7
|
+
import { fmtDate, slugify } from "./utils.js";
|
|
8
|
+
import { VaultWriteError, assertWritableVault } from "./vault-format.js";
|
|
9
|
+
/**
|
|
10
|
+
* Background ingest synthesis (issue #65, part of epic #63).
|
|
11
|
+
*
|
|
12
|
+
* Moves the work the main agent used to do during `wiki_ingest` — reading a
|
|
13
|
+
* captured source's extracted text and writing the source page + entity /
|
|
14
|
+
* concept pages — onto a background sub-agent, so capturing/ingesting never
|
|
15
|
+
* stalls the user.
|
|
16
|
+
*
|
|
17
|
+
* Design: the sub-agent produces ONE structured `commit_synthesis` call; the
|
|
18
|
+
* persistence (`commitSynthesis`) is fully deterministic and unit-testable
|
|
19
|
+
* without an LLM. This mirrors pi-observational-memory's single-structured-tool
|
|
20
|
+
* pattern and keeps the file-writing logic verifiable in isolation.
|
|
21
|
+
*/
|
|
22
|
+
// ── structured synthesis schema ───────────────────────────
|
|
23
|
+
export const CommitSynthesisSchema = Type.Object({
|
|
24
|
+
summary: Type.String({
|
|
25
|
+
minLength: 1,
|
|
26
|
+
description: "2-3 paragraph summary of the source's key content.",
|
|
27
|
+
}),
|
|
28
|
+
key_takeaways: Type.Array(Type.String({ minLength: 1 }), {
|
|
29
|
+
description: "The most important points, one per item.",
|
|
30
|
+
}),
|
|
31
|
+
entities: Type.Array(Type.Object({
|
|
32
|
+
title: Type.String({
|
|
33
|
+
minLength: 1,
|
|
34
|
+
description: "Entity name (person, org, tool, product).",
|
|
35
|
+
}),
|
|
36
|
+
description: Type.String({ description: "One-line description of the entity." }),
|
|
37
|
+
}), { description: "Named entities mentioned in the source." }),
|
|
38
|
+
concepts: Type.Array(Type.Object({
|
|
39
|
+
title: Type.String({ minLength: 1, description: "Concept name (idea, pattern, framework)." }),
|
|
40
|
+
definition: Type.String({ description: "One-line definition of the concept." }),
|
|
41
|
+
}), { description: "Concepts discussed in the source." }),
|
|
42
|
+
quotes: Type.Optional(Type.Array(Type.Object({
|
|
43
|
+
text: Type.String({ minLength: 1 }),
|
|
44
|
+
attribution: Type.Optional(Type.String()),
|
|
45
|
+
}), { description: "Notable verbatim quotes." })),
|
|
46
|
+
contradictions: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
|
47
|
+
description: "Tensions/contradictions with existing wiki content, if any.",
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
// ── deterministic persistence (no LLM) ────────────────────
|
|
51
|
+
/** Localized headings for generated pages (issue #124). */
|
|
52
|
+
const HEADINGS = {
|
|
53
|
+
ru: {
|
|
54
|
+
summary: "Резюме",
|
|
55
|
+
keyTakeaways: "Ключевые выводы",
|
|
56
|
+
entitiesMentioned: "Упомянутые сущности",
|
|
57
|
+
conceptsMentioned: "Упомянутые концепции",
|
|
58
|
+
notableQuotes: "Заметные цитаты",
|
|
59
|
+
contradictions: "Противоречия",
|
|
60
|
+
sourcePacket: "Пакет источника",
|
|
61
|
+
overview: "Обзор",
|
|
62
|
+
definition: "Определение",
|
|
63
|
+
noneRecorded: "[Не записано]",
|
|
64
|
+
none: "[Нет]",
|
|
65
|
+
id: "ID",
|
|
66
|
+
extracted: "Извлечено",
|
|
67
|
+
manifest: "Манифест",
|
|
68
|
+
contradiction: "⚠️ **Противоречие**",
|
|
69
|
+
},
|
|
70
|
+
fr: {
|
|
71
|
+
summary: "Résumé",
|
|
72
|
+
keyTakeaways: "Points clés",
|
|
73
|
+
entitiesMentioned: "Entités mentionnées",
|
|
74
|
+
conceptsMentioned: "Concepts mentionnés",
|
|
75
|
+
notableQuotes: "Citations notables",
|
|
76
|
+
contradictions: "Contradictions",
|
|
77
|
+
sourcePacket: "Paquet source",
|
|
78
|
+
overview: "Aperçu",
|
|
79
|
+
definition: "Définition",
|
|
80
|
+
noneRecorded: "[Aucun enregistré]",
|
|
81
|
+
none: "[Aucun]",
|
|
82
|
+
id: "ID",
|
|
83
|
+
extracted: "Extrait",
|
|
84
|
+
manifest: "Manifeste",
|
|
85
|
+
contradiction: "⚠️ **Contradiction**",
|
|
86
|
+
},
|
|
87
|
+
de: {
|
|
88
|
+
summary: "Zusammenfassung",
|
|
89
|
+
keyTakeaways: "Wichtige Erkenntnisse",
|
|
90
|
+
entitiesMentioned: "Erwähnte Entitäten",
|
|
91
|
+
conceptsMentioned: "Erwähnte Konzepte",
|
|
92
|
+
notableQuotes: "Bemerkenswerte Zitate",
|
|
93
|
+
contradictions: "Widersprüche",
|
|
94
|
+
sourcePacket: "Quellenpaket",
|
|
95
|
+
overview: "Übersicht",
|
|
96
|
+
definition: "Definition",
|
|
97
|
+
noneRecorded: "[Keine aufgezeichnet]",
|
|
98
|
+
none: "[Keine]",
|
|
99
|
+
id: "ID",
|
|
100
|
+
extracted: "Extrahiert",
|
|
101
|
+
manifest: "Manifest",
|
|
102
|
+
contradiction: "⚠️ **Widerspruch**",
|
|
103
|
+
},
|
|
104
|
+
ja: {
|
|
105
|
+
summary: "要約",
|
|
106
|
+
keyTakeaways: "主な要点",
|
|
107
|
+
entitiesMentioned: "言及されたエンティティ",
|
|
108
|
+
conceptsMentioned: "言及された概念",
|
|
109
|
+
notableQuotes: "注目すべき引用",
|
|
110
|
+
contradictions: "矛盾",
|
|
111
|
+
sourcePacket: "ソースパケット",
|
|
112
|
+
overview: "概要",
|
|
113
|
+
definition: "定義",
|
|
114
|
+
noneRecorded: "[記録なし]",
|
|
115
|
+
none: "[なし]",
|
|
116
|
+
id: "ID",
|
|
117
|
+
extracted: "抽出済み",
|
|
118
|
+
manifest: "マニフェスト",
|
|
119
|
+
contradiction: "⚠️ **矛盾**",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
function getHeadings(lang) {
|
|
123
|
+
if (lang && HEADINGS[lang])
|
|
124
|
+
return HEADINGS[lang];
|
|
125
|
+
// English defaults
|
|
126
|
+
return {
|
|
127
|
+
summary: "Summary",
|
|
128
|
+
keyTakeaways: "Key Takeaways",
|
|
129
|
+
entitiesMentioned: "Entities Mentioned",
|
|
130
|
+
conceptsMentioned: "Concepts Mentioned",
|
|
131
|
+
notableQuotes: "Notable Quotes",
|
|
132
|
+
contradictions: "Contradictions",
|
|
133
|
+
sourcePacket: "Source Packet",
|
|
134
|
+
overview: "Overview",
|
|
135
|
+
definition: "Definition",
|
|
136
|
+
noneRecorded: "[None recorded]",
|
|
137
|
+
none: "[None]",
|
|
138
|
+
id: "ID",
|
|
139
|
+
extracted: "Extracted",
|
|
140
|
+
manifest: "Manifest",
|
|
141
|
+
contradiction: "⚠️ **Contradiction**",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function buildEntityPageBody(title, description, sourceId, lang) {
|
|
145
|
+
const desc = description.trim() || "One-line description.";
|
|
146
|
+
const h = getHeadings(lang);
|
|
147
|
+
return `# ${title}
|
|
148
|
+
|
|
149
|
+
${desc}
|
|
150
|
+
|
|
151
|
+
## ${h.overview}
|
|
152
|
+
|
|
153
|
+
[Key facts]
|
|
154
|
+
|
|
155
|
+
## Links
|
|
156
|
+
|
|
157
|
+
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
158
|
+
}
|
|
159
|
+
function buildConceptPageBody(title, definition, sourceId, lang) {
|
|
160
|
+
const def = definition.trim() || "One-line definition.";
|
|
161
|
+
const h = getHeadings(lang);
|
|
162
|
+
return `# ${title}
|
|
163
|
+
|
|
164
|
+
${def}
|
|
165
|
+
|
|
166
|
+
## ${h.definition}
|
|
167
|
+
|
|
168
|
+
[Clear explanation]
|
|
169
|
+
|
|
170
|
+
## Links
|
|
171
|
+
|
|
172
|
+
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
173
|
+
}
|
|
174
|
+
/** Rebuild the source page body from synthesis data, marking it ingested. */
|
|
175
|
+
export function buildIngestedSourcePageBody(manifest, data, _date, lang) {
|
|
176
|
+
const id = String(manifest.id);
|
|
177
|
+
const title = String(manifest.title || id);
|
|
178
|
+
const url = manifest.url ? `\n> _Original: [${manifest.url}](${manifest.url})_` : "";
|
|
179
|
+
const h = getHeadings(lang);
|
|
180
|
+
const takeaways = data.key_takeaways.length > 0
|
|
181
|
+
? data.key_takeaways.map((t) => `- ${t.trim()}`).join("\n")
|
|
182
|
+
: `- ${h.noneRecorded}`;
|
|
183
|
+
const entities = data.entities.length > 0
|
|
184
|
+
? data.entities.map((e) => `- [${e.title}](/entities/${slugify(e.title)}.md)`).join("\n")
|
|
185
|
+
: `- ${h.none}`;
|
|
186
|
+
const concepts = data.concepts.length > 0
|
|
187
|
+
? data.concepts.map((c) => `- [${c.title}](/concepts/${slugify(c.title)}.md)`).join("\n")
|
|
188
|
+
: `- ${h.none}`;
|
|
189
|
+
const quotes = data.quotes && data.quotes.length > 0
|
|
190
|
+
? data.quotes
|
|
191
|
+
.map((q) => `> ${q.text.trim()}${q.attribution ? ` — ${q.attribution}` : ""}`)
|
|
192
|
+
.join("\n\n")
|
|
193
|
+
: `> ${h.noneRecorded}`;
|
|
194
|
+
const contradictions = data.contradictions && data.contradictions.length > 0
|
|
195
|
+
? `\n## ${h.contradictions}\n\n${data.contradictions.map((c) => `${h.contradiction}: ${c.trim()}`).join("\n")}\n`
|
|
196
|
+
: "";
|
|
197
|
+
return `# ${title}${url}
|
|
198
|
+
|
|
199
|
+
## ${h.summary}
|
|
200
|
+
|
|
201
|
+
${data.summary.trim()}
|
|
202
|
+
|
|
203
|
+
## ${h.keyTakeaways}
|
|
204
|
+
|
|
205
|
+
${takeaways}
|
|
206
|
+
|
|
207
|
+
## ${h.entitiesMentioned}
|
|
208
|
+
|
|
209
|
+
${entities}
|
|
210
|
+
|
|
211
|
+
## ${h.conceptsMentioned}
|
|
212
|
+
|
|
213
|
+
${concepts}
|
|
214
|
+
|
|
215
|
+
## ${h.notableQuotes}
|
|
216
|
+
|
|
217
|
+
${quotes}
|
|
218
|
+
${contradictions}## ${h.sourcePacket}
|
|
219
|
+
|
|
220
|
+
- **${h.id}:** \`sources/${id}\`
|
|
221
|
+
- **${h.extracted}:** \`raw/sources/${id}/extracted.md\`
|
|
222
|
+
- **${h.manifest}:** \`raw/sources/${id}/manifest.json\`
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
/** Rebuild the source page from synthesis data, marking it ingested. */
|
|
226
|
+
export function buildIngestedSourcePage(manifest, data, date, lang) {
|
|
227
|
+
const id = String(manifest.id);
|
|
228
|
+
const title = String(manifest.title || id);
|
|
229
|
+
const format = String(manifest.format || "unknown");
|
|
230
|
+
const captured = String(manifest.captured || date);
|
|
231
|
+
const body = buildIngestedSourcePageBody(manifest, data, date, lang);
|
|
232
|
+
const doc = createKnowledgeDocument(`sources/${id}.md`, {
|
|
233
|
+
type: "source",
|
|
234
|
+
title,
|
|
235
|
+
format,
|
|
236
|
+
source_id: id,
|
|
237
|
+
raw_path: `raw/sources/${id}/extracted.md`,
|
|
238
|
+
captured,
|
|
239
|
+
status: "ingested",
|
|
240
|
+
updated: date,
|
|
241
|
+
}, body);
|
|
242
|
+
return serializeKnowledgeDocument(doc);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Persist a synthesis deterministically: rewrite the source page (status →
|
|
246
|
+
* ingested), create missing entity/concept pages (existing pages are linked,
|
|
247
|
+
* never overwritten), and log the event. Pure file I/O — no LLM, no network.
|
|
248
|
+
*/
|
|
249
|
+
export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate(), lang) {
|
|
250
|
+
const result = {
|
|
251
|
+
sourceId,
|
|
252
|
+
sourcePage: join(paths.wiki, "sources", `${sourceId}.md`),
|
|
253
|
+
entitiesCreated: [],
|
|
254
|
+
conceptsCreated: [],
|
|
255
|
+
entitiesLinked: [],
|
|
256
|
+
conceptsLinked: [],
|
|
257
|
+
contradictions: data.contradictions?.length ?? 0,
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
assertWritableVault(paths);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error instanceof VaultWriteError) {
|
|
264
|
+
return { ok: false, sourceId, diagnostics: error.diagnostics };
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
// Patch existing documents so unknown fields, legacy sources, and titles survive.
|
|
269
|
+
let sourceDocument;
|
|
270
|
+
if (existsSync(result.sourcePage)) {
|
|
271
|
+
const parsed = readKnowledgeDocumentFile(result.sourcePage, `sources/${sourceId}.md`);
|
|
272
|
+
if (!parsed.ok)
|
|
273
|
+
return { ok: false, sourceId, diagnostics: parsed.diagnostics };
|
|
274
|
+
sourceDocument = patchKnowledgeDocument(parsed.document, {
|
|
275
|
+
fields: { status: "ingested", updated: date },
|
|
276
|
+
body: buildIngestedSourcePageBody(manifest, data, date, lang),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
sourceDocument = createKnowledgeDocument(`sources/${sourceId}.md`, {
|
|
281
|
+
type: "source",
|
|
282
|
+
title: String(manifest.title || sourceId),
|
|
283
|
+
format: String(manifest.format || "unknown"),
|
|
284
|
+
source_id: sourceId,
|
|
285
|
+
raw_path: `raw/sources/${sourceId}/extracted.md`,
|
|
286
|
+
captured: String(manifest.captured || date),
|
|
287
|
+
status: "ingested",
|
|
288
|
+
updated: date,
|
|
289
|
+
}, buildIngestedSourcePageBody(manifest, data, date, lang));
|
|
290
|
+
}
|
|
291
|
+
mkdirSync(join(paths.wiki, "sources"), { recursive: true });
|
|
292
|
+
writeKnowledgeDocumentFile(result.sourcePage, sourceDocument);
|
|
293
|
+
// Entity pages — create if absent, link if present.
|
|
294
|
+
mkdirSync(join(paths.wiki, "entities"), { recursive: true });
|
|
295
|
+
for (const e of data.entities) {
|
|
296
|
+
const slug = slugify(e.title);
|
|
297
|
+
if (!slug)
|
|
298
|
+
continue;
|
|
299
|
+
const pagePath = join(paths.wiki, "entities", `${slug}.md`);
|
|
300
|
+
if (existsSync(pagePath)) {
|
|
301
|
+
result.entitiesLinked.push(slug);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const entityDoc = createKnowledgeDocument(`entities/${slug}.md`, {
|
|
305
|
+
type: "entity",
|
|
306
|
+
title: e.title,
|
|
307
|
+
description: e.description.trim() || "One-line description.",
|
|
308
|
+
created: date,
|
|
309
|
+
updated: date,
|
|
310
|
+
}, buildEntityPageBody(e.title, e.description, sourceId, lang), [{ id: sourceId, resource: `/sources/${sourceId}.md` }]);
|
|
311
|
+
writeKnowledgeDocumentFile(pagePath, entityDoc);
|
|
312
|
+
result.entitiesCreated.push(slug);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Concept pages — create if absent, link if present.
|
|
316
|
+
mkdirSync(join(paths.wiki, "concepts"), { recursive: true });
|
|
317
|
+
for (const c of data.concepts) {
|
|
318
|
+
const slug = slugify(c.title);
|
|
319
|
+
if (!slug)
|
|
320
|
+
continue;
|
|
321
|
+
const pagePath = join(paths.wiki, "concepts", `${slug}.md`);
|
|
322
|
+
if (existsSync(pagePath)) {
|
|
323
|
+
result.conceptsLinked.push(slug);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const conceptDoc = createKnowledgeDocument(`concepts/${slug}.md`, {
|
|
327
|
+
type: "concept",
|
|
328
|
+
title: c.title,
|
|
329
|
+
description: c.definition.trim() || "One-line definition.",
|
|
330
|
+
created: date,
|
|
331
|
+
updated: date,
|
|
332
|
+
}, buildConceptPageBody(c.title, c.definition, sourceId, lang), [{ id: sourceId, resource: `/sources/${sourceId}.md` }]);
|
|
333
|
+
writeKnowledgeDocumentFile(pagePath, conceptDoc);
|
|
334
|
+
result.conceptsCreated.push(slug);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
appendEvent(paths, {
|
|
338
|
+
kind: "ingest",
|
|
339
|
+
source_id: sourceId,
|
|
340
|
+
entities_created: result.entitiesCreated.length,
|
|
341
|
+
concepts_created: result.conceptsCreated.length,
|
|
342
|
+
contradictions: result.contradictions,
|
|
343
|
+
background: true,
|
|
344
|
+
});
|
|
345
|
+
return { ok: true, ...result };
|
|
346
|
+
}
|
|
347
|
+
// ── sub-agent synthesis (LLM) ─────────────────────────────
|
|
348
|
+
export const INGEST_SYSTEM = `You are the LLM Wiki ingestion synthesizer. You turn a single captured source's extracted text into structured wiki knowledge.
|
|
349
|
+
|
|
350
|
+
Read the source content, then call \`commit_synthesis\` EXACTLY ONCE with:
|
|
351
|
+
- summary: a faithful 2-3 paragraph summary (no fabrication).
|
|
352
|
+
- key_takeaways: the most important points.
|
|
353
|
+
- entities: named people, organizations, tools, products actually mentioned.
|
|
354
|
+
- concepts: ideas, patterns, frameworks actually discussed.
|
|
355
|
+
- quotes: notable verbatim quotes (optional).
|
|
356
|
+
- contradictions: tensions with general knowledge or noted in the text (optional).
|
|
357
|
+
|
|
358
|
+
Rules:
|
|
359
|
+
- Never fabricate. Only include entities/concepts present in the source.
|
|
360
|
+
- Keep descriptions to one line.
|
|
361
|
+
- After calling commit_synthesis once, reply with a one-line confirmation and stop.`;
|
|
362
|
+
/**
|
|
363
|
+
* Run the synthesis sub-agent for a single source, then commit + rebuild
|
|
364
|
+
* metadata. Returns the commit result, or undefined if the model produced no
|
|
365
|
+
* synthesis.
|
|
366
|
+
*/
|
|
367
|
+
export async function runIngestSynthesis(args) {
|
|
368
|
+
const { model, apiKey, headers, paths, sourceId, manifest, extracted, maxChars, signal, synthesisLanguage, } = args;
|
|
369
|
+
const content = extracted.slice(0, maxChars ?? 24_000);
|
|
370
|
+
if (!content.trim())
|
|
371
|
+
return undefined;
|
|
372
|
+
const languageInstruction = synthesisLanguage
|
|
373
|
+
? `\n\nWrite all generated content in ${synthesisLanguage}, including titles, headings, summaries, descriptions, and concept/entity names. Only preserve code, API names, file paths, commands, exact technical identifiers, and verbatim quotations in their original form.`
|
|
374
|
+
: "";
|
|
375
|
+
const systemPrompt = INGEST_SYSTEM + languageInstruction;
|
|
376
|
+
let committed;
|
|
377
|
+
const commitTool = {
|
|
378
|
+
name: "commit_synthesis",
|
|
379
|
+
label: "Commit synthesis",
|
|
380
|
+
description: "Persist the structured synthesis of this source into wiki pages. Call exactly once.",
|
|
381
|
+
parameters: CommitSynthesisSchema,
|
|
382
|
+
execute: async (_id, params) => {
|
|
383
|
+
const outcome = commitSynthesis(paths, sourceId, manifest, params, undefined, synthesisLanguage);
|
|
384
|
+
if (!outcome.ok) {
|
|
385
|
+
return {
|
|
386
|
+
content: [{ type: "text", text: `Failed: ${outcome.diagnostics[0].message}` }],
|
|
387
|
+
details: { sourceId },
|
|
388
|
+
isError: true,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
committed = outcome;
|
|
392
|
+
const ack = `Committed: source page + ${committed.entitiesCreated.length} new entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} new concept${committed.conceptsCreated.length === 1 ? "" : "s"}. Reply with a one-line confirmation and stop.`;
|
|
393
|
+
return { content: [{ type: "text", text: ack }], details: { sourceId } };
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
const title = String(manifest.title || sourceId);
|
|
397
|
+
const userPrompt = `Synthesize this captured source into wiki knowledge by calling commit_synthesis once.\n\nSOURCE: ${title} (${sourceId})\n\nEXTRACTED CONTENT:\n${content}`;
|
|
398
|
+
await runSubAgent({
|
|
399
|
+
model,
|
|
400
|
+
apiKey,
|
|
401
|
+
headers,
|
|
402
|
+
systemPrompt,
|
|
403
|
+
userPrompt,
|
|
404
|
+
tools: [commitTool],
|
|
405
|
+
signal,
|
|
406
|
+
});
|
|
407
|
+
if (committed)
|
|
408
|
+
rebuildMetadataLight(paths);
|
|
409
|
+
return committed;
|
|
410
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-prompt context injection primitives (issues #87, #92).
|
|
3
|
+
*
|
|
4
|
+
* The `before_agent_start` hook augments the chained system prompt with a
|
|
5
|
+
* visible wiki-status footer. This module isolates that append so it can be
|
|
6
|
+
* unit-tested for idempotency — a turn that aborts (network error / ESC) and is
|
|
7
|
+
* retried can carry the prior injection forward, and a naive append stacks the
|
|
8
|
+
* footer 2x, 3x, ...
|
|
9
|
+
*
|
|
10
|
+
* It also owns the cache-safety split (issue #92): VOLATILE per-turn context
|
|
11
|
+
* (recall results, one-time topic-inference directive) must never enter the
|
|
12
|
+
* system prompt — that is the provider's primary cache prefix, so per-turn
|
|
13
|
+
* variation there forces a full cache miss every turn. Volatile content is
|
|
14
|
+
* routed into a tail conversation message instead. `buildAgentStartInjection`
|
|
15
|
+
* is the single, pure decision point for that split.
|
|
16
|
+
*/
|
|
17
|
+
/** The always-injected wiki-status footer (sans surrounding whitespace). */
|
|
18
|
+
export const WIKI_STATUS_BLOCK = "<wiki_status>LLM Wiki active — use wiki_recall for deeper search, wiki_observe to record observations, wiki_retro to save insights.</wiki_status>";
|
|
19
|
+
/**
|
|
20
|
+
* Append the wiki-status footer to a system prompt — idempotently (issue #87).
|
|
21
|
+
*
|
|
22
|
+
* Strips any already-present footer (with its leading blank line) before
|
|
23
|
+
* appending exactly one. This makes the injection safe across aborted/retried
|
|
24
|
+
* agent starts that carry the prior injection forward in the chained system
|
|
25
|
+
* prompt, so the footer never stacks (2x, 3x, ...).
|
|
26
|
+
* See test/inject-idempotent.test.ts.
|
|
27
|
+
*/
|
|
28
|
+
export function appendWikiStatus(systemPrompt) {
|
|
29
|
+
const base = systemPrompt.split(`\n\n${WIKI_STATUS_BLOCK}`).join("");
|
|
30
|
+
return `${base}\n\n${WIKI_STATUS_BLOCK}`;
|
|
31
|
+
}
|
|
32
|
+
/** Normalize upstream Pi and OMP system-prompt representations. */
|
|
33
|
+
export function normalizeSystemPrompt(systemPrompt) {
|
|
34
|
+
if (Array.isArray(systemPrompt))
|
|
35
|
+
return systemPrompt.join("\n\n");
|
|
36
|
+
return typeof systemPrompt === "string" ? systemPrompt : "";
|
|
37
|
+
}
|
|
38
|
+
/** customType of the hidden tail message carrying volatile per-turn context. */
|
|
39
|
+
export const WIKI_RECALL_MESSAGE_TYPE = "wiki-recall-context";
|
|
40
|
+
/**
|
|
41
|
+
* Split a turn's injection into a cache-stable system prompt and a volatile
|
|
42
|
+
* tail message (issue #92).
|
|
43
|
+
*
|
|
44
|
+
* - `systemPrompt` is always `appendWikiStatus(baseSystemPrompt)` — the static
|
|
45
|
+
* footer only. It carries NONE of `dynamicBlocks`, so it does not vary with
|
|
46
|
+
* recall results and never breaks the provider cache prefix.
|
|
47
|
+
* - `dynamicBlocks` (recall context, topic-inference directive, ...) are
|
|
48
|
+
* trimmed, emptied entries dropped, and joined with a blank line into the
|
|
49
|
+
* message body. When nothing survives, no message is emitted.
|
|
50
|
+
*
|
|
51
|
+
* Pure and side-effect free — see test/agent-start-injection.test.ts.
|
|
52
|
+
*/
|
|
53
|
+
export function buildAgentStartInjection(baseSystemPrompt, dynamicBlocks) {
|
|
54
|
+
const systemPrompt = appendWikiStatus(normalizeSystemPrompt(baseSystemPrompt));
|
|
55
|
+
const content = dynamicBlocks
|
|
56
|
+
.map((b) => b?.trim())
|
|
57
|
+
.filter((b) => Boolean(b))
|
|
58
|
+
.join("\n\n");
|
|
59
|
+
if (!content)
|
|
60
|
+
return { systemPrompt };
|
|
61
|
+
return {
|
|
62
|
+
systemPrompt,
|
|
63
|
+
message: { customType: WIKI_RECALL_MESSAGE_TYPE, content, display: false },
|
|
64
|
+
};
|
|
65
|
+
}
|