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
package/DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# melchizedek-agents — reference documentation
|
|
2
|
+
|
|
3
|
+
The framework in one sentence: **a syndicate is a YAML file describing an
|
|
4
|
+
agent graph; the runtime compiles it into live Google-ADK agents with
|
|
5
|
+
tools, sessions, and memory attached.** This document is the reference
|
|
6
|
+
for that file format and the machinery around it. For a guided first run,
|
|
7
|
+
see [`QUICKSTART.md`](./QUICKSTART.md).
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
1. [Architecture](#1-architecture)
|
|
12
|
+
2. [The syndicate YAML](#2-the-syndicate-yaml)
|
|
13
|
+
3. [Tools](#3-tools)
|
|
14
|
+
4. [Sessions & long-term memory](#4-sessions--long-term-memory)
|
|
15
|
+
5. [Multi-model support](#5-multi-model-support)
|
|
16
|
+
6. [A2A service mode](#6-a2a-service-mode)
|
|
17
|
+
7. [Extending the framework](#7-extending-the-framework)
|
|
18
|
+
8. [Security notes](#8-security-notes)
|
|
19
|
+
9. [The knowledge bundle (wiki/)](#9-the-knowledge-bundle-wiki)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
config/agents/ YOUR syndicate definitions (the engine's input)
|
|
27
|
+
config/agents/examples/ the starter pack — shipped example syndicates
|
|
28
|
+
lib/loadSyndicate.ts YAML → validated config (+ variable binding)
|
|
29
|
+
lib/dispatch.ts plan-dispatch route resolution (§6, A2A-only)
|
|
30
|
+
lib/toolRegistry.ts tool name → live ADK tool instance
|
|
31
|
+
lib/models/claudeLlm.ts Claude adapter registered into the ADK registry
|
|
32
|
+
lib/models/ollamaLlm.ts open-weight local adapter (Ollama, keyless)
|
|
33
|
+
lib/tools/mcpToolFactory.ts MCP client: remote tools → live ADK tools
|
|
34
|
+
scripts/demo_mcp_server.ts demo MCP server (library catalog, SSE)
|
|
35
|
+
lib/session/… Supabase-backed session service
|
|
36
|
+
lib/memory/… pgvector long-term memory service
|
|
37
|
+
lib/observability/… OpenTelemetry run tracing
|
|
38
|
+
scripts/syndicate_chat.ts CLI REPL / one-shot runner (compiles the graph)
|
|
39
|
+
scripts/a2a_server.ts HTTP JSON-RPC service mode (same compiler)
|
|
40
|
+
db/hardening.sql deny-by-default RLS for the Supabase tables
|
|
41
|
+
tests/agents.test.ts compiles every shipped syndicate; opt-in live check
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Execution flow: `loadSyndicate` reads and validates the YAML and binds
|
|
45
|
+
`{{variables}}` → the runner builds an `LlmAgent` per agent, wiring
|
|
46
|
+
subagents as `AgentTool`s and tool names through the registry → the ADK
|
|
47
|
+
`Runner` executes the turn, persisting events to the session service →
|
|
48
|
+
on session end, the memory service distills the transcript into tagged
|
|
49
|
+
facts, embeds them (768-d), and stores them for future recall.
|
|
50
|
+
|
|
51
|
+
## 2. The syndicate YAML
|
|
52
|
+
|
|
53
|
+
Minimal complete example:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
syndicate_name: "My Council"
|
|
57
|
+
memory_system: "session-only" # internal-only | session-only | long-term
|
|
58
|
+
|
|
59
|
+
variables: # bound into {{placeholders}} at load
|
|
60
|
+
headline_count: 5 # current_date is injected automatically
|
|
61
|
+
|
|
62
|
+
orchestrator:
|
|
63
|
+
name: "Conductor"
|
|
64
|
+
model: "gemini-3.7-flash"
|
|
65
|
+
instruction: |
|
|
66
|
+
You are the Conductor… (persona, objective, workflow contract)
|
|
67
|
+
tools:
|
|
68
|
+
- "google_search" # names resolved via lib/toolRegistry.ts
|
|
69
|
+
generateContentConfig:
|
|
70
|
+
temperature: 0.7
|
|
71
|
+
maxOutputTokens: 4096
|
|
72
|
+
thinkingConfig:
|
|
73
|
+
thinkingLevel: "MEDIUM" # or thinkingBudget on older models
|
|
74
|
+
includeThoughts: true
|
|
75
|
+
|
|
76
|
+
subagents:
|
|
77
|
+
- name: "Researcher"
|
|
78
|
+
description: "Use this subagent to… Pass it one focused query."
|
|
79
|
+
model: "gemini-3.7-flash"
|
|
80
|
+
instruction: |
|
|
81
|
+
You are the Researcher…
|
|
82
|
+
tools: ["google_search"]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Field reference:
|
|
86
|
+
|
|
87
|
+
| Field | Where | Meaning |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `syndicate_name` | root | Display name; also namespaces memory user keys. |
|
|
90
|
+
| `memory_system` | root | `internal-only` (nothing persists), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
|
|
91
|
+
| `variables` | root | Key/values bound into `{{placeholders}}` anywhere in instructions. `current_date` is always injected; CLI `--bind key=value` overrides. |
|
|
92
|
+
| `memory_extraction_rules` | root | Domain rules appended to the shared fact-extraction prompt for THIS syndicate only (requires `memory_system: "long-term"`). The extraction prompt is global — anything domain-specific belongs here, never edited into it. Unset, the prompt renders byte-identical to before the slot existed (§4). |
|
|
93
|
+
| `dispatch` | root | Switches the syndicate from DELEGATE to PLAN-DISPATCH routing (§6). `default_route` (required) names the fail-static subagent; `route_key` / `reason_key` name the router's JSON properties (defaults `route` / `reason`). A2A-only. |
|
|
94
|
+
| `name` / `model` / `instruction` | agent | The agent triple. Any Gemini id, `claude-*`, or `ollama/*` for open-weight local models (see §5). |
|
|
95
|
+
| `description` | subagent | **The delegation API.** The orchestrator reads this when deciding to hand off — write it like a function signature ("Use this subagent to…, pass it…"). |
|
|
96
|
+
| `tools` | agent | Names resolved by the tool registry (§3). Long-term memory agents add `preload_memory` / `load_memory`. |
|
|
97
|
+
| `generateContentConfig` | agent | Temperature, output caps, thinking budget/level. |
|
|
98
|
+
| `outputSchema` | agent | Structured-JSON contract. **Constraint:** an agent holding `outputSchema` cannot also hold transfer powers — the ADK deadlocks it. Keep schema-holders as leaf agents (see `critic.yaml`'s header comment for the war story). |
|
|
99
|
+
| `yaml_reference` | subagent | Mount another syndicate file as a nested subagent. |
|
|
100
|
+
| `mcp_server_url` | subagent | Discover this subagent's tools from a remote MCP server at load time (§3). SSRF-guarded; `ALLOW_PRIVATE_MCP=true` permits localhost for development. |
|
|
101
|
+
|
|
102
|
+
Validation happens at load: missing names, legacy option blocks, and
|
|
103
|
+
malformed agents fail with pointed errors before any model is called.
|
|
104
|
+
Tool names are deliberately *not* strictly validated — unknown names are
|
|
105
|
+
skipped with a warning at compile time.
|
|
106
|
+
|
|
107
|
+
## 3. Tools
|
|
108
|
+
|
|
109
|
+
Registered in `lib/toolRegistry.ts` — one map from YAML name to ADK tool
|
|
110
|
+
instance:
|
|
111
|
+
|
|
112
|
+
| Name | Kind | Does |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `web_search` | Provider-agnostic | Live web search via the agent model's NATIVE search: Gemini grounding, Anthropic `web_search` server tool, OpenAI Responses `web_search`, xAI Agent Tools `web_search`. On local `ollama/*` models the tool is omitted with a one-time warning (keyless stays keyless). On grok-* agents, optional server-side domain filters via `XAI_WEB_SEARCH_ALLOWED_DOMAINS` / `_EXCLUDED_DOMAINS` in `.env` (max 5, mutually exclusive; xAI accepts no date bounds — those are `x_search`-only). Prefer this in new YAMLs. |
|
|
115
|
+
| `web_extract` | FunctionTool | Deterministic page reading: fetches 1–5 agent-chosen URLs and returns clean page text (no LLM summarization). Keyless — works on every provider including local `ollama/*`. Per-page char budget (default 15k, `WEB_EXTRACT_CHAR_LIMIT`); long pages return a head+tail window with an `offset` continuation call served from a 15-minute cache. SSRF-guarded (http(s) only, private/link-local hosts refused, redirects re-checked). Block pages (bot checks, paywall stubs, JS shells) are detected code-side and returned as labeled `Error:` blocks, never as content. Pair with `web_search`: search to find, extract to read past the headline. |
|
|
116
|
+
| `x_search` | xAI-only | Live search over X (Twitter) posts via xAI Agent Tools. Self-gates to `grok-*` agents; a silent no-op on every other provider, so mixed-provider YAMLs stay safe. Optional server-side constraints in `.env`: `XAI_X_SEARCH_FROM_DATE`/`_TO_DATE` (inclusive `YYYY-MM-DD`) and `_ALLOWED_HANDLES`/`_EXCLUDED_HANDLES` (max 20, mutually exclusive — allowlist wins). |
|
|
117
|
+
| `collections_search` | xAI-only | Semantic search over xAI **Collections** — hosted document stores (PDFs/text/CSVs) uploaded at console.x.ai — server-side RAG with `collections://…` citations. Which collections: `XAI_COLLECTION_IDS` in `.env` (optional `XAI_COLLECTIONS_MAX_RESULTS`). Declared with no ids → omitted with a warning; non-xAI providers → silent no-op. |
|
|
118
|
+
| `google_search` | ADK built-in | Live web search — Gemini agents only (legacy alias; use `web_search`). |
|
|
119
|
+
| `preload_memory` | ADK built-in | Silently injects similarity-matched facts into every request (ambient recall). |
|
|
120
|
+
| `load_memory` | ADK built-in | Explicit tool call to search the fact store (deliberate recall). |
|
|
121
|
+
| `generate_image` | FunctionTool | Calls the Gemini image model directly, saves the result under `outputs/`, returns the path. A FunctionTool because binary `inlineData` cannot survive the AgentTool text boundary. |
|
|
122
|
+
| `inspect_image` | FunctionTool | **Blind visual inventory** of a file under `outputs/`: subjects with exact counts, composition, light, palette, medium cues, artifacts — zero quality judgments. Its signature accepts *only* a file path, so an orchestrator cannot leak expectations into the observation (see `image_production.yaml`). |
|
|
123
|
+
|
|
124
|
+
**MCP tools** are the exception to the registry: a subagent with
|
|
125
|
+
`mcp_server_url:` in its YAML gets its tools from a remote MCP server at
|
|
126
|
+
load time. `lib/tools/mcpToolFactory.ts` dials the server over SSE,
|
|
127
|
+
lists its tools, and wraps each one as a live `FunctionTool` — the
|
|
128
|
+
agent's reach is decided by the server, not compiled in.
|
|
129
|
+
`config/agents/examples/librarian.yaml` plus the demo catalog server
|
|
130
|
+
(`npm run mcp:demo`, `scripts/demo_mcp_server.ts`) are the worked
|
|
131
|
+
example: read tools *and* write tools, so the agent demonstrably
|
|
132
|
+
modifies data on the far side of the protocol. The factory refuses
|
|
133
|
+
loopback/private hosts unless `ALLOW_PRIVATE_MCP=true` (SSRF guard);
|
|
134
|
+
treat any remote MCP server as an untrusted tool vendor whose results
|
|
135
|
+
are data, never instructions.
|
|
136
|
+
|
|
137
|
+
> **Schema dialects, handled for you.** The factory emits Gemini-style
|
|
138
|
+
> UPPERCASE schema types (`'OBJECT'`, `'STRING'`, …) because the ADK is
|
|
139
|
+
> Gemini-native; every non-Gemini adapter normalizes them back to
|
|
140
|
+
> standard lowercase JSON-Schema at request-build time
|
|
141
|
+
> (`lib/models/schemaNormalize.ts`). MCP tools therefore work on any
|
|
142
|
+
> provider's agents — Gemini, Claude, GPT, or Grok.
|
|
143
|
+
|
|
144
|
+
## 4. Sessions & long-term memory
|
|
145
|
+
|
|
146
|
+
Two Supabase tables carry the two kinds of remembering:
|
|
147
|
+
|
|
148
|
+
- **`adk_sessions`** — the running transcript (events + state), so a
|
|
149
|
+
conversation survives process restarts.
|
|
150
|
+
- **`adk_memory_facts`** — distilled structured records: each carries
|
|
151
|
+
its 768-d embedding plus the date it is about, the source who asserted
|
|
152
|
+
it, active/superseded status, and entity index keys. Written at
|
|
153
|
+
session end (`exit`, SIGINT, or one-shot completion), keyed per
|
|
154
|
+
syndicate + user. Corrections supersede old rows (kept as linked
|
|
155
|
+
history); recall is cosine similarity re-ranked by keys and dates.
|
|
156
|
+
Full pipeline: [`lib/memory/README.md`](./lib/memory/README.md).
|
|
157
|
+
|
|
158
|
+
Provision both in the Supabase SQL Editor:
|
|
159
|
+
|
|
160
|
+
```sql
|
|
161
|
+
-- 1. Enable pgvector
|
|
162
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
163
|
+
|
|
164
|
+
-- 2. Sessions
|
|
165
|
+
CREATE TABLE adk_sessions (
|
|
166
|
+
id TEXT PRIMARY KEY,
|
|
167
|
+
app_name TEXT NOT NULL,
|
|
168
|
+
user_id TEXT NOT NULL,
|
|
169
|
+
state JSONB DEFAULT '{}'::jsonb,
|
|
170
|
+
events JSONB DEFAULT '[]'::jsonb,
|
|
171
|
+
last_update_time BIGINT,
|
|
172
|
+
expire_at TIMESTAMPTZ,
|
|
173
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
174
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
-- 3. Memory facts (structured records: date, source, status, index keys
|
|
178
|
+
-- live beside the embedding — see lib/memory/README.md)
|
|
179
|
+
CREATE TABLE adk_memory_facts (
|
|
180
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
181
|
+
user_key TEXT NOT NULL,
|
|
182
|
+
fact TEXT NOT NULL,
|
|
183
|
+
embedding vector(768),
|
|
184
|
+
tag TEXT,
|
|
185
|
+
fact_date DATE,
|
|
186
|
+
source TEXT,
|
|
187
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
188
|
+
keys TEXT[] NOT NULL DEFAULT '{}',
|
|
189
|
+
superseded_by UUID,
|
|
190
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
191
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
-- 4. Indexes: similarity, entity keys, dates
|
|
195
|
+
CREATE INDEX ON adk_memory_facts USING ivfflat (embedding vector_cosine_ops)
|
|
196
|
+
WITH (lists = 100);
|
|
197
|
+
CREATE INDEX adk_memory_facts_keys_idx ON adk_memory_facts USING gin (keys);
|
|
198
|
+
CREATE INDEX adk_memory_facts_date_idx ON adk_memory_facts (user_key, fact_date);
|
|
199
|
+
|
|
200
|
+
-- 5. Cosine-similarity RPC (returns the structured columns so the
|
|
201
|
+
-- service can re-rank by keys/dates and relabel superseded records).
|
|
202
|
+
-- filter_user_key is REQUIRED: a NULL key would return every user's facts.
|
|
203
|
+
CREATE OR REPLACE FUNCTION match_memory_facts (
|
|
204
|
+
query_embedding vector(768),
|
|
205
|
+
filter_user_key text,
|
|
206
|
+
match_count int DEFAULT 10
|
|
207
|
+
) RETURNS TABLE (
|
|
208
|
+
id UUID,
|
|
209
|
+
user_key TEXT,
|
|
210
|
+
fact TEXT,
|
|
211
|
+
tag TEXT,
|
|
212
|
+
fact_date DATE,
|
|
213
|
+
source TEXT,
|
|
214
|
+
status TEXT,
|
|
215
|
+
keys TEXT[],
|
|
216
|
+
created_at TIMESTAMPTZ,
|
|
217
|
+
similarity float
|
|
218
|
+
)
|
|
219
|
+
LANGUAGE plpgsql
|
|
220
|
+
AS $$
|
|
221
|
+
BEGIN
|
|
222
|
+
IF filter_user_key IS NULL THEN
|
|
223
|
+
RAISE EXCEPTION 'filter_user_key is required';
|
|
224
|
+
END IF;
|
|
225
|
+
RETURN QUERY
|
|
226
|
+
SELECT
|
|
227
|
+
adk_memory_facts.id,
|
|
228
|
+
adk_memory_facts.user_key,
|
|
229
|
+
adk_memory_facts.fact,
|
|
230
|
+
adk_memory_facts.tag,
|
|
231
|
+
adk_memory_facts.fact_date,
|
|
232
|
+
adk_memory_facts.source,
|
|
233
|
+
adk_memory_facts.status,
|
|
234
|
+
adk_memory_facts.keys,
|
|
235
|
+
adk_memory_facts.created_at,
|
|
236
|
+
1 - (adk_memory_facts.embedding <=> query_embedding) AS similarity
|
|
237
|
+
FROM adk_memory_facts
|
|
238
|
+
WHERE adk_memory_facts.user_key = filter_user_key
|
|
239
|
+
ORDER BY adk_memory_facts.embedding <=> query_embedding
|
|
240
|
+
LIMIT match_count;
|
|
241
|
+
END;
|
|
242
|
+
$$;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Then run [`db/hardening.sql`](./db/hardening.sql) (RLS deny-by-default;
|
|
246
|
+
see §8). Upgrading an existing project to the structured columns:
|
|
247
|
+
[`db/memory_v2.sql`](./db/memory_v2.sql). `npm run db:purge` clears both
|
|
248
|
+
tables; for per-record inspection and hand-clearing, `npm run memory`
|
|
249
|
+
(`scripts/memory_admin.ts`) lists silos, groups likely restatements
|
|
250
|
+
(`--dupes`), and deletes chosen records — a dry run unless `--yes`, with
|
|
251
|
+
every delete scoped to its `--silo`. Cleanup means DELETE, not
|
|
252
|
+
`status='superseded'`: `match_memory_facts` filters on `user_key` alone
|
|
253
|
+
and applies status afterwards in the re-rank, so a retired row still
|
|
254
|
+
consumes a candidate slot (see `lib/memory/README.md`).
|
|
255
|
+
|
|
256
|
+
**Ingestion is incremental, and dedup is semantic.** A stateless service
|
|
257
|
+
(the A2A server, §6) ingests after *every* completed task with the whole
|
|
258
|
+
session — so an N-turn conversation would be distilled N times over a
|
|
259
|
+
growing transcript, and byte-equality dedup never catches it because the
|
|
260
|
+
extraction model rephrases on each pass. Two mechanisms in
|
|
261
|
+
`lib/memory/supabaseMemoryService.ts` prevent the store from filling
|
|
262
|
+
with restatements:
|
|
263
|
+
|
|
264
|
+
- A per-session **high-water mark** (`eventsToIngest`) distils only the
|
|
265
|
+
turns added since the last ingestion. It is in-process on purpose — a
|
|
266
|
+
restart re-reads one session once, absorbed by the check below, which
|
|
267
|
+
is why it needs no table of its own.
|
|
268
|
+
- A **similarity probe** before each insert (`isSemanticDuplicate`,
|
|
269
|
+
`MEMORY_DEDUP_SIMILARITY = 0.93`) drops restatements, reusing the same
|
|
270
|
+
`match_memory_facts` RPC the supersession path calls. It requires the
|
|
271
|
+
same tag — an `[EPISODE]` about a topic must never suppress the
|
|
272
|
+
`[FACT]` it mentions — and retired rows never suppress a new one. A
|
|
273
|
+
probe that errors **fails open and stores**: a duplicate costs a
|
|
274
|
+
shortlist slot, a lost fact costs the user something they said.
|
|
275
|
+
|
|
276
|
+
**Per-consumer extraction rules.** The fact-extraction prompt is shared
|
|
277
|
+
by every long-term consumer, so domain rules arrive through the
|
|
278
|
+
`memory_extraction_rules` field on the syndicate (§2) rather than being
|
|
279
|
+
edited into the prompt. It fills a `{domain_rules}` slot that renders
|
|
280
|
+
empty when unset — a syndicate declaring none gets byte-identical
|
|
281
|
+
behaviour to before the slot existed. Use it to say what is worth
|
|
282
|
+
remembering in your domain: what the user asserted, decided, or
|
|
283
|
+
committed to is usually worth storing; a value that goes stale on its
|
|
284
|
+
own usually is not, because a stored copy can never be used, only crowd
|
|
285
|
+
out records that can. `config/agents/syndicateSchema.yaml` shows the
|
|
286
|
+
shape.
|
|
287
|
+
|
|
288
|
+
A memory store is a PII store: key facts to users, honor deletion, set
|
|
289
|
+
retention deliberately. Vectorization is not anonymization — the
|
|
290
|
+
plain-text fact sits beside its embedding.
|
|
291
|
+
|
|
292
|
+
## 5. Multi-model support
|
|
293
|
+
|
|
294
|
+
The agent's `model:` id names its provider, and the framework routes
|
|
295
|
+
accordingly — model optionality is a single YAML line per agent:
|
|
296
|
+
|
|
297
|
+
| Model id | Provider | Adapter | Key | Native `web_search` |
|
|
298
|
+
|---|---|---|---|---|
|
|
299
|
+
| `gemini-*` | Google Gemini | ADK-native (`TracedGemini`) | `GOOGLE_GENAI_API_KEY` | ✅ grounding |
|
|
300
|
+
| `claude-*` | Anthropic | `lib/models/claudeLlm.ts` | `ANTHROPIC_API_KEY` | ✅ server tool |
|
|
301
|
+
| `gpt-*`, o-series | OpenAI | `lib/models/gptLlm.ts` (Responses API) | `OPENAI_API_KEY` | ✅ web_search tool |
|
|
302
|
+
| `grok-*` | xAI | `lib/models/grokLlm.ts` (Responses API) | `XAI_API_KEY` | ✅ Agent Tools search |
|
|
303
|
+
|
|
304
|
+
The xAI adapter carries the deepest capability surface: `grok-4.5`
|
|
305
|
+
requests pin `reasoning.effort: "medium"` (`lib/config.ts`), SSE
|
|
306
|
+
streaming works end-to-end (`runConfig: { streamingMode: 'sse' }` —
|
|
307
|
+
partial delta events stream, one aggregated event persists with usage),
|
|
308
|
+
structured outputs ride `outputSchema` → `text.format`, and two
|
|
309
|
+
xAI-only tools — `x_search` and `collections_search` (§3) — turn on
|
|
310
|
+
live X search and hosted-document RAG. All verified live on grok-4.5.
|
|
311
|
+
| `ollama/*` | Local Ollama | `lib/models/ollamaLlm.ts` | none | ⚠ omitted + warning |
|
|
312
|
+
|
|
313
|
+
`lib/models/registry.ts` is the single routing seam:
|
|
314
|
+
`registerAvailableProviders()` registers every adapter whose key is
|
|
315
|
+
present (Ollama needs none) into the ADK's LLM registry, so the YAML
|
|
316
|
+
string finds its provider; missing keys produce clear skip messages,
|
|
317
|
+
and only the providers a syndicate actually declares are required.
|
|
318
|
+
Mixed graphs are supported — each agent picks its own provider, one
|
|
319
|
+
line each. `config/agents/examples/claude.yaml` is the minimal Claude example;
|
|
320
|
+
`config/agents/examples/model_zoo.yaml` declares one lightweight agent per
|
|
321
|
+
provider, and `npm run demo:models` proves the whole surface: one
|
|
322
|
+
prompt to every available provider, printing input, thinking (qwen3
|
|
323
|
+
`<think>` blocks, Claude extended thinking, GPT reasoning summaries,
|
|
324
|
+
Grok reasoning), output, and a per-request token/latency trace; add
|
|
325
|
+
`-- --search` to watch four native web searches plus the local
|
|
326
|
+
omission. Providers without keys are skipped, never fatal.
|
|
327
|
+
|
|
328
|
+
Reasoning/thinking: scratchpads from every provider are surfaced as
|
|
329
|
+
dimmed THINKING output and kept out of session history. On Claude,
|
|
330
|
+
`generateContentConfig.thinkingConfig.thinkingBudget` enables
|
|
331
|
+
Anthropic extended thinking (thinking + tool use on the same Claude
|
|
332
|
+
agent is not supported yet).
|
|
333
|
+
|
|
334
|
+
Every model request also emits an `llm.request` OpenTelemetry span
|
|
335
|
+
(provider, model, input/output/thinking tokens, latency) printed as an
|
|
336
|
+
`[OTEL_SPAN_JSON]` line; set `TELEMETRY_SUPABASE=true` to persist
|
|
337
|
+
spans to the `adk_telemetry` table (run `db/telemetry.sql`, then
|
|
338
|
+
re-run `db/hardening.sql` — schema below):
|
|
339
|
+
|
|
340
|
+
```sql
|
|
341
|
+
CREATE TABLE adk_telemetry (
|
|
342
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
343
|
+
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
344
|
+
trace_id TEXT NOT NULL,
|
|
345
|
+
span_id TEXT NOT NULL,
|
|
346
|
+
span_name TEXT NOT NULL, -- 'llm.request' | 'Syndicate Execution: <name>'
|
|
347
|
+
syndicate TEXT,
|
|
348
|
+
agent TEXT,
|
|
349
|
+
provider TEXT, -- 'gemini' | 'anthropic' | 'openai' | 'xai' | 'ollama'
|
|
350
|
+
model TEXT,
|
|
351
|
+
input_tokens INTEGER,
|
|
352
|
+
output_tokens INTEGER,
|
|
353
|
+
thinking_tokens INTEGER,
|
|
354
|
+
latency_ms DOUBLE PRECISION,
|
|
355
|
+
span JSONB NOT NULL
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
CREATE INDEX idx_adk_telemetry_ts ON adk_telemetry (ts DESC);
|
|
359
|
+
CREATE INDEX idx_adk_telemetry_trace ON adk_telemetry (trace_id);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Open-weight local models**: `ollama/*` ids (e.g. `ollama/qwen3:8b`)
|
|
363
|
+
route through `lib/models/ollamaLlm.ts` to a local Ollama daemon over
|
|
364
|
+
its OpenAI-compatible API (`OLLAMA_BASE_URL`, default
|
|
365
|
+
`http://localhost:11434/v1`). No key is required, and a syndicate whose
|
|
366
|
+
*every* agent is `ollama/*` runs with no `.env` at all —
|
|
367
|
+
`config/agents/examples/tutor.yaml` (single agent) and `council.yaml` (council)
|
|
368
|
+
are the worked examples. The adapter translates ADK content to
|
|
369
|
+
OpenAI-style messages, including tool calls (so delegation works),
|
|
370
|
+
image parts as data URIs (so `ollama/qwen3-vl:8b` can see), and JSON
|
|
371
|
+
response mode; reasoning models' `<think>…</think>` scratchpads are
|
|
372
|
+
stripped from replies. Choose models by capability: qwen3:8b is the
|
|
373
|
+
smallest pulled model with reliable tool calling; qwen3-vl:8b adds
|
|
374
|
+
vision.
|
|
375
|
+
|
|
376
|
+
Model floor: agent transfer (subagent delegation) requires
|
|
377
|
+
`gemini-3.7-flash` or newer — older flash models reject it with
|
|
378
|
+
`[400] Tool call context circulation is not enabled`. For `ollama/*`
|
|
379
|
+
agents the equivalent floor is tool-calling support in the model
|
|
380
|
+
itself; delegation is exercised through AgentTool function calls.
|
|
381
|
+
|
|
382
|
+
## 6. A2A service mode
|
|
383
|
+
|
|
384
|
+
`npm run start:a2a` serves a syndicate as a stateless JSON-RPC
|
|
385
|
+
agent-to-agent endpoint (Express): an agent card describing the service,
|
|
386
|
+
`message/send` for turns, bearer-token auth via `A2A_SERVER_SECRET`, and
|
|
387
|
+
rate limiting. With `PUBLIC_URL` set (a real deployment), the server
|
|
388
|
+
refuses to start without the secret and refuses to run against an
|
|
389
|
+
unhardened database unless explicitly overridden. `demo/a2a_demo.mjs` is
|
|
390
|
+
a complete client.
|
|
391
|
+
|
|
392
|
+
### Plan-dispatch routing (`dispatch:`) — the second orchestration method
|
|
393
|
+
|
|
394
|
+
A syndicate that declares a `dispatch:` block stops delegating and
|
|
395
|
+
starts **dispatching**. The two methods differ in who speaks to the
|
|
396
|
+
user:
|
|
397
|
+
|
|
398
|
+
| | DELEGATE (default) | PLAN-DISPATCH (`dispatch:` present) |
|
|
399
|
+
|---|---|---|
|
|
400
|
+
| Subagents are | `AgentTool`s on the orchestrator | plain configs the server selects from |
|
|
401
|
+
| Orchestrator holds | subagent tools, no `outputSchema` | an `outputSchema`, no subagent tools |
|
|
402
|
+
| Routing decision is | implicit in which tool it calls | an explicit value in code |
|
|
403
|
+
| The final answer comes from | the orchestrator re-emitting the answer | **the specialist itself** |
|
|
404
|
+
| LLM calls per request | classify + specialist + relay | classify + specialist |
|
|
405
|
+
|
|
406
|
+
```yaml
|
|
407
|
+
dispatch:
|
|
408
|
+
default_route: "Generalist" # fail-static target; must be a declared subagent
|
|
409
|
+
|
|
410
|
+
orchestrator:
|
|
411
|
+
name: "Triage"
|
|
412
|
+
model: "gemini-3.5-flash-lite"
|
|
413
|
+
instruction: |
|
|
414
|
+
Name exactly ONE specialist for this message. ...
|
|
415
|
+
outputSchema: # a leaf holding a schema — no subagent tools
|
|
416
|
+
type: "OBJECT"
|
|
417
|
+
properties:
|
|
418
|
+
route: { type: "STRING", description: "Exact specialist name" }
|
|
419
|
+
reason: { type: "STRING", description: "≤8 plain words for the waiting user" }
|
|
420
|
+
required: ["route"]
|
|
421
|
+
generateContentConfig:
|
|
422
|
+
responseMimeType: "application/json"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Why it exists.** In DELEGATE mode the orchestrator receives the
|
|
426
|
+
specialist's answer as a tool response and must re-emit it to close the
|
|
427
|
+
turn. That relay is a full LLM call whose only job is copying text it is
|
|
428
|
+
forbidden to edit, and it is the least reliable step in the chain — in
|
|
429
|
+
production it has finished with zero output tokens (a blank reply) and
|
|
430
|
+
emitted the bare tool name in place of a 2,599-character answer.
|
|
431
|
+
Plan-dispatch has no relay turn to fail, and the classifier's output
|
|
432
|
+
shrinks from a whole relayed answer to ~15 tokens of JSON.
|
|
433
|
+
|
|
434
|
+
**Why the classifier is tool-less.** ADK refuses to combine
|
|
435
|
+
`outputSchema` with AgentTool delegation on one agent (see
|
|
436
|
+
`config/agents/examples/critic.yaml` — an orchestrator holding both deadlocks).
|
|
437
|
+
That constraint shapes the method: the classifier is a leaf, and the
|
|
438
|
+
hand-off happens in code, where it can be logged, traced, and streamed
|
|
439
|
+
to the user as progress.
|
|
440
|
+
|
|
441
|
+
**Sessions.** Every *route* runs in the shared `<contextId>` session, so
|
|
442
|
+
one transcript accumulates across routes and long-term memory ingests
|
|
443
|
+
real answers instead of a relay copy of them. Sharing the session is
|
|
444
|
+
necessary but not sufficient: ADK renders an event by comparing
|
|
445
|
+
`event.author` against the agent now running, and under plan-dispatch
|
|
446
|
+
every route is its own root agent, so the whole history fails that
|
|
447
|
+
comparison and `convertForeignEvent` rewrites it to `role: "user"`
|
|
448
|
+
prefixed "For context:". A route reading the raw shared session
|
|
449
|
+
therefore receives the thread as one undifferentiated user monologue —
|
|
450
|
+
with the previous route's private `thought:` reasoning cloned in as user
|
|
451
|
+
speech and raw tool payloads inlined beside it. Routes read the session
|
|
452
|
+
through `ProjectedSessionService` (`lib/session/transcript.ts`) instead:
|
|
453
|
+
past agent turns are re-authored to the running agent so they survive as
|
|
454
|
+
real `role: "model"` turns, labelled `[XScout]` when another desk spoke,
|
|
455
|
+
with thoughts and tool traffic dropped. Writes still land on the real
|
|
456
|
+
session, which keeps everything.
|
|
457
|
+
|
|
458
|
+
Both ceilings on that projection are bounds the prompt cannot escape:
|
|
459
|
+
40,000 characters of history AND 16 turns, whichever binds first. The turn
|
|
460
|
+
cap is not redundant — a thread of short exchanges fits 43 turns inside the
|
|
461
|
+
character budget, and 43 turns of history to answer one question is
|
|
462
|
+
attention cost no byte budget describes.
|
|
463
|
+
|
|
464
|
+
The stored row is trimmed on the way out, by `trimEventForStorage`. It has
|
|
465
|
+
exactly two readers and neither touches Gemini's opaque `thoughtSignature`
|
|
466
|
+
blobs or tool-result payloads: the projection drops thought parts and tool
|
|
467
|
+
traffic before any prompt, and the memory service's `serializeEvents` walks
|
|
468
|
+
`part.text` alone. Measured across 128 live sessions those two fields were
|
|
469
|
+
~90% of every byte stored (`thoughtSignature` alone 73.3%), so they are
|
|
470
|
+
stripped from the SERIALIZED COPY — the live in-memory session keeps them,
|
|
471
|
+
or the agent's own tool loop breaks mid-turn. An elided tool result keeps
|
|
472
|
+
its `id` and `name` and gains a size marker, because ADK pairs calls to
|
|
473
|
+
responses by id and throws on a widowed half. 21.72 MB → 4.88 MB; existing
|
|
474
|
+
rows shrink retroactively on their next write. Note the constraint this
|
|
475
|
+
rests on: dropping the signature is safe only because stored events are
|
|
476
|
+
never replayed to a model. The quadratic upload is untouched — `appendEvent`
|
|
477
|
+
still rewrites the whole array per event — and is the next lever if row size
|
|
478
|
+
returns.
|
|
479
|
+
|
|
480
|
+
The classifier does not write to that session — its JSON verdicts would
|
|
481
|
+
be read as conversation by the next specialist — but it must still SEE
|
|
482
|
+
it, or it cannot tell a follow-up from a standalone remark. It runs in a
|
|
483
|
+
per-request in-memory session and receives `renderTranscriptDigest`, a
|
|
484
|
+
compact both-sides summary of the exchange, above a
|
|
485
|
+
`--- MESSAGE TO CLASSIFY ---` marker. It used to get a durable
|
|
486
|
+
`<contextId>::route` lane of its own instead, holding only the user's
|
|
487
|
+
messages and its own verdicts; that made every rule about follow-ups and
|
|
488
|
+
redos unusable, and left holes wherever a `route_overrides` hit skipped
|
|
489
|
+
the classifier entirely.
|
|
490
|
+
|
|
491
|
+
**Fail-static.** Malformed JSON, an unknown route name, an empty
|
|
492
|
+
payload, or a classifier that errors outright all resolve to
|
|
493
|
+
`default_route` and still answer the user — routing is an optimisation,
|
|
494
|
+
answering is the contract. `default_route` must therefore name a
|
|
495
|
+
specialist that can handle anything. The whole resolver is pure; the
|
|
496
|
+
contract lives in `lib/dispatch.ts`.
|
|
497
|
+
|
|
498
|
+
**Telemetry.** The chosen route is published as a
|
|
499
|
+
`[STATUS] Routed to <Name> — <reason>` progress event before the
|
|
500
|
+
specialist runs, so an A2A client can show a waiting user what is
|
|
501
|
+
happening. The `reason` field is written for that reader, not for logs.
|
|
502
|
+
|
|
503
|
+
Implemented in `scripts/a2a_server.ts`; like `yaml_reference`, it is
|
|
504
|
+
A2A-only — the CLI runner (`scripts/syndicate_chat.ts`) still compiles
|
|
505
|
+
every syndicate in DELEGATE mode.
|
|
506
|
+
|
|
507
|
+
## 7. Extending the framework
|
|
508
|
+
|
|
509
|
+
**Call ADK directly (no syndicate)**: the YAML layer is a convenience,
|
|
510
|
+
never a requirement. `scripts/direct_call.ts` (`npm run demo:direct`) is
|
|
511
|
+
the canonical minimal block — `LlmAgent` + `Runner` +
|
|
512
|
+
`InMemorySessionService` straight from `@google/adk`, ~30 lines you can
|
|
513
|
+
copy into any repo that has `@google/adk` installed. Add
|
|
514
|
+
`registerAvailableProviders()` from `lib/models/registry.ts` and the
|
|
515
|
+
same block runs `claude-*` / `gpt-*` / `grok-*` / `ollama/*` ids too.
|
|
516
|
+
|
|
517
|
+
**Add a syndicate**: create `config/agents/<name>.yaml` — copy the
|
|
518
|
+
closest starter-pack file from `config/agents/examples/` or start from
|
|
519
|
+
`syndicateSchema.yaml` — then `npm run chat:syndicate -- --syndicate
|
|
520
|
+
<name>`. No code changes. The loader checks the root first, then
|
|
521
|
+
`examples/`, so your syndicate and the starter pack never collide.
|
|
522
|
+
|
|
523
|
+
**Add a tool**: implement a `FunctionTool` in `lib/tools/`, register the
|
|
524
|
+
name in `lib/toolRegistry.ts`, reference it from YAML. The two image
|
|
525
|
+
tools are the worked examples — including why binary data forces
|
|
526
|
+
FunctionTools over subagents, and how a tool signature can enforce an
|
|
527
|
+
epistemic rule (the blind inventory).
|
|
528
|
+
|
|
529
|
+
**Add a provider**: follow `claudeLlm.ts` (SDK-based, key-gated) or
|
|
530
|
+
`ollamaLlm.ts` (fetch-based, keyless) — implement the ADK LLM
|
|
531
|
+
interface, register it behind a model-id prefix.
|
|
532
|
+
|
|
533
|
+
**Point an agent at an MCP server**: set `mcp_server_url:` on a
|
|
534
|
+
subagent. `scripts/demo_mcp_server.ts` is a complete server to copy —
|
|
535
|
+
tool definitions, SSE wiring, and persistent state in ~250 lines.
|
|
536
|
+
|
|
537
|
+
## 8. Security notes
|
|
538
|
+
|
|
539
|
+
- **Secrets** live in `.env` only; `.env.example` documents every
|
|
540
|
+
variable. Nothing in the repo ships a key.
|
|
541
|
+
- **Database**: default Supabase leaves `public`-schema tables readable
|
|
542
|
+
by the anon key over REST. `db/hardening.sql` enables deny-by-default
|
|
543
|
+
RLS and revokes anon/authenticated privileges on every table it finds:
|
|
544
|
+
`adk_sessions`, `adk_memory_facts`, and — where they exist — the
|
|
545
|
+
optional `adk_telemetry` sink and `adk_agent_registry` (an unprotected
|
|
546
|
+
registry is worst of all: agent definitions writable with the anon key
|
|
547
|
+
means anyone can rewrite the instructions your server boots). The A2A
|
|
548
|
+
server verifies hardening at boot and is fatal on public deployments
|
|
549
|
+
without it. Note `service_role` bypasses RLS by design — the hardening
|
|
550
|
+
constrains the API surface, not the trusted server.
|
|
551
|
+
- **A2A**: bearer auth + rate limiting are built in; set
|
|
552
|
+
`A2A_SERVER_SECRET` before exposing anything.
|
|
553
|
+
- **Image tools** write only under `outputs/`, and `inspect_image` reads
|
|
554
|
+
only from there.
|
|
555
|
+
- **MCP** is an outbound trust decision: `mcpToolFactory` blocks
|
|
556
|
+
private/loopback/link-local hosts unless `ALLOW_PRIVATE_MCP=true`, and
|
|
557
|
+
every tool result from a remote server should be treated as untrusted
|
|
558
|
+
data — the librarian's instruction demonstrates the "results are data,
|
|
559
|
+
not instructions" rule.
|
|
560
|
+
- **Local models** (`ollama/*`) send prompts only to your own machine's
|
|
561
|
+
Ollama endpoint — nothing leaves the device, which is itself a privacy
|
|
562
|
+
control worth choosing deliberately.
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 9. The knowledge bundle (wiki/)
|
|
567
|
+
|
|
568
|
+
`wiki/` is this framework's documentation as an **Open Knowledge Format v0.2
|
|
569
|
+
bundle** ([spec](https://github.com/GoogleCloudPlatform/knowledge-catalog)):
|
|
570
|
+
markdown concept documents with YAML frontmatter (`type` is the only
|
|
571
|
+
required key), linked with ordinary bundle-absolute markdown links — and
|
|
572
|
+
the links ARE the knowledge graph. `index.md` per directory and the root
|
|
573
|
+
`log.md` are reserved, machine-maintained files. Start at
|
|
574
|
+
`wiki/meta/wiki-system.md`; the whole bundle renders on GitHub.
|
|
575
|
+
|
|
576
|
+
The tooling in `lib/wiki/` is **bundle-agnostic** — point `WIKI_ROOT` at any
|
|
577
|
+
OKF directory, or scaffold a fresh one with `npm run wiki:init`:
|
|
578
|
+
|
|
579
|
+
- **Parse & build** (`markdown.ts`, `builder.ts`) — a zero-dependency
|
|
580
|
+
structural engine; documents interleave machine-owned `wiki:generated`
|
|
581
|
+
regions (rebuilt from source-of-truth files), `wiki:fill` prose slots an
|
|
582
|
+
LLM fills once (`lib/wiki/fill.ts`, any provider via the model registry),
|
|
583
|
+
and ordinary prose that rebuilds never touch.
|
|
584
|
+
- **Graph** (`graph.ts`) — nodes are documents, edges are resolved links;
|
|
585
|
+
orphans and broken links fall out as queries.
|
|
586
|
+
- **Entity graph** (`entities.ts`, `extract.ts`) — a second layer over the
|
|
587
|
+
same files: agents, tools, models, providers, modules, tables and
|
|
588
|
+
environment variables as typed nodes. Structural relations are DERIVED
|
|
589
|
+
from repo truth on every build (zero-dependency scanners for imports,
|
|
590
|
+
`process.env` reads, DDL and npm scripts) into `.graph/graph.json`;
|
|
591
|
+
judgments that only prose carries are ASSERTED with evidence and an actor
|
|
592
|
+
into `.graph/relations.json`, which the build never rewrites.
|
|
593
|
+
- **Lint** (`lint.ts`, `npm run wiki:check`) — OKF conformance, link
|
|
594
|
+
integrity, index coverage, staleness, and the private-subtree closure
|
|
595
|
+
rule; errors gate every write.
|
|
596
|
+
- **Navigate & garden** (`lib/tools/wikiTools.ts`) — tool contracts (§3
|
|
597
|
+
pattern) in three tiers: navigation (`wiki_map`, `wiki_search`,
|
|
598
|
+
`wiki_read`, `wiki_links`, `wiki_dive` — a "repo dive" returns an ordered
|
|
599
|
+
reading plan for a task — and `wiki_graph`, which answers the relational
|
|
600
|
+
questions documents cannot: who calls this tool, what needs this key, how
|
|
601
|
+
do these two connect), the gated writes (`wiki_save` — lint-validated,
|
|
602
|
+
path-jailed, auto-updates the directory index and `log.md`; `wiki_relate`
|
|
603
|
+
— one evidenced relation, refusing anything the build derives), and
|
|
604
|
+
agentic composites (`wiki_query`, `wiki_garden` — one-shot agents with
|
|
605
|
+
citations and honest actor attribution in frontmatter provenance).
|
|
606
|
+
|
|
607
|
+
Serving: syndicate agents declare the navigation/write tools by name
|
|
608
|
+
(`config/agents/examples/scriptorium.yaml` works the prose,
|
|
609
|
+
`config/agents/examples/cartographers.yaml` the graph — `npm run syndicate:scriptorium`,
|
|
610
|
+
`npm run syndicate:cartographers`); outside MCP clients get all ten from
|
|
611
|
+
`npm run mcp:wiki` (loopback SSE on `:8933`). Trust is explicit in
|
|
612
|
+
frontmatter: `generated.by` records who wrote a document (`human:<id>`,
|
|
613
|
+
`process:<id>`, or `<producer>/<model>`), and only a `human:` entry in
|
|
614
|
+
`verified` makes it human-reviewed.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James Wadman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|