melchizedek-agents 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_SETUP.md +182 -0
- package/DOCUMENTATION.md +614 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +164 -0
- package/README.md +151 -0
- package/config/agents/README.md +27 -0
- package/config/agents/examples/ares.yaml +74 -0
- package/config/agents/examples/cartographers.yaml +115 -0
- package/config/agents/examples/claude.yaml +17 -0
- package/config/agents/examples/council.yaml +65 -0
- package/config/agents/examples/critic.yaml +74 -0
- package/config/agents/examples/delegation.yaml +29 -0
- package/config/agents/examples/hierarchical.yaml +26 -0
- package/config/agents/examples/image_production.yaml +112 -0
- package/config/agents/examples/librarian.yaml +75 -0
- package/config/agents/examples/model_zoo.yaml +81 -0
- package/config/agents/examples/patient_advocate.yaml +107 -0
- package/config/agents/examples/scriptorium.yaml +96 -0
- package/config/agents/examples/style_council.yaml +83 -0
- package/config/agents/examples/syndicate.yaml +34 -0
- package/config/agents/examples/tutor.yaml +66 -0
- package/config/agents/syndicateSchema.yaml +291 -0
- package/dist/lib/config.d.ts +108 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/dispatch.d.ts +133 -0
- package/dist/lib/dispatch.js +182 -0
- package/dist/lib/grounding.d.ts +28 -0
- package/dist/lib/grounding.js +61 -0
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/loadEnv.d.ts +21 -0
- package/dist/lib/loadEnv.js +58 -0
- package/dist/lib/loadSyndicate.d.ts +177 -0
- package/dist/lib/loadSyndicate.js +264 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
- package/dist/lib/memory/supabaseMemoryService.js +499 -0
- package/dist/lib/models/claudeLlm.d.ts +93 -0
- package/dist/lib/models/claudeLlm.js +332 -0
- package/dist/lib/models/gptLlm.d.ts +110 -0
- package/dist/lib/models/gptLlm.js +497 -0
- package/dist/lib/models/grokLlm.d.ts +64 -0
- package/dist/lib/models/grokLlm.js +86 -0
- package/dist/lib/models/ollamaLlm.d.ts +105 -0
- package/dist/lib/models/ollamaLlm.js +134 -0
- package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
- package/dist/lib/models/openAiCompatibleLlm.js +331 -0
- package/dist/lib/models/providerMap.d.ts +28 -0
- package/dist/lib/models/providerMap.js +45 -0
- package/dist/lib/models/registry.d.ts +71 -0
- package/dist/lib/models/registry.js +159 -0
- package/dist/lib/models/schemaNormalize.d.ts +22 -0
- package/dist/lib/models/schemaNormalize.js +58 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
- package/dist/lib/observability/supabaseSpanExporter.js +115 -0
- package/dist/lib/observability/tracer.d.ts +44 -0
- package/dist/lib/observability/tracer.js +242 -0
- package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
- package/dist/lib/persistence/supabaseProvider.js +88 -0
- package/dist/lib/session/supabaseSessionService.d.ts +26 -0
- package/dist/lib/session/supabaseSessionService.js +193 -0
- package/dist/lib/session/transcript.d.ts +193 -0
- package/dist/lib/session/transcript.js +309 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +58 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
- package/dist/lib/tools/collectionsSearchTool.js +86 -0
- package/dist/lib/tools/generateImageTool.d.ts +16 -0
- package/dist/lib/tools/generateImageTool.js +119 -0
- package/dist/lib/tools/inspectImageTool.d.ts +21 -0
- package/dist/lib/tools/inspectImageTool.js +97 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +116 -0
- package/dist/lib/tools/toolContract.d.ts +78 -0
- package/dist/lib/tools/toolContract.js +129 -0
- package/dist/lib/tools/webExtractTool.d.ts +70 -0
- package/dist/lib/tools/webExtractTool.js +461 -0
- package/dist/lib/tools/webSearchTool.d.ts +53 -0
- package/dist/lib/tools/webSearchTool.js +126 -0
- package/dist/lib/tools/wikiTools.d.ts +112 -0
- package/dist/lib/tools/wikiTools.js +697 -0
- package/dist/lib/tools/xSearchTool.d.ts +61 -0
- package/dist/lib/tools/xSearchTool.js +125 -0
- package/dist/lib/wiki/agentRun.d.ts +39 -0
- package/dist/lib/wiki/agentRun.js +79 -0
- package/dist/lib/wiki/builder.d.ts +71 -0
- package/dist/lib/wiki/builder.js +165 -0
- package/dist/lib/wiki/dive.d.ts +56 -0
- package/dist/lib/wiki/dive.js +208 -0
- package/dist/lib/wiki/entities.d.ts +220 -0
- package/dist/lib/wiki/entities.js +661 -0
- package/dist/lib/wiki/extract.d.ts +97 -0
- package/dist/lib/wiki/extract.js +305 -0
- package/dist/lib/wiki/fill.d.ts +37 -0
- package/dist/lib/wiki/fill.js +175 -0
- package/dist/lib/wiki/format.d.ts +132 -0
- package/dist/lib/wiki/format.js +177 -0
- package/dist/lib/wiki/graph.d.ts +80 -0
- package/dist/lib/wiki/graph.js +147 -0
- package/dist/lib/wiki/lint.d.ts +58 -0
- package/dist/lib/wiki/lint.js +267 -0
- package/dist/lib/wiki/markdown.d.ts +127 -0
- package/dist/lib/wiki/markdown.js +336 -0
- package/dist/lib/wiki/pipeline.d.ts +69 -0
- package/dist/lib/wiki/pipeline.js +119 -0
- package/dist/lib/wiki/vault.d.ts +84 -0
- package/dist/lib/wiki/vault.js +194 -0
- package/dist/scripts/a2a_server.d.ts +2 -0
- package/dist/scripts/a2a_server.js +997 -0
- package/dist/scripts/syndicate_chat.d.ts +2 -0
- package/dist/scripts/syndicate_chat.js +485 -0
- package/package.json +96 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config.ts — Framework-level configuration constants.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Previously, model identifiers were hardcoded inside individual service
|
|
6
|
+
* files, making them invisible to contributors and impossible to change
|
|
7
|
+
* without hunting through source.
|
|
8
|
+
*
|
|
9
|
+
* Centralising them here creates a single, obvious place to:
|
|
10
|
+
* 1. Swap the default inference model used by the A2A server
|
|
11
|
+
* 2. Change summary/extraction model without touching service logic
|
|
12
|
+
* 3. Understand the full set of third-party model dependencies at a glance
|
|
13
|
+
*
|
|
14
|
+
* These are intentionally plain constants — not env vars — because model
|
|
15
|
+
* IDs are code decisions, not deployment secrets. If you want env-var
|
|
16
|
+
* overrides, wrap the const: `process.env.GEMINI_MODEL ?? DEFAULT_GEMINI_MODEL`.
|
|
17
|
+
*/
|
|
18
|
+
// ── Inference Models ──────────────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Default Gemini model used by the A2A server when no model is specified
|
|
21
|
+
* in the caller's YAML or request.
|
|
22
|
+
*
|
|
23
|
+
* Use gemini-3.1-flash-lite for lightweight/cost-efficient loads (subagents,
|
|
24
|
+
* data retrieval). Use gemini-3.7-flash in YAML for production orchestrators.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: gemini-2.5-flash is known incompatible with this framework's
|
|
27
|
+
* includeServerSideToolInvocations flag on AI Studio Tier 1 — do not use it
|
|
28
|
+
* as the default. See DOCUMENTATION.md §7.0 for the full compatibility table.
|
|
29
|
+
*
|
|
30
|
+
* Supported Gemini identifiers: https://ai.google.dev/gemini-api/docs/models
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_GEMINI_MODEL = 'gemini-3.1-flash-lite';
|
|
33
|
+
/**
|
|
34
|
+
* Default Claude model used by the A2A server when a claude-* model is
|
|
35
|
+
* requested but no specific identifier is provided.
|
|
36
|
+
*
|
|
37
|
+
* Requires ANTHROPIC_API_KEY to be set. The ClaudeLlm provider in
|
|
38
|
+
* lib/models/claudeLlm.ts handles routing automatically via LLMRegistry.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
|
|
41
|
+
/**
|
|
42
|
+
* Default OpenAI model used when a gpt-* model is requested but no specific
|
|
43
|
+
* identifier is provided. Requires OPENAI_API_KEY. gpt-5-mini is the
|
|
44
|
+
* lightweight reasoning tier — enough for subagent work, and it exposes
|
|
45
|
+
* reasoning summaries via the Responses API (lib/models/gptLlm.ts).
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_GPT_MODEL = 'gpt-5-mini';
|
|
48
|
+
/**
|
|
49
|
+
* Default xAI model used when a grok-* model is requested but no specific
|
|
50
|
+
* identifier is provided. Requires XAI_API_KEY (lib/models/grokLlm.ts).
|
|
51
|
+
* grok-4.5 is a reasoning model (reasoning cannot be disabled); it returns
|
|
52
|
+
* reasoning summaries, which the adapter surfaces as thinking.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_GROK_MODEL = 'grok-4.5';
|
|
55
|
+
/**
|
|
56
|
+
* Reasoning effort sent with grok-4.5 requests: 'low' | 'medium' | 'high'
|
|
57
|
+
* (xAI's own default is 'high'). Pinned to MEDIUM — deeper than low
|
|
58
|
+
* without high's latency and token cost. Source: docs.x.ai › Model
|
|
59
|
+
* capabilities › Text › Reasoning › Effort levels. Older grok ids don't
|
|
60
|
+
* accept the param and never receive it (lib/models/grokLlm.ts).
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_GROK_REASONING_EFFORT = 'medium';
|
|
63
|
+
/**
|
|
64
|
+
* Default open-weight model, served locally by Ollama (lib/models/ollamaLlm.ts).
|
|
65
|
+
* qwen3:8b is the smallest pulled model that supports tool calling — the
|
|
66
|
+
* floor capability for syndicate delegation. Vision work uses ollama/qwen3-vl:8b.
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_OLLAMA_MODEL = 'ollama/qwen3:8b';
|
|
69
|
+
/**
|
|
70
|
+
* Model used by SupabaseVectorMemoryService to extract discrete facts
|
|
71
|
+
* from a session transcript before embedding them.
|
|
72
|
+
*
|
|
73
|
+
* Must be a text generation model (not an embedding model).
|
|
74
|
+
*/
|
|
75
|
+
export const MEMORY_EXTRACTION_MODEL = 'gemini-3.7-flash';
|
|
76
|
+
/**
|
|
77
|
+
* Default model for wiki agent operations (wiki_query, wiki_garden, and the
|
|
78
|
+
* gap-fill pass in lib/wiki/fill.ts). Prose quality matters more than
|
|
79
|
+
* latency here, so this sits one tier above the subagent default. Any
|
|
80
|
+
* provider works — override per call or via WIKI_AGENT_MODEL env
|
|
81
|
+
* ('claude-*', 'ollama/*', …); routing goes through lib/models/registry.ts.
|
|
82
|
+
*/
|
|
83
|
+
export const WIKI_AGENT_MODEL = 'gemini-3.7-flash';
|
|
84
|
+
// ── Embedding Models ──────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Model used by SupabaseVectorMemoryService to produce vector embeddings
|
|
87
|
+
* for semantic memory storage and retrieval.
|
|
88
|
+
*
|
|
89
|
+
* ⚠ IMPORTANT CONSTRAINT: The output dimensionality set below MUST match the
|
|
90
|
+
* dimensionality of the Supabase pgvector index you have provisioned. The index
|
|
91
|
+
* is created once and cannot be resized. The default (768) fits within
|
|
92
|
+
* Supabase's free-tier limits and is compatible with the match_memory_facts
|
|
93
|
+
* RPC defined in the database schema.
|
|
94
|
+
*
|
|
95
|
+
* If you switch embedding models, you must:
|
|
96
|
+
* 1. Update EMBEDDING_MODEL to the new model identifier.
|
|
97
|
+
* 2. Update EMBEDDING_DIMENSIONS to match the new model's output.
|
|
98
|
+
* 3. Drop and recreate your adk_memory_facts table and ivfflat index.
|
|
99
|
+
* 4. Re-ingest all existing facts (old vectors are dimensionally incompatible).
|
|
100
|
+
*
|
|
101
|
+
* Gemini options:
|
|
102
|
+
* 'gemini-embedding-001' → supports outputDimensionality up to 3072
|
|
103
|
+
* 'text-embedding-004' → supports outputDimensionality up to 768
|
|
104
|
+
*/
|
|
105
|
+
export const EMBEDDING_MODEL = 'gemini-embedding-001';
|
|
106
|
+
/**
|
|
107
|
+
* Output dimensionality for the embedding model.
|
|
108
|
+
* Must match the Supabase pgvector index dimension. Default: 768.
|
|
109
|
+
*/
|
|
110
|
+
export const EMBEDDING_DIMENSIONS = 768;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-dispatch routing — the second orchestration method.
|
|
3
|
+
*
|
|
4
|
+
* ── The two methods ───────────────────────────────────────────────────────
|
|
5
|
+
* DELEGATE (default, every syndicate without a `dispatch:` block): subagents
|
|
6
|
+
* are AgentTools. The orchestrator calls one, receives its answer as a tool
|
|
7
|
+
* response, and must then RE-EMIT that answer as its own text to close the
|
|
8
|
+
* turn. Classification is implicit in which tool it calls.
|
|
9
|
+
*
|
|
10
|
+
* PLAN-DISPATCH (`dispatch:` block present): the orchestrator holds NO
|
|
11
|
+
* subagent tools at all. It is a pure classifier with an `outputSchema` that
|
|
12
|
+
* names one route. Code reads that name, selects the matching subagent (or
|
|
13
|
+
* nested syndicate) and runs it directly — its output IS the task's answer.
|
|
14
|
+
*
|
|
15
|
+
* ── Why the second method exists ──────────────────────────────────────────
|
|
16
|
+
* The relay turn in DELEGATE mode is a whole LLM call whose only job is to
|
|
17
|
+
* copy text it was explicitly forbidden to edit, and in production it was the
|
|
18
|
+
* least reliable step in the chain: observed failures included finishing with
|
|
19
|
+
* zero output tokens (blank reply) and emitting the bare tool name
|
|
20
|
+
* ("FinancialAnalyst") in place of a 2,599-character answer. PLAN-DISPATCH
|
|
21
|
+
* removes the failure by construction — there is no relay to fail — and drops
|
|
22
|
+
* one LLM round-trip per request, since the router now emits ~15 tokens of
|
|
23
|
+
* JSON instead of re-typing the specialist's whole answer.
|
|
24
|
+
*
|
|
25
|
+
* It also makes the hand-off legible: the route is a value in code, so it can
|
|
26
|
+
* be logged, traced, and streamed to a waiting user as progress. In DELEGATE
|
|
27
|
+
* mode the choice is only ever implicit in a tool call.
|
|
28
|
+
*
|
|
29
|
+
* ── Why the router MUST be tool-less ──────────────────────────────────────
|
|
30
|
+
* ADK refuses to combine `outputSchema` with agent transfer/AgentTool on the
|
|
31
|
+
* same agent (see config/agents/critic.yaml — an orchestrator holding both
|
|
32
|
+
* deadlocks). That constraint is what shapes this design rather than limiting
|
|
33
|
+
* it: the classifier is a leaf, and dispatch happens in code where it belongs.
|
|
34
|
+
*
|
|
35
|
+
* ── Fail-static ───────────────────────────────────────────────────────────
|
|
36
|
+
* Every failure mode here resolves to `default_route` and answers the user.
|
|
37
|
+
* A router that returns malformed JSON, an unknown name, an empty string, or
|
|
38
|
+
* throws outright must never cost the user their reply — the default route is
|
|
39
|
+
* the one that can answer anything (for the financial router, the full
|
|
40
|
+
* consultant). Routing is an optimisation; answering is the contract.
|
|
41
|
+
*/
|
|
42
|
+
import type { SyndicateYamlConfig } from './loadSyndicate.ts';
|
|
43
|
+
/** The `dispatch:` block. Its presence switches a syndicate to plan-dispatch. */
|
|
44
|
+
export interface DispatchConfig {
|
|
45
|
+
/**
|
|
46
|
+
* Subagent name used whenever routing does not produce a usable answer —
|
|
47
|
+
* malformed output, an unknown route, or a router that failed outright.
|
|
48
|
+
* Must name a subagent that can handle ANY input.
|
|
49
|
+
*/
|
|
50
|
+
default_route: string;
|
|
51
|
+
/**
|
|
52
|
+
* Property of the router's JSON holding the chosen route. Default "route".
|
|
53
|
+
*/
|
|
54
|
+
route_key?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Property holding the router's short justification, surfaced as progress
|
|
57
|
+
* telemetry while the user waits. Default "reason". Optional in the schema.
|
|
58
|
+
*/
|
|
59
|
+
reason_key?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Deterministic pre-classification. Each entry pins a route for messages
|
|
62
|
+
* whose text matches `pattern`; the first match wins and the classifier
|
|
63
|
+
* never runs. For facts the message CONTAINS rather than judgments about
|
|
64
|
+
* what it means — a URL only one specialist can open, an explicit demand
|
|
65
|
+
* for a named tool. Never for topic guesses: a keyword is not a subject.
|
|
66
|
+
*/
|
|
67
|
+
route_overrides?: RouteOverride[];
|
|
68
|
+
}
|
|
69
|
+
/** One deterministic routing rule, evaluated against the user's message. */
|
|
70
|
+
export interface RouteOverride {
|
|
71
|
+
/** Subagent to run on a match. Ignored (with a warning) if undeclared. */
|
|
72
|
+
route: string;
|
|
73
|
+
/** JS regular expression source, tested against the raw message text. */
|
|
74
|
+
pattern: string;
|
|
75
|
+
/** Regex flags. Default "i". */
|
|
76
|
+
flags?: string;
|
|
77
|
+
/** Shown to the waiting user in place of the classifier's `reason`. */
|
|
78
|
+
reason?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface RouteResolution {
|
|
81
|
+
/** Name of the subagent to run. Always a real subagent. */
|
|
82
|
+
route: string;
|
|
83
|
+
/** The router's stated justification, or '' when it gave none. */
|
|
84
|
+
reason: string;
|
|
85
|
+
/** True when routing failed and `default_route` was substituted. */
|
|
86
|
+
fellBack: boolean;
|
|
87
|
+
/** Why the fallback happened — for logs. Empty when routing succeeded. */
|
|
88
|
+
fallbackReason: string;
|
|
89
|
+
/** True when a `route_overrides` rule pinned this route without a model. */
|
|
90
|
+
viaOverride: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** True when this syndicate opts into plan-dispatch. */
|
|
93
|
+
export declare function isDispatchSyndicate(config: SyndicateYamlConfig): config is SyndicateYamlConfig & {
|
|
94
|
+
dispatch: DispatchConfig;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Turn the router's raw output into a route that definitely exists.
|
|
98
|
+
*
|
|
99
|
+
* Pure and synchronous — the whole routing contract is unit-testable offline
|
|
100
|
+
* (tests/dispatch.test.ts) with no model, no network and no keys.
|
|
101
|
+
*
|
|
102
|
+
* @param rawOutput The router agent's text output (expected: one JSON object)
|
|
103
|
+
* @param config The dispatch syndicate's config
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveRoute(rawOutput: string, config: SyndicateYamlConfig & {
|
|
106
|
+
dispatch: DispatchConfig;
|
|
107
|
+
}): RouteResolution;
|
|
108
|
+
/**
|
|
109
|
+
* Deterministic routing, evaluated BEFORE the classifier runs.
|
|
110
|
+
*
|
|
111
|
+
* Some routes are decided by what a message CONTAINS, not by what a model
|
|
112
|
+
* judges it to be about. An x.com link is the motivating case: XScout is the
|
|
113
|
+
* only route that can open X, so a message carrying one has exactly one
|
|
114
|
+
* correct destination — yet a flash-lite classifier reading "analyze this
|
|
115
|
+
* post and tell me if the solution is quantum <link>" saw an analysis
|
|
116
|
+
* request and chose the consultant, which answered from web knowledge and
|
|
117
|
+
* said the post could not be read (2026-08-15, twice, with the routing rule
|
|
118
|
+
* already stating the opposite). Prompt text competes with the rest of the
|
|
119
|
+
* message; a regex does not.
|
|
120
|
+
*
|
|
121
|
+
* Scope discipline: overrides are for facts in the text, never topic
|
|
122
|
+
* guesses. A misapplied override is worse than a misroute — it cannot be
|
|
123
|
+
* reasoned around by the classifier at all.
|
|
124
|
+
*
|
|
125
|
+
* Pure and synchronous, like resolveRoute. Fail-static: an override naming
|
|
126
|
+
* an undeclared subagent or carrying an invalid regex is SKIPPED (reported
|
|
127
|
+
* via `warnings`), leaving normal classification to answer the user.
|
|
128
|
+
*
|
|
129
|
+
* @returns the pinned resolution, or null when nothing matched.
|
|
130
|
+
*/
|
|
131
|
+
export declare function matchRouteOverride(messageText: string, config: SyndicateYamlConfig & {
|
|
132
|
+
dispatch: DispatchConfig;
|
|
133
|
+
}, warnings?: string[]): RouteResolution | null;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-dispatch routing — the second orchestration method.
|
|
3
|
+
*
|
|
4
|
+
* ── The two methods ───────────────────────────────────────────────────────
|
|
5
|
+
* DELEGATE (default, every syndicate without a `dispatch:` block): subagents
|
|
6
|
+
* are AgentTools. The orchestrator calls one, receives its answer as a tool
|
|
7
|
+
* response, and must then RE-EMIT that answer as its own text to close the
|
|
8
|
+
* turn. Classification is implicit in which tool it calls.
|
|
9
|
+
*
|
|
10
|
+
* PLAN-DISPATCH (`dispatch:` block present): the orchestrator holds NO
|
|
11
|
+
* subagent tools at all. It is a pure classifier with an `outputSchema` that
|
|
12
|
+
* names one route. Code reads that name, selects the matching subagent (or
|
|
13
|
+
* nested syndicate) and runs it directly — its output IS the task's answer.
|
|
14
|
+
*
|
|
15
|
+
* ── Why the second method exists ──────────────────────────────────────────
|
|
16
|
+
* The relay turn in DELEGATE mode is a whole LLM call whose only job is to
|
|
17
|
+
* copy text it was explicitly forbidden to edit, and in production it was the
|
|
18
|
+
* least reliable step in the chain: observed failures included finishing with
|
|
19
|
+
* zero output tokens (blank reply) and emitting the bare tool name
|
|
20
|
+
* ("FinancialAnalyst") in place of a 2,599-character answer. PLAN-DISPATCH
|
|
21
|
+
* removes the failure by construction — there is no relay to fail — and drops
|
|
22
|
+
* one LLM round-trip per request, since the router now emits ~15 tokens of
|
|
23
|
+
* JSON instead of re-typing the specialist's whole answer.
|
|
24
|
+
*
|
|
25
|
+
* It also makes the hand-off legible: the route is a value in code, so it can
|
|
26
|
+
* be logged, traced, and streamed to a waiting user as progress. In DELEGATE
|
|
27
|
+
* mode the choice is only ever implicit in a tool call.
|
|
28
|
+
*
|
|
29
|
+
* ── Why the router MUST be tool-less ──────────────────────────────────────
|
|
30
|
+
* ADK refuses to combine `outputSchema` with agent transfer/AgentTool on the
|
|
31
|
+
* same agent (see config/agents/critic.yaml — an orchestrator holding both
|
|
32
|
+
* deadlocks). That constraint is what shapes this design rather than limiting
|
|
33
|
+
* it: the classifier is a leaf, and dispatch happens in code where it belongs.
|
|
34
|
+
*
|
|
35
|
+
* ── Fail-static ───────────────────────────────────────────────────────────
|
|
36
|
+
* Every failure mode here resolves to `default_route` and answers the user.
|
|
37
|
+
* A router that returns malformed JSON, an unknown name, an empty string, or
|
|
38
|
+
* throws outright must never cost the user their reply — the default route is
|
|
39
|
+
* the one that can answer anything (for the financial router, the full
|
|
40
|
+
* consultant). Routing is an optimisation; answering is the contract.
|
|
41
|
+
*/
|
|
42
|
+
/** True when this syndicate opts into plan-dispatch. */
|
|
43
|
+
export function isDispatchSyndicate(config) {
|
|
44
|
+
return !!config.dispatch && typeof config.dispatch.default_route === 'string';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Strip a markdown code fence if the model wrapped its JSON in one.
|
|
48
|
+
* `responseMimeType: application/json` normally prevents this, but the
|
|
49
|
+
* resolver stays tolerant so a provider that ignores the hint (or a future
|
|
50
|
+
* non-Gemini router) degrades to a correct route instead of the default.
|
|
51
|
+
*/
|
|
52
|
+
function unfence(raw) {
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
const fence = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
55
|
+
return fence ? fence[1].trim() : trimmed;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Match a model-supplied route name against the declared subagents.
|
|
59
|
+
* Comparison ignores case and non-alphanumerics, so "financial analyst",
|
|
60
|
+
* "FinancialAnalyst" and "financial_analyst" all reach the same subagent —
|
|
61
|
+
* the route is a name the model retypes, and punctuation drift is not a
|
|
62
|
+
* reason to lose the user's answer.
|
|
63
|
+
*/
|
|
64
|
+
function matchSubagent(candidate, subagents) {
|
|
65
|
+
const norm = (s) => s.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
66
|
+
const target = norm(candidate);
|
|
67
|
+
if (!target)
|
|
68
|
+
return undefined;
|
|
69
|
+
return subagents.find(sub => norm(sub.name) === target);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Turn the router's raw output into a route that definitely exists.
|
|
73
|
+
*
|
|
74
|
+
* Pure and synchronous — the whole routing contract is unit-testable offline
|
|
75
|
+
* (tests/dispatch.test.ts) with no model, no network and no keys.
|
|
76
|
+
*
|
|
77
|
+
* @param rawOutput The router agent's text output (expected: one JSON object)
|
|
78
|
+
* @param config The dispatch syndicate's config
|
|
79
|
+
*/
|
|
80
|
+
export function resolveRoute(rawOutput, config) {
|
|
81
|
+
const subagents = config.subagents || [];
|
|
82
|
+
const routeKey = config.dispatch.route_key || 'route';
|
|
83
|
+
const reasonKey = config.dispatch.reason_key || 'reason';
|
|
84
|
+
// The default must itself be a real subagent. When it is not, the config is
|
|
85
|
+
// wrong and the first subagent is the only safe answer left — surfaced as a
|
|
86
|
+
// fallbackReason so it reaches the logs rather than failing silently.
|
|
87
|
+
const declaredDefault = matchSubagent(config.dispatch.default_route, subagents);
|
|
88
|
+
const safeDefault = declaredDefault?.name ?? subagents[0]?.name ?? '';
|
|
89
|
+
const defaultResolution = (fallbackReason) => ({
|
|
90
|
+
route: safeDefault,
|
|
91
|
+
reason: '',
|
|
92
|
+
fellBack: true,
|
|
93
|
+
viaOverride: false,
|
|
94
|
+
fallbackReason: declaredDefault
|
|
95
|
+
? fallbackReason
|
|
96
|
+
: `${fallbackReason}; default_route '${config.dispatch.default_route}' is not a declared subagent`,
|
|
97
|
+
});
|
|
98
|
+
if (!rawOutput || !rawOutput.trim()) {
|
|
99
|
+
return defaultResolution('router returned no output');
|
|
100
|
+
}
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(unfence(rawOutput));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return defaultResolution(`router output was not JSON: ${rawOutput.trim().slice(0, 80)}`);
|
|
107
|
+
}
|
|
108
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
109
|
+
return defaultResolution('router output was not a JSON object');
|
|
110
|
+
}
|
|
111
|
+
const record = parsed;
|
|
112
|
+
const rawRoute = record[routeKey];
|
|
113
|
+
if (typeof rawRoute !== 'string' || !rawRoute.trim()) {
|
|
114
|
+
return defaultResolution(`router output has no '${routeKey}' string`);
|
|
115
|
+
}
|
|
116
|
+
const matched = matchSubagent(rawRoute, subagents);
|
|
117
|
+
if (!matched) {
|
|
118
|
+
return defaultResolution(`router chose unknown route '${rawRoute}'`);
|
|
119
|
+
}
|
|
120
|
+
const rawReason = record[reasonKey];
|
|
121
|
+
return {
|
|
122
|
+
route: matched.name,
|
|
123
|
+
reason: typeof rawReason === 'string' ? rawReason.trim() : '',
|
|
124
|
+
fellBack: false,
|
|
125
|
+
fallbackReason: '',
|
|
126
|
+
viaOverride: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Deterministic routing, evaluated BEFORE the classifier runs.
|
|
131
|
+
*
|
|
132
|
+
* Some routes are decided by what a message CONTAINS, not by what a model
|
|
133
|
+
* judges it to be about. An x.com link is the motivating case: XScout is the
|
|
134
|
+
* only route that can open X, so a message carrying one has exactly one
|
|
135
|
+
* correct destination — yet a flash-lite classifier reading "analyze this
|
|
136
|
+
* post and tell me if the solution is quantum <link>" saw an analysis
|
|
137
|
+
* request and chose the consultant, which answered from web knowledge and
|
|
138
|
+
* said the post could not be read (2026-08-15, twice, with the routing rule
|
|
139
|
+
* already stating the opposite). Prompt text competes with the rest of the
|
|
140
|
+
* message; a regex does not.
|
|
141
|
+
*
|
|
142
|
+
* Scope discipline: overrides are for facts in the text, never topic
|
|
143
|
+
* guesses. A misapplied override is worse than a misroute — it cannot be
|
|
144
|
+
* reasoned around by the classifier at all.
|
|
145
|
+
*
|
|
146
|
+
* Pure and synchronous, like resolveRoute. Fail-static: an override naming
|
|
147
|
+
* an undeclared subagent or carrying an invalid regex is SKIPPED (reported
|
|
148
|
+
* via `warnings`), leaving normal classification to answer the user.
|
|
149
|
+
*
|
|
150
|
+
* @returns the pinned resolution, or null when nothing matched.
|
|
151
|
+
*/
|
|
152
|
+
export function matchRouteOverride(messageText, config, warnings) {
|
|
153
|
+
const overrides = config.dispatch.route_overrides || [];
|
|
154
|
+
if (!overrides.length || !messageText)
|
|
155
|
+
return null;
|
|
156
|
+
const subagents = config.subagents || [];
|
|
157
|
+
for (const override of overrides) {
|
|
158
|
+
const matched = matchSubagent(override.route ?? '', subagents);
|
|
159
|
+
if (!matched) {
|
|
160
|
+
warnings?.push(`route_overrides: '${override.route}' is not a declared subagent — rule skipped`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let re;
|
|
164
|
+
try {
|
|
165
|
+
re = new RegExp(override.pattern, override.flags ?? 'i');
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
warnings?.push(`route_overrides: invalid pattern for '${override.route}' — rule skipped`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!re.test(messageText))
|
|
172
|
+
continue;
|
|
173
|
+
return {
|
|
174
|
+
route: matched.name,
|
|
175
|
+
reason: (override.reason || '').trim(),
|
|
176
|
+
fellBack: false,
|
|
177
|
+
fallbackReason: '',
|
|
178
|
+
viaOverride: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/grounding.ts — pure helpers for reading native web-search grounding
|
|
3
|
+
* (Gemini `groundingMetadata`) off ADK events. Grounding is a server-side
|
|
4
|
+
* tool: it never appears as a function call, so without this a searched
|
|
5
|
+
* answer and a recalled one are indistinguishable in logs and to clients.
|
|
6
|
+
* scripts/a2a_server.ts folds these into `[A2A] ⌕ Grounding` log lines and
|
|
7
|
+
* the `Invoking tool: web_search` / `Web sources: …` status lines that
|
|
8
|
+
* nihilistic-penguin's RouteTrace renders into the Discord footer.
|
|
9
|
+
*/
|
|
10
|
+
export interface GroundingState {
|
|
11
|
+
queries: Set<string>;
|
|
12
|
+
sources: Set<string>;
|
|
13
|
+
}
|
|
14
|
+
export declare function newGroundingState(): GroundingState;
|
|
15
|
+
/** Domain of a grounding chunk: Gemini's `web.title` is already the bare
|
|
16
|
+
* domain (its `uri` is an opaque redirect); fall back to the URI's host. */
|
|
17
|
+
export declare function groundingDomain(chunk: any): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Fold one event's groundingMetadata into `state`. Returns true when the
|
|
20
|
+
* event added something new (a query or a source not seen before) — the
|
|
21
|
+
* caller logs/publishes only then, so a metadata block repeated on every
|
|
22
|
+
* streaming chunk produces one line, not many.
|
|
23
|
+
*/
|
|
24
|
+
export declare function collectGrounding(event: any, state: GroundingState): boolean;
|
|
25
|
+
/** The `[A2A] ⌕ Grounding: …` log line body. */
|
|
26
|
+
export declare function describeGrounding(state: GroundingState, maxSources?: number): string;
|
|
27
|
+
/** The consumer contract line (penguin RouteTrace `_SOURCES_RE`), or null. */
|
|
28
|
+
export declare function webSourcesLine(state: GroundingState, max?: number): string | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/grounding.ts — pure helpers for reading native web-search grounding
|
|
3
|
+
* (Gemini `groundingMetadata`) off ADK events. Grounding is a server-side
|
|
4
|
+
* tool: it never appears as a function call, so without this a searched
|
|
5
|
+
* answer and a recalled one are indistinguishable in logs and to clients.
|
|
6
|
+
* scripts/a2a_server.ts folds these into `[A2A] ⌕ Grounding` log lines and
|
|
7
|
+
* the `Invoking tool: web_search` / `Web sources: …` status lines that
|
|
8
|
+
* nihilistic-penguin's RouteTrace renders into the Discord footer.
|
|
9
|
+
*/
|
|
10
|
+
export function newGroundingState() {
|
|
11
|
+
return { queries: new Set(), sources: new Set() };
|
|
12
|
+
}
|
|
13
|
+
/** Domain of a grounding chunk: Gemini's `web.title` is already the bare
|
|
14
|
+
* domain (its `uri` is an opaque redirect); fall back to the URI's host. */
|
|
15
|
+
export function groundingDomain(chunk) {
|
|
16
|
+
const title = chunk?.web?.title;
|
|
17
|
+
if (title && /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(title))
|
|
18
|
+
return title.toLowerCase();
|
|
19
|
+
const uri = chunk?.web?.uri;
|
|
20
|
+
if (uri) {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(uri).hostname.replace(/^www\./, '');
|
|
23
|
+
}
|
|
24
|
+
catch { /* opaque */ }
|
|
25
|
+
}
|
|
26
|
+
return title || null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fold one event's groundingMetadata into `state`. Returns true when the
|
|
30
|
+
* event added something new (a query or a source not seen before) — the
|
|
31
|
+
* caller logs/publishes only then, so a metadata block repeated on every
|
|
32
|
+
* streaming chunk produces one line, not many.
|
|
33
|
+
*/
|
|
34
|
+
export function collectGrounding(event, state) {
|
|
35
|
+
const gm = event?.groundingMetadata;
|
|
36
|
+
if (!gm)
|
|
37
|
+
return false;
|
|
38
|
+
const before = state.queries.size + state.sources.size;
|
|
39
|
+
for (const q of gm.webSearchQueries ?? []) {
|
|
40
|
+
if (typeof q === 'string' && q.trim())
|
|
41
|
+
state.queries.add(q.trim());
|
|
42
|
+
}
|
|
43
|
+
for (const c of gm.groundingChunks ?? []) {
|
|
44
|
+
const d = groundingDomain(c);
|
|
45
|
+
if (d)
|
|
46
|
+
state.sources.add(d);
|
|
47
|
+
}
|
|
48
|
+
return state.queries.size + state.sources.size > before;
|
|
49
|
+
}
|
|
50
|
+
/** The `[A2A] ⌕ Grounding: …` log line body. */
|
|
51
|
+
export function describeGrounding(state, maxSources = 8) {
|
|
52
|
+
const q = state.queries.size, s = state.sources.size;
|
|
53
|
+
const list = s ? ` (${[...state.sources].slice(0, maxSources).join(', ')})` : '';
|
|
54
|
+
return `${q} quer${q === 1 ? 'y' : 'ies'} · ${s} source${s === 1 ? '' : 's'}${list}`;
|
|
55
|
+
}
|
|
56
|
+
/** The consumer contract line (penguin RouteTrace `_SOURCES_RE`), or null. */
|
|
57
|
+
export function webSourcesLine(state, max = 10) {
|
|
58
|
+
if (!state.sources.size)
|
|
59
|
+
return null;
|
|
60
|
+
return `Web sources: ${[...state.sources].slice(0, max).join(', ')}`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/index.ts — the package's main entry (public repo only).
|
|
3
|
+
*
|
|
4
|
+
* This barrel IS the primary API surface of the `melchizedek-agents`
|
|
5
|
+
* package: what it re-exports is covered by semver; everything else is
|
|
6
|
+
* reachable only through the subpath exports declared in package.json.
|
|
7
|
+
* The cloned repo's npm scripts never import this file — they run the
|
|
8
|
+
* modules directly.
|
|
9
|
+
*/
|
|
10
|
+
export { loadSyndicate, loadSyndicateFromRegistry, parseCliBindings, validateRegistryConfig, } from './loadSyndicate.ts';
|
|
11
|
+
export type { AgentYamlConfig, LoadSyndicateOptions, SubagentYamlConfig, SyndicateYamlConfig, VariableMap, } from './loadSyndicate.ts';
|
|
12
|
+
export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, registerAvailableProviders, resolveModel, } from './models/registry.ts';
|
|
13
|
+
export { resolveTools } from './toolRegistry.ts';
|
|
14
|
+
export { loadEnv } from './loadEnv.ts';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/index.ts — the package's main entry (public repo only).
|
|
3
|
+
*
|
|
4
|
+
* This barrel IS the primary API surface of the `melchizedek-agents`
|
|
5
|
+
* package: what it re-exports is covered by semver; everything else is
|
|
6
|
+
* reachable only through the subpath exports declared in package.json.
|
|
7
|
+
* The cloned repo's npm scripts never import this file — they run the
|
|
8
|
+
* modules directly.
|
|
9
|
+
*/
|
|
10
|
+
export { loadSyndicate, loadSyndicateFromRegistry, parseCliBindings, validateRegistryConfig, } from "./loadSyndicate.js";
|
|
11
|
+
export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, registerAvailableProviders, resolveModel, } from "./models/registry.js";
|
|
12
|
+
export { resolveTools } from "./toolRegistry.js";
|
|
13
|
+
export { loadEnv } from "./loadEnv.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/loadEnv.ts — minimal, dependency-free .env loader.
|
|
3
|
+
*
|
|
4
|
+
* WHY this exists:
|
|
5
|
+
* The project runs via `node --experimental-strip-types` and intentionally
|
|
6
|
+
* avoids a runtime dependency on `dotenv`. Several scripts previously did
|
|
7
|
+
* `import 'dotenv/config'`, which throws because `dotenv` is not installed.
|
|
8
|
+
* This shared helper replicates the same parse used inline by a2a_server.ts
|
|
9
|
+
* and syndicate_chat.ts, so every entrypoint loads `.env` the same way.
|
|
10
|
+
*
|
|
11
|
+
* Existing process.env values always win — the file only fills in what's unset.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Populate process.env from the repo-root `.env` file.
|
|
15
|
+
*
|
|
16
|
+
* @param moduleUrl Pass `import.meta.url` from any entrypoint script; the
|
|
17
|
+
* repo root is found by walking up from the script to the nearest
|
|
18
|
+
* directory containing `package.json`, so scripts may live at any depth
|
|
19
|
+
* under `scripts/`. When omitted, `process.cwd()` is used.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadEnv(moduleUrl?: string): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/loadEnv.ts — minimal, dependency-free .env loader.
|
|
3
|
+
*
|
|
4
|
+
* WHY this exists:
|
|
5
|
+
* The project runs via `node --experimental-strip-types` and intentionally
|
|
6
|
+
* avoids a runtime dependency on `dotenv`. Several scripts previously did
|
|
7
|
+
* `import 'dotenv/config'`, which throws because `dotenv` is not installed.
|
|
8
|
+
* This shared helper replicates the same parse used inline by a2a_server.ts
|
|
9
|
+
* and syndicate_chat.ts, so every entrypoint loads `.env` the same way.
|
|
10
|
+
*
|
|
11
|
+
* Existing process.env values always win — the file only fills in what's unset.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
/**
|
|
17
|
+
* Populate process.env from the repo-root `.env` file.
|
|
18
|
+
*
|
|
19
|
+
* @param moduleUrl Pass `import.meta.url` from any entrypoint script; the
|
|
20
|
+
* repo root is found by walking up from the script to the nearest
|
|
21
|
+
* directory containing `package.json`, so scripts may live at any depth
|
|
22
|
+
* under `scripts/`. When omitted, `process.cwd()` is used.
|
|
23
|
+
*/
|
|
24
|
+
export function loadEnv(moduleUrl) {
|
|
25
|
+
try {
|
|
26
|
+
const root = moduleUrl
|
|
27
|
+
? findRepoRoot(dirname(fileURLToPath(moduleUrl)))
|
|
28
|
+
: process.cwd();
|
|
29
|
+
const raw = readFileSync(join(root, '.env'), 'utf-8');
|
|
30
|
+
for (const line of raw.split('\n')) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
33
|
+
continue;
|
|
34
|
+
const eq = trimmed.indexOf('=');
|
|
35
|
+
if (eq === -1)
|
|
36
|
+
continue;
|
|
37
|
+
const key = trimmed.slice(0, eq).trim();
|
|
38
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
39
|
+
if (!process.env[key])
|
|
40
|
+
process.env[key] = val;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// .env not present — rely on the ambient environment.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Nearest ancestor of `dir` containing package.json; `dir` itself if none. */
|
|
48
|
+
function findRepoRoot(dir) {
|
|
49
|
+
let current = dir;
|
|
50
|
+
while (true) {
|
|
51
|
+
if (existsSync(join(current, 'package.json')))
|
|
52
|
+
return current;
|
|
53
|
+
const parent = dirname(current);
|
|
54
|
+
if (parent === current)
|
|
55
|
+
return dir;
|
|
56
|
+
current = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|