melchizedek-agents 0.9.6 → 0.16.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/.env.example +272 -0
- package/AGENT_SETUP.md +39 -23
- package/CHANGELOG.md +623 -0
- package/DOCUMENTATION.md +429 -109
- package/QUICKSTART.md +92 -18
- package/README.md +45 -5
- package/config/agents/README.md +46 -1
- package/config/agents/examples/ares.yaml +117 -40
- package/config/agents/examples/assistant.yaml +159 -0
- package/config/agents/examples/augustin.yaml +154 -51
- package/config/agents/examples/cartographers.yaml +91 -23
- package/config/agents/examples/claude.yaml +45 -6
- package/config/agents/examples/council.yaml +23 -8
- package/config/agents/examples/critic.yaml +57 -21
- package/config/agents/examples/delegation.yaml +76 -9
- package/config/agents/examples/hierarchical.yaml +55 -8
- package/config/agents/examples/image_production.yaml +54 -16
- package/config/agents/examples/librarian.yaml +70 -14
- package/config/agents/examples/model_zoo.yaml +105 -29
- package/config/agents/examples/patient_advocate.yaml +102 -34
- package/config/agents/examples/research.yaml +250 -0
- package/config/agents/examples/scribe.yaml +242 -0
- package/config/agents/examples/scriptorium.yaml +87 -24
- package/config/agents/examples/style_council.yaml +31 -9
- package/config/agents/examples/syndicate.yaml +29 -10
- package/config/agents/examples/tutor.yaml +10 -3
- package/config/agents/syndicateSchema.yaml +44 -9
- package/config/agents/templates/README.md +58 -0
- package/config/agents/templates/account_memory.yaml +153 -0
- package/config/agents/templates/case_desk.yaml +188 -0
- package/config/agents/templates/conversational.yaml +110 -0
- package/config/agents/templates/draft_review.yaml +130 -0
- package/config/agents/templates/intake_extractor.yaml +152 -0
- package/config/agents/templates/research_brief.yaml +169 -0
- package/config/agents/templates/research_desk.yaml +152 -0
- package/config/agents/templates/review_panel.yaml +188 -0
- package/config/agents/templates/support_triage.yaml +273 -0
- package/config/agents/templates/systems_operator.yaml +157 -0
- package/db/hardening.sql +202 -0
- package/db/memory_v2.sql +77 -0
- package/db/migrations/0001_base.sql +167 -0
- package/db/migrations/0002_erase_scope.sql +172 -0
- package/db/migrations/0003_postgres_storage.sql +58 -0
- package/db/migrations/0004_usage.sql +94 -0
- package/db/migrations/0005_agent_registry.sql +200 -0
- package/db/migrations/0006_task_leases.sql +25 -0
- package/db/migrations/0007_memory_commit.sql +134 -0
- package/db/migrations/0008_memory_retention.sql +37 -0
- package/db/migrations/0009_task_queue.sql +51 -0
- package/db/migrations/0010_erase_complete.sql +180 -0
- package/db/telemetry.sql +401 -0
- package/demo/README.md +77 -0
- package/demo/a2a_demo.mjs +95 -0
- package/dist/lib/a2a/app.d.ts +253 -0
- package/dist/lib/a2a/app.js +751 -0
- package/dist/lib/a2a/executor.d.ts +167 -0
- package/dist/lib/a2a/executor.js +548 -0
- package/dist/lib/a2a/identity.d.ts +122 -0
- package/dist/lib/a2a/identity.js +254 -0
- package/dist/lib/a2a/policy.d.ts +110 -0
- package/dist/lib/a2a/policy.js +178 -0
- package/dist/lib/a2a/redisLimits.d.ts +26 -0
- package/dist/lib/a2a/redisLimits.js +31 -0
- package/dist/lib/a2a/remoteAgent.d.ts +91 -0
- package/dist/lib/a2a/remoteAgent.js +268 -0
- package/dist/lib/a2a/turnLock.d.ts +28 -0
- package/dist/lib/a2a/turnLock.js +76 -0
- package/dist/lib/compile.d.ts +11 -1
- package/dist/lib/compile.js +109 -8
- package/dist/lib/config.d.ts +7 -7
- package/dist/lib/config.js +7 -7
- package/dist/lib/doctor.d.ts +145 -0
- package/dist/lib/doctor.js +517 -0
- package/dist/lib/grounding.d.ts +2 -2
- package/dist/lib/grounding.js +2 -2
- package/dist/lib/guards/index.d.ts +37 -0
- package/dist/lib/guards/index.js +66 -0
- package/dist/lib/guards/science.d.ts +117 -0
- package/dist/lib/guards/science.js +444 -0
- package/dist/lib/index.d.ts +37 -2
- package/dist/lib/index.js +35 -4
- package/dist/lib/loadEnv.d.ts +23 -11
- package/dist/lib/loadEnv.js +77 -29
- package/dist/lib/loadSyndicate.d.ts +97 -5
- package/dist/lib/loadSyndicate.js +176 -76
- package/dist/lib/memory/erase.d.ts +24 -0
- package/dist/lib/memory/erase.js +47 -0
- package/dist/lib/memory/namespace.d.ts +37 -0
- package/dist/lib/memory/namespace.js +74 -0
- package/dist/lib/memory/providers.d.ts +99 -0
- package/dist/lib/memory/providers.js +240 -0
- package/dist/lib/memory/store.d.ts +74 -0
- package/dist/lib/memory/store.js +108 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
- package/dist/lib/memory/supabaseMemoryService.js +239 -125
- package/dist/lib/models/capabilities.d.ts +103 -0
- package/dist/lib/models/capabilities.js +291 -0
- package/dist/lib/models/claudeLlm.d.ts +10 -1
- package/dist/lib/models/claudeLlm.js +57 -27
- package/dist/lib/models/endpoints.d.ts +100 -0
- package/dist/lib/models/endpoints.js +282 -0
- package/dist/lib/models/gateway.d.ts +107 -0
- package/dist/lib/models/gateway.js +157 -0
- package/dist/lib/models/gatewayLlm.d.ts +57 -0
- package/dist/lib/models/gatewayLlm.js +105 -0
- package/dist/lib/models/gptLlm.d.ts +51 -2
- package/dist/lib/models/gptLlm.js +169 -26
- package/dist/lib/models/grokLlm.d.ts +19 -10
- package/dist/lib/models/grokLlm.js +29 -14
- package/dist/lib/models/ollamaLlm.d.ts +26 -0
- package/dist/lib/models/ollamaLlm.js +43 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
- package/dist/lib/models/openAiCompatibleLlm.js +200 -37
- package/dist/lib/models/providerMap.js +9 -2
- package/dist/lib/models/registry.d.ts +42 -5
- package/dist/lib/models/registry.js +140 -29
- package/dist/lib/models/retry.d.ts +146 -0
- package/dist/lib/models/retry.js +305 -0
- package/dist/lib/models/schemaNormalize.d.ts +20 -0
- package/dist/lib/models/schemaNormalize.js +43 -0
- package/dist/lib/net/addressGuard.d.ts +60 -0
- package/dist/lib/net/addressGuard.js +216 -0
- package/dist/lib/observability/embeddings.d.ts +11 -2
- package/dist/lib/observability/embeddings.js +18 -14
- package/dist/lib/observability/metrics.d.ts +36 -0
- package/dist/lib/observability/metrics.js +86 -0
- package/dist/lib/observability/redact.d.ts +29 -0
- package/dist/lib/observability/redact.js +102 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
- package/dist/lib/observability/supabaseSpanExporter.js +22 -6
- package/dist/lib/observability/tracer.d.ts +10 -0
- package/dist/lib/observability/tracer.js +70 -3
- package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
- package/dist/lib/persistence/supabaseProvider.js +32 -6
- package/dist/lib/registry.d.ts +87 -0
- package/dist/lib/registry.js +146 -0
- package/dist/lib/runtime/approvals.d.ts +44 -0
- package/dist/lib/runtime/approvals.js +67 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
- package/dist/lib/runtime/syndicateTurn.js +597 -0
- package/dist/lib/runtime/turnControl.d.ts +90 -0
- package/dist/lib/runtime/turnControl.js +131 -0
- package/dist/lib/session/supabaseSessionService.js +1 -1
- package/dist/lib/session/transcript.d.ts +9 -1
- package/dist/lib/session/transcript.js +15 -7
- package/dist/lib/skills.d.ts +50 -0
- package/dist/lib/skills.js +138 -0
- package/dist/lib/storage/postgres/index.d.ts +98 -0
- package/dist/lib/storage/postgres/index.js +146 -0
- package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
- package/dist/lib/storage/postgres/memoryStore.js +82 -0
- package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
- package/dist/lib/storage/postgres/sessionService.js +225 -0
- package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
- package/dist/lib/storage/postgres/taskQueue.js +101 -0
- package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
- package/dist/lib/storage/postgres/taskStore.js +168 -0
- package/dist/lib/storage/schema.d.ts +27 -0
- package/dist/lib/storage/schema.js +50 -0
- package/dist/lib/storage/schemaVersion.d.ts +25 -0
- package/dist/lib/storage/schemaVersion.js +46 -0
- package/dist/lib/syndicateSchema.d.ts +275 -0
- package/dist/lib/syndicateSchema.js +534 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +59 -10
- package/dist/lib/tools/collectionsSearchTool.js +1 -1
- package/dist/lib/tools/mcpServe.d.ts +45 -0
- package/dist/lib/tools/mcpServe.js +88 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +41 -21
- package/dist/lib/tools/science/corrections.d.ts +42 -0
- package/dist/lib/tools/science/corrections.js +115 -0
- package/dist/lib/tools/science/europepmc.d.ts +51 -0
- package/dist/lib/tools/science/europepmc.js +127 -0
- package/dist/lib/tools/science/http.d.ts +31 -0
- package/dist/lib/tools/science/http.js +104 -0
- package/dist/lib/tools/science/openalex.d.ts +30 -0
- package/dist/lib/tools/science/openalex.js +81 -0
- package/dist/lib/tools/science/trials.d.ts +58 -0
- package/dist/lib/tools/science/trials.js +203 -0
- package/dist/lib/tools/science/types.d.ts +69 -0
- package/dist/lib/tools/science/types.js +48 -0
- package/dist/lib/tools/scienceTools.d.ts +100 -0
- package/dist/lib/tools/scienceTools.js +277 -0
- package/dist/lib/tools/taskTools.d.ts +204 -0
- package/dist/lib/tools/taskTools.js +374 -0
- package/dist/lib/tools/toolContract.d.ts +17 -4
- package/dist/lib/tools/toolContract.js +13 -5
- package/dist/lib/tools/webExtractTool.d.ts +13 -10
- package/dist/lib/tools/webExtractTool.js +23 -55
- package/dist/lib/tools/webSearchTool.d.ts +2 -1
- package/dist/lib/tools/webSearchTool.js +4 -3
- package/dist/lib/tools/wikiTools.d.ts +2 -2
- package/dist/lib/tools/wikiTools.js +12 -12
- package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
- package/dist/lib/tools/xApiSearchTool.js +583 -0
- package/dist/lib/tools/xSearchTool.js +1 -1
- package/dist/lib/wiki/agentRun.js +1 -1
- package/dist/lib/wiki/builder.js +21 -6
- package/dist/lib/wiki/dive.js +2 -2
- package/dist/lib/wiki/entities.d.ts +2 -2
- package/dist/lib/wiki/entities.js +7 -7
- package/dist/lib/wiki/extract.js +5 -1
- package/dist/lib/wiki/fill.js +5 -5
- package/dist/lib/wiki/format.d.ts +7 -5
- package/dist/lib/wiki/format.js +2 -2
- package/dist/lib/wiki/graph.js +2 -2
- package/dist/lib/wiki/lint.js +2 -2
- package/dist/lib/wiki/markdown.d.ts +1 -1
- package/dist/lib/wiki/markdown.js +6 -1
- package/dist/lib/wiki/pipeline.js +2 -2
- package/dist/lib/wiki/vault.js +2 -2
- package/dist/scripts/a2a_server.d.ts +17 -11
- package/dist/scripts/a2a_server.js +405 -1028
- package/dist/scripts/assistant_worker.d.ts +33 -0
- package/dist/scripts/assistant_worker.js +193 -0
- package/dist/scripts/db.d.ts +26 -0
- package/dist/scripts/db.js +179 -0
- package/dist/scripts/doctor.d.ts +23 -0
- package/dist/scripts/doctor.js +90 -0
- package/dist/scripts/init.d.ts +43 -0
- package/dist/scripts/init.js +152 -0
- package/dist/scripts/registry.d.ts +25 -0
- package/dist/scripts/registry.js +182 -0
- package/dist/scripts/skills_install.d.ts +7 -0
- package/dist/scripts/skills_install.js +137 -0
- package/dist/scripts/syndicate_chat.js +159 -276
- package/package.json +90 -16
- package/skills/README.md +74 -0
- package/skills/briefs/README.md +16 -0
- package/skills/briefs/_shared.md +16 -0
- package/skills/briefs/melchizedek-author.md +60 -0
- package/skills/briefs/melchizedek-memory.md +47 -0
- package/skills/briefs/melchizedek-models.md +47 -0
- package/skills/briefs/melchizedek-scribe.md +37 -0
- package/skills/briefs/melchizedek-serve.md +56 -0
- package/skills/briefs/melchizedek.md +69 -0
- package/skills/briefs/skills-readme.md +33 -0
- package/skills/melchizedek/SKILL.md +159 -0
- package/skills/melchizedek-author/SKILL.md +99 -0
- package/skills/melchizedek-author/templates/minimal.yaml +60 -0
- package/skills/melchizedek-memory/SKILL.md +138 -0
- package/skills/melchizedek-models/SKILL.md +137 -0
- package/skills/melchizedek-scribe/SKILL.md +72 -0
- package/skills/melchizedek-scribe/templates/brief.md +27 -0
- package/skills/melchizedek-serve/SKILL.md +119 -0
package/DOCUMENTATION.md
CHANGED
|
@@ -26,7 +26,7 @@ see [`QUICKSTART.md`](./QUICKSTART.md).
|
|
|
26
26
|
config/agents/ YOUR syndicate definitions (the engine's input)
|
|
27
27
|
config/agents/examples/ the starter pack — shipped example syndicates
|
|
28
28
|
lib/loadSyndicate.ts YAML → validated config (+ variable binding)
|
|
29
|
-
lib/dispatch.ts plan-dispatch route resolution (§6
|
|
29
|
+
lib/dispatch.ts plan-dispatch route resolution (§6)
|
|
30
30
|
lib/toolRegistry.ts tool name → live ADK tool instance
|
|
31
31
|
lib/models/claudeLlm.ts Claude adapter registered into the ADK registry
|
|
32
32
|
lib/models/ollamaLlm.ts open-weight local adapter (Ollama, keyless)
|
|
@@ -35,8 +35,12 @@ scripts/demo_mcp_server.ts demo MCP server (library catalog, SSE)
|
|
|
35
35
|
lib/session/… Supabase-backed session service
|
|
36
36
|
lib/memory/… pgvector long-term memory service
|
|
37
37
|
lib/observability/… OpenTelemetry run tracing
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
lib/runtime/syndicateTurn.ts THE turn runner: every surface below calls it
|
|
39
|
+
lib/a2a/app.ts the A2A server as a library (createA2AApp)
|
|
40
|
+
lib/a2a/remoteAgent.ts A2A client: remote agents as subagents
|
|
41
|
+
scripts/syndicate_chat.ts CLI REPL / one-shot runner
|
|
42
|
+
scripts/a2a_server.ts the A2A server bin (melchizedek-serve)
|
|
43
|
+
db/schema.sql the base schema (sessions, memory, expiry)
|
|
40
44
|
db/hardening.sql deny-by-default RLS for the Supabase tables
|
|
41
45
|
tests/agents.test.ts compiles every shipped syndicate; opt-in live check
|
|
42
46
|
```
|
|
@@ -56,6 +60,8 @@ Minimal complete example:
|
|
|
56
60
|
syndicate_name: "My Council"
|
|
57
61
|
memory_system: "session-only" # internal-only | session-only | long-term
|
|
58
62
|
|
|
63
|
+
# guards: [my_guard] # [optional] post-answer guards, by name
|
|
64
|
+
|
|
59
65
|
variables: # bound into {{placeholders}} at load
|
|
60
66
|
headline_count: 5 # current_date is injected automatically
|
|
61
67
|
|
|
@@ -85,17 +91,23 @@ Field reference:
|
|
|
85
91
|
|
|
86
92
|
| Field | Where | Meaning |
|
|
87
93
|
|---|---|---|
|
|
88
|
-
| `syndicate_name` | root | Display name
|
|
89
|
-
| `memory_system` | root | `internal-only` (nothing persists), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
|
|
94
|
+
| `syndicate_name` | root | Display name. On the CLI it also namespaces memory user keys; on the A2A server memory is keyed by the caller's silo across every long-term syndicate that server serves. |
|
|
95
|
+
| `memory_system` | root | `internal-only` (nothing persists — on the server too: its transcripts stay in process memory), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
|
|
90
96
|
| `variables` | root | Key/values bound into `{{placeholders}}` anywhere in instructions. `current_date` is always injected; CLI `--bind key=value` overrides. |
|
|
91
97
|
| `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). |
|
|
92
|
-
| `
|
|
98
|
+
| `memory_extraction_model` | root | The model that distils this syndicate's turns into memory records (long-term only); any model id. Default: the deployment's `MEMORY_EXTRACTION_MODEL`. |
|
|
99
|
+
| `memory_retention_days` | root | Days a fact in this syndicate's namespace is kept; the server deletes older facts when it loads the syndicate and daily after. Requires `memory_namespace` (never applied to the shared default namespace). |
|
|
100
|
+
| `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`). Honoured by every surface (the CLI, the server, the worker, evals). |
|
|
101
|
+
| `guards` | root | Optional list of post-answer guard NAMES (`lib/guards/index.ts`). Each runs after the answering turn and before the reply publishes, receiving the final text plus every tool-result text of that turn, and rewrites in place rather than re-asking the model; its notes land in the `[STATUS]` stream. Guards declared by a syndicate reached through `yaml_reference:` count too — the server resolves the union via `collectGuards()`. Resolved by name, never by module path, so adding one is a deliberate act in code; **the published registry ships one, `science`** (citation checks for `research.yaml`); `registerGuard()` adds your own from code, and an unregistered name is warned about and skipped. |
|
|
93
102
|
| `name` / `model` / `instruction` | agent | The agent triple. Any Gemini id, `claude-*`, or `ollama/*` for open-weight local models (see §5). |
|
|
94
103
|
| `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…"). |
|
|
95
104
|
| `tools` | agent | Names resolved by the tool registry (§3). Long-term memory agents add `preload_memory` / `load_memory`. |
|
|
96
105
|
| `generateContentConfig` | agent | Temperature, output caps, thinking budget/level. |
|
|
97
106
|
| `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). |
|
|
98
107
|
| `yaml_reference` | subagent | Mount another syndicate file as a nested subagent. |
|
|
108
|
+
| `a2a_agent_url` | subagent | A REMOTE agent over A2A (§6): the orchestrator delegates to it with one `request` argument; in plan-dispatch it can be a route. No `model`/`instruction` — the remote agent has its own. Credentials come from `A2A_AGENT_TOKENS`, never YAML. |
|
|
109
|
+
| `max_steps` | root | Cap on model calls per turn, counted across every agent the turn reaches (orchestrator, subagents, nested syndicates). Exceeding it fails the turn with `STEP_LIMIT`. |
|
|
110
|
+
| `includeContents` / `outputKey` / `globalInstruction` / `disallowTransferToParent` / `disallowTransferToPeers` | agent | Passed through to ADK's LlmAgent. `includeContents: none` makes an agent see only the current message. |
|
|
99
111
|
| `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. |
|
|
100
112
|
|
|
101
113
|
Validation happens at load: missing names, legacy option blocks, and
|
|
@@ -119,6 +131,8 @@ instance:
|
|
|
119
131
|
| `load_memory` | ADK built-in | Explicit tool call to search the fact store (deliberate recall). |
|
|
120
132
|
| `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. |
|
|
121
133
|
| `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`). |
|
|
134
|
+
| `task_add` / `task_list` / `task_get` / `task_update` | FunctionTool | A to-do list and job queue. Default: a single-user JSON file (`MELCHIZEDEK_TASKS_FILE`, default `outputs/tasks.json`), so every caller of a shared endpoint shares one list. With `DATABASE_URL` (migration 0009) each caller has its own list, scoped by the caller's scope key, and any number of workers take jobs safely. |
|
|
135
|
+
| `task_queue` | FunctionTool | Queues a background job (a self-contained instruction). The tool only writes the queue; `npm run assistant:worker` (`melchizedek-worker`) claims each job, runs it through one agent compiled from YAML (default: the Assistant's Worker), and writes the result back for `task_get`. `--once` drains and exits, for cron. |
|
|
122
136
|
|
|
123
137
|
**MCP tools** are the exception to the registry: a subagent with
|
|
124
138
|
`mcp_server_url:` in its YAML gets its tools from a remote MCP server at
|
|
@@ -154,93 +168,33 @@ Two Supabase tables carry the two kinds of remembering:
|
|
|
154
168
|
history); recall is cosine similarity re-ranked by keys and dates.
|
|
155
169
|
Full pipeline: [`lib/memory/README.md`](./lib/memory/README.md).
|
|
156
170
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
-- live beside the embedding — see lib/memory/README.md)
|
|
178
|
-
CREATE TABLE adk_memory_facts (
|
|
179
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
180
|
-
user_key TEXT NOT NULL,
|
|
181
|
-
fact TEXT NOT NULL,
|
|
182
|
-
embedding vector(768),
|
|
183
|
-
tag TEXT,
|
|
184
|
-
fact_date DATE,
|
|
185
|
-
source TEXT,
|
|
186
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
187
|
-
keys TEXT[] NOT NULL DEFAULT '{}',
|
|
188
|
-
superseded_by UUID,
|
|
189
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
190
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
-- 4. Indexes: similarity, entity keys, dates
|
|
194
|
-
CREATE INDEX ON adk_memory_facts USING ivfflat (embedding vector_cosine_ops)
|
|
195
|
-
WITH (lists = 100);
|
|
196
|
-
CREATE INDEX adk_memory_facts_keys_idx ON adk_memory_facts USING gin (keys);
|
|
197
|
-
CREATE INDEX adk_memory_facts_date_idx ON adk_memory_facts (user_key, fact_date);
|
|
198
|
-
|
|
199
|
-
-- 5. Cosine-similarity RPC (returns the structured columns so the
|
|
200
|
-
-- service can re-rank by keys/dates and relabel superseded records).
|
|
201
|
-
-- filter_user_key is REQUIRED: a NULL key would return every user's facts.
|
|
202
|
-
CREATE OR REPLACE FUNCTION match_memory_facts (
|
|
203
|
-
query_embedding vector(768),
|
|
204
|
-
filter_user_key text,
|
|
205
|
-
match_count int DEFAULT 10
|
|
206
|
-
) RETURNS TABLE (
|
|
207
|
-
id UUID,
|
|
208
|
-
user_key TEXT,
|
|
209
|
-
fact TEXT,
|
|
210
|
-
tag TEXT,
|
|
211
|
-
fact_date DATE,
|
|
212
|
-
source TEXT,
|
|
213
|
-
status TEXT,
|
|
214
|
-
keys TEXT[],
|
|
215
|
-
created_at TIMESTAMPTZ,
|
|
216
|
-
similarity float
|
|
217
|
-
)
|
|
218
|
-
LANGUAGE plpgsql
|
|
219
|
-
AS $$
|
|
220
|
-
BEGIN
|
|
221
|
-
IF filter_user_key IS NULL THEN
|
|
222
|
-
RAISE EXCEPTION 'filter_user_key is required';
|
|
223
|
-
END IF;
|
|
224
|
-
RETURN QUERY
|
|
225
|
-
SELECT
|
|
226
|
-
adk_memory_facts.id,
|
|
227
|
-
adk_memory_facts.user_key,
|
|
228
|
-
adk_memory_facts.fact,
|
|
229
|
-
adk_memory_facts.tag,
|
|
230
|
-
adk_memory_facts.fact_date,
|
|
231
|
-
adk_memory_facts.source,
|
|
232
|
-
adk_memory_facts.status,
|
|
233
|
-
adk_memory_facts.keys,
|
|
234
|
-
adk_memory_facts.created_at,
|
|
235
|
-
1 - (adk_memory_facts.embedding <=> query_embedding) AS similarity
|
|
236
|
-
FROM adk_memory_facts
|
|
237
|
-
WHERE adk_memory_facts.user_key = filter_user_key
|
|
238
|
-
ORDER BY adk_memory_facts.embedding <=> query_embedding
|
|
239
|
-
LIMIT match_count;
|
|
240
|
-
END;
|
|
241
|
-
$$;
|
|
171
|
+
Install both, with their indexes, the recall function and the nightly
|
|
172
|
+
session expiry, from the one canonical file:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx melchizedek-db print # paste into the Supabase SQL Editor (clone: npm run db -- print)
|
|
176
|
+
npx melchizedek-db apply # or apply with psql, DATABASE_URL set
|
|
177
|
+
npx melchizedek-db status # schema version, hardening, session counts
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
That runs the migrations in [`db/migrations/`](./db/migrations/) and then
|
|
181
|
+
[`db/hardening.sql`](./db/hardening.sql). Both are idempotent, so re-running
|
|
182
|
+
them is also the upgrade path from any earlier layout.
|
|
183
|
+
|
|
184
|
+
The tables go in the `public` schema by default. To keep them out of reach of
|
|
185
|
+
a REST layer (Supabase exposes `public`), install into a private schema and
|
|
186
|
+
run the server with the same setting:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
MELCHIZEDEK_DB_SCHEMA=melchizedek npx melchizedek-db apply # or: apply --schema melchizedek
|
|
190
|
+
MELCHIZEDEK_DB_SCHEMA=melchizedek DATABASE_URL=... npx melchizedek-a2a
|
|
242
191
|
```
|
|
243
192
|
|
|
193
|
+
A private schema needs `DATABASE_URL` (the pg driver); supabase-js storage
|
|
194
|
+
reaches only exposed schemas, so the server refuses that combination. Moving an
|
|
195
|
+
existing `public` install is a data migration, not a setting: keep `public`
|
|
196
|
+
there.
|
|
197
|
+
|
|
244
198
|
Then run [`db/hardening.sql`](./db/hardening.sql) (RLS deny-by-default;
|
|
245
199
|
see §8). Upgrading an existing project to the structured columns:
|
|
246
200
|
[`db/memory_v2.sql`](./db/memory_v2.sql). `npm run db:purge` clears both
|
|
@@ -260,10 +214,17 @@ extraction model rephrases on each pass. Two mechanisms in
|
|
|
260
214
|
`lib/memory/supabaseMemoryService.ts` prevent the store from filling
|
|
261
215
|
with restatements:
|
|
262
216
|
|
|
263
|
-
- A per-session **
|
|
264
|
-
turns added since the last ingestion. It is in
|
|
265
|
-
|
|
266
|
-
|
|
217
|
+
- A per-session **processed marker** (`eventsToIngest`) distils only the
|
|
218
|
+
turns added since the last ingestion. It is stored in
|
|
219
|
+
`melchizedek_memory_ingest` (migration 0007) and advances in the same
|
|
220
|
+
transaction as the facts (`melchizedek_memory_commit`, which also retires
|
|
221
|
+
superseded rows), so a restart re-extracts nothing and a failure leaves
|
|
222
|
+
facts, supersessions and marker as they were. A custom store without the
|
|
223
|
+
commit keeps the marker in process memory.
|
|
224
|
+
- At boot the server compares the embedding column's size
|
|
225
|
+
(`melchizedek_memory_dimensions()`) with the configured embedder and
|
|
226
|
+
refuses a mismatch: changing the embedding model is a re-embed, never a
|
|
227
|
+
dropped table.
|
|
267
228
|
- A **similarity probe** before each insert (`isSemanticDuplicate`,
|
|
268
229
|
`MEMORY_DEDUP_SIMILARITY = 0.93`) drops restatements, reusing the same
|
|
269
230
|
`match_memory_facts` RPC the supersession path calls. It requires the
|
|
@@ -299,6 +260,8 @@ accordingly — model optionality is a single YAML line per agent:
|
|
|
299
260
|
| `claude-*` | Anthropic | `lib/models/claudeLlm.ts` | `ANTHROPIC_API_KEY` | ✅ server tool |
|
|
300
261
|
| `gpt-*`, o-series | OpenAI | `lib/models/gptLlm.ts` (Responses API) | `OPENAI_API_KEY` | ✅ web_search tool |
|
|
301
262
|
| `grok-*` | xAI | `lib/models/grokLlm.ts` (Responses API) | `XAI_API_KEY` | ✅ Agent Tools search |
|
|
263
|
+
| `ollama/*` | Local Ollama | `lib/models/ollamaLlm.ts` | none | ⚠ omitted + warning |
|
|
264
|
+
| *any cloud id whose direct key is absent* | the id's own provider, via a gateway | `lib/models/gatewayLlm.ts` (chat completions) | `MODEL_GATEWAY` + `MODEL_GATEWAY_API_KEY` | ⚠ omitted + reported |
|
|
302
265
|
|
|
303
266
|
The xAI adapter carries the deepest capability surface: `grok-4.5`
|
|
304
267
|
requests pin `reasoning.effort: "medium"` (`lib/config.ts`), SSE
|
|
@@ -307,7 +270,64 @@ partial delta events stream, one aggregated event persists with usage),
|
|
|
307
270
|
structured outputs ride `outputSchema` → `text.format`, and two
|
|
308
271
|
xAI-only tools — `x_search` and `collections_search` (§3) — turn on
|
|
309
272
|
live X search and hosted-document RAG. All verified live on grok-4.5.
|
|
310
|
-
|
|
273
|
+
|
|
274
|
+
**Which keys do I need?** `npm run doctor` (the `melchizedek-doctor` bin)
|
|
275
|
+
reads every syndicate YAML, resolves each agent's model under your `.env`,
|
|
276
|
+
and prints one table — agent, model, provider, which declared server-side
|
|
277
|
+
tools that path runs natively (✓) or drops (✗), and whether the path is
|
|
278
|
+
funded — with one verdict per syndicate and the variables that would
|
|
279
|
+
unlock the most. Read-only; no key value is ever printed. Every
|
|
280
|
+
starter-pack file opens with a `# tier:` header (`keyless`, one provider
|
|
281
|
+
such as `gemini`, or `multi-provider`) the doctor checks against the
|
|
282
|
+
models. `GOOGLE_GENAI_API_KEY` alone runs fourteen of the nineteen examples.
|
|
283
|
+
|
|
284
|
+
**One key instead of several — the gateway fallback.** Direct adapters
|
|
285
|
+
are canonical: the native features above exist only on a provider's own
|
|
286
|
+
endpoint. But with `MODEL_GATEWAY=vercel` (or `openrouter`) and
|
|
287
|
+
`MODEL_GATEWAY_API_KEY` set, any cloud model id whose direct key is
|
|
288
|
+
*absent* is served through that gateway's OpenAI-compatible endpoint
|
|
289
|
+
(`lib/models/gateway.ts` owns the rule). A present provider key always
|
|
290
|
+
wins for its own ids, Ollama never routes through a gateway, and adding a
|
|
291
|
+
direct key later restores that provider's native search with no YAML
|
|
292
|
+
change. Through the gateway every server-side sentinel (`web_search`,
|
|
293
|
+
`google_search`, `x_search`, `collections_search`) is dropped — the doctor
|
|
294
|
+
and the startup log say so per agent, and the span records
|
|
295
|
+
`llm.transport = gateway:<id>` and `llm.capability.dropped` while
|
|
296
|
+
`llm.provider` keeps the upstream attribution. Tool calling, structured
|
|
297
|
+
output, `reasoning_effort` and streaming work unchanged. Optional dials:
|
|
298
|
+
`MODEL_GATEWAY_BASE_URL` (a self-hosted proxy speaking the same dialect),
|
|
299
|
+
`MODEL_GATEWAY_MODEL_MAP` (`from=to,…` wire-name overrides; the mapper
|
|
300
|
+
already turns `claude-sonnet-4-6` into `anthropic/claude-sonnet-4.6`). The
|
|
301
|
+
A2A `X-API-Key` never selects the gateway — the gateway key is server
|
|
302
|
+
environment only.
|
|
303
|
+
|
|
304
|
+
**Cloud platforms and proxies (ADR 0023).** The model id picks the provider;
|
|
305
|
+
`<PROVIDER>_PLATFORM` picks how it is reached. Keys in the environment against
|
|
306
|
+
the vendor's public API stay the default.
|
|
307
|
+
|
|
308
|
+
| Path | Set | Credential |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| Gemini on Vertex AI | `GEMINI_PLATFORM=vertex` (or genai's `GOOGLE_GENAI_USE_VERTEXAI=true`), `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION` | Google Application Default Credentials |
|
|
311
|
+
| Claude on Bedrock | `ANTHROPIC_PLATFORM=bedrock`, `AWS_REGION` | the AWS credential chain; needs `@anthropic-ai/bedrock-sdk` |
|
|
312
|
+
| Claude on Vertex AI | `ANTHROPIC_PLATFORM=vertex`, `ANTHROPIC_VERTEX_PROJECT_ID` (or `GOOGLE_CLOUD_PROJECT`), `CLOUD_ML_REGION` | Google ADC; needs `@anthropic-ai/vertex-sdk` |
|
|
313
|
+
| GPT on Azure OpenAI | `OPENAI_PLATFORM=azure`, `AZURE_OPENAI_ENDPOINT` | `AZURE_OPENAI_API_KEY`, else Entra ID through `@azure/identity` |
|
|
314
|
+
| A proxy in front of Anthropic or OpenAI | `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` | the provider's key |
|
|
315
|
+
|
|
316
|
+
`ANTHROPIC_MODEL_MAP`, `OPENAI_MODEL_MAP` and `GEMINI_MODEL_MAP` (JSON, YAML id
|
|
317
|
+
→ platform id) translate ids a platform names differently: a Bedrock inference
|
|
318
|
+
profile, a Vertex AI version suffix, an Azure deployment name. Unlisted ids pass
|
|
319
|
+
through. The A2A `credentials` plug point may return, per request, an API key
|
|
320
|
+
or a partial endpoint (`baseURL`, `apiKey`, a `token` source, `project`,
|
|
321
|
+
`location`, `region`, `models`) merged over the environment's. Memory
|
|
322
|
+
extraction and embeddings follow the Gemini platform, so a Vertex AI
|
|
323
|
+
deployment needs no AI Studio key.
|
|
324
|
+
|
|
325
|
+
Native web search is not sent on Bedrock, Claude-on-Vertex or Azure (Gemini
|
|
326
|
+
grounding is kept on Vertex AI); the capability report, the startup log and
|
|
327
|
+
the doctor say so per agent. `npm run doctor` prints one `endpoint` line per
|
|
328
|
+
configured platform with its credential source and anything missing. These
|
|
329
|
+
paths are tested against mocked SDK clients and have **not** been run against
|
|
330
|
+
the live clouds from this repository.
|
|
311
331
|
|
|
312
332
|
`lib/models/registry.ts` is the single routing seam:
|
|
313
333
|
`registerAvailableProviders()` registers every adapter whose key is
|
|
@@ -357,7 +377,8 @@ route through `lib/models/ollamaLlm.ts` to a local Ollama daemon over
|
|
|
357
377
|
its OpenAI-compatible API (`OLLAMA_BASE_URL`, default
|
|
358
378
|
`http://localhost:11434/v1`). No key is required, and a syndicate whose
|
|
359
379
|
*every* agent is `ollama/*` runs with no `.env` at all —
|
|
360
|
-
`config/agents/examples/tutor.yaml` (single agent)
|
|
380
|
+
`config/agents/examples/tutor.yaml` (single agent), `council.yaml` (council)
|
|
381
|
+
and `assistant.yaml` (conversation, summaries, a task list, background jobs)
|
|
361
382
|
are the worked examples. The adapter translates ADK content to
|
|
362
383
|
OpenAI-style messages, including tool calls (so delegation works),
|
|
363
384
|
image parts as data URIs (so `ollama/qwen3-vl:8b` can see), and JSON
|
|
@@ -374,13 +395,288 @@ itself; delegation is exercised through AgentTool function calls.
|
|
|
374
395
|
|
|
375
396
|
## 6. A2A service mode
|
|
376
397
|
|
|
377
|
-
`npm run start:a2a
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
398
|
+
`npm run start:a2a -- <file>.yaml` (package: `npx melchizedek-serve
|
|
399
|
+
<file>.yaml`) serves a syndicate as an A2A 1.0 agent that also accepts A2A
|
|
400
|
+
0.3 clients (most platforms still speak 0.3; a request without an
|
|
401
|
+
`A2A-Version` header is treated as 0.3, per the spec): an agent card listing
|
|
402
|
+
both versions' endpoints, JSON-RPC and REST transports, and the task
|
|
403
|
+
lifecycle. In your own Express
|
|
404
|
+
app, mount `(await createA2AApp(options)).app` instead — same server, same
|
|
405
|
+
options. `demo/a2a_demo.mjs` is a complete client.
|
|
406
|
+
|
|
407
|
+
#### Routes
|
|
408
|
+
|
|
409
|
+
| Route | Auth | Purpose |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| `GET /healthz` | none | liveness — always 200 while the process runs |
|
|
412
|
+
| `GET /readyz` | none | readiness — 503 while draining for shutdown |
|
|
413
|
+
| `GET /metrics` | `A2A_METRICS_TOKEN` | Prometheus text: tasks by agent, outcome and caller; model calls; tokens by kind; task duration; tasks in flight. Absent unless the token is set |
|
|
414
|
+
| `GET /.well-known/agent-card.json` | bearer | the default syndicate's card |
|
|
415
|
+
| `POST /a2a/jsonrpc`, `/a2a/rest` | bearer | the default syndicate |
|
|
416
|
+
| `GET /<agentId>/.well-known/agent-card.json` | bearer | another syndicate's card; its URLs point at `/<agentId>/a2a/...` |
|
|
417
|
+
| `POST /<agentId>/a2a/jsonrpc`, `/<agentId>/a2a/rest` | bearer | another syndicate (`A2A_SERVED_AGENTS` restricts which) |
|
|
418
|
+
| `DELETE /memory` | bearer | erase everything stored for the calling scope: facts, sessions (with subagent rows) and ledger rows, with per-store counts; `?all=1` covers every memory namespace |
|
|
419
|
+
|
|
420
|
+
"bearer" means the credential `A2A_AUTH` asks for (below); with no
|
|
421
|
+
`A2A_SERVER_SECRET` and no authenticator the server binds `127.0.0.1` only.
|
|
422
|
+
In BYOK mode every task route also needs `X-API-Key`. The card declares
|
|
423
|
+
what is required in `securitySchemes`.
|
|
424
|
+
|
|
425
|
+
#### Who the caller is, and whose data it is
|
|
426
|
+
|
|
427
|
+
`A2A_AUTH` picks a built-in authenticator (`lib/a2a/identity.ts`, export
|
|
428
|
+
`melchizedek-agents/a2a/identity`), and the authenticator decides the
|
|
429
|
+
scope key every session and memory is stored under:
|
|
430
|
+
|
|
431
|
+
- **`secret`** (default): every caller presents `A2A_SERVER_SECRET`, and
|
|
432
|
+
the calling backend names its end user in `X-User-Id`.
|
|
433
|
+
- **`callers`**: one bearer token per calling backend. `A2A_CALLERS` lists
|
|
434
|
+
`name:sha256[:scope]` entries separated by `;`, so the configuration holds
|
|
435
|
+
only token hashes. A caller owns its scope (its name, unless one is given),
|
|
436
|
+
and an `X-User-Id` it sends nests beneath it. The scope does not depend on
|
|
437
|
+
any model key. Mint a caller with
|
|
438
|
+
`npx melchizedek-serve --new-caller <name> [--scope <scope>] [--token-file <path>]`.
|
|
439
|
+
While `A2A_SERVER_SECRET` is still set it keeps working beside the tokens,
|
|
440
|
+
with its old scoping, so callers move over one at a time.
|
|
441
|
+
- **`jwt`**: a JWT from your identity provider: `A2A_JWT_JWKS_URL` (or an
|
|
442
|
+
HS256 `A2A_JWT_SECRET`), with `A2A_JWT_ISSUER` and `A2A_JWT_AUDIENCE`
|
|
443
|
+
required and `exp` checked. The scope is the `sub` claim
|
|
444
|
+
(`A2A_JWT_SCOPE_CLAIM`), under `A2A_JWT_TENANT_CLAIM` when set.
|
|
445
|
+
`X-User-Id` is ignored: the token is the user.
|
|
446
|
+
- **`header`**: a gateway in front authenticates users and names them in
|
|
447
|
+
`A2A_TRUSTED_USER_HEADER`. Requires `A2A_SERVER_SECRET`, which only the
|
|
448
|
+
gateway may hold, so no other client can set the header.
|
|
449
|
+
|
|
450
|
+
In code, spread an authenticator into the factory:
|
|
451
|
+
`createA2AApp({ ...callerTokens(parseCallers(spec)), keyMode: 'byok', … })`,
|
|
452
|
+
or pass your own `resolveRequest`.
|
|
453
|
+
|
|
454
|
+
#### Budgets, the task record, and the ledger
|
|
455
|
+
|
|
456
|
+
- **Budgets** (`A2A_BUDGETS`, option `policy: budgets(config)`): daily limits
|
|
457
|
+
per UTC day on tasks, model calls and tokens, per caller (`perCaller`, with
|
|
458
|
+
overrides by name in `callers`) and per scope (`perScope`, one end user).
|
|
459
|
+
Example: `{"perCaller":{"tokens":5000000},"callers":{"reports":{"tasks":100}},"perScope":{"tasks":50}}`.
|
|
460
|
+
A task over budget ends `rejected` with the reason, before it takes a slot.
|
|
461
|
+
The counts live in the `melchizedek_usage` table (`db/migrations/0004_usage.sql`)
|
|
462
|
+
when Postgres or Supabase is configured, else in process memory. A store
|
|
463
|
+
that cannot be read refuses the task.
|
|
464
|
+
- **One record per task** (option `onTaskEnd`): agent, caller, a hash of the
|
|
465
|
+
scope, status, reason, duration, model calls and tokens.
|
|
466
|
+
`A2A_LOG_FORMAT=json` prints every server line as JSON, this record included.
|
|
467
|
+
- **Rate limit**: with an authenticator it counts per caller (an operator's
|
|
468
|
+
backend) or per scope (an end user); under the shared secret, per IP.
|
|
469
|
+
- **The telemetry ledger is redacted before it is written**:
|
|
470
|
+
`TELEMETRY_REDACT=secret` (default) removes key-shaped credentials; add
|
|
471
|
+
`email`, `phone`, `card` (Luhn-checked) or `ssn`, or plug in your own with
|
|
472
|
+
`setTelemetryRedactor(fn)`. Sessions and memory are not redacted; use
|
|
473
|
+
`DELETE /memory` there.
|
|
474
|
+
|
|
475
|
+
#### Who pays
|
|
476
|
+
|
|
477
|
+
`A2A_KEY_MODE` (option `keyMode`), whatever the authenticator:
|
|
478
|
+
|
|
479
|
+
- **`server`** (default): the server's own provider keys pay for every
|
|
480
|
+
model call (or your `credentials` plug point supplies a key per request).
|
|
481
|
+
- **`byok`**: the caller's `X-API-Key` funds agents on the provider named
|
|
482
|
+
by `X-Provider`; agents on other providers, tools and memory extraction
|
|
483
|
+
still run on the server's keys. Under `A2A_AUTH=secret` only, the key's
|
|
484
|
+
hash also scopes the data (`a2a-<hash>`, with `X-User-Id` beneath it).
|
|
485
|
+
That was the only behaviour before 0.16. A deployment holding data from
|
|
486
|
+
then keeps reaching it with `byok`, or by moving to `A2A_AUTH=callers`
|
|
487
|
+
with each caller's scope set to its existing `a2a-<hash>` silo.
|
|
488
|
+
|
|
489
|
+
#### Headers
|
|
490
|
+
|
|
491
|
+
| Header | Meaning |
|
|
492
|
+
|---|---|
|
|
493
|
+
| `Authorization: Bearer <token>` | the server secret, this caller's token, or a JWT, per `A2A_AUTH` |
|
|
494
|
+
| `X-User-Id` | the end user this request is for (`[A-Za-z0-9._-]{1,64}`); sessions and memory are stored under it (under a caller token, beneath the caller's scope; ignored with a JWT). Authenticate your users before sending it. |
|
|
495
|
+
| `X-API-Key` | BYOK mode only: the caller's model key (see above) |
|
|
496
|
+
| `X-Provider` | BYOK mode only: which provider `X-API-Key` belongs to (default `google`) |
|
|
497
|
+
| `X-Surface`, `X-Surface-Guild`, `-Channel`, `-User` | optional, telemetry only |
|
|
498
|
+
|
|
499
|
+
#### Sessions
|
|
500
|
+
|
|
501
|
+
`message.contextId` names the conversation — it goes **inside** `message`.
|
|
502
|
+
Calls with the same value continue one session; a `contextId` beside
|
|
503
|
+
`message` is ignored and every call starts fresh. Sessions are durable with
|
|
504
|
+
Supabase and expire seven days after the last turn (a nightly prune from
|
|
505
|
+
`db/migrations/0001_base.sql` deletes them). A syndicate declaring `memory_system: internal-only` keeps its
|
|
506
|
+
transcripts in process memory even when Supabase is configured.
|
|
507
|
+
|
|
508
|
+
#### Tasks, streaming, cancel
|
|
509
|
+
|
|
510
|
+
(Method names below are 0.3's; 1.0 clients use `SendMessage`,
|
|
511
|
+
`GetTask`, `SendStreamingMessage`, `CancelTask` and the 1.0 enum spellings.)
|
|
512
|
+
`message/send` blocks until the turn finishes unless the request sets
|
|
513
|
+
`configuration.blocking: false`; then poll `tasks/get`. `message/stream`
|
|
514
|
+
emits `[STATUS]` progress updates (tool calls, the chosen route, guard
|
|
515
|
+
notes) and the answer as the final status message. With
|
|
516
|
+
`A2A_STREAM_TEXT=true` (`streamText` in code) it also streams the answer as
|
|
517
|
+
the model writes it, as chunks of an artifact named `answer`: the first
|
|
518
|
+
chunk opens it, later chunks append, text the agent wrote before calling a
|
|
519
|
+
tool is withdrawn (an empty replacement), and the artifact is closed
|
|
520
|
+
(`lastChunk`) with the text the user actually receives, which the final
|
|
521
|
+
status message repeats. A syndicate with `guards:` never streams, because a
|
|
522
|
+
guard reads the whole answer before any of it leaves; the dispatch
|
|
523
|
+
classifier never streams either. One conversation runs one turn at a time:
|
|
524
|
+
a second message on a busy `contextId` waits for the first turn (up to
|
|
525
|
+
`A2A_TURN_LOCK_WAIT_MS`, default 30 s) and is then `rejected` with "still
|
|
526
|
+
running". The lock is in-process by default and a Postgres advisory lock
|
|
527
|
+
with `DATABASE_URL`, so it holds across instances. `tasks/cancel` stops a running task, including the model call
|
|
528
|
+
in flight, and the task ends `canceled`. Final states: `completed`,
|
|
529
|
+
`failed` (the message names the stage and the provider's reason),
|
|
530
|
+
`canceled`, `rejected` (a file part, an empty message, or the server at
|
|
531
|
+
capacity). Message parts may be `text` or `data` (sent to the model as
|
|
532
|
+
JSON); `file` parts are refused.
|
|
533
|
+
|
|
534
|
+
#### Approval gates (`require_approval`)
|
|
535
|
+
|
|
536
|
+
A tool can run only after a person approves the exact call
|
|
537
|
+
([ADR 0028](./wiki/decisions/0028-approval-gates.md)). The agent lists it:
|
|
538
|
+
|
|
539
|
+
```yaml
|
|
540
|
+
orchestrator:
|
|
541
|
+
name: Desk
|
|
542
|
+
tools: [send_email]
|
|
543
|
+
require_approval: [send_email] # names from this agent's own tools
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
When the model calls it, nothing runs: the task ends `input-required`, final.
|
|
547
|
+
Its status message says what is waiting ("Desk wants to run
|
|
548
|
+
send_email({...})") and carries a data part
|
|
549
|
+
`{ type: 'approval_request', approval_id, agent, tool, args }`. Answer on the
|
|
550
|
+
same conversation (same `contextId`; the same `taskId` works too) with the text
|
|
551
|
+
`approve` or `reject`, or a data part
|
|
552
|
+
`{ "approval": { "id": "<approval_id>", "approved": true } }`. Approved, the
|
|
553
|
+
call runs with the arguments shown; rejected, the model is told and answers
|
|
554
|
+
without it. A message that is not an answer gets the same request back,
|
|
555
|
+
without a model call. ADK pins the call and its arguments, so an approval
|
|
556
|
+
cannot run a different call.
|
|
557
|
+
|
|
558
|
+
Gates are allowed on the orchestrator and on the subagents of a
|
|
559
|
+
plan-dispatch syndicate, which run as the turn's own agent. A delegated
|
|
560
|
+
subagent runs inside a tool call, where a pause cannot reach the caller, so a
|
|
561
|
+
gate there is a load error, as is any gate inside a nested `yaml_reference`
|
|
562
|
+
syndicate. Only function tools from the registry can be gated, not MCP tools
|
|
563
|
+
or native-search sentinels. In code, `runSyndicateTurn` returns
|
|
564
|
+
`status: 'input-required'` with `approval`, and the next turn's part
|
|
565
|
+
`approvalResponsePart(approval.id, approved)` answers it.
|
|
566
|
+
|
|
567
|
+
#### Limits
|
|
568
|
+
|
|
569
|
+
| Setting | Default |
|
|
570
|
+
|---|---|
|
|
571
|
+
| `A2A_RATE_LIMIT_MAX` per `A2A_RATE_LIMIT_WINDOW_MS`, per client IP (POSTs) | 60 per 15 min |
|
|
572
|
+
| `A2A_AUTH_FAILURE_MAX` failed logins per IP per 15 min, then blocked | 30 |
|
|
573
|
+
| `A2A_TASK_TIMEOUT_MS` per task | 15 min |
|
|
574
|
+
| `A2A_MAX_CONCURRENT_TASKS` | unlimited |
|
|
575
|
+
| `max_steps` (YAML): model calls per turn, subagents included | none |
|
|
576
|
+
| `A2A_BODY_LIMIT` | 1 MB |
|
|
577
|
+
| `A2A_SHUTDOWN_GRACE_MS`: SIGTERM waits for running tasks | 25 s |
|
|
578
|
+
|
|
579
|
+
The rate limit and the failed-login limit count in the process by default,
|
|
580
|
+
so each replica keeps its own window. With `A2A_REDIS_URL` set (and the
|
|
581
|
+
optional `redis` package installed) both count in Redis, one window across
|
|
582
|
+
every replica; in code, pass `limitStore` with
|
|
583
|
+
`redisRateLimitStore({ command })` from `melchizedek-agents/a2a/limits`, which
|
|
584
|
+
takes any client's raw-command function (node-redis `sendCommand`, ioredis
|
|
585
|
+
`call`). Budgets need no Redis: they are Postgres counters.
|
|
586
|
+
|
|
587
|
+
#### What is per-process
|
|
588
|
+
|
|
589
|
+
The per-agent config cache (a config change needs a restart), the
|
|
590
|
+
rate-limit counters and the concurrency count always live in the process. Tasks do too unless `DATABASE_URL` is set: then
|
|
591
|
+
Postgres holds sessions, memory, A2A tasks and budget counters, and several
|
|
592
|
+
replicas are safe behind one load balancer (rate limits and the concurrency
|
|
593
|
+
cap then apply per replica, unless `A2A_REDIS_URL` shares the rate limits).
|
|
594
|
+
With Postgres, a running task is leased to the
|
|
595
|
+
instance running it (renewed every 20 s; `taskLeaseMs`, default 60 s): if
|
|
596
|
+
that instance dies, another marks the task `failed` ("the server running
|
|
597
|
+
this task stopped") instead of a client polling it forever, and one turn
|
|
598
|
+
runs at a time per conversation across replicas. With Supabase only, or no
|
|
599
|
+
store, run one replica.
|
|
600
|
+
|
|
601
|
+
#### Posture at boot
|
|
602
|
+
|
|
603
|
+
Without `A2A_SERVER_SECRET` the server binds `127.0.0.1` only; binding
|
|
604
|
+
another `HOST` requires the secret or `ALLOW_UNAUTHENTICATED=true`. With
|
|
605
|
+
`PUBLIC_URL` set it refuses to start without the secret, with the
|
|
606
|
+
`.env.example` placeholder as the secret, or against an unhardened
|
|
607
|
+
database (unless `ALLOW_UNHARDENED_DB=true`). With durable storage it reads
|
|
608
|
+
`melchizedek_schema_version` and refuses to start on a database behind the
|
|
609
|
+
migrations it ships, naming `melchizedek-db apply` (or
|
|
610
|
+
`ALLOW_SCHEMA_MISMATCH=true` to start anyway); a database ahead of it only
|
|
611
|
+
warns, so a rolling deploy can migrate first. Storage through supabase-js
|
|
612
|
+
(Supabase credentials without `DATABASE_URL`) logs a deprecation notice:
|
|
613
|
+
set `DATABASE_URL` to the database's Postgres connection string.
|
|
614
|
+
Conversation content is not printed to stdout unless
|
|
615
|
+
`OTEL_CONSOLE_SPANS=true`.
|
|
616
|
+
|
|
617
|
+
#### Calling remote agents
|
|
618
|
+
|
|
619
|
+
A subagent with `a2a_agent_url: https://other-team.example/billing` is a
|
|
620
|
+
REMOTE agent, speaking A2A 1.0 or 0.3 (the card decides): the orchestrator
|
|
621
|
+
delegates to it as to a local subagent (one `request` argument), and in
|
|
622
|
+
plan-dispatch it can be a route. Its card and
|
|
623
|
+
endpoint pass the SSRF guard (`ALLOW_PRIVATE_A2A=true` for local hosts);
|
|
624
|
+
credentials come from `A2A_AGENT_TOKENS`, a JSON map of host → bearer
|
|
625
|
+
token or host → headers, sent only over https (or to loopback). The same
|
|
626
|
+
local conversation keeps talking to the same remote conversation.
|
|
627
|
+
|
|
628
|
+
#### Deploying
|
|
629
|
+
|
|
630
|
+
`Dockerfile` builds the compiled server and runs it as a non-root user with
|
|
631
|
+
a health check; `compose.yaml` adds optional Ollama and Phoenix (traces).
|
|
632
|
+
`npx melchizedek-db print|apply|status` installs and checks the database.
|
|
633
|
+
Set `PUBLIC_URL`, `A2A_SERVER_SECRET` and the provider keys from your
|
|
634
|
+
secret manager; give the orchestrator's stop timeout at least
|
|
635
|
+
`A2A_SHUTDOWN_GRACE_MS`.
|
|
636
|
+
|
|
637
|
+
#### The agent registry
|
|
638
|
+
|
|
639
|
+
A file is the default home of a syndicate. The registry is for a definition
|
|
640
|
+
an operator changes without a redeploy: `registry:<id>` loads it, and so does
|
|
641
|
+
a bare id listed in `A2A_REGISTRY_AGENTS`. It lives in two tables (migration
|
|
642
|
+
`0005_agent_registry.sql`):
|
|
643
|
+
|
|
644
|
+
- `adk_agent_registry` — one row per id, the active definition. The server
|
|
645
|
+
reads only this table.
|
|
646
|
+
- `adk_agent_registry_versions` — every definition an id has held, numbered
|
|
647
|
+
per id, with its config hash, author, note and publish time. It is
|
|
648
|
+
append-only: updates and deletes are refused.
|
|
649
|
+
|
|
650
|
+
The database records a version on every write to the active table, whoever
|
|
651
|
+
makes it, so history does not depend on the tool. Writing a definition an id
|
|
652
|
+
already held re-activates that version rather than duplicating it, which is
|
|
653
|
+
how a rollback works.
|
|
654
|
+
|
|
655
|
+
```bash
|
|
656
|
+
npx melchizedek-registry publish config/agents/desk.yaml desk --note "tighter triage"
|
|
657
|
+
npx melchizedek-registry versions desk # * marks the active version
|
|
658
|
+
npx melchizedek-registry diff desk 3 # v3 against the active version
|
|
659
|
+
npx melchizedek-registry rollback desk 3 --note "v4 misroutes refunds"
|
|
660
|
+
npx melchizedek-registry retire desk --yes # stop serving; history stays
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
`publish` and `rollback` validate against the same schema the loader uses
|
|
664
|
+
before anything is written. The author recorded is
|
|
665
|
+
`MELCHIZEDEK_REGISTRY_AUTHOR`, else the local user name; a direct write
|
|
666
|
+
records the database role. `registry:<id>@<version>` loads one stored
|
|
667
|
+
version (as the boot syndicate or from code), so a version can be tried
|
|
668
|
+
before it is activated; the HTTP routes accept only active ids. A running
|
|
669
|
+
server caches each agent for its lifetime: **restart it after a publish or
|
|
670
|
+
a rollback.** The library is `melchizedek-agents/registry`.
|
|
671
|
+
|
|
672
|
+
A syndicate is versioned as a unit. `publish` reads every nested
|
|
673
|
+
`yaml_reference` the definition reaches (nested ones of nested ones too) from
|
|
674
|
+
the agents root, validates each, and stores them with it under
|
|
675
|
+
`bundled_references`; the server loads nested syndicates of that version from
|
|
676
|
+
the bundle, never from files that may have changed since. Change a nested file
|
|
677
|
+
and the parent changes only when it is published again. `--no-bundle` stores
|
|
678
|
+
the references bare, to resolve from files at run time; a version published
|
|
679
|
+
before bundling existed behaves that way too.
|
|
384
680
|
|
|
385
681
|
### Plan-dispatch routing (`dispatch:`) — the second orchestration method
|
|
386
682
|
|
|
@@ -527,22 +823,46 @@ interface, register it behind a model-id prefix.
|
|
|
527
823
|
subagent. `scripts/demo_mcp_server.ts` is a complete server to copy —
|
|
528
824
|
tool definitions, SSE wiring, and persistent state in ~250 lines.
|
|
529
825
|
|
|
826
|
+
**Teach your coding agent the framework**: `skills/` holds six Agent
|
|
827
|
+
Skills (the open SKILL.md standard — a directory per skill, `name` and
|
|
828
|
+
`description` frontmatter) that give Claude Code, Codex, Cursor,
|
|
829
|
+
OpenCode or Gemini CLI the catalog of syndicates and the procedures in
|
|
830
|
+
this document: `melchizedek` (find and run, delegate a task),
|
|
831
|
+
`melchizedek-author`, `melchizedek-serve`, `melchizedek-memory`,
|
|
832
|
+
`melchizedek-models`, `melchizedek-scribe`. `npx melchizedek-skills
|
|
833
|
+
install` copies them into `.claude/skills/` and `.agents/skills/` of the
|
|
834
|
+
current project (`--for claude,codex,cursor,opencode,gemini,agents,all`,
|
|
835
|
+
`--global`, `--dir <path>`, `--only <names>`, `--force`, `--dry-run`;
|
|
836
|
+
`npm run skills:install` in a clone). The installer (`lib/skills.ts`)
|
|
837
|
+
copies only from the package's own `skills/` directory, follows no
|
|
838
|
+
symlinks, and never overwrites a differing file without `--force`. Each
|
|
839
|
+
SKILL.md body was written by the Scribe syndicate
|
|
840
|
+
(`config/agents/examples/scribe.yaml`) from a brief of facts and reviewed
|
|
841
|
+
by a person; `skills/README.md` records the procedure.
|
|
842
|
+
|
|
530
843
|
## 8. Security notes
|
|
531
844
|
|
|
532
845
|
- **Secrets** live in `.env` only; `.env.example` documents every
|
|
533
|
-
variable
|
|
846
|
+
variable and ships no values (placeholders are ignored if copied in).
|
|
847
|
+
Nothing in the repo ships a key. Report vulnerabilities per SECURITY.md.
|
|
534
848
|
- **Database**: default Supabase leaves `public`-schema tables readable
|
|
535
849
|
by the anon key over REST. `db/hardening.sql` enables deny-by-default
|
|
536
850
|
RLS and revokes anon/authenticated privileges on every table it finds:
|
|
537
851
|
`adk_sessions`, `adk_memory_facts`, and — where they exist — the
|
|
538
|
-
optional `adk_telemetry` sink and `adk_agent_registry`
|
|
539
|
-
registry is worst of all: agent definitions writable with the anon key
|
|
852
|
+
optional `adk_telemetry` sink and `adk_agent_registry` with its version
|
|
853
|
+
history (an unprotected registry is worst of all: agent definitions writable with the anon key
|
|
540
854
|
means anyone can rewrite the instructions your server boots). The A2A
|
|
541
855
|
server verifies hardening at boot and is fatal on public deployments
|
|
542
856
|
without it. Note `service_role` bypasses RLS by design — the hardening
|
|
543
857
|
constrains the API surface, not the trusted server.
|
|
544
|
-
- **A2A**: bearer auth
|
|
545
|
-
`A2A_SERVER_SECRET`
|
|
858
|
+
- **A2A**: bearer auth, a failed-login limiter and a request rate limit
|
|
859
|
+
are built in; without `A2A_SERVER_SECRET` the server binds loopback only.
|
|
860
|
+
See §6 for the posture checks at boot.
|
|
861
|
+
- **Outbound fetches** (`web_extract`, MCP servers, remote A2A agents) pass
|
|
862
|
+
one SSRF guard (`lib/net/addressGuard.ts`): local names and non-public
|
|
863
|
+
addresses in every encoding are refused, and names are resolved and
|
|
864
|
+
refused when any address is non-public. DNS rebinding between the check
|
|
865
|
+
and the connection is the remaining, stated limit.
|
|
546
866
|
- **Image tools** write only under `outputs/`, and `inspect_image` reads
|
|
547
867
|
only from there.
|
|
548
868
|
- **MCP** is an outbound trust decision: `mcpToolFactory` blocks
|