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.
Files changed (111) hide show
  1. package/AGENT_SETUP.md +182 -0
  2. package/DOCUMENTATION.md +614 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +164 -0
  5. package/README.md +151 -0
  6. package/config/agents/README.md +27 -0
  7. package/config/agents/examples/ares.yaml +74 -0
  8. package/config/agents/examples/cartographers.yaml +115 -0
  9. package/config/agents/examples/claude.yaml +17 -0
  10. package/config/agents/examples/council.yaml +65 -0
  11. package/config/agents/examples/critic.yaml +74 -0
  12. package/config/agents/examples/delegation.yaml +29 -0
  13. package/config/agents/examples/hierarchical.yaml +26 -0
  14. package/config/agents/examples/image_production.yaml +112 -0
  15. package/config/agents/examples/librarian.yaml +75 -0
  16. package/config/agents/examples/model_zoo.yaml +81 -0
  17. package/config/agents/examples/patient_advocate.yaml +107 -0
  18. package/config/agents/examples/scriptorium.yaml +96 -0
  19. package/config/agents/examples/style_council.yaml +83 -0
  20. package/config/agents/examples/syndicate.yaml +34 -0
  21. package/config/agents/examples/tutor.yaml +66 -0
  22. package/config/agents/syndicateSchema.yaml +291 -0
  23. package/dist/lib/config.d.ts +108 -0
  24. package/dist/lib/config.js +110 -0
  25. package/dist/lib/dispatch.d.ts +133 -0
  26. package/dist/lib/dispatch.js +182 -0
  27. package/dist/lib/grounding.d.ts +28 -0
  28. package/dist/lib/grounding.js +61 -0
  29. package/dist/lib/index.d.ts +14 -0
  30. package/dist/lib/index.js +13 -0
  31. package/dist/lib/loadEnv.d.ts +21 -0
  32. package/dist/lib/loadEnv.js +58 -0
  33. package/dist/lib/loadSyndicate.d.ts +177 -0
  34. package/dist/lib/loadSyndicate.js +264 -0
  35. package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
  36. package/dist/lib/memory/supabaseMemoryService.js +499 -0
  37. package/dist/lib/models/claudeLlm.d.ts +93 -0
  38. package/dist/lib/models/claudeLlm.js +332 -0
  39. package/dist/lib/models/gptLlm.d.ts +110 -0
  40. package/dist/lib/models/gptLlm.js +497 -0
  41. package/dist/lib/models/grokLlm.d.ts +64 -0
  42. package/dist/lib/models/grokLlm.js +86 -0
  43. package/dist/lib/models/ollamaLlm.d.ts +105 -0
  44. package/dist/lib/models/ollamaLlm.js +134 -0
  45. package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
  46. package/dist/lib/models/openAiCompatibleLlm.js +331 -0
  47. package/dist/lib/models/providerMap.d.ts +28 -0
  48. package/dist/lib/models/providerMap.js +45 -0
  49. package/dist/lib/models/registry.d.ts +71 -0
  50. package/dist/lib/models/registry.js +159 -0
  51. package/dist/lib/models/schemaNormalize.d.ts +22 -0
  52. package/dist/lib/models/schemaNormalize.js +58 -0
  53. package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
  54. package/dist/lib/observability/supabaseSpanExporter.js +115 -0
  55. package/dist/lib/observability/tracer.d.ts +44 -0
  56. package/dist/lib/observability/tracer.js +242 -0
  57. package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
  58. package/dist/lib/persistence/supabaseProvider.js +88 -0
  59. package/dist/lib/session/supabaseSessionService.d.ts +26 -0
  60. package/dist/lib/session/supabaseSessionService.js +193 -0
  61. package/dist/lib/session/transcript.d.ts +193 -0
  62. package/dist/lib/session/transcript.js +309 -0
  63. package/dist/lib/toolRegistry.d.ts +15 -0
  64. package/dist/lib/toolRegistry.js +58 -0
  65. package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
  66. package/dist/lib/tools/collectionsSearchTool.js +86 -0
  67. package/dist/lib/tools/generateImageTool.d.ts +16 -0
  68. package/dist/lib/tools/generateImageTool.js +119 -0
  69. package/dist/lib/tools/inspectImageTool.d.ts +21 -0
  70. package/dist/lib/tools/inspectImageTool.js +97 -0
  71. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  72. package/dist/lib/tools/mcpToolFactory.js +116 -0
  73. package/dist/lib/tools/toolContract.d.ts +78 -0
  74. package/dist/lib/tools/toolContract.js +129 -0
  75. package/dist/lib/tools/webExtractTool.d.ts +70 -0
  76. package/dist/lib/tools/webExtractTool.js +461 -0
  77. package/dist/lib/tools/webSearchTool.d.ts +53 -0
  78. package/dist/lib/tools/webSearchTool.js +126 -0
  79. package/dist/lib/tools/wikiTools.d.ts +112 -0
  80. package/dist/lib/tools/wikiTools.js +697 -0
  81. package/dist/lib/tools/xSearchTool.d.ts +61 -0
  82. package/dist/lib/tools/xSearchTool.js +125 -0
  83. package/dist/lib/wiki/agentRun.d.ts +39 -0
  84. package/dist/lib/wiki/agentRun.js +79 -0
  85. package/dist/lib/wiki/builder.d.ts +71 -0
  86. package/dist/lib/wiki/builder.js +165 -0
  87. package/dist/lib/wiki/dive.d.ts +56 -0
  88. package/dist/lib/wiki/dive.js +208 -0
  89. package/dist/lib/wiki/entities.d.ts +220 -0
  90. package/dist/lib/wiki/entities.js +661 -0
  91. package/dist/lib/wiki/extract.d.ts +97 -0
  92. package/dist/lib/wiki/extract.js +305 -0
  93. package/dist/lib/wiki/fill.d.ts +37 -0
  94. package/dist/lib/wiki/fill.js +175 -0
  95. package/dist/lib/wiki/format.d.ts +132 -0
  96. package/dist/lib/wiki/format.js +177 -0
  97. package/dist/lib/wiki/graph.d.ts +80 -0
  98. package/dist/lib/wiki/graph.js +147 -0
  99. package/dist/lib/wiki/lint.d.ts +58 -0
  100. package/dist/lib/wiki/lint.js +267 -0
  101. package/dist/lib/wiki/markdown.d.ts +127 -0
  102. package/dist/lib/wiki/markdown.js +336 -0
  103. package/dist/lib/wiki/pipeline.d.ts +69 -0
  104. package/dist/lib/wiki/pipeline.js +119 -0
  105. package/dist/lib/wiki/vault.d.ts +84 -0
  106. package/dist/lib/wiki/vault.js +194 -0
  107. package/dist/scripts/a2a_server.d.ts +2 -0
  108. package/dist/scripts/a2a_server.js +997 -0
  109. package/dist/scripts/syndicate_chat.d.ts +2 -0
  110. package/dist/scripts/syndicate_chat.js +485 -0
  111. 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
+ }