melchizedek-agents 0.16.2 → 0.18.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 (100) hide show
  1. package/.env.example +40 -3
  2. package/AGENT_SETUP.md +4 -2
  3. package/CHANGELOG.md +246 -0
  4. package/DOCUMENTATION.md +311 -15
  5. package/QUICKSTART.md +2 -2
  6. package/README.md +125 -149
  7. package/config/agents/examples/harness.yaml +135 -0
  8. package/config/agents/examples/model_zoo.yaml +23 -9
  9. package/config/agents/examples/pipeline.yaml +120 -0
  10. package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
  11. package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
  12. package/config/agents/examples/weather.yaml +59 -0
  13. package/config/agents/syndicateSchema.yaml +125 -1
  14. package/db/hardening.sql +4 -3
  15. package/db/migrations/0011_erase_expired.sql +198 -0
  16. package/db/migrations/0012_audit_log.sql +98 -0
  17. package/dist/lib/a2a/app.d.ts +27 -0
  18. package/dist/lib/a2a/app.js +101 -5
  19. package/dist/lib/a2a/executor.d.ts +34 -4
  20. package/dist/lib/a2a/executor.js +109 -9
  21. package/dist/lib/compile.d.ts +41 -3
  22. package/dist/lib/compile.js +162 -17
  23. package/dist/lib/config.d.ts +24 -0
  24. package/dist/lib/config.js +24 -0
  25. package/dist/lib/doctor.js +17 -4
  26. package/dist/lib/index.d.ts +2 -0
  27. package/dist/lib/index.js +1 -0
  28. package/dist/lib/loadEnv.d.ts +5 -0
  29. package/dist/lib/loadEnv.js +7 -0
  30. package/dist/lib/loadSyndicate.d.ts +50 -0
  31. package/dist/lib/loadSyndicate.js +19 -3
  32. package/dist/lib/memory/erase.d.ts +5 -0
  33. package/dist/lib/memory/erase.js +5 -0
  34. package/dist/lib/memory/providers.d.ts +27 -0
  35. package/dist/lib/memory/providers.js +28 -0
  36. package/dist/lib/models/capabilities.js +10 -0
  37. package/dist/lib/models/endpoints.d.ts +1 -1
  38. package/dist/lib/models/endpoints.js +4 -1
  39. package/dist/lib/models/fallback.d.ts +41 -0
  40. package/dist/lib/models/fallback.js +105 -0
  41. package/dist/lib/models/gateway.d.ts +2 -2
  42. package/dist/lib/models/gateway.js +4 -4
  43. package/dist/lib/models/kimiLlm.d.ts +123 -0
  44. package/dist/lib/models/kimiLlm.js +171 -0
  45. package/dist/lib/models/providerMap.d.ts +2 -2
  46. package/dist/lib/models/providerMap.js +4 -1
  47. package/dist/lib/models/registry.d.ts +1 -1
  48. package/dist/lib/models/registry.js +11 -3
  49. package/dist/lib/net/redirects.d.ts +14 -0
  50. package/dist/lib/net/redirects.js +94 -0
  51. package/dist/lib/observability/audit.d.ts +21 -0
  52. package/dist/lib/observability/audit.js +39 -0
  53. package/dist/lib/observability/metrics.d.ts +6 -0
  54. package/dist/lib/observability/tracer.d.ts +14 -1
  55. package/dist/lib/observability/tracer.js +16 -2
  56. package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
  57. package/dist/lib/persistence/supabaseProvider.js +2 -31
  58. package/dist/lib/runtime/questions.d.ts +55 -0
  59. package/dist/lib/runtime/questions.js +107 -0
  60. package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
  61. package/dist/lib/runtime/syndicateTurn.js +165 -17
  62. package/dist/lib/storage/postgres/index.d.ts +9 -0
  63. package/dist/lib/storage/postgres/index.js +13 -0
  64. package/dist/lib/storage/rlsStatus.d.ts +45 -0
  65. package/dist/lib/storage/rlsStatus.js +69 -0
  66. package/dist/lib/syndicateSchema.d.ts +177 -0
  67. package/dist/lib/syndicateSchema.js +343 -3
  68. package/dist/lib/toolRegistry.js +9 -0
  69. package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
  70. package/dist/lib/tools/mcpToolFactory.js +61 -4
  71. package/dist/lib/tools/openapiTools.d.ts +52 -0
  72. package/dist/lib/tools/openapiTools.js +225 -0
  73. package/dist/lib/tools/skillToolset.d.ts +135 -0
  74. package/dist/lib/tools/skillToolset.js +247 -0
  75. package/dist/lib/tools/urlContextTool.d.ts +29 -0
  76. package/dist/lib/tools/urlContextTool.js +39 -0
  77. package/dist/lib/workflow.d.ts +72 -0
  78. package/dist/lib/workflow.js +180 -0
  79. package/dist/lib/workflowConfig.d.ts +95 -0
  80. package/dist/lib/workflowConfig.js +93 -0
  81. package/dist/scripts/a2a_server.js +51 -8
  82. package/dist/scripts/db.d.ts +9 -0
  83. package/dist/scripts/db.js +30 -15
  84. package/dist/scripts/syndicate_chat.js +39 -5
  85. package/package.json +6 -3
  86. package/skills/briefs/_shared.md +1 -1
  87. package/skills/briefs/melchizedek-author.md +8 -4
  88. package/skills/briefs/melchizedek-memory.md +1 -1
  89. package/skills/briefs/melchizedek-models.md +1 -1
  90. package/skills/briefs/melchizedek-scribe.md +4 -4
  91. package/skills/briefs/melchizedek-serve.md +1 -1
  92. package/skills/briefs/melchizedek.md +4 -1
  93. package/skills/melchizedek/SKILL.md +4 -1
  94. package/skills/melchizedek-author/SKILL.md +7 -3
  95. package/skills/melchizedek-memory/SKILL.md +1 -1
  96. package/skills/melchizedek-models/SKILL.md +1 -1
  97. package/skills/melchizedek-scribe/SKILL.md +3 -3
  98. package/skills/melchizedek-serve/SKILL.md +1 -1
  99. /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
  100. /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * lib/tools/urlContextTool.ts — `url_context`: the model reads the pages
3
+ * whose URLs appear in the conversation, server-side, on Gemini.
4
+ *
5
+ * WHY: ADK's URL_CONTEXT is Gemini's built-in URL reading, and it THROWS for
6
+ * any other model, which would fail a mixed-provider syndicate at request
7
+ * time. This is the same tool made safe to declare anywhere, the pattern
8
+ * web_search uses: on a Gemini model it adds `{ urlContext: {} }` to the
9
+ * request; on any other it does nothing, and `npm run doctor` reports it as
10
+ * dropped (lib/models/capabilities.ts). For reading a page on every
11
+ * provider, declare `web_extract` (client-side, SSRF-guarded).
12
+ *
13
+ * Exposure: Google fetches the URLs, not this host, so no address of this
14
+ * deployment's network is reachable through it. Page text arrives as model
15
+ * input; treat it as data, as any fetched page.
16
+ */
17
+ import { BaseTool } from '@google/adk';
18
+ import { providerForModel } from '../models/providerMap.js';
19
+ export const URL_CONTEXT_TOOL_NAME = 'url_context';
20
+ export class UrlContextTool extends BaseTool {
21
+ constructor() {
22
+ super({ name: URL_CONTEXT_TOOL_NAME, description: 'Gemini reads the pages at URLs in the conversation (server-side).' });
23
+ }
24
+ /** Never a client-side function tool. */
25
+ _getDeclaration() {
26
+ return undefined;
27
+ }
28
+ async runAsync() {
29
+ return Promise.resolve();
30
+ }
31
+ async processLlmRequest({ llmRequest }) {
32
+ if (!llmRequest.model || providerForModel(llmRequest.model) !== 'gemini')
33
+ return;
34
+ llmRequest.config = llmRequest.config ?? {};
35
+ llmRequest.config.tools = llmRequest.config.tools ?? [];
36
+ llmRequest.config.tools.push({ urlContext: {} });
37
+ }
38
+ }
39
+ export const URL_CONTEXT = new UrlContextTool();
@@ -0,0 +1,72 @@
1
+ /**
2
+ * lib/workflow.ts — the `workflow:` block: a syndicate as a graph.
3
+ *
4
+ * ── The third orchestration method ───────────────────────────────────────
5
+ * DELEGATE lets an orchestrator call subagents as tools; PLAN-DISPATCH
6
+ * (lib/dispatch.ts) lets a classifier pick ONE subagent to answer. Neither
7
+ * can say "run these two in parallel, join their outputs, review, ask the
8
+ * person, then publish". A `workflow:` block does: it is a graph whose
9
+ * nodes are the syndicate's own agents plus a few node kinds the engine
10
+ * supplies, and whose edges say what runs after what and on which route.
11
+ *
12
+ * workflow:
13
+ * edges:
14
+ * - [START, Planner, { article: [Writer, Checker], answer: Answerer }]
15
+ * - [[Writer, Checker], Both, Editor, Confirm, Publisher]
16
+ * nodes:
17
+ * Both: { join: true } # waits for every predecessor
18
+ * Confirm: { ask_user: "Publish? yes, or say what to change." }
19
+ * Planner: { route_key: "kind", retry: { max_attempts: 2 } }
20
+ *
21
+ * ADK 2.2 runs it (`Workflow`, `FunctionNode`, `JoinNode`, `ParallelWorker`,
22
+ * `ToolNode`, `RequestInput`): per-node retries and timeouts, fan-out and
23
+ * fan-in, a pause that waits for a person and resumes on the next message,
24
+ * and resumption from the session's own events. Every agent is already a
25
+ * node (`LlmAgent extends BaseNode`): a node agent receives the previous
26
+ * node's output as its user turn and, unless its YAML sets
27
+ * `includeContents`, sees nothing else of the conversation.
28
+ *
29
+ * ── What the engine adds to ADK ───────────────────────────────────────────
30
+ * ROUTING. ADK's TypeScript port never derives a route from an agent's
31
+ * output, so a routing map in YAML gets a hidden step after the agent
32
+ * (`<Agent>__route`) that reads the route from the output — the
33
+ * `route_key` property of a JSON output, else the trimmed text — and
34
+ * re-emits the output with it. A `default` key catches what no key
35
+ * matched.
36
+ * THE PAUSE. `ask_user` is a node that raises ADK's `RequestInput`. The
37
+ * turn ends `input-required` carrying the question (`result.input`); the
38
+ * next message on the conversation is the answer and becomes that node's
39
+ * output. The A2A server publishes the question; the chat prints it.
40
+ * NAMES. Nodes are addressed by their YAML names everywhere (edges, events,
41
+ * the wiki), including a `map` node, which ADK would otherwise name after
42
+ * the agent it wraps.
43
+ *
44
+ * ── Not in this version ───────────────────────────────────────────────────
45
+ * Approval gates (`require_approval`, `skills.scripts: local`) and remote
46
+ * `a2a_agent_url` subagents are refused inside a workflow by the schema: a
47
+ * gated call pauses a node through ADK's interrupt path, whose resume is not
48
+ * the one ADR 0028 built, and a remote agent is reachable only as a tool.
49
+ * Both are open for a later record. A `Workflow` cannot yet be another
50
+ * agent's subagent (an ADK limit), so `yaml_reference` to a workflow
51
+ * syndicate compiles its orchestrator alone.
52
+ */
53
+ import { Workflow } from '@google/adk';
54
+ import type { BaseNode, LlmAgent } from '@google/adk';
55
+ import type { CompileOptions } from './compile.ts';
56
+ import type { SyndicateYamlConfig } from './loadSyndicate.ts';
57
+ export * from './workflowConfig.ts';
58
+ export interface CompiledWorkflow {
59
+ workflow: Workflow;
60
+ /** Every agent node by YAML name, compiled. */
61
+ agents: Map<string, LlmAgent>;
62
+ /** Every node by YAML name, hidden route steps excluded. */
63
+ nodes: Map<string, BaseNode>;
64
+ }
65
+ /**
66
+ * Compile a workflow syndicate: every agent through `compileSubagent` (its
67
+ * tools, skills and MCP server as in any mode), the declared nodes, the
68
+ * hidden route steps, and the edge chains, into one ADK `Workflow` named
69
+ * after the syndicate. Throws on a name the schema let through, a tool that
70
+ * is not registered, or a graph ADK rejects (unreachable node, cycle).
71
+ */
72
+ export declare function compileWorkflow(config: SyndicateYamlConfig, opts?: CompileOptions, transformAgent?: (agent: LlmAgent) => LlmAgent): Promise<CompiledWorkflow>;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * lib/workflow.ts — the `workflow:` block: a syndicate as a graph.
3
+ *
4
+ * ── The third orchestration method ───────────────────────────────────────
5
+ * DELEGATE lets an orchestrator call subagents as tools; PLAN-DISPATCH
6
+ * (lib/dispatch.ts) lets a classifier pick ONE subagent to answer. Neither
7
+ * can say "run these two in parallel, join their outputs, review, ask the
8
+ * person, then publish". A `workflow:` block does: it is a graph whose
9
+ * nodes are the syndicate's own agents plus a few node kinds the engine
10
+ * supplies, and whose edges say what runs after what and on which route.
11
+ *
12
+ * workflow:
13
+ * edges:
14
+ * - [START, Planner, { article: [Writer, Checker], answer: Answerer }]
15
+ * - [[Writer, Checker], Both, Editor, Confirm, Publisher]
16
+ * nodes:
17
+ * Both: { join: true } # waits for every predecessor
18
+ * Confirm: { ask_user: "Publish? yes, or say what to change." }
19
+ * Planner: { route_key: "kind", retry: { max_attempts: 2 } }
20
+ *
21
+ * ADK 2.2 runs it (`Workflow`, `FunctionNode`, `JoinNode`, `ParallelWorker`,
22
+ * `ToolNode`, `RequestInput`): per-node retries and timeouts, fan-out and
23
+ * fan-in, a pause that waits for a person and resumes on the next message,
24
+ * and resumption from the session's own events. Every agent is already a
25
+ * node (`LlmAgent extends BaseNode`): a node agent receives the previous
26
+ * node's output as its user turn and, unless its YAML sets
27
+ * `includeContents`, sees nothing else of the conversation.
28
+ *
29
+ * ── What the engine adds to ADK ───────────────────────────────────────────
30
+ * ROUTING. ADK's TypeScript port never derives a route from an agent's
31
+ * output, so a routing map in YAML gets a hidden step after the agent
32
+ * (`<Agent>__route`) that reads the route from the output — the
33
+ * `route_key` property of a JSON output, else the trimmed text — and
34
+ * re-emits the output with it. A `default` key catches what no key
35
+ * matched.
36
+ * THE PAUSE. `ask_user` is a node that raises ADK's `RequestInput`. The
37
+ * turn ends `input-required` carrying the question (`result.input`); the
38
+ * next message on the conversation is the answer and becomes that node's
39
+ * output. The A2A server publishes the question; the chat prints it.
40
+ * NAMES. Nodes are addressed by their YAML names everywhere (edges, events,
41
+ * the wiki), including a `map` node, which ADK would otherwise name after
42
+ * the agent it wraps.
43
+ *
44
+ * ── Not in this version ───────────────────────────────────────────────────
45
+ * Approval gates (`require_approval`, `skills.scripts: local`) and remote
46
+ * `a2a_agent_url` subagents are refused inside a workflow by the schema: a
47
+ * gated call pauses a node through ADK's interrupt path, whose resume is not
48
+ * the one ADR 0028 built, and a remote agent is reachable only as a tool.
49
+ * Both are open for a later record. A `Workflow` cannot yet be another
50
+ * agent's subagent (an ADK limit), so `yaml_reference` to a workflow
51
+ * syndicate compiles its orchestrator alone.
52
+ */
53
+ import { DEFAULT_ROUTE, FunctionNode, JoinNode, ParallelWorker, RequestInput, ToolNode, Workflow, createEvent, } from '@google/adk';
54
+ import { compileSubagent } from './compile.js';
55
+ import { resolveTools as resolveNamedTools } from './toolRegistry.js';
56
+ import { DEFAULT_ROUTE_KEY, ROUTE_STEP_SUFFIX, START_NAME, isWorkflowSyndicate, nodeKind, nodeSettings, routeOf } from './workflowConfig.js';
57
+ export * from './workflowConfig.js';
58
+ /**
59
+ * Compile a workflow syndicate: every agent through `compileSubagent` (its
60
+ * tools, skills and MCP server as in any mode), the declared nodes, the
61
+ * hidden route steps, and the edge chains, into one ADK `Workflow` named
62
+ * after the syndicate. Throws on a name the schema let through, a tool that
63
+ * is not registered, or a graph ADK rejects (unreachable node, cycle).
64
+ */
65
+ export async function compileWorkflow(config, opts = {}, transformAgent = (a) => a) {
66
+ if (!isWorkflowSyndicate(config))
67
+ throw new Error(`${config.syndicate_name}: no workflow block`);
68
+ const wf = config.workflow;
69
+ const nodeYaml = wf.nodes ?? {};
70
+ const agentConfigs = new Map();
71
+ agentConfigs.set(config.orchestrator.name, { description: '', ...config.orchestrator });
72
+ for (const sub of config.subagents ?? [])
73
+ agentConfigs.set(sub.name, sub);
74
+ // Agents, with their node modifiers (retry, timeout) at construction.
75
+ const compileOpts = {
76
+ ...opts,
77
+ nodeConfig: (name) => {
78
+ const settings = nodeSettings(nodeYaml[name]);
79
+ return Object.keys(settings).length ? settings : undefined;
80
+ },
81
+ };
82
+ const agents = new Map();
83
+ for (const [name, sub] of agentConfigs)
84
+ agents.set(name, transformAgent(await compileSubagent(sub, compileOpts)));
85
+ // Declared nodes.
86
+ const nodes = new Map(agents);
87
+ for (const [name, entry] of Object.entries(nodeYaml)) {
88
+ const kind = nodeKind(entry);
89
+ if (!kind)
90
+ continue; // modifiers on an agent
91
+ const settings = nodeSettings(entry);
92
+ if (kind === 'ask_user') {
93
+ const message = entry.ask_user ?? '';
94
+ const schema = entry.schema;
95
+ // The node runs again on resume (rerunOnResume): ADK then hands it the
96
+ // same input and the answers resolved so far, so its output can carry
97
+ // BOTH the reply and what the person was asked about. A node fast-
98
+ // forwarded by ADK would output the bare reply, and the next agent
99
+ // would never see the draft it is meant to act on.
100
+ nodes.set(name, new FunctionNode(name, (ctx, input) => {
101
+ const replies = Object.values(ctx.resumeInputs);
102
+ if (replies.length > 0)
103
+ return { reply: replies[replies.length - 1], input };
104
+ return new RequestInput({ message, payload: input, ...(schema ? { responseSchema: schema } : {}) });
105
+ }, { ...settings, rerunOnResume: true }));
106
+ }
107
+ else if (kind === 'join') {
108
+ nodes.set(name, new JoinNode({ name, ...settings }));
109
+ }
110
+ else if (kind === 'map') {
111
+ const inner = agents.get(entry.map);
112
+ if (!inner)
113
+ throw new Error(`workflow node '${name}': map names '${entry.map}', which is not an agent of this syndicate`);
114
+ const worker = new ParallelWorker(inner, entry.max_parallel !== undefined ? { maxParallelWorkers: entry.max_parallel } : {});
115
+ // ADK names a worker after the agent it wraps; the YAML name is what
116
+ // the edges, the events and the docs use.
117
+ Object.defineProperty(worker, 'name', { value: name, enumerable: true });
118
+ nodes.set(name, worker);
119
+ }
120
+ else if (kind === 'tool') {
121
+ const [tool] = resolveNamedTools([entry.tool], opts.onUnknownTool);
122
+ if (!tool)
123
+ throw new Error(`workflow node '${name}': tool '${entry.tool}' is not registered`);
124
+ nodes.set(name, new ToolNode(tool, { name, ...settings }));
125
+ }
126
+ }
127
+ const nodeOf = (name) => {
128
+ const node = nodes.get(name);
129
+ if (!node)
130
+ throw new Error(`workflow: '${name}' is not an agent or a declared node of this syndicate`);
131
+ return node;
132
+ };
133
+ const routeSteps = new Map();
134
+ const routeStepAfter = (name) => {
135
+ let step = routeSteps.get(name);
136
+ if (step)
137
+ return step;
138
+ const routeKey = nodeYaml[name]?.route_key ?? 'route';
139
+ const stepName = `${name}${ROUTE_STEP_SUFFIX}`;
140
+ step = new FunctionNode(stepName, (ctx, input) => createEvent({
141
+ author: stepName,
142
+ invocationId: ctx.invocationId,
143
+ branch: ctx.branch,
144
+ output: input,
145
+ route: routeOf(input, routeKey),
146
+ }));
147
+ routeSteps.set(name, step);
148
+ return step;
149
+ };
150
+ const edges = wf.edges.map((chain) => {
151
+ const out = [];
152
+ chain.forEach((element, i) => {
153
+ if (typeof element === 'string') {
154
+ out.push(element === START_NAME ? 'START' : nodeOf(element));
155
+ }
156
+ else if (Array.isArray(element)) {
157
+ out.push(element.map(nodeOf));
158
+ }
159
+ else {
160
+ const previous = chain[i - 1];
161
+ if (typeof previous !== 'string' || previous === START_NAME) {
162
+ throw new Error(`workflow: a routing map must follow the name of the node whose output it routes`);
163
+ }
164
+ out.push(routeStepAfter(previous));
165
+ const map = {};
166
+ for (const [key, target] of Object.entries(element)) {
167
+ map[key === DEFAULT_ROUTE_KEY ? DEFAULT_ROUTE : key] = Array.isArray(target) ? target.map(nodeOf) : nodeOf(target);
168
+ }
169
+ out.push(map);
170
+ }
171
+ });
172
+ return out;
173
+ });
174
+ const workflow = new Workflow({
175
+ name: config.syndicate_name,
176
+ edges,
177
+ ...(wf.max_concurrency !== undefined ? { maxConcurrency: wf.max_concurrency } : {}),
178
+ });
179
+ return { workflow, agents, nodes };
180
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * lib/workflowConfig.ts — the `workflow:` block's contract: its types, the
3
+ * names it reserves, and the pure helpers the schema (lib/syndicateSchema.ts),
4
+ * the compiler (lib/workflow.ts) and the turn runner share. No imports
5
+ * beyond types, so the schema can read it without pulling the compiler in.
6
+ */
7
+ import type { RetryConfig as AdkRetryConfig } from '@google/adk';
8
+ /** `START` in an edge chain: the graph's entry. Not a node. */
9
+ export declare const START_NAME = "START";
10
+ /** The routing-map key that catches an unmatched route. */
11
+ export declare const DEFAULT_ROUTE_KEY = "default";
12
+ /** Suffix of the hidden step inserted after an agent a routing map follows. */
13
+ export declare const ROUTE_STEP_SUFFIX = "__route";
14
+ /** One element of an edge chain: a name, several names (fan-out or fan-in), or a routing map. */
15
+ export type EdgeElement = string | string[] | Record<string, string | string[]>;
16
+ export interface RetryYaml {
17
+ /** Attempts including the first. 1 = no retry. Default 5 in ADK. */
18
+ max_attempts?: number;
19
+ /** Seconds before the first retry. */
20
+ initial_delay?: number;
21
+ max_delay?: number;
22
+ backoff_factor?: number;
23
+ }
24
+ /** Per-node settings. A node is an agent (modifiers only) or exactly one of the kinds. */
25
+ export interface WorkflowNodeYaml {
26
+ /** Pause and ask the person this; the reply becomes the node's output. */
27
+ ask_user?: string;
28
+ /** JSON Schema a structured reply to `ask_user` must satisfy. A plain-text reply passes as is. */
29
+ schema?: Record<string, unknown>;
30
+ /** Wait for every predecessor, then output `{ <predecessor>: <output> }`. */
31
+ join?: true;
32
+ /** Run this agent once per item of a list input, concurrently; output the list of results. */
33
+ map?: string;
34
+ /** Concurrency of `map`. Default 8. */
35
+ max_parallel?: number;
36
+ /** Run this registry tool with the node input as its arguments. */
37
+ tool?: string;
38
+ /** Property of a JSON output holding the route (agents). Default "route". */
39
+ route_key?: string;
40
+ retry?: RetryYaml;
41
+ /** Seconds this node may run before it fails. */
42
+ timeout?: number;
43
+ }
44
+ export interface WorkflowConfig {
45
+ /** Chains, each a list of elements; `START` begins at least one of them. */
46
+ edges: EdgeElement[][];
47
+ nodes?: Record<string, WorkflowNodeYaml>;
48
+ /** Nodes that may run at once. Default: unbounded. */
49
+ max_concurrency?: number;
50
+ }
51
+ /** True when the syndicate is a graph (a `workflow:` block is present). */
52
+ export declare function isWorkflowSyndicate(config: {
53
+ workflow?: unknown;
54
+ }): config is {
55
+ workflow: WorkflowConfig;
56
+ };
57
+ export declare const NODE_KINDS: readonly ['ask_user', 'join', 'map', 'tool'];
58
+ /** The kind a node entry declares, or undefined for a modifier-only entry (an agent). */
59
+ export declare function nodeKind(node: WorkflowNodeYaml | undefined): (typeof NODE_KINDS)[number] | undefined;
60
+ /** Every name an edge element references (`START` excluded). */
61
+ export declare function elementNames(element: EdgeElement): string[];
62
+ /** ADK's retry config from the YAML spelling. */
63
+ export declare function toRetryConfig(retry: RetryYaml | undefined): AdkRetryConfig | undefined;
64
+ /** The BaseNode fields a node entry's modifiers map to. */
65
+ export declare function nodeSettings(node: WorkflowNodeYaml | undefined): {
66
+ retryConfig?: AdkRetryConfig;
67
+ timeout?: number;
68
+ };
69
+ /**
70
+ * The route an output names: the `routeKey` property of an object (a parsed
71
+ * JSON output), else the trimmed text. Never undefined, so an edge with a
72
+ * `default` key always has something to fall back from.
73
+ */
74
+ export declare function routeOf(output: unknown, routeKey?: string): string;
75
+ /** A pause raised by an `ask_user` node, read from the run's events (`adk_request_input`). */
76
+ export interface PendingInput {
77
+ /** The interrupt id. A plain-text next message answers it. */
78
+ id: string;
79
+ /** The node that asked. */
80
+ node: string;
81
+ /** The question. */
82
+ message: string;
83
+ payload?: unknown;
84
+ /** JSON Schema a structured answer must satisfy, when the node declared one. */
85
+ schema?: unknown;
86
+ }
87
+ /** ADK's function-call name for a workflow input request. */
88
+ export declare const INPUT_REQUEST = "adk_request_input";
89
+ /** The request an `adk_request_input` call carries, or undefined for another call. */
90
+ export declare function inputRequestFrom(author: string | undefined, call: {
91
+ name?: string;
92
+ args?: Record<string, unknown>;
93
+ }): PendingInput | undefined;
94
+ /** One line a person can read: what the workflow is waiting for. */
95
+ export declare function describeInput(input: PendingInput): string;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * lib/workflowConfig.ts — the `workflow:` block's contract: its types, the
3
+ * names it reserves, and the pure helpers the schema (lib/syndicateSchema.ts),
4
+ * the compiler (lib/workflow.ts) and the turn runner share. No imports
5
+ * beyond types, so the schema can read it without pulling the compiler in.
6
+ */
7
+ /** `START` in an edge chain: the graph's entry. Not a node. */
8
+ export const START_NAME = 'START';
9
+ /** The routing-map key that catches an unmatched route. */
10
+ export const DEFAULT_ROUTE_KEY = 'default';
11
+ /** Suffix of the hidden step inserted after an agent a routing map follows. */
12
+ export const ROUTE_STEP_SUFFIX = '__route';
13
+ /** True when the syndicate is a graph (a `workflow:` block is present). */
14
+ export function isWorkflowSyndicate(config) {
15
+ return !!config.workflow && typeof config.workflow === 'object';
16
+ }
17
+ export const NODE_KINDS = ['ask_user', 'join', 'map', 'tool'];
18
+ /** The kind a node entry declares, or undefined for a modifier-only entry (an agent). */
19
+ export function nodeKind(node) {
20
+ if (!node)
21
+ return undefined;
22
+ return NODE_KINDS.find((k) => node[k] !== undefined);
23
+ }
24
+ /** Every name an edge element references (`START` excluded). */
25
+ export function elementNames(element) {
26
+ if (typeof element === 'string')
27
+ return element === START_NAME ? [] : [element];
28
+ if (Array.isArray(element))
29
+ return element;
30
+ return Object.values(element).flatMap((v) => (Array.isArray(v) ? v : [v]));
31
+ }
32
+ /** ADK's retry config from the YAML spelling. */
33
+ export function toRetryConfig(retry) {
34
+ if (!retry)
35
+ return undefined;
36
+ const out = {};
37
+ if (retry.max_attempts !== undefined)
38
+ out.maxAttempts = retry.max_attempts;
39
+ if (retry.initial_delay !== undefined)
40
+ out.initialDelay = retry.initial_delay;
41
+ if (retry.max_delay !== undefined)
42
+ out.maxDelay = retry.max_delay;
43
+ if (retry.backoff_factor !== undefined)
44
+ out.backoffFactor = retry.backoff_factor;
45
+ return out;
46
+ }
47
+ /** The BaseNode fields a node entry's modifiers map to. */
48
+ export function nodeSettings(node) {
49
+ const out = {};
50
+ const retry = toRetryConfig(node?.retry);
51
+ if (retry)
52
+ out.retryConfig = retry;
53
+ if (node?.timeout !== undefined)
54
+ out.timeout = node.timeout;
55
+ return out;
56
+ }
57
+ /**
58
+ * The route an output names: the `routeKey` property of an object (a parsed
59
+ * JSON output), else the trimmed text. Never undefined, so an edge with a
60
+ * `default` key always has something to fall back from.
61
+ */
62
+ export function routeOf(output, routeKey = 'route') {
63
+ if (output && typeof output === 'object' && !Array.isArray(output)) {
64
+ const value = output[routeKey];
65
+ if (value === undefined || value === null)
66
+ return '';
67
+ return String(value).trim();
68
+ }
69
+ if (typeof output === 'string')
70
+ return output.trim();
71
+ return output === undefined || output === null ? '' : String(output).trim();
72
+ }
73
+ /** ADK's function-call name for a workflow input request. */
74
+ export const INPUT_REQUEST = 'adk_request_input';
75
+ /** The request an `adk_request_input` call carries, or undefined for another call. */
76
+ export function inputRequestFrom(author, call) {
77
+ if (call.name !== INPUT_REQUEST)
78
+ return undefined;
79
+ const args = call.args ?? {};
80
+ return {
81
+ id: String(args.interruptId ?? ''),
82
+ node: author ?? '',
83
+ message: typeof args.message === 'string' ? args.message : '',
84
+ ...(args.payload !== undefined && args.payload !== null ? { payload: args.payload } : {}),
85
+ ...(args.response_schema !== undefined && args.response_schema !== null ? { schema: args.response_schema } : {}),
86
+ };
87
+ }
88
+ /** One line a person can read: what the workflow is waiting for. */
89
+ export function describeInput(input) {
90
+ const options = input.payload?.options;
91
+ const choices = Array.isArray(options) && options.length ? ` (${options.join(' / ')})` : '';
92
+ return `${input.node} asks: ${input.message || '(no question text)'}${choices}`;
93
+ }
@@ -25,7 +25,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
25
25
  * with A2A_AUTH=callers it stays valid beside the caller
26
26
  * tokens, with its old scoping, until you remove it
27
27
  * A2A_AUTH who a caller is (ADR 0025): secret (default) | callers |
28
- * jwt | header
28
+ * jwt | header (required with PUBLIC_URL, ADR 0039)
29
29
  * A2A_CALLERS callers: `name:sha256[:scope]; …` — one token per calling
30
30
  * backend. Mint: melchizedek-serve --new-caller <name>
31
31
  * A2A_JWT_JWKS_URL jwt: your identity provider's JWKS (or A2A_JWT_SECRET, HS256)
@@ -39,20 +39,25 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
39
39
  * ALLOW_UNHARDENED_DB "true" to accept an unhardened Supabase schema
40
40
  * A2A_TASK_TIMEOUT_MS per-task wall-clock budget (900000; 0 = none)
41
41
  * A2A_MAX_CONCURRENT_TASKS concurrent tasks across all agents (0 = unlimited)
42
+ * A2A_MAX_CONCURRENT_PER_SCOPE concurrent tasks for one end user (4; 0 = unlimited)
43
+ * A2A_MAX_CONCURRENT_PER_CALLER concurrent tasks for one caller (0 = unlimited)
42
44
  * A2A_RATE_LIMIT_MAX task submissions per window per IP (60)
43
45
  * A2A_RATE_LIMIT_WINDOW_MS rate-limit window (900000)
44
46
  * A2A_AUTH_FAILURE_MAX failed authentications per window per IP (30)
45
- * A2A_TRUST_PROXY Express trust-proxy: hop count, true/false, or subnets (1)
47
+ * A2A_TRUST_PROXY Express trust-proxy: hop count, true/false, or subnets
48
+ * (false; required with PUBLIC_URL)
46
49
  * A2A_BODY_LIMIT JSON body limit ("1mb")
47
50
  * A2A_KEY_MODE server (default): models run on the server's keys;
48
51
  * byok: the caller's X-API-Key pays. Under A2A_AUTH=secret
49
52
  * the mode also scopes data: X-User-Id in server mode,
50
53
  * the key's hash in byok (the pre-0.16 behaviour)
51
54
  * A2A_SERVED_AGENTS comma list: the only agent ids /:agentId/ serves;
52
- * only these may fall back to examples/ and templates/
55
+ * only these may fall back to examples/ and templates/;
56
+ * * serves every one (required with PUBLIC_URL)
53
57
  * A2A_REGISTRY_AGENTS comma list: bare ids that load from adk_agent_registry
54
58
  * (others are files; registry:<id> always is the registry)
55
59
  * A2A_SHUTDOWN_GRACE_MS how long SIGTERM waits for running tasks (25000)
60
+ * A2A_SHUTDOWN_DELAY_MS of that, how long /readyz fails while requests are still served (5000)
56
61
  * A2A_BUDGETS daily budgets per UTC day (ADR 0026), JSON:
57
62
  * {"perCaller":{"tasks":500,"llmCalls":5000,"tokens":5000000},
58
63
  * "callers":{"reports":{"tasks":100}},"perScope":{"tasks":50}}.
@@ -101,8 +106,10 @@ function envInt(name, fallback) {
101
106
  }
102
107
  function envTrustProxy() {
103
108
  const raw = process.env.A2A_TRUST_PROXY?.trim();
109
+ // Unset: trust no proxy, so a client cannot choose its own IP with
110
+ // X-Forwarded-For. A public server must set it (the posture check below).
104
111
  if (!raw)
105
- return 1;
112
+ return false;
106
113
  if (/^\d+$/.test(raw))
107
114
  return Number(raw);
108
115
  if (raw === 'true')
@@ -219,7 +226,28 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
219
226
  }
220
227
  return n;
221
228
  };
222
- const servedAgents = list('A2A_SERVED_AGENTS');
229
+ // `*` serves every file in the agents directory and every registry row,
230
+ // stated on purpose; unset means the same off a public URL (ADR 0039).
231
+ const servedList = list('A2A_SERVED_AGENTS');
232
+ const servedAgents = servedList?.length === 1 && servedList[0] === '*' ? undefined : servedList;
233
+ // ── Public posture (ADR 0039) ──────────────────────────────────────────────
234
+ // A server on a public URL states how it authenticates, which agents it
235
+ // answers, and how many proxies stand in front of it. Each default was the
236
+ // permissive choice, so none is taken silently here.
237
+ if (publicUrl) {
238
+ const missing = [];
239
+ if (!process.env.A2A_AUTH?.trim()) {
240
+ missing.push('A2A_AUTH: callers or jwt (one identity per caller; recommended), header (a gateway authenticates users), or secret (one shared secret; any holder can act as any user by naming them in X-User-Id)');
241
+ }
242
+ if (!servedList?.length) {
243
+ missing.push('A2A_SERVED_AGENTS: the agent ids this server answers, comma-separated, or * for every file in the agents directory and every registry row');
244
+ }
245
+ if (!process.env.A2A_TRUST_PROXY?.trim()) {
246
+ missing.push('A2A_TRUST_PROXY: how many proxies stand in front of this server (1 behind one load balancer), or false when clients connect directly');
247
+ }
248
+ if (missing.length)
249
+ fatal(`PUBLIC_URL is set, so this server must state its security posture. Set:\n - ${missing.join('\n - ')}`);
250
+ }
223
251
  const registryAgents = list('A2A_REGISTRY_AGENTS');
224
252
  // Storage: Postgres when DATABASE_URL is set (every durable store, safe
225
253
  // across instances); otherwise createA2AApp's default (Supabase, else memory).
@@ -305,6 +333,8 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
305
333
  allowSchemaMismatch: process.env.ALLOW_SCHEMA_MISMATCH?.trim().toLowerCase() === 'true',
306
334
  turnLockWaitMs: envInt('A2A_TURN_LOCK_WAIT_MS', 30_000),
307
335
  maxConcurrentTasks: envInt('A2A_MAX_CONCURRENT_TASKS', 0),
336
+ maxConcurrentPerScope: envInt('A2A_MAX_CONCURRENT_PER_SCOPE', 4),
337
+ maxConcurrentPerCaller: envInt('A2A_MAX_CONCURRENT_PER_CALLER', 0),
308
338
  rateLimit: { windowMs: envInt('A2A_RATE_LIMIT_WINDOW_MS', 15 * 60 * 1000), max: envInt('A2A_RATE_LIMIT_MAX', 60) },
309
339
  authFailureLimit: { windowMs: 15 * 60 * 1000, max: envInt('A2A_AUTH_FAILURE_MAX', 30) },
310
340
  trustProxy: envTrustProxy(),
@@ -334,7 +364,7 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
334
364
  const authMode = `${identityLabel || (secret ? 'bearer secret' : 'no secret (loopback only)')}; keys: ${keyMode === 'byok' ? "caller's X-API-Key (byok)" : "server's own"}`;
335
365
  // The secret is not needed past this point; drop the reference.
336
366
  secret = undefined;
337
- const { app, config, sessionBackend, shutdown } = built;
367
+ const { app, config, sessionBackend, shutdown, markUnready } = built;
338
368
  const server = await new Promise((resolve) => {
339
369
  const s = host ? app.listen(port, host, () => resolve(s)) : app.listen(port, () => resolve(s));
340
370
  });
@@ -364,13 +394,26 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
364
394
  // ── Lifecycle ──────────────────────────────────────────────────────────────
365
395
  let stopping = false;
366
396
  const graceMs = envInt('A2A_SHUTDOWN_GRACE_MS', 25_000);
397
+ // A load balancer keeps sending requests for a few seconds after SIGTERM,
398
+ // until it sees /readyz fail and deregisters this instance. Closing the
399
+ // listener at once would refuse those requests; fail readiness first and
400
+ // keep serving for the delay, then close and drain. The delay is part of
401
+ // the grace budget (running tasks keep running through it), so a stop still
402
+ // takes at most A2A_SHUTDOWN_GRACE_MS, inside Kubernetes' default 30 s.
403
+ const delayMs = Math.min(envInt('A2A_SHUTDOWN_DELAY_MS', 5_000), graceMs);
367
404
  const stop = async (signal) => {
368
405
  if (stopping)
369
406
  return;
370
407
  stopping = true;
371
- emit('info', `[A2A] ${signal}: draining — no new tasks; waiting up to ${graceMs} ms for running ones.`);
408
+ markUnready();
409
+ if (delayMs > 0) {
410
+ emit('info', `[A2A] ${signal}: /readyz now fails; still serving for ${delayMs} ms while the load balancer deregisters this instance.`);
411
+ await new Promise((r) => setTimeout(r, delayMs));
412
+ }
413
+ const drainMs = graceMs - delayMs;
414
+ emit('info', `[A2A] ${signal}: draining — no new tasks; waiting up to ${drainMs} ms for running ones.`);
372
415
  server.close();
373
- const canceled = await shutdown(graceMs);
416
+ const canceled = await shutdown(drainMs);
374
417
  if (canceled > 0)
375
418
  emit('warn', `[A2A] ⚠ ${canceled} task(s) did not finish in time and were canceled.`);
376
419
  // Give canceled tasks a moment to publish their final status, then flush
@@ -22,5 +22,14 @@
22
22
  /** db/migrations/NNNN_name.sql, in numeric order. */
23
23
  export declare function migrationFiles(dir?: string): string[];
24
24
  export declare function installOrder(withTelemetry: boolean): string[];
25
+ /**
26
+ * The whole install as one psql script: an advisory lock first, then every
27
+ * file in order with a progress line before each. psql runs it with
28
+ * --single-transaction and ON_ERROR_STOP, so a failure anywhere rolls the
29
+ * whole install back (no half-applied migration), and a second `apply`
30
+ * started meanwhile waits on the lock instead of interleaving with this one.
31
+ */
32
+ export declare const APPLY_LOCK_KEY = "melchizedek-db apply";
33
+ export declare function applyScript(files: string[], sqlOf: (file: string) => string, label?: (file: string) => string): string;
25
34
  /** postgres://user:pass@host:port/db?sslmode=… → libpq environment variables. */
26
35
  export declare function pgEnv(url: string): Record<string, string>;