melchizedek-agents 0.9.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/AGENT_SETUP.md +182 -0
- package/DOCUMENTATION.md +614 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +164 -0
- package/README.md +151 -0
- package/config/agents/README.md +27 -0
- package/config/agents/examples/ares.yaml +74 -0
- package/config/agents/examples/cartographers.yaml +115 -0
- package/config/agents/examples/claude.yaml +17 -0
- package/config/agents/examples/council.yaml +65 -0
- package/config/agents/examples/critic.yaml +74 -0
- package/config/agents/examples/delegation.yaml +29 -0
- package/config/agents/examples/hierarchical.yaml +26 -0
- package/config/agents/examples/image_production.yaml +112 -0
- package/config/agents/examples/librarian.yaml +75 -0
- package/config/agents/examples/model_zoo.yaml +81 -0
- package/config/agents/examples/patient_advocate.yaml +107 -0
- package/config/agents/examples/scriptorium.yaml +96 -0
- package/config/agents/examples/style_council.yaml +83 -0
- package/config/agents/examples/syndicate.yaml +34 -0
- package/config/agents/examples/tutor.yaml +66 -0
- package/config/agents/syndicateSchema.yaml +291 -0
- package/dist/lib/config.d.ts +108 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/dispatch.d.ts +133 -0
- package/dist/lib/dispatch.js +182 -0
- package/dist/lib/grounding.d.ts +28 -0
- package/dist/lib/grounding.js +61 -0
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/loadEnv.d.ts +21 -0
- package/dist/lib/loadEnv.js +58 -0
- package/dist/lib/loadSyndicate.d.ts +177 -0
- package/dist/lib/loadSyndicate.js +264 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
- package/dist/lib/memory/supabaseMemoryService.js +499 -0
- package/dist/lib/models/claudeLlm.d.ts +93 -0
- package/dist/lib/models/claudeLlm.js +332 -0
- package/dist/lib/models/gptLlm.d.ts +110 -0
- package/dist/lib/models/gptLlm.js +497 -0
- package/dist/lib/models/grokLlm.d.ts +64 -0
- package/dist/lib/models/grokLlm.js +86 -0
- package/dist/lib/models/ollamaLlm.d.ts +105 -0
- package/dist/lib/models/ollamaLlm.js +134 -0
- package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
- package/dist/lib/models/openAiCompatibleLlm.js +331 -0
- package/dist/lib/models/providerMap.d.ts +28 -0
- package/dist/lib/models/providerMap.js +45 -0
- package/dist/lib/models/registry.d.ts +71 -0
- package/dist/lib/models/registry.js +159 -0
- package/dist/lib/models/schemaNormalize.d.ts +22 -0
- package/dist/lib/models/schemaNormalize.js +58 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
- package/dist/lib/observability/supabaseSpanExporter.js +115 -0
- package/dist/lib/observability/tracer.d.ts +44 -0
- package/dist/lib/observability/tracer.js +242 -0
- package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
- package/dist/lib/persistence/supabaseProvider.js +88 -0
- package/dist/lib/session/supabaseSessionService.d.ts +26 -0
- package/dist/lib/session/supabaseSessionService.js +193 -0
- package/dist/lib/session/transcript.d.ts +193 -0
- package/dist/lib/session/transcript.js +309 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +58 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
- package/dist/lib/tools/collectionsSearchTool.js +86 -0
- package/dist/lib/tools/generateImageTool.d.ts +16 -0
- package/dist/lib/tools/generateImageTool.js +119 -0
- package/dist/lib/tools/inspectImageTool.d.ts +21 -0
- package/dist/lib/tools/inspectImageTool.js +97 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +116 -0
- package/dist/lib/tools/toolContract.d.ts +78 -0
- package/dist/lib/tools/toolContract.js +129 -0
- package/dist/lib/tools/webExtractTool.d.ts +70 -0
- package/dist/lib/tools/webExtractTool.js +461 -0
- package/dist/lib/tools/webSearchTool.d.ts +53 -0
- package/dist/lib/tools/webSearchTool.js +126 -0
- package/dist/lib/tools/wikiTools.d.ts +112 -0
- package/dist/lib/tools/wikiTools.js +697 -0
- package/dist/lib/tools/xSearchTool.d.ts +61 -0
- package/dist/lib/tools/xSearchTool.js +125 -0
- package/dist/lib/wiki/agentRun.d.ts +39 -0
- package/dist/lib/wiki/agentRun.js +79 -0
- package/dist/lib/wiki/builder.d.ts +71 -0
- package/dist/lib/wiki/builder.js +165 -0
- package/dist/lib/wiki/dive.d.ts +56 -0
- package/dist/lib/wiki/dive.js +208 -0
- package/dist/lib/wiki/entities.d.ts +220 -0
- package/dist/lib/wiki/entities.js +661 -0
- package/dist/lib/wiki/extract.d.ts +97 -0
- package/dist/lib/wiki/extract.js +305 -0
- package/dist/lib/wiki/fill.d.ts +37 -0
- package/dist/lib/wiki/fill.js +175 -0
- package/dist/lib/wiki/format.d.ts +132 -0
- package/dist/lib/wiki/format.js +177 -0
- package/dist/lib/wiki/graph.d.ts +80 -0
- package/dist/lib/wiki/graph.js +147 -0
- package/dist/lib/wiki/lint.d.ts +58 -0
- package/dist/lib/wiki/lint.js +267 -0
- package/dist/lib/wiki/markdown.d.ts +127 -0
- package/dist/lib/wiki/markdown.js +336 -0
- package/dist/lib/wiki/pipeline.d.ts +69 -0
- package/dist/lib/wiki/pipeline.js +119 -0
- package/dist/lib/wiki/vault.d.ts +84 -0
- package/dist/lib/wiki/vault.js +194 -0
- package/dist/scripts/a2a_server.d.ts +2 -0
- package/dist/scripts/a2a_server.js +997 -0
- package/dist/scripts/syndicate_chat.d.ts +2 -0
- package/dist/scripts/syndicate_chat.js +485 -0
- package/package.json +96 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/wikiTools.ts — the wiki's tool surface, defined once.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Everything an agent (or MCP client) can DO with the knowledge bundle is
|
|
6
|
+
* a ToolContract here — zod schema single source of truth, derived into
|
|
7
|
+
* ADK FunctionTools for syndicate agents and MCP definitions for outside
|
|
8
|
+
* clients, exactly like the rest of lib/tools.
|
|
9
|
+
*
|
|
10
|
+
* Three capability tiers, deliberately separated:
|
|
11
|
+
*
|
|
12
|
+
* NAVIGATE (wiki_map, wiki_read, wiki_search, wiki_links, wiki_dive,
|
|
13
|
+
* wiki_graph) — pure functions over the bundle. No model, no writes,
|
|
14
|
+
* instant. These are what "repo dive" means: an agent on a knowledge
|
|
15
|
+
* task orients, searches, follows links, and gets an ordered reading
|
|
16
|
+
* plan. wiki_graph adds the second layer — the ENTITY graph of agents,
|
|
17
|
+
* tools, models, modules, tables and env vars derived from repo truth
|
|
18
|
+
* (lib/wiki/entities.ts) — which answers relational questions no
|
|
19
|
+
* amount of document search can ("who calls this tool").
|
|
20
|
+
*
|
|
21
|
+
* WRITE (wiki_save, wiki_relate) — the only two write paths, both gated.
|
|
22
|
+
* wiki_save: parse → profile validation → lint (errors block,
|
|
23
|
+
* including the private-closure rule) → path-jailed write → directory
|
|
24
|
+
* index refresh → log.md entry. wiki_relate: one asserted relation,
|
|
25
|
+
* refused unless the relation is an INFERRED one (structural relations
|
|
26
|
+
* are derived by the build), both endpoints exist, no public document
|
|
27
|
+
* points into the private annex, and evidence is supplied. Agents
|
|
28
|
+
* cannot corrupt the bundle through either door; the worst they can do
|
|
29
|
+
* is write mediocre prose, which lint's warnings surface and git can
|
|
30
|
+
* revert.
|
|
31
|
+
*
|
|
32
|
+
* AGENTIC (wiki_query, wiki_garden) — composites that put a model in
|
|
33
|
+
* the loop, running a one-shot agent equipped with the tier-1 (and for
|
|
34
|
+
* garden, tier-2) tools. Exposed so an MCP client can ask the wiki a
|
|
35
|
+
* question or delegate an edit as ONE call.
|
|
36
|
+
*
|
|
37
|
+
* Exposure stays deliberate (the registry/server decide); defining these
|
|
38
|
+
* contracts publishes nothing by itself.
|
|
39
|
+
*/
|
|
40
|
+
import { z } from 'zod';
|
|
41
|
+
import { WIKI_AGENT_MODEL } from "../config.js";
|
|
42
|
+
import { runWikiAgent } from "../wiki/agentRun.js";
|
|
43
|
+
import { appendLog, indexSpec, upsertDoc } from "../wiki/builder.js";
|
|
44
|
+
import { repoDive, scoreDocs } from "../wiki/dive.js";
|
|
45
|
+
import { actorSchema, BUILD_ACTOR, CONCEPT_TYPES, isPrivatePath, isReservedFilename, trustTier, } from "../wiki/format.js";
|
|
46
|
+
import { edgeKey, entityStats, findNodes, isPrivateNode, loadEntityGraph, loadRelations, neighbors, nodesOfKind, NODE_KINDS, RELATIONS, relationsByTier, saveRelations, shortestPath, snapshotDrift, } from "../wiki/entities.js";
|
|
47
|
+
import { buildGraph, graphStats, neighborhood } from "../wiki/graph.js";
|
|
48
|
+
import { formatLintReport, lintDoc, lintVault } from "../wiki/lint.js";
|
|
49
|
+
import { sectionSlice } from "../wiki/markdown.js";
|
|
50
|
+
import { docsInDirectory, jailedAbsPath, loadVault, lookupTarget, makeWikiDoc, resolveLinkTarget, resolveWikiRoot, writeDocFile, } from "../wiki/vault.js";
|
|
51
|
+
import { defineTool, toFunctionTool } from "./toolContract.js";
|
|
52
|
+
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
53
|
+
function todayIso() {
|
|
54
|
+
return new Date().toISOString().slice(0, 10);
|
|
55
|
+
}
|
|
56
|
+
function wikiModel(override) {
|
|
57
|
+
return override ?? process.env.WIKI_AGENT_MODEL ?? WIKI_AGENT_MODEL;
|
|
58
|
+
}
|
|
59
|
+
/** Every execute() loads fresh — the bundle on disk is the only truth. */
|
|
60
|
+
function vaultOrError() {
|
|
61
|
+
try {
|
|
62
|
+
return loadVault(resolveWikiRoot());
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Which log an entry belongs in. `log.md` at the bundle root EXPORTS, so an
|
|
70
|
+
* entry naming a private document or a private entity would leak exactly
|
|
71
|
+
* what the private annex exists to withhold; those go to the annex's own
|
|
72
|
+
* log instead, which the export never copies.
|
|
73
|
+
*/
|
|
74
|
+
function logTargetFor(privateTouched) {
|
|
75
|
+
return privateTouched ? '/private/log.md' : '/log.md';
|
|
76
|
+
}
|
|
77
|
+
function normalizeDocPath(raw) {
|
|
78
|
+
let path = raw.trim();
|
|
79
|
+
if (!path.startsWith('/'))
|
|
80
|
+
path = `/${path}`;
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
function docSummaryLine(doc) {
|
|
84
|
+
const desc = doc.fm?.description ? ` — ${doc.fm.description}` : '';
|
|
85
|
+
return `${doc.bundlePath} · ${doc.title} [${doc.fm?.type ?? doc.kind}]${desc}`;
|
|
86
|
+
}
|
|
87
|
+
function navigationFooter(vault, doc) {
|
|
88
|
+
const graph = buildGraph(vault);
|
|
89
|
+
const out = (graph.outbound.get(doc.bundlePath) ?? [])
|
|
90
|
+
.filter((e) => e.resolved)
|
|
91
|
+
.map((e) => e.target);
|
|
92
|
+
const inn = (graph.inbound.get(doc.bundlePath) ?? []).map((e) => e.source);
|
|
93
|
+
const fmt = (paths) => paths.length > 0 ? [...new Set(paths)].join(', ') : '(none)';
|
|
94
|
+
return `— navigation —\nlinks out: ${fmt(out)}\nlinked from: ${fmt(inn)}`;
|
|
95
|
+
}
|
|
96
|
+
// ── NAVIGATE ─────────────────────────────────────────────────────────────────
|
|
97
|
+
export const wikiMapContract = defineTool({
|
|
98
|
+
name: 'wiki_map',
|
|
99
|
+
description: 'Orient in the knowledge bundle: its purpose, directory map, document census, and graph health. Call this FIRST on any knowledge task, then follow with wiki_search, wiki_read, or wiki_dive.',
|
|
100
|
+
schema: z.object({}),
|
|
101
|
+
execute: async () => {
|
|
102
|
+
const vault = vaultOrError();
|
|
103
|
+
if (typeof vault === 'string')
|
|
104
|
+
return vault;
|
|
105
|
+
const graph = buildGraph(vault);
|
|
106
|
+
const stats = graphStats(graph);
|
|
107
|
+
const rootIndex = vault.docs.get('/index.md');
|
|
108
|
+
const lines = [];
|
|
109
|
+
if (rootIndex) {
|
|
110
|
+
const fm = rootIndex.parsed.frontmatter;
|
|
111
|
+
lines.push(`${String(fm?.title ?? 'Knowledge bundle')} — ${String(fm?.description ?? '')}`.trim());
|
|
112
|
+
}
|
|
113
|
+
lines.push(`documents: ${stats.documents} (${stats.concepts} concepts) · links: ${stats.edges} · words: ${stats.totalWords}`, `health: ${stats.brokenLinks} broken link(s), ${stats.orphans} orphan(s)`, `types: ${Object.entries(stats.byType)
|
|
114
|
+
.sort((a, b) => b[1] - a[1])
|
|
115
|
+
.map(([t, n]) => `${t}(${n})`)
|
|
116
|
+
.join(', ')}`, '', 'directories:');
|
|
117
|
+
const dirs = new Map();
|
|
118
|
+
for (const doc of vault.docs.values()) {
|
|
119
|
+
const top = doc.bundlePath.split('/')[1];
|
|
120
|
+
if (doc.bundlePath.split('/').length > 2) {
|
|
121
|
+
dirs.set(top, (dirs.get(top) ?? 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const [dir, count] of [...dirs.entries()].sort()) {
|
|
125
|
+
const index = vault.docs.get(`/${dir}/index.md`);
|
|
126
|
+
const h1 = index?.parsed.headings.find((h) => h.depth === 1)?.text ?? dir;
|
|
127
|
+
lines.push(` /${dir}/ — ${h1} (${count} file(s))`);
|
|
128
|
+
}
|
|
129
|
+
const entities = loadEntityGraph(resolveWikiRoot());
|
|
130
|
+
if (entities.snapshot) {
|
|
131
|
+
const eStats = entityStats(entities.graph);
|
|
132
|
+
lines.push('', `entity graph: ${eStats.nodes} nodes / ${eStats.edges} typed relations (${eStats.byTier.inferred} asserted) — query with wiki_graph`);
|
|
133
|
+
}
|
|
134
|
+
lines.push('', 'read /index.md for the annotated map; log.md for recent changes.');
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
export const wikiReadContract = defineTool({
|
|
139
|
+
name: 'wiki_read',
|
|
140
|
+
description: 'Read one wiki document by bundle path (e.g. "/memory/schema.md"), or one section of it. Returns frontmatter, content, and the links in and out — follow those paths for context.',
|
|
141
|
+
schema: z.object({
|
|
142
|
+
path: z.string().min(1).describe('Bundle-absolute path like /memory/schema.md'),
|
|
143
|
+
section: z
|
|
144
|
+
.string()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('Optional heading name or slug to read just that section'),
|
|
147
|
+
}),
|
|
148
|
+
execute: async ({ path, section }) => {
|
|
149
|
+
const vault = vaultOrError();
|
|
150
|
+
if (typeof vault === 'string')
|
|
151
|
+
return vault;
|
|
152
|
+
const resolved = resolveLinkTarget('/', normalizeDocPath(path));
|
|
153
|
+
if (resolved.kind !== 'internal') {
|
|
154
|
+
return `Error: not a bundle path: ${path}`;
|
|
155
|
+
}
|
|
156
|
+
const doc = lookupTarget(vault, resolved.bundlePath);
|
|
157
|
+
if (!doc) {
|
|
158
|
+
const near = scoreDocs(vault, path.replace(/[/_.-]/g, ' ')).slice(0, 3);
|
|
159
|
+
return `Error: no document at ${resolved.bundlePath}.${near.length > 0
|
|
160
|
+
? ` Nearest matches:\n${near.map((n) => docSummaryLine(n.doc)).join('\n')}`
|
|
161
|
+
: ''}`;
|
|
162
|
+
}
|
|
163
|
+
const header = [
|
|
164
|
+
`path: ${doc.bundlePath}`,
|
|
165
|
+
`title: ${doc.title}`,
|
|
166
|
+
`type: ${doc.fm?.type ?? doc.kind} · status: ${doc.fm?.status ?? 'stable'} · trust: ${doc.fm ? trustTier(doc.fm) : 'unverified'} · words: ${doc.wordCount}`,
|
|
167
|
+
].join('\n');
|
|
168
|
+
let content;
|
|
169
|
+
if (section) {
|
|
170
|
+
const slice = sectionSlice(doc.parsed, section);
|
|
171
|
+
if (!slice) {
|
|
172
|
+
return `Error: no section "${section}" in ${doc.bundlePath}. Sections: ${doc.parsed.headings
|
|
173
|
+
.map((h) => h.slug)
|
|
174
|
+
.join(', ')}`;
|
|
175
|
+
}
|
|
176
|
+
content = slice.text;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
content = doc.parsed.raw;
|
|
180
|
+
}
|
|
181
|
+
const MAX = 40_000;
|
|
182
|
+
if (content.length > MAX) {
|
|
183
|
+
content = `${content.slice(0, MAX)}\n…[truncated — use the section parameter]`;
|
|
184
|
+
}
|
|
185
|
+
return `${header}\n\n${content}\n\n${navigationFooter(vault, doc)}`;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
export const wikiSearchContract = defineTool({
|
|
189
|
+
name: 'wiki_search',
|
|
190
|
+
description: 'Lexical search over the knowledge bundle (titles, tags, headings, paths, body). Returns ranked bundle paths with snippets — read the winners with wiki_read.',
|
|
191
|
+
schema: z.object({
|
|
192
|
+
query: z.string().min(2).describe('Search terms'),
|
|
193
|
+
limit: z.number().int().min(1).max(25).default(8),
|
|
194
|
+
}),
|
|
195
|
+
execute: async ({ query, limit }) => {
|
|
196
|
+
const vault = vaultOrError();
|
|
197
|
+
if (typeof vault === 'string')
|
|
198
|
+
return vault;
|
|
199
|
+
const results = scoreDocs(vault, query).slice(0, limit);
|
|
200
|
+
if (results.length === 0) {
|
|
201
|
+
return `No matches for "${query}". Try wiki_map for the directory census, or broaden the terms.`;
|
|
202
|
+
}
|
|
203
|
+
return results
|
|
204
|
+
.map((r, i) => `${i + 1}. ${docSummaryLine(r.doc)}\n score ${r.score} (${r.matched.join(', ')})${r.snippet ? `\n "${r.snippet}"` : ''}`)
|
|
205
|
+
.join('\n');
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
export const wikiLinksContract = defineTool({
|
|
209
|
+
name: 'wiki_links',
|
|
210
|
+
description: 'Walk the knowledge graph from one document: what it links to and what links to it, out to a chosen depth. Use to find related concepts search terms would miss.',
|
|
211
|
+
schema: z.object({
|
|
212
|
+
path: z.string().min(1).describe('Bundle-absolute path of the origin document'),
|
|
213
|
+
depth: z.number().int().min(1).max(3).default(1),
|
|
214
|
+
direction: z.enum(['out', 'in', 'both']).default('both'),
|
|
215
|
+
}),
|
|
216
|
+
execute: async ({ path, depth, direction }) => {
|
|
217
|
+
const vault = vaultOrError();
|
|
218
|
+
if (typeof vault === 'string')
|
|
219
|
+
return vault;
|
|
220
|
+
const resolved = resolveLinkTarget('/', normalizeDocPath(path));
|
|
221
|
+
if (resolved.kind !== 'internal')
|
|
222
|
+
return `Error: not a bundle path: ${path}`;
|
|
223
|
+
const doc = lookupTarget(vault, resolved.bundlePath);
|
|
224
|
+
if (!doc)
|
|
225
|
+
return `Error: no document at ${resolved.bundlePath}`;
|
|
226
|
+
const graph = buildGraph(vault);
|
|
227
|
+
const neighbors = neighborhood(graph, doc.bundlePath, depth, direction);
|
|
228
|
+
if (neighbors.length === 0) {
|
|
229
|
+
return `${doc.bundlePath} has no ${direction === 'both' ? '' : `${direction}bound `}links within depth ${depth} — it may be an orphan; consider gardening.`;
|
|
230
|
+
}
|
|
231
|
+
return [
|
|
232
|
+
`neighborhood of ${doc.bundlePath} (depth ${depth}, ${direction}):`,
|
|
233
|
+
...neighbors.map((n) => ` ${'·'.repeat(n.distance)} ${n.node.path} · ${n.node.title} [${n.node.type}] (${n.direction === 'out' ? '→' : '←'} via ${n.via})`),
|
|
234
|
+
].join('\n');
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
export const wikiDiveContract = defineTool({
|
|
238
|
+
name: 'wiki_dive',
|
|
239
|
+
description: 'Repo dive: given a TASK, get an ordered reading plan through the bundle — orientation indexes first, then matched concepts, then graph-linked context, within a word budget. Deterministic and instant; do the reading with wiki_read.',
|
|
240
|
+
schema: z.object({
|
|
241
|
+
task: z
|
|
242
|
+
.string()
|
|
243
|
+
.min(3)
|
|
244
|
+
.describe('The knowledge task, e.g. "add a new provider adapter"'),
|
|
245
|
+
budget_words: z.number().int().min(500).max(50_000).default(6000),
|
|
246
|
+
}),
|
|
247
|
+
execute: async ({ task, budget_words }) => {
|
|
248
|
+
const vault = vaultOrError();
|
|
249
|
+
if (typeof vault === 'string')
|
|
250
|
+
return vault;
|
|
251
|
+
const plan = repoDive(vault, task, budget_words);
|
|
252
|
+
if (plan.stops.length === 0) {
|
|
253
|
+
return `No relevant documents found for "${task}". The bundle may not cover this yet — wiki_map shows what exists.`;
|
|
254
|
+
}
|
|
255
|
+
const lines = [
|
|
256
|
+
`reading plan for: ${plan.task}`,
|
|
257
|
+
`${plan.stops.length} stop(s), ~${plan.totalWords} words (budget ${plan.budgetWords})`,
|
|
258
|
+
'',
|
|
259
|
+
...plan.stops.map((s) => `${s.order}. [${s.role}] ${s.path} · ${s.title} (~${s.wordCount}w)\n why: ${s.reason}`),
|
|
260
|
+
];
|
|
261
|
+
if (plan.overflow.length > 0) {
|
|
262
|
+
lines.push('', `did not fit the budget: ${plan.overflow.map((o) => o.path).join(', ')}`);
|
|
263
|
+
}
|
|
264
|
+
return lines.join('\n');
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
// ── NAVIGATE: the entity layer ───────────────────────────────────────────────
|
|
268
|
+
/** Snapshot + asserted relations, or a message explaining what to run. */
|
|
269
|
+
function entityGraphOrError() {
|
|
270
|
+
const loaded = loadEntityGraph(resolveWikiRoot());
|
|
271
|
+
if (!loaded.snapshot) {
|
|
272
|
+
return 'Error: no entity graph snapshot in this bundle (.graph/graph.json). It is derived from repo truth by the build — run `npm run wiki:build`. Document-only navigation (wiki_links) works without it.';
|
|
273
|
+
}
|
|
274
|
+
return loaded;
|
|
275
|
+
}
|
|
276
|
+
function describeNode(node) {
|
|
277
|
+
const attrs = Object.entries(node.attrs ?? {})
|
|
278
|
+
.filter(([key]) => key !== 'description')
|
|
279
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
280
|
+
.join(' · ');
|
|
281
|
+
const description = node.attrs?.description ? ` — ${String(node.attrs.description)}` : '';
|
|
282
|
+
return `${node.id} [${node.kind}]${node.private ? ' (private)' : ''}${attrs ? ` {${attrs}}` : ''}${description}`;
|
|
283
|
+
}
|
|
284
|
+
function stalenessNote(loaded) {
|
|
285
|
+
let vault;
|
|
286
|
+
try {
|
|
287
|
+
vault = loadVault(resolveWikiRoot());
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (typeof vault === 'string')
|
|
293
|
+
return null;
|
|
294
|
+
const current = [...vault.docs.values()]
|
|
295
|
+
.filter((d) => d.kind === 'concept')
|
|
296
|
+
.map((d) => d.bundlePath);
|
|
297
|
+
const drift = snapshotDrift(loaded.snapshot, current);
|
|
298
|
+
if (drift.missing.length === 0 && drift.removed.length === 0)
|
|
299
|
+
return null;
|
|
300
|
+
const parts = [];
|
|
301
|
+
if (drift.missing.length > 0)
|
|
302
|
+
parts.push(`${drift.missing.length} document(s) added since`);
|
|
303
|
+
if (drift.removed.length > 0)
|
|
304
|
+
parts.push(`${drift.removed.length} removed`);
|
|
305
|
+
return `snapshot is stale (${parts.join(', ')}) — run \`npm run wiki:build\` to refresh it.`;
|
|
306
|
+
}
|
|
307
|
+
export const wikiGraphContract = defineTool({
|
|
308
|
+
name: 'wiki_graph',
|
|
309
|
+
description: 'Query the ENTITY graph: agents, syndicates, models, providers, tools, MCP servers, modules, tables, env vars, npm scripts and documents, joined by typed relations (uses_tool, uses_model, imports, reads_table, requires_env, documents, depends_on, constrains…). Answers what search cannot: which syndicates call a tool, what breaks without a key, how two things connect. No arguments = census. Use `find` when you do not know a node id.',
|
|
310
|
+
schema: z.object({
|
|
311
|
+
node: z
|
|
312
|
+
.string()
|
|
313
|
+
.optional()
|
|
314
|
+
.describe('Node to centre on: "tool:web_search", "agent:critic/DrafterAgent", "/memory/schema.md"'),
|
|
315
|
+
find: z.string().optional().describe('Lexical node search when the exact id is unknown'),
|
|
316
|
+
kind: z
|
|
317
|
+
.string()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe('List every node of one kind: agent, syndicate, model, provider, tool, mcp-server, module, file, table, env, script, doc, external'),
|
|
320
|
+
path_to: z
|
|
321
|
+
.string()
|
|
322
|
+
.optional()
|
|
323
|
+
.describe('With `node`: the shortest chain of relations joining the two'),
|
|
324
|
+
relations: z
|
|
325
|
+
.array(z.string())
|
|
326
|
+
.optional()
|
|
327
|
+
.describe('Restrict the walk to these relation ids'),
|
|
328
|
+
depth: z.number().int().min(1).max(3).default(1),
|
|
329
|
+
direction: z.enum(['out', 'in', 'both']).default('both'),
|
|
330
|
+
limit: z.number().int().min(1).max(200).default(40),
|
|
331
|
+
}),
|
|
332
|
+
execute: async ({ node, find, kind, path_to, relations, depth, direction, limit }) => {
|
|
333
|
+
const loaded = entityGraphOrError();
|
|
334
|
+
if (typeof loaded === 'string')
|
|
335
|
+
return loaded;
|
|
336
|
+
const { graph } = loaded;
|
|
337
|
+
const stale = stalenessNote(loaded);
|
|
338
|
+
const footer = [
|
|
339
|
+
stale ? `\nnote: ${stale}` : '',
|
|
340
|
+
loaded.issues.length > 0 ? `\nrelation store issues: ${loaded.issues.join('; ')}` : '',
|
|
341
|
+
].join('');
|
|
342
|
+
if (find) {
|
|
343
|
+
const matches = findNodes(graph, find, limit);
|
|
344
|
+
if (matches.length === 0) {
|
|
345
|
+
return `No node matches "${find}". Try wiki_graph with no arguments for the census of kinds.${footer}`;
|
|
346
|
+
}
|
|
347
|
+
return [
|
|
348
|
+
`nodes matching "${find}":`,
|
|
349
|
+
...matches.map((m) => ` ${describeNode(m.node)}`),
|
|
350
|
+
].join('\n') + footer;
|
|
351
|
+
}
|
|
352
|
+
if (kind) {
|
|
353
|
+
const list = nodesOfKind(graph, kind);
|
|
354
|
+
if (list.length === 0) {
|
|
355
|
+
return `No nodes of kind "${kind}". Kinds present: ${Object.keys(entityStats(graph).byKind).sort().join(', ')}${footer}`;
|
|
356
|
+
}
|
|
357
|
+
return [
|
|
358
|
+
`${list.length} ${kind} node(s):`,
|
|
359
|
+
...list.slice(0, limit).map((n) => ` ${describeNode(n)}`),
|
|
360
|
+
...(list.length > limit ? [` …${list.length - limit} more (raise limit)`] : []),
|
|
361
|
+
].join('\n') + footer;
|
|
362
|
+
}
|
|
363
|
+
if (node && path_to) {
|
|
364
|
+
const from = graph.nodes.get(node) ?? findNodes(graph, node, 1)[0]?.node;
|
|
365
|
+
const to = graph.nodes.get(path_to) ?? findNodes(graph, path_to, 1)[0]?.node;
|
|
366
|
+
if (!from)
|
|
367
|
+
return `Error: no node "${node}" — try wiki_graph with find.${footer}`;
|
|
368
|
+
if (!to)
|
|
369
|
+
return `Error: no node "${path_to}" — try wiki_graph with find.${footer}`;
|
|
370
|
+
const chain = shortestPath(graph, from.id, to.id);
|
|
371
|
+
if (!chain) {
|
|
372
|
+
return `No relation chain connects ${from.id} and ${to.id} within 6 hops.${footer}`;
|
|
373
|
+
}
|
|
374
|
+
return [
|
|
375
|
+
`${from.id} → ${to.id} (${chain.length - 1} hop(s)):`,
|
|
376
|
+
...chain.map((step, i) => i === 0
|
|
377
|
+
? ` ${step.node.id}`
|
|
378
|
+
: ` ${step.direction === 'out' ? '—' : '←'}${step.rel}${step.direction === 'out' ? '→' : '—'} ${step.node.id} [${step.node.kind}]`),
|
|
379
|
+
].join('\n') + footer;
|
|
380
|
+
}
|
|
381
|
+
if (node) {
|
|
382
|
+
const origin = graph.nodes.get(node) ?? findNodes(graph, node, 1)[0]?.node;
|
|
383
|
+
if (!origin) {
|
|
384
|
+
const near = findNodes(graph, node, 5);
|
|
385
|
+
return `Error: no node "${node}".${near.length > 0 ? ` Nearest:\n${near.map((n) => ` ${describeNode(n.node)}`).join('\n')}` : ''}${footer}`;
|
|
386
|
+
}
|
|
387
|
+
const steps = neighbors(graph, origin.id, { depth, direction, ...(relations ? { relations } : {}) });
|
|
388
|
+
if (steps.length === 0) {
|
|
389
|
+
return `${describeNode(origin)}\n no ${direction === 'both' ? '' : `${direction}bound `}relations within depth ${depth}.${footer}`;
|
|
390
|
+
}
|
|
391
|
+
const grouped = new Map();
|
|
392
|
+
for (const step of steps.slice(0, limit)) {
|
|
393
|
+
const spec = RELATIONS[step.rel];
|
|
394
|
+
const heading = `${step.direction === 'out' ? `—${step.rel}→` : `←${step.rel}—`} (${spec ? spec.phrase : step.rel}, ${step.tier})`;
|
|
395
|
+
const line = ` ${step.node.id} [${step.node.kind}]${step.distance > 1 ? ` · ${step.distance} hops via ${step.via}` : ''}${step.tier === 'inferred' && step.evidence ? `\n evidence: ${step.evidence}` : ''}`;
|
|
396
|
+
grouped.set(heading, [...(grouped.get(heading) ?? []), line]);
|
|
397
|
+
}
|
|
398
|
+
return [
|
|
399
|
+
describeNode(origin),
|
|
400
|
+
...[...grouped.entries()].sort().flatMap(([heading, lines]) => [` ${heading}`, ...lines]),
|
|
401
|
+
...(steps.length > limit ? [` …${steps.length - limit} more relation(s)`] : []),
|
|
402
|
+
].join('\n') + footer;
|
|
403
|
+
}
|
|
404
|
+
const stats = entityStats(graph);
|
|
405
|
+
return [
|
|
406
|
+
`entity graph — built ${loaded.snapshot?.generated.at ?? '?'} by ${loaded.snapshot?.generated.by ?? '?'}`,
|
|
407
|
+
`${stats.nodes} nodes, ${stats.edges} relations (${stats.byTier.extracted} extracted from repo truth, ${stats.byTier.inferred} asserted with evidence)`,
|
|
408
|
+
'',
|
|
409
|
+
'kinds:',
|
|
410
|
+
...Object.entries(stats.byKind)
|
|
411
|
+
.sort((a, b) => b[1] - a[1])
|
|
412
|
+
.map(([k, n]) => ` ${k} (${n}) — ${NODE_KINDS[k] ?? 'unknown kind'}`),
|
|
413
|
+
'',
|
|
414
|
+
'relations:',
|
|
415
|
+
...Object.entries(stats.byRelation)
|
|
416
|
+
.sort((a, b) => b[1] - a[1])
|
|
417
|
+
.map(([r, n]) => ` ${r} (${n}) — ${RELATIONS[r]?.gloss ?? 'outside the vocabulary'}`),
|
|
418
|
+
'',
|
|
419
|
+
'centre on a node with `node`, or search with `find`.',
|
|
420
|
+
].join('\n') + footer;
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
// ── WRITE: assert a relation the build cannot derive ─────────────────────────
|
|
424
|
+
export const wikiRelateContract = defineTool({
|
|
425
|
+
name: 'wiki_relate',
|
|
426
|
+
description: 'Assert ONE typed relation the build cannot derive — a judgment read out of prose (depends_on, constrains, supersedes, explains, alternative_to, mitigates, contradicts). Requires evidence: quote the text or name the file that says so. Structural relations (uses_tool, imports…) are refused: those are derived by `npm run wiki:build` and asserting them would rot.',
|
|
427
|
+
schema: z.object({
|
|
428
|
+
from: z.string().min(1).describe('Source node id, e.g. "/decisions/0003-path-based-visibility.md"'),
|
|
429
|
+
to: z.string().min(1).describe('Target node id, e.g. "module:scripts/export-public/export.sh"'),
|
|
430
|
+
relation: z
|
|
431
|
+
.string()
|
|
432
|
+
.min(1)
|
|
433
|
+
.describe('One of: depends_on, constrains, supersedes, explains, alternative_to, mitigates, contradicts'),
|
|
434
|
+
evidence: z
|
|
435
|
+
.string()
|
|
436
|
+
.min(10)
|
|
437
|
+
.describe('Why this holds: a quotation from the document, or the path that states it'),
|
|
438
|
+
actor: actorSchema.describe('"human:<id>", "process:<id>", or "<producer>/<model>"'),
|
|
439
|
+
note: z.string().optional().describe('Optional clarification for a later reader'),
|
|
440
|
+
}),
|
|
441
|
+
execute: async ({ from, to, relation, evidence, actor, note }) => {
|
|
442
|
+
const root = resolveWikiRoot();
|
|
443
|
+
const loaded = entityGraphOrError();
|
|
444
|
+
if (typeof loaded === 'string')
|
|
445
|
+
return loaded;
|
|
446
|
+
const { graph } = loaded;
|
|
447
|
+
const spec = RELATIONS[relation];
|
|
448
|
+
if (!spec) {
|
|
449
|
+
return `REJECTED — "${relation}" is not in the vocabulary. Assertable relations: ${relationsByTier('inferred').join(', ')}.`;
|
|
450
|
+
}
|
|
451
|
+
if (spec.tier !== 'inferred') {
|
|
452
|
+
return `REJECTED — "${relation}" is an EXTRACTED relation: the build derives it from repo truth on every run, so an assertion would go stale silently. Change the source of truth instead, then run \`npm run wiki:build\`.`;
|
|
453
|
+
}
|
|
454
|
+
const resolveEnd = (id) => graph.nodes.get(id) ?? (findNodes(graph, id, 1)[0]?.node ?? null);
|
|
455
|
+
const fromNode = resolveEnd(from);
|
|
456
|
+
const toNode = resolveEnd(to);
|
|
457
|
+
for (const [label, raw, resolved] of [
|
|
458
|
+
['from', from, fromNode],
|
|
459
|
+
['to', to, toNode],
|
|
460
|
+
]) {
|
|
461
|
+
if (!resolved) {
|
|
462
|
+
const near = findNodes(graph, raw, 5);
|
|
463
|
+
return [
|
|
464
|
+
`REJECTED — no node "${raw}" (${label}).`,
|
|
465
|
+
near.length > 0
|
|
466
|
+
? `Nearest:\n${near.map((n) => ` ${describeNode(n.node)}`).join('\n')}`
|
|
467
|
+
: 'Use wiki_graph with `find` or `kind` to see what exists.',
|
|
468
|
+
].join('\n');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (fromNode.id === toNode.id) {
|
|
472
|
+
return `REJECTED — a node cannot relate to itself (${fromNode.id}).`;
|
|
473
|
+
}
|
|
474
|
+
if (fromNode.kind === 'doc' && !isPrivateNode(fromNode) && isPrivateNode(toNode)) {
|
|
475
|
+
return `REJECTED — ${fromNode.id} is a public document and ${toNode.id} is private knowledge. Public documents never point into the private annex; move the claim to a /private/ document instead.`;
|
|
476
|
+
}
|
|
477
|
+
const { records } = loadRelations(root);
|
|
478
|
+
const key = edgeKey({ from: fromNode.id, rel: relation, to: toNode.id });
|
|
479
|
+
if (records.some((r) => edgeKey(r) === key)) {
|
|
480
|
+
return `UNCHANGED — ${key} is already asserted. wiki_graph on either node shows it.`;
|
|
481
|
+
}
|
|
482
|
+
const record = {
|
|
483
|
+
from: fromNode.id,
|
|
484
|
+
to: toNode.id,
|
|
485
|
+
rel: relation,
|
|
486
|
+
evidence,
|
|
487
|
+
by: actor,
|
|
488
|
+
at: todayIso(),
|
|
489
|
+
...(note ? { note } : {}),
|
|
490
|
+
};
|
|
491
|
+
saveRelations(root, [...records, record]);
|
|
492
|
+
appendLog(root, todayIso(), 'relate', `${fromNode.id} —${relation}→ ${toNode.id}`, `by ${actor}`, logTargetFor(isPrivateNode(fromNode) || isPrivateNode(toNode)));
|
|
493
|
+
const reverse = records.find((r) => r.from === toNode.id && r.to === fromNode.id && r.rel === relation);
|
|
494
|
+
return [
|
|
495
|
+
`ASSERTED ${fromNode.id} —${relation}→ ${toNode.id} · logged to /log.md`,
|
|
496
|
+
`reads: ${fromNode.label} ${RELATIONS[relation].phrase} ${toNode.label}`,
|
|
497
|
+
reverse ? `advisory: the reverse (${edgeKey(reverse)}) is also asserted — one direction is usually enough.` : 'advisories: none',
|
|
498
|
+
].join('\n');
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
// ── WRITE ────────────────────────────────────────────────────────────────────
|
|
502
|
+
export const wikiSaveContract = defineTool({
|
|
503
|
+
name: 'wiki_save',
|
|
504
|
+
description: 'Write one concept document into the bundle (create or revise). The draft must be complete markdown with YAML frontmatter (`type` required). Validation gates the write: lint errors REJECT it; the directory index and log.md update automatically. Reserved files (index.md, log.md) cannot be written.',
|
|
505
|
+
schema: z.object({
|
|
506
|
+
path: z
|
|
507
|
+
.string()
|
|
508
|
+
.min(4)
|
|
509
|
+
.describe('Bundle-absolute target like /decisions/0005-topic.md'),
|
|
510
|
+
content: z
|
|
511
|
+
.string()
|
|
512
|
+
.min(20)
|
|
513
|
+
.describe('Full document text: --- frontmatter --- then markdown body'),
|
|
514
|
+
actor: actorSchema.describe('Who authored this: "human:<id>", "process:<id>", or "<producer>/<model>"'),
|
|
515
|
+
summary: z
|
|
516
|
+
.string()
|
|
517
|
+
.min(5)
|
|
518
|
+
.max(200)
|
|
519
|
+
.describe('One-line change summary for log.md'),
|
|
520
|
+
}),
|
|
521
|
+
execute: async ({ path, content, actor, summary }) => {
|
|
522
|
+
const vault = vaultOrError();
|
|
523
|
+
if (typeof vault === 'string')
|
|
524
|
+
return vault;
|
|
525
|
+
const bundlePath = normalizeDocPath(path);
|
|
526
|
+
if (!bundlePath.endsWith('.md')) {
|
|
527
|
+
return `Error: documents are .md files: ${bundlePath}`;
|
|
528
|
+
}
|
|
529
|
+
const fileName = bundlePath.split('/').pop() ?? '';
|
|
530
|
+
if (isReservedFilename(fileName)) {
|
|
531
|
+
return `Error: ${fileName} is reserved and machine-maintained — write concept documents only.`;
|
|
532
|
+
}
|
|
533
|
+
let absPath;
|
|
534
|
+
try {
|
|
535
|
+
absPath = jailedAbsPath(vault.root, bundlePath);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
539
|
+
}
|
|
540
|
+
// Evaluate the draft exactly as the bundle would see it.
|
|
541
|
+
const draft = makeWikiDoc(bundlePath, absPath, content);
|
|
542
|
+
const existed = vault.docs.has(bundlePath);
|
|
543
|
+
vault.docs.set(bundlePath, draft);
|
|
544
|
+
const findings = lintDoc(vault, draft);
|
|
545
|
+
const errors = findings.filter((f) => f.severity === 'error');
|
|
546
|
+
if (errors.length > 0) {
|
|
547
|
+
return [
|
|
548
|
+
`REJECTED — fix these and save again:`,
|
|
549
|
+
...errors.map((e) => ` ${e.rule}${e.line ? `:${e.line}` : ''} — ${e.message}`),
|
|
550
|
+
'',
|
|
551
|
+
`Profile: frontmatter needs non-empty \`type\` (vocabulary: ${CONCEPT_TYPES.join(', ')});`,
|
|
552
|
+
'links are bundle-absolute markdown links; public docs never link into /private/.',
|
|
553
|
+
].join('\n');
|
|
554
|
+
}
|
|
555
|
+
const today = todayIso();
|
|
556
|
+
writeDocFile(vault.root, bundlePath, content);
|
|
557
|
+
// Structural aftermath: refresh the directory index, append the log.
|
|
558
|
+
const dir = bundlePath.slice(0, bundlePath.lastIndexOf('/') + 1);
|
|
559
|
+
if (dir !== '/') {
|
|
560
|
+
const siblings = docsInDirectory(vault, dir)
|
|
561
|
+
.filter((d) => d.kind === 'concept')
|
|
562
|
+
.map((d) => ({
|
|
563
|
+
bundlePath: d.bundlePath,
|
|
564
|
+
title: d.title,
|
|
565
|
+
description: String(d.fm?.description ?? d.fm?.type ?? 'concept'),
|
|
566
|
+
}))
|
|
567
|
+
.sort((a, b) => a.bundlePath.localeCompare(b.bundlePath));
|
|
568
|
+
const indexTitle = vault.docs.get(`${dir}index.md`)?.parsed.headings.find((h) => h.depth === 1)
|
|
569
|
+
?.text ?? dir.replaceAll('/', ' ').trim();
|
|
570
|
+
upsertDoc(vault.root, indexSpec(dir, indexTitle, siblings), {
|
|
571
|
+
actor: BUILD_ACTOR,
|
|
572
|
+
date: today,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
appendLog(vault.root, today, 'garden', `${summary} (${bundlePath})`, `by ${actor}`, logTargetFor(isPrivatePath(bundlePath)));
|
|
576
|
+
const warnings = findings.filter((f) => f.severity !== 'error');
|
|
577
|
+
return [
|
|
578
|
+
`${existed ? 'REVISED' : 'CREATED'} ${bundlePath} (${draft.wordCount} words) · logged to /log.md`,
|
|
579
|
+
warnings.length > 0
|
|
580
|
+
? `advisories:\n${warnings.map((w) => ` ${w.rule} — ${w.message}`).join('\n')}`
|
|
581
|
+
: 'advisories: none',
|
|
582
|
+
].join('\n');
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
// ── AGENTIC ──────────────────────────────────────────────────────────────────
|
|
586
|
+
const NAVIGATE_CONTRACTS = [
|
|
587
|
+
wikiMapContract,
|
|
588
|
+
wikiReadContract,
|
|
589
|
+
wikiSearchContract,
|
|
590
|
+
wikiLinksContract,
|
|
591
|
+
wikiDiveContract,
|
|
592
|
+
wikiGraphContract,
|
|
593
|
+
];
|
|
594
|
+
export const wikiQueryContract = defineTool({
|
|
595
|
+
name: 'wiki_query',
|
|
596
|
+
description: 'Ask the knowledge bundle a question in natural language. An agent navigates the wiki (map → search → read → links) and answers with bundle-path citations. Costs a model call; for raw lookup prefer wiki_search/wiki_read.',
|
|
597
|
+
schema: z.object({
|
|
598
|
+
question: z.string().min(5),
|
|
599
|
+
model: z
|
|
600
|
+
.string()
|
|
601
|
+
.optional()
|
|
602
|
+
.describe('Override model (any provider string the registry routes)'),
|
|
603
|
+
}),
|
|
604
|
+
execute: async ({ question, model }) => {
|
|
605
|
+
const result = await runWikiAgent({
|
|
606
|
+
name: 'wiki_librarian',
|
|
607
|
+
description: 'Answers questions from the knowledge bundle, with citations.',
|
|
608
|
+
model: wikiModel(model),
|
|
609
|
+
instruction: `You answer questions using ONLY the knowledge bundle reachable through your wiki tools.
|
|
610
|
+
Method: wiki_map to orient, then wiki_search (and wiki_links from strong hits), then wiki_read the few most relevant documents. THEN answer.
|
|
611
|
+
Rules:
|
|
612
|
+
- Everything you assert must come from documents you actually read this run; if the bundle does not cover it, say so plainly.
|
|
613
|
+
- Answer first, provenance second: end with "Sources:" listing the bundle paths you drew on.
|
|
614
|
+
- Note trust when it matters: an unverified or draft document is a weaker source than a human-reviewed one.
|
|
615
|
+
- Be concrete and brief. No filler.`,
|
|
616
|
+
userText: question,
|
|
617
|
+
tools: NAVIGATE_CONTRACTS.map(toFunctionTool),
|
|
618
|
+
temperature: 0.2,
|
|
619
|
+
maxOutputTokens: 4096,
|
|
620
|
+
});
|
|
621
|
+
if (result.error)
|
|
622
|
+
return `Error: wiki_query failed: ${result.error}`;
|
|
623
|
+
return result.text === '' ? 'Error: wiki_query produced no answer.' : result.text;
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
export const wikiGardenContract = defineTool({
|
|
627
|
+
name: 'wiki_garden',
|
|
628
|
+
description: 'Delegate a wiki edit to a gardener agent: author a new concept document or revise existing ones from an instruction. The agent reads relevant docs first, then writes through the validated wiki_save gate and reports exactly what changed. Costs model calls.',
|
|
629
|
+
schema: z.object({
|
|
630
|
+
instruction: z
|
|
631
|
+
.string()
|
|
632
|
+
.min(10)
|
|
633
|
+
.describe('What to plant or prune, e.g. "document the retry doctrine decided today: <details>"'),
|
|
634
|
+
model: z.string().optional(),
|
|
635
|
+
}),
|
|
636
|
+
execute: async ({ instruction, model }) => {
|
|
637
|
+
const resolvedModel = wikiModel(model);
|
|
638
|
+
const result = await runWikiAgent({
|
|
639
|
+
name: 'wiki_gardener',
|
|
640
|
+
description: 'Authors and revises knowledge bundle documents.',
|
|
641
|
+
model: resolvedModel,
|
|
642
|
+
instruction: `You garden a knowledge bundle: plant new concept documents and prune existing ones.
|
|
643
|
+
Method — always in this order:
|
|
644
|
+
1. wiki_map, then wiki_search/wiki_read to find where the topic lives today. Revise the existing document rather than planting a duplicate.
|
|
645
|
+
2. Draft the full document: YAML frontmatter, then body.
|
|
646
|
+
3. wiki_save it. If REJECTED, fix every listed error and save again. Report the advisories you accept.
|
|
647
|
+
Format profile:
|
|
648
|
+
- frontmatter: \`type\` REQUIRED — vocabulary: ${CONCEPT_TYPES.join(', ')}; plus title, description, tags; keep any existing status/verified fields when revising.
|
|
649
|
+
- links: bundle-absolute markdown links like [Memory schema](/memory/schema.md), ONLY to paths you confirmed exist (wiki_search/wiki_read). Link generously — the graph is the product.
|
|
650
|
+
- body: H1 title, then short sections. Plain, concrete, present tense. State facts you were given or read in the bundle; never invent.
|
|
651
|
+
- private knowledge lives under /private/ — public documents never link there.
|
|
652
|
+
Save with actor "melchizedek/${resolvedModel}". After saving, summarize: paths written, what changed, advisories.`,
|
|
653
|
+
userText: instruction,
|
|
654
|
+
tools: [...NAVIGATE_CONTRACTS, wikiSaveContract].map(toFunctionTool),
|
|
655
|
+
temperature: 0.4,
|
|
656
|
+
maxOutputTokens: 8192,
|
|
657
|
+
});
|
|
658
|
+
if (result.error)
|
|
659
|
+
return `Error: wiki_garden failed: ${result.error}`;
|
|
660
|
+
return result.text === '' ? 'Error: wiki_garden produced no report.' : result.text;
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
// ── Rollups (exposure still happens elsewhere) ───────────────────────────────
|
|
664
|
+
/** Everything an MCP client may be offered (see the wiki MCP server script). */
|
|
665
|
+
export const WIKI_TOOL_CONTRACTS = [
|
|
666
|
+
...NAVIGATE_CONTRACTS,
|
|
667
|
+
wikiSaveContract,
|
|
668
|
+
wikiRelateContract,
|
|
669
|
+
wikiQueryContract,
|
|
670
|
+
wikiGardenContract,
|
|
671
|
+
];
|
|
672
|
+
/**
|
|
673
|
+
* What syndicate agents may declare in YAML (via lib/toolRegistry.ts).
|
|
674
|
+
* The agentic composites are excluded on purpose: a syndicate reaches
|
|
675
|
+
* query/garden behavior by BEING the agent with these primitives, not by
|
|
676
|
+
* nesting another agent run inside a tool call.
|
|
677
|
+
*/
|
|
678
|
+
export const WIKI_AGENT_TOOL_CONTRACTS = [
|
|
679
|
+
...NAVIGATE_CONTRACTS,
|
|
680
|
+
wikiSaveContract,
|
|
681
|
+
wikiRelateContract,
|
|
682
|
+
];
|
|
683
|
+
/** Lint the whole bundle — shared by the build script and tests. */
|
|
684
|
+
export function lintWholeVault() {
|
|
685
|
+
const vault = vaultOrError();
|
|
686
|
+
if (typeof vault === 'string')
|
|
687
|
+
return vault;
|
|
688
|
+
return formatLintReport(lintVault(vault));
|
|
689
|
+
}
|
|
690
|
+
/** Census helper for scripts (not a tool): concepts + word count. */
|
|
691
|
+
export function vaultCensus() {
|
|
692
|
+
const vault = vaultOrError();
|
|
693
|
+
if (typeof vault === 'string')
|
|
694
|
+
return vault;
|
|
695
|
+
const stats = graphStats(buildGraph(vault));
|
|
696
|
+
return `${stats.documents} documents, ${stats.concepts} concepts, ${stats.edges} links, ${stats.totalWords} words`;
|
|
697
|
+
}
|