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.
- package/.env.example +40 -3
- package/AGENT_SETUP.md +4 -2
- package/CHANGELOG.md +246 -0
- package/DOCUMENTATION.md +311 -15
- package/QUICKSTART.md +2 -2
- package/README.md +125 -149
- package/config/agents/examples/harness.yaml +135 -0
- package/config/agents/examples/model_zoo.yaml +23 -9
- package/config/agents/examples/pipeline.yaml +120 -0
- package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
- package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
- package/config/agents/examples/weather.yaml +59 -0
- package/config/agents/syndicateSchema.yaml +125 -1
- package/db/hardening.sql +4 -3
- package/db/migrations/0011_erase_expired.sql +198 -0
- package/db/migrations/0012_audit_log.sql +98 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +101 -5
- package/dist/lib/a2a/executor.d.ts +34 -4
- package/dist/lib/a2a/executor.js +109 -9
- package/dist/lib/compile.d.ts +41 -3
- package/dist/lib/compile.js +162 -17
- package/dist/lib/config.d.ts +24 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/doctor.js +17 -4
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/loadEnv.d.ts +5 -0
- package/dist/lib/loadEnv.js +7 -0
- package/dist/lib/loadSyndicate.d.ts +50 -0
- package/dist/lib/loadSyndicate.js +19 -3
- package/dist/lib/memory/erase.d.ts +5 -0
- package/dist/lib/memory/erase.js +5 -0
- package/dist/lib/memory/providers.d.ts +27 -0
- package/dist/lib/memory/providers.js +28 -0
- package/dist/lib/models/capabilities.js +10 -0
- package/dist/lib/models/endpoints.d.ts +1 -1
- package/dist/lib/models/endpoints.js +4 -1
- package/dist/lib/models/fallback.d.ts +41 -0
- package/dist/lib/models/fallback.js +105 -0
- package/dist/lib/models/gateway.d.ts +2 -2
- package/dist/lib/models/gateway.js +4 -4
- package/dist/lib/models/kimiLlm.d.ts +123 -0
- package/dist/lib/models/kimiLlm.js +171 -0
- package/dist/lib/models/providerMap.d.ts +2 -2
- package/dist/lib/models/providerMap.js +4 -1
- package/dist/lib/models/registry.d.ts +1 -1
- package/dist/lib/models/registry.js +11 -3
- package/dist/lib/net/redirects.d.ts +14 -0
- package/dist/lib/net/redirects.js +94 -0
- package/dist/lib/observability/audit.d.ts +21 -0
- package/dist/lib/observability/audit.js +39 -0
- package/dist/lib/observability/metrics.d.ts +6 -0
- package/dist/lib/observability/tracer.d.ts +14 -1
- package/dist/lib/observability/tracer.js +16 -2
- package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
- package/dist/lib/persistence/supabaseProvider.js +2 -31
- package/dist/lib/runtime/questions.d.ts +55 -0
- package/dist/lib/runtime/questions.js +107 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
- package/dist/lib/runtime/syndicateTurn.js +165 -17
- package/dist/lib/storage/postgres/index.d.ts +9 -0
- package/dist/lib/storage/postgres/index.js +13 -0
- package/dist/lib/storage/rlsStatus.d.ts +45 -0
- package/dist/lib/storage/rlsStatus.js +69 -0
- package/dist/lib/syndicateSchema.d.ts +177 -0
- package/dist/lib/syndicateSchema.js +343 -3
- package/dist/lib/toolRegistry.js +9 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
- package/dist/lib/tools/mcpToolFactory.js +61 -4
- package/dist/lib/tools/openapiTools.d.ts +52 -0
- package/dist/lib/tools/openapiTools.js +225 -0
- package/dist/lib/tools/skillToolset.d.ts +135 -0
- package/dist/lib/tools/skillToolset.js +247 -0
- package/dist/lib/tools/urlContextTool.d.ts +29 -0
- package/dist/lib/tools/urlContextTool.js +39 -0
- package/dist/lib/workflow.d.ts +72 -0
- package/dist/lib/workflow.js +180 -0
- package/dist/lib/workflowConfig.d.ts +95 -0
- package/dist/lib/workflowConfig.js +93 -0
- package/dist/scripts/a2a_server.js +51 -8
- package/dist/scripts/db.d.ts +9 -0
- package/dist/scripts/db.js +30 -15
- package/dist/scripts/syndicate_chat.js +39 -5
- package/package.json +6 -3
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +8 -4
- package/skills/briefs/melchizedek-memory.md +1 -1
- package/skills/briefs/melchizedek-models.md +1 -1
- package/skills/briefs/melchizedek-scribe.md +4 -4
- package/skills/briefs/melchizedek-serve.md +1 -1
- package/skills/briefs/melchizedek.md +4 -1
- package/skills/melchizedek/SKILL.md +4 -1
- package/skills/melchizedek-author/SKILL.md +7 -3
- package/skills/melchizedek-memory/SKILL.md +1 -1
- package/skills/melchizedek-models/SKILL.md +1 -1
- package/skills/melchizedek-scribe/SKILL.md +3 -3
- package/skills/melchizedek-serve/SKILL.md +1 -1
- /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
- /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* SDKs whose fields this repo does not own.
|
|
23
23
|
*/
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
+
import { DEFAULT_ROUTE_KEY, NODE_KINDS, ROUTE_STEP_SUFFIX, START_NAME, elementNames, nodeKind } from './workflowConfig.js';
|
|
25
26
|
// ── Leaf rules ───────────────────────────────────────────────────────────────
|
|
26
27
|
/**
|
|
27
28
|
* ADK's own rule (validateAgentName in @google/adk base_agent): checked here
|
|
@@ -64,6 +65,55 @@ const generateContentConfig = z
|
|
|
64
65
|
thinkingConfig: thinkingConfig.optional(),
|
|
65
66
|
})
|
|
66
67
|
.describe('Model generation config (@google/genai GenerateContentConfig). Do not set tools here.');
|
|
68
|
+
// ── Skills ───────────────────────────────────────────────────────────────────
|
|
69
|
+
export const SKILL_SCRIPT_MODES = ['none', 'local'];
|
|
70
|
+
/**
|
|
71
|
+
* Agent Skills for one agent (lib/tools/skillToolset.ts): the frontmatter
|
|
72
|
+
* index is injected into the instruction at compile time, a skill is read in
|
|
73
|
+
* full on demand, and its scripts run only when `scripts` allows and a person
|
|
74
|
+
* approves each run.
|
|
75
|
+
*/
|
|
76
|
+
const skillsSchema = z
|
|
77
|
+
.strictObject({
|
|
78
|
+
dir: z
|
|
79
|
+
.string()
|
|
80
|
+
.min(1)
|
|
81
|
+
.describe('A directory of skills: one subdirectory per skill, named as its SKILL.md frontmatter names it. Relative to the working directory.'),
|
|
82
|
+
scripts: z
|
|
83
|
+
.enum(SKILL_SCRIPT_MODES)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Whether a skill\'s scripts/ may run. "local" runs them on this machine, each after a person approves (ADR 0028); default "none".'),
|
|
86
|
+
tools: z
|
|
87
|
+
.array(z.string().min(1))
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('Registry tool names a skill may unlock through its `allowed-tools` frontmatter once it is loaded. Omitted, no skill unlocks anything.'),
|
|
90
|
+
})
|
|
91
|
+
.describe('Agent Skills (SKILL.md directories) this agent reads the way a coding harness does.');
|
|
92
|
+
// ── OpenAPI ──────────────────────────────────────────────────────────────────
|
|
93
|
+
const envName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'an environment variable name (A–Z, 0–9, _)');
|
|
94
|
+
const openapiEntry = z
|
|
95
|
+
.strictObject({
|
|
96
|
+
spec: z.string().min(1).describe('An OpenAPI 3 spec file (.yaml, .yml, .json), relative to this syndicate file. Files only, never URLs.'),
|
|
97
|
+
operations: z
|
|
98
|
+
.array(z.string().min(1))
|
|
99
|
+
.min(1)
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Operations to expose, by operationId or tool name. Omitted: every GET operation, and nothing that writes.'),
|
|
102
|
+
base_url: z.string().url().optional().describe('Overrides the spec\'s servers[0].url. Checked by the SSRF guard.'),
|
|
103
|
+
prefix: z.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe('Prepended to each tool name, to keep two APIs apart.'),
|
|
104
|
+
auth: z
|
|
105
|
+
.strictObject({
|
|
106
|
+
bearer_env: envName.optional().describe('Environment variable holding a bearer token.'),
|
|
107
|
+
api_key: z
|
|
108
|
+
.strictObject({ env: envName, in: z.enum(['header', 'query']), name: z.string().min(1) })
|
|
109
|
+
.optional()
|
|
110
|
+
.describe('An API key read from `env`, sent in a header or the query under `name`.'),
|
|
111
|
+
})
|
|
112
|
+
.refine((a) => !!a.bearer_env !== !!a.api_key, 'exactly one of bearer_env or api_key')
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Credentials, always from the environment, never written in YAML.'),
|
|
115
|
+
})
|
|
116
|
+
.describe('An HTTP API as tools, from its OpenAPI spec (lib/tools/openapiTools.ts).');
|
|
67
117
|
// ── Agents ───────────────────────────────────────────────────────────────────
|
|
68
118
|
const agentFields = {
|
|
69
119
|
name: agentName,
|
|
@@ -75,6 +125,11 @@ const agentFields = {
|
|
|
75
125
|
.string()
|
|
76
126
|
.min(1)
|
|
77
127
|
.describe('Model id; its prefix picks the provider (gemini-*, claude-*, gpt-*/o*, grok-*, ollama/*).'),
|
|
128
|
+
fallback_model: z
|
|
129
|
+
.string()
|
|
130
|
+
.min(1)
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('A model on another provider that answers when this agent\'s model fails provider-side (5xx, 429, network) before producing anything, or while that provider\'s circuit is open (ADR 0044).'),
|
|
78
133
|
instruction: z.string().min(1).describe('System prompt / persona.'),
|
|
79
134
|
globalInstruction: z
|
|
80
135
|
.string()
|
|
@@ -104,6 +159,35 @@ const agentFields = {
|
|
|
104
159
|
.string()
|
|
105
160
|
.optional()
|
|
106
161
|
.describe('MCP server (SSE) whose tools are discovered at runtime and merged with `tools`.'),
|
|
162
|
+
mcp_tools: z
|
|
163
|
+
.array(z.string().min(1))
|
|
164
|
+
.min(1)
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('The MCP server\'s tools this agent may use, by name; any other tool the server lists is not exposed. Without it, every listed tool is. require_approval may name these. Needs mcp_server_url.'),
|
|
167
|
+
skills: skillsSchema.optional(),
|
|
168
|
+
openapi: z.array(openapiEntry).min(1).optional(),
|
|
169
|
+
code_execution: z
|
|
170
|
+
.literal('gemini')
|
|
171
|
+
.optional()
|
|
172
|
+
.describe('"gemini": the model writes and runs Python in Gemini\'s server-side sandbox; nothing runs on this host. Gemini models only.'),
|
|
173
|
+
context: z
|
|
174
|
+
.strictObject({
|
|
175
|
+
compact_after_tokens: z.number().int().min(1000).describe('Compact when the last request\'s prompt passed this many tokens.'),
|
|
176
|
+
keep_recent_events: z.number().int().min(1).max(100).optional().describe('Events kept verbatim after the summary. Default 6.'),
|
|
177
|
+
summary_model: z.string().min(1).optional().describe('The model that writes the summary. Default: the agent\'s own.'),
|
|
178
|
+
})
|
|
179
|
+
.optional()
|
|
180
|
+
.describe('Summarize a long conversation instead of overflowing the window. The orchestrator of a delegate syndicate only.'),
|
|
181
|
+
examples: z
|
|
182
|
+
.array(z.strictObject({ input: z.string().min(1), output: z.string().min(1) }))
|
|
183
|
+
.min(1)
|
|
184
|
+
.max(20)
|
|
185
|
+
.optional()
|
|
186
|
+
.describe('Few-shot exchanges (input, output) added to every request\'s instruction.'),
|
|
187
|
+
mode: z
|
|
188
|
+
.literal('task')
|
|
189
|
+
.optional()
|
|
190
|
+
.describe('"task": the agent works until it calls finish_task; its arguments (matching outputSchema) are the node\'s output. Workflow nodes only.'),
|
|
107
191
|
orchestration: z
|
|
108
192
|
.strictObject({
|
|
109
193
|
role: z.enum(['primary', 'sub-agent']).optional(),
|
|
@@ -159,6 +243,48 @@ const dispatchSchema = z
|
|
|
159
243
|
route_overrides: z.array(routeOverride).optional(),
|
|
160
244
|
})
|
|
161
245
|
.describe('Opts into PLAN-DISPATCH orchestration (lib/dispatch.ts).');
|
|
246
|
+
// ── Workflow ─────────────────────────────────────────────────────────────────
|
|
247
|
+
const nodeName = z.string().min(1);
|
|
248
|
+
const edgeTargets = z.union([nodeName, z.array(nodeName).min(1)]);
|
|
249
|
+
/** One element of an edge chain: a name, several names, or a routing map. */
|
|
250
|
+
const edgeElement = z.union([
|
|
251
|
+
nodeName,
|
|
252
|
+
z.array(nodeName).min(1).describe('Several nodes: fan-out when they follow one node, fan-in when one node follows them.'),
|
|
253
|
+
z
|
|
254
|
+
.record(z.string().min(1), edgeTargets)
|
|
255
|
+
.describe('A routing map after a node: `{ <route>: <node or nodes>, default: <node> }`, matched against that node\'s output.'),
|
|
256
|
+
]);
|
|
257
|
+
const retrySchema = z
|
|
258
|
+
.strictObject({
|
|
259
|
+
max_attempts: z.number().int().positive().optional().describe('Attempts including the first; 1 = no retry. ADK default 5.'),
|
|
260
|
+
initial_delay: z.number().nonnegative().optional().describe('Seconds before the first retry.'),
|
|
261
|
+
max_delay: z.number().nonnegative().optional(),
|
|
262
|
+
backoff_factor: z.number().positive().optional(),
|
|
263
|
+
})
|
|
264
|
+
.describe('Retry a node on failure (lib/workflow.ts).');
|
|
265
|
+
const workflowNodeSchema = z
|
|
266
|
+
.strictObject({
|
|
267
|
+
ask_user: z.string().min(1).optional().describe('Pause and ask the person this; the reply becomes the node\'s output.'),
|
|
268
|
+
schema: z.record(z.string(), z.unknown()).optional().describe('JSON Schema a structured reply to ask_user must satisfy; plain text passes as is.'),
|
|
269
|
+
join: z.literal(true).optional().describe('Wait for every predecessor; output `{ <predecessor>: <output> }`.'),
|
|
270
|
+
map: z.string().min(1).optional().describe('Run this agent once per item of a list input, concurrently; output the list of results.'),
|
|
271
|
+
max_parallel: z.number().int().positive().optional().describe('Concurrency of map. Default 8.'),
|
|
272
|
+
tool: z.string().min(1).optional().describe('Run this registry tool with the node input as its arguments.'),
|
|
273
|
+
route_key: z.string().min(1).optional().describe('Property of a JSON output holding the route. Default "route".'),
|
|
274
|
+
retry: retrySchema.optional(),
|
|
275
|
+
timeout: z.number().positive().optional().describe('Seconds this node may run before it fails.'),
|
|
276
|
+
})
|
|
277
|
+
.describe('A declared node (exactly one of ask_user, join, map, tool) or modifiers for an agent node (retry, timeout, route_key).');
|
|
278
|
+
const workflowSchema = z
|
|
279
|
+
.strictObject({
|
|
280
|
+
edges: z
|
|
281
|
+
.array(z.array(edgeElement).min(2))
|
|
282
|
+
.min(1)
|
|
283
|
+
.describe('Chains of nodes, each a list; `START` begins at least one. A routing map follows the node whose output it routes.'),
|
|
284
|
+
nodes: z.record(nodeName, workflowNodeSchema).optional(),
|
|
285
|
+
max_concurrency: z.number().int().positive().optional().describe('Nodes that may run at once. Default: unbounded.'),
|
|
286
|
+
})
|
|
287
|
+
.describe('Opts into a WORKFLOW: the syndicate as a graph (lib/workflow.ts). Cannot be combined with dispatch.');
|
|
162
288
|
// ── Syndicate ────────────────────────────────────────────────────────────────
|
|
163
289
|
export const syndicateSchema = z
|
|
164
290
|
.strictObject({
|
|
@@ -169,6 +295,14 @@ export const syndicateSchema = z
|
|
|
169
295
|
.optional()
|
|
170
296
|
.describe('The orchestrator\'s team. Omit it (or write `subagents: []`) for a single-agent syndicate.'),
|
|
171
297
|
dispatch: dispatchSchema.optional(),
|
|
298
|
+
workflow: workflowSchema.optional(),
|
|
299
|
+
retries: z
|
|
300
|
+
.strictObject({
|
|
301
|
+
model_errors: z.number().int().min(0).max(5).optional().describe('Retries of a malformed model reply. Default 2; 0 off.'),
|
|
302
|
+
tool_errors: z.number().int().min(0).max(5).optional().describe('Retries of a tool that threw, with reflection guidance. Default 3; 0 off.'),
|
|
303
|
+
})
|
|
304
|
+
.optional()
|
|
305
|
+
.describe('Self-correction on model and tool errors (ADK reflect-and-retry plugins). On by default.'),
|
|
172
306
|
variables: z
|
|
173
307
|
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
174
308
|
.optional()
|
|
@@ -206,7 +340,7 @@ export const syndicateSchema = z
|
|
|
206
340
|
.int()
|
|
207
341
|
.positive()
|
|
208
342
|
.optional()
|
|
209
|
-
.describe('Hard cap on
|
|
343
|
+
.describe('Hard cap on model calls per turn, subagents included. Default 50 (DEFAULT_MAX_STEPS) when unset.'),
|
|
210
344
|
bundled_references: z
|
|
211
345
|
.record(z.string(), z.record(z.string(), z.unknown()))
|
|
212
346
|
.optional()
|
|
@@ -387,19 +521,85 @@ function crossFieldProblems(raw) {
|
|
|
387
521
|
return;
|
|
388
522
|
}
|
|
389
523
|
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
|
524
|
+
// An OpenAPI operation can be gated once it is named under `operations`,
|
|
525
|
+
// and an MCP tool once it is named under `mcp_tools`.
|
|
526
|
+
const operations = (Array.isArray(agent.openapi) ? agent.openapi : []).flatMap((e) => (isObj(e) && Array.isArray(e.operations) ? e.operations : []));
|
|
527
|
+
const mcpTools = Array.isArray(agent.mcp_tools) ? agent.mcp_tools : [];
|
|
390
528
|
(Array.isArray(agent.require_approval) ? agent.require_approval : []).forEach((name, j) => {
|
|
391
|
-
if (typeof name === 'string' && !tools.includes(name)) {
|
|
392
|
-
out.push({ path: [...path, 'require_approval', j], message: `'${name}' is not in this agent's tools` });
|
|
529
|
+
if (typeof name === 'string' && !tools.includes(name) && !operations.includes(name) && !mcpTools.includes(name)) {
|
|
530
|
+
out.push({ path: [...path, 'require_approval', j], message: `'${name}' is not in this agent's tools, its openapi operations or its mcp_tools` });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
// A skill script run pauses for approval the same way, so it is allowed in
|
|
535
|
+
// the same places; `skills.tools` may only unlock tools the agent does not
|
|
536
|
+
// already carry outright.
|
|
537
|
+
const skillProblems = (agent, path, allowed) => {
|
|
538
|
+
if (!isObj(agent.skills))
|
|
539
|
+
return;
|
|
540
|
+
if (agent.skills.scripts === 'local' && !allowed) {
|
|
541
|
+
out.push({
|
|
542
|
+
path: [...path, 'skills', 'scripts'],
|
|
543
|
+
message: 'skill scripts pause for approval, which only the orchestrator or a plan-dispatch route can do; a delegated subagent cannot pause the turn (ADR 0028)',
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
|
547
|
+
(Array.isArray(agent.skills.tools) ? agent.skills.tools : []).forEach((name, j) => {
|
|
548
|
+
if (typeof name === 'string' && tools.includes(name)) {
|
|
549
|
+
out.push({ path: [...path, 'skills', 'tools', j], message: `'${name}' is already in this agent's tools; a skill cannot unlock what is always on` });
|
|
393
550
|
}
|
|
394
551
|
});
|
|
395
552
|
};
|
|
553
|
+
// ask_user ends the turn waiting for the person, the same pause as an
|
|
554
|
+
// approval, so it is allowed in the same places (lib/runtime/questions.ts).
|
|
555
|
+
const questionProblems = (agent, path, allowed) => {
|
|
556
|
+
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
|
557
|
+
const at = tools.indexOf('ask_user');
|
|
558
|
+
if (at === -1)
|
|
559
|
+
return;
|
|
560
|
+
if (isObj(raw.workflow)) {
|
|
561
|
+
out.push({ path: [...path, 'tools', at], message: 'ask_user is not supported on a workflow node yet; use an ask_user node (workflow.nodes)' });
|
|
562
|
+
}
|
|
563
|
+
else if (!allowed) {
|
|
564
|
+
out.push({ path: [...path, 'tools', at], message: 'ask_user pauses the turn, which only the orchestrator or a plan-dispatch route can do; a delegated subagent cannot' });
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
// Execution keys (ADR 0033): where each one means something.
|
|
568
|
+
const executionProblems = (agent, path, isOrchestrator) => {
|
|
569
|
+
const model = typeof agent.model === 'string' ? agent.model : isObj(raw.orchestrator) && typeof raw.orchestrator.model === 'string' ? raw.orchestrator.model : '';
|
|
570
|
+
if (agent.code_execution !== undefined && model && !/^gemini-/.test(model)) {
|
|
571
|
+
out.push({ path: [...path, 'code_execution'], message: `code_execution: gemini needs a gemini-* model (this agent's is ${model})` });
|
|
572
|
+
}
|
|
573
|
+
if (agent.context !== undefined && (!isOrchestrator || isObj(raw.dispatch) || isObj(raw.workflow))) {
|
|
574
|
+
out.push({
|
|
575
|
+
path: [...path, 'context'],
|
|
576
|
+
message: 'context compaction applies to the orchestrator of a delegate syndicate: a dispatch route reads a bounded projection, a workflow node sees only its input, and a subagent starts fresh on every call',
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (agent.mode !== undefined && !isObj(raw.workflow)) {
|
|
580
|
+
out.push({ path: [...path, 'mode'], message: 'mode: task applies to workflow nodes, whose output is the finish_task arguments' });
|
|
581
|
+
}
|
|
582
|
+
if (agent.mcp_tools !== undefined && typeof agent.mcp_server_url !== 'string') {
|
|
583
|
+
out.push({ path: [...path, 'mcp_tools'], message: 'mcp_tools chooses among an MCP server\'s tools; it needs mcp_server_url' });
|
|
584
|
+
}
|
|
585
|
+
};
|
|
396
586
|
if (isObj(raw.orchestrator))
|
|
587
|
+
executionProblems(raw.orchestrator, ['orchestrator'], true);
|
|
588
|
+
for (const [i, sub] of (Array.isArray(raw.subagents) ? raw.subagents : []).entries())
|
|
589
|
+
if (isObj(sub))
|
|
590
|
+
executionProblems(sub, ['subagents', i], false);
|
|
591
|
+
if (isObj(raw.orchestrator)) {
|
|
397
592
|
gateProblems(raw.orchestrator, ['orchestrator'], true);
|
|
593
|
+
skillProblems(raw.orchestrator, ['orchestrator'], true);
|
|
594
|
+
questionProblems(raw.orchestrator, ['orchestrator'], true);
|
|
595
|
+
}
|
|
398
596
|
const dispatching = isObj(raw.dispatch);
|
|
399
597
|
subs.forEach((sub, i) => {
|
|
400
598
|
if (!isObj(sub))
|
|
401
599
|
return;
|
|
402
600
|
gateProblems(sub, ['subagents', i], dispatching);
|
|
601
|
+
skillProblems(sub, ['subagents', i], dispatching);
|
|
602
|
+
questionProblems(sub, ['subagents', i], dispatching);
|
|
403
603
|
const hasRef = typeof sub.yaml_reference === 'string';
|
|
404
604
|
const hasRemote = typeof sub.a2a_agent_url === 'string';
|
|
405
605
|
if (hasRef && hasRemote) {
|
|
@@ -427,6 +627,8 @@ function crossFieldProblems(raw) {
|
|
|
427
627
|
}
|
|
428
628
|
}
|
|
429
629
|
});
|
|
630
|
+
if (isObj(raw.workflow))
|
|
631
|
+
out.push(...workflowProblems(raw, subs));
|
|
430
632
|
// Names must be unique across the tree this file declares: two AgentTools
|
|
431
633
|
// with one name collide, and dispatch routes by name.
|
|
432
634
|
const seen = new Map();
|
|
@@ -457,6 +659,144 @@ function crossFieldProblems(raw) {
|
|
|
457
659
|
}
|
|
458
660
|
return out;
|
|
459
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* The rules a `workflow:` block must keep (lib/workflow.ts), which a JSON
|
|
664
|
+
* Schema cannot say: every name is an agent or a declared node, `START`
|
|
665
|
+
* opens a chain and nothing else, a routing map follows the node it routes,
|
|
666
|
+
* a declared node is exactly one kind, and the pauses and remotes the graph
|
|
667
|
+
* cannot carry yet are refused with the reason.
|
|
668
|
+
*/
|
|
669
|
+
function workflowProblems(raw, subs) {
|
|
670
|
+
const out = [];
|
|
671
|
+
const wf = raw.workflow;
|
|
672
|
+
if (isObj(raw.dispatch)) {
|
|
673
|
+
out.push({ path: ['workflow'], message: 'a syndicate is a workflow or a plan-dispatch router, not both; remove `dispatch`' });
|
|
674
|
+
}
|
|
675
|
+
const agentNames = [];
|
|
676
|
+
if (isObj(raw.orchestrator) && typeof raw.orchestrator.name === 'string')
|
|
677
|
+
agentNames.push(raw.orchestrator.name);
|
|
678
|
+
for (const sub of subs)
|
|
679
|
+
if (isObj(sub) && typeof sub.name === 'string')
|
|
680
|
+
agentNames.push(sub.name);
|
|
681
|
+
const nodes = isObj(wf.nodes) ? wf.nodes : {};
|
|
682
|
+
const declared = Object.keys(nodes);
|
|
683
|
+
const known = new Set([...agentNames, ...declared]);
|
|
684
|
+
const mapped = new Set();
|
|
685
|
+
// Declared nodes: an agent takes modifiers only; anything else is one kind.
|
|
686
|
+
for (const [name, entry] of Object.entries(nodes)) {
|
|
687
|
+
if (!isObj(entry))
|
|
688
|
+
continue;
|
|
689
|
+
const kinds = NODE_KINDS.filter((k) => entry[k] !== undefined);
|
|
690
|
+
const isAgent = agentNames.includes(name);
|
|
691
|
+
if (isAgent && kinds.length > 0) {
|
|
692
|
+
out.push({ path: ['workflow', 'nodes', name], message: `'${name}' is an agent; its node entry may carry only route_key, retry and timeout` });
|
|
693
|
+
}
|
|
694
|
+
else if (!isAgent && kinds.length !== 1) {
|
|
695
|
+
out.push({ path: ['workflow', 'nodes', name], message: `a declared node is exactly one of ${NODE_KINDS.join(', ')}${kinds.length ? ` (has ${kinds.join(', ')})` : ''}` });
|
|
696
|
+
}
|
|
697
|
+
if (name.endsWith(ROUTE_STEP_SUFFIX) || name === START_NAME) {
|
|
698
|
+
out.push({ path: ['workflow', 'nodes', name], message: `'${name}' is reserved` });
|
|
699
|
+
}
|
|
700
|
+
const kind = nodeKind(entry);
|
|
701
|
+
if (kind === 'map') {
|
|
702
|
+
const target = entry.map;
|
|
703
|
+
if (!agentNames.includes(target)) {
|
|
704
|
+
const hint = suggest(target, agentNames);
|
|
705
|
+
out.push({ path: ['workflow', 'nodes', name, 'map'], message: `'${target}' is not an agent of this syndicate${hint ? ` (did you mean "${hint}"?)` : ''}` });
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
mapped.add(target);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (kind !== 'ask_user' && entry.schema !== undefined) {
|
|
712
|
+
out.push({ path: ['workflow', 'nodes', name, 'schema'], message: 'schema applies to ask_user only' });
|
|
713
|
+
}
|
|
714
|
+
if (kind !== 'map' && entry.max_parallel !== undefined) {
|
|
715
|
+
out.push({ path: ['workflow', 'nodes', name, 'max_parallel'], message: 'max_parallel applies to map only' });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const name of agentNames) {
|
|
719
|
+
if (name.endsWith(ROUTE_STEP_SUFFIX))
|
|
720
|
+
out.push({ path: ['workflow'], message: `agent name '${name}' ends with the reserved suffix ${ROUTE_STEP_SUFFIX}` });
|
|
721
|
+
}
|
|
722
|
+
// Edges.
|
|
723
|
+
const edges = Array.isArray(wf.edges) ? wf.edges : [];
|
|
724
|
+
let starts = 0;
|
|
725
|
+
const referenced = new Set();
|
|
726
|
+
const checkName = (name, path) => {
|
|
727
|
+
if (known.has(name)) {
|
|
728
|
+
referenced.add(name);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const hint = suggest(name, [...known]);
|
|
732
|
+
out.push({ path, message: `'${name}' is not an agent or a declared node${hint ? ` (did you mean "${hint}"?)` : ''}` });
|
|
733
|
+
};
|
|
734
|
+
edges.forEach((chain, i) => {
|
|
735
|
+
if (!Array.isArray(chain))
|
|
736
|
+
return;
|
|
737
|
+
chain.forEach((element, j) => {
|
|
738
|
+
const path = ['workflow', 'edges', i, j];
|
|
739
|
+
if (typeof element === 'string') {
|
|
740
|
+
if (element === START_NAME) {
|
|
741
|
+
if (j !== 0)
|
|
742
|
+
out.push({ path, message: 'START opens a chain; it cannot follow a node' });
|
|
743
|
+
else
|
|
744
|
+
starts++;
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
checkName(element, path);
|
|
748
|
+
}
|
|
749
|
+
else if (Array.isArray(element)) {
|
|
750
|
+
element.forEach((name, k) => typeof name === 'string' && checkName(name, [...path, k]));
|
|
751
|
+
}
|
|
752
|
+
else if (isObj(element)) {
|
|
753
|
+
const previous = chain[j - 1];
|
|
754
|
+
if (typeof previous !== 'string' || previous === START_NAME) {
|
|
755
|
+
out.push({ path, message: 'a routing map follows the name of the node whose output it routes' });
|
|
756
|
+
}
|
|
757
|
+
else if (!agentNames.includes(previous) && nodeKind(nodes[previous]) !== 'tool') {
|
|
758
|
+
out.push({ path, message: `'${previous}' is a ${nodeKind(nodes[previous]) ?? 'node'}; only an agent or a tool node emits a route` });
|
|
759
|
+
}
|
|
760
|
+
if (j !== chain.length - 1)
|
|
761
|
+
out.push({ path, message: 'a routing map ends its chain; start another chain from each target' });
|
|
762
|
+
for (const [key, target] of Object.entries(element)) {
|
|
763
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
764
|
+
targets.forEach((name, k) => typeof name === 'string' && checkName(name, [...path, key, k]));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (const name of isObj(element) || Array.isArray(element) || typeof element === 'string' ? elementNames(element) : []) {
|
|
768
|
+
if (mapped.has(name))
|
|
769
|
+
out.push({ path, message: `'${name}' is run by a map node; it cannot also appear in an edge` });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
if (edges.length && starts === 0)
|
|
774
|
+
out.push({ path: ['workflow', 'edges'], message: `no chain begins with ${START_NAME}` });
|
|
775
|
+
for (const name of declared) {
|
|
776
|
+
if (!referenced.has(name) && !agentNames.includes(name))
|
|
777
|
+
out.push({ path: ['workflow', 'nodes', name], message: 'declared but used in no edge' });
|
|
778
|
+
}
|
|
779
|
+
void DEFAULT_ROUTE_KEY;
|
|
780
|
+
// What a node cannot carry yet (lib/workflow.ts, "Not in this version").
|
|
781
|
+
const agents = [
|
|
782
|
+
[['orchestrator'], raw.orchestrator],
|
|
783
|
+
...subs.map((sub, i) => [['subagents', i], sub]),
|
|
784
|
+
];
|
|
785
|
+
for (const [path, agent] of agents) {
|
|
786
|
+
if (!isObj(agent))
|
|
787
|
+
continue;
|
|
788
|
+
if (Array.isArray(agent.require_approval) && agent.require_approval.length) {
|
|
789
|
+
out.push({ path: [...path, 'require_approval'], message: 'approval gates are not supported inside a workflow yet' });
|
|
790
|
+
}
|
|
791
|
+
if (isObj(agent.skills) && agent.skills.scripts === 'local') {
|
|
792
|
+
out.push({ path: [...path, 'skills', 'scripts'], message: 'skill scripts (an approval pause) are not supported inside a workflow yet' });
|
|
793
|
+
}
|
|
794
|
+
if (typeof agent.a2a_agent_url === 'string') {
|
|
795
|
+
out.push({ path: [...path, 'a2a_agent_url'], message: 'a remote agent cannot be a workflow node yet' });
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
460
800
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
461
801
|
/**
|
|
462
802
|
* Thrown by validateSyndicateConfig. `problems` holds one `<file>: <path> —
|
package/dist/lib/toolRegistry.js
CHANGED
|
@@ -18,6 +18,8 @@ import { WIKI_AGENT_TOOL_CONTRACTS } from './tools/wikiTools.js';
|
|
|
18
18
|
import { SCIENCE_TOOL_CONTRACTS } from './tools/scienceTools.js';
|
|
19
19
|
import { TASK_TOOL_CONTRACTS } from './tools/taskTools.js';
|
|
20
20
|
import { X_SEARCH } from './tools/xSearchTool.js';
|
|
21
|
+
import { askUserTool } from './runtime/questions.js';
|
|
22
|
+
import { URL_CONTEXT } from './tools/urlContextTool.js';
|
|
21
23
|
import { xApiSearchTool } from './tools/xApiSearchTool.js';
|
|
22
24
|
// Knowledge-bundle tools, derived from their contracts so the YAML names
|
|
23
25
|
// can never drift from the definitions. The agentic composites
|
|
@@ -52,6 +54,9 @@ const BUILTIN_TOOLS = {
|
|
|
52
54
|
// reading (keyless — works on every provider, including local Ollama).
|
|
53
55
|
// augustin.yaml and librarian-style research agents declare it.
|
|
54
56
|
web_extract: webExtractTool,
|
|
57
|
+
// Gemini reads URLs in the conversation server-side; a no-op (reported as
|
|
58
|
+
// dropped by the doctor) on other providers. lib/tools/urlContextTool.ts.
|
|
59
|
+
url_context: URL_CONTEXT,
|
|
55
60
|
x_search: X_SEARCH,
|
|
56
61
|
// X API v2 recent search as a client-side contract, photos transcribed
|
|
57
62
|
// inline — runs on every provider; needs X_BEARER_TOKEN in the server env.
|
|
@@ -62,6 +67,10 @@ const BUILTIN_TOOLS = {
|
|
|
62
67
|
google_search: GOOGLE_SEARCH,
|
|
63
68
|
generate_image: generateImageTool,
|
|
64
69
|
inspect_image: inspectImageTool,
|
|
70
|
+
// Ask the person mid-turn (lib/runtime/questions.ts): a long-running call
|
|
71
|
+
// that ends the turn input-required; the next message is its answer. Only
|
|
72
|
+
// on an agent the turn runs directly (the schema enforces it).
|
|
73
|
+
ask_user: askUserTool,
|
|
65
74
|
load_memory: LOAD_MEMORY,
|
|
66
75
|
preload_memory: PRELOAD_MEMORY,
|
|
67
76
|
};
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { FunctionTool } from '@google/adk';
|
|
2
|
+
import type { RedirectPolicy } from '../net/redirects.ts';
|
|
2
3
|
export declare function assertSafeMcpUrl(raw: string, env?: NodeJS.ProcessEnv): Promise<URL>;
|
|
3
4
|
export declare function mcpAuthHeaders(url: URL, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Where an MCP connection may be redirected (ADR 0036's rule, applied to MCP):
|
|
7
|
+
* a hop on the server's own origin gets the server's check (ALLOW_PRIVATE_MCP
|
|
8
|
+
* applies), a hop anywhere else the full guard with no development exception,
|
|
9
|
+
* and lib/net/redirects.ts strips every credential header from it, so a
|
|
10
|
+
* bearer token from MCP_BEARER_TOKENS never follows a redirect off its host.
|
|
11
|
+
* The SDK already refuses a POST endpoint on another origin.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MCP_REDIRECTS: RedirectPolicy;
|
|
14
|
+
/** The fetch an MCP transport uses: every redirect hop checked before it is followed. */
|
|
15
|
+
export declare const mcpFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
16
|
+
/**
|
|
17
|
+
* A remote MCP server is an untrusted tool vendor: its tool descriptions go
|
|
18
|
+
* into the prompt and its results into the conversation. Both are bounded,
|
|
19
|
+
* so a server cannot flood the context (or the bill) with either.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MAX_MCP_DESCRIPTION_CHARS = 1000;
|
|
22
|
+
export declare const MAX_MCP_RESULT_CHARS = 20000;
|
|
23
|
+
/**
|
|
24
|
+
* Close every open MCP connection. A connection otherwise lives as long as
|
|
25
|
+
* the process (its tools are called for the life of the compiled agent), and
|
|
26
|
+
* its stream reconnects when the server restarts; an embedding app that
|
|
27
|
+
* stops cleanly, or a test, calls this.
|
|
28
|
+
*/
|
|
29
|
+
export declare function closeMcpConnections(): Promise<void>;
|
|
4
30
|
export declare function createMcpTools(mcpServerUrl: string): Promise<FunctionTool[]>;
|
|
@@ -2,6 +2,7 @@ import { FunctionTool } from '@google/adk';
|
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
4
4
|
import { checkHost } from '../net/addressGuard.js';
|
|
5
|
+
import { fetchWithRedirectPolicy } from '../net/redirects.js';
|
|
5
6
|
// Security (SSRF): mcp_server_url can arrive from a registry-stored syndicate
|
|
6
7
|
// config. Only http(s), and the host must pass lib/net/addressGuard.ts (the
|
|
7
8
|
// same guard web_extract uses): local names and non-public addresses —
|
|
@@ -53,11 +54,61 @@ export function mcpAuthHeaders(url, env = process.env) {
|
|
|
53
54
|
const token = tokens[host];
|
|
54
55
|
return typeof token === 'string' && token ? { Authorization: `Bearer ${token}` } : {};
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Where an MCP connection may be redirected (ADR 0036's rule, applied to MCP):
|
|
59
|
+
* a hop on the server's own origin gets the server's check (ALLOW_PRIVATE_MCP
|
|
60
|
+
* applies), a hop anywhere else the full guard with no development exception,
|
|
61
|
+
* and lib/net/redirects.ts strips every credential header from it, so a
|
|
62
|
+
* bearer token from MCP_BEARER_TOKENS never follows a redirect off its host.
|
|
63
|
+
* The SDK already refuses a POST endpoint on another origin.
|
|
64
|
+
*/
|
|
65
|
+
export const MCP_REDIRECTS = {
|
|
66
|
+
async hopProblem(url, crossOrigin) {
|
|
67
|
+
if (!crossOrigin) {
|
|
68
|
+
try {
|
|
69
|
+
await assertSafeMcpUrl(url.href);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return err instanceof Error ? err.message : String(err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
77
|
+
return `${url.protocol} is not http(s)`;
|
|
78
|
+
const reason = await checkHost(url.hostname);
|
|
79
|
+
return reason ? `refusing MCP redirect to ${url.hostname}: ${reason}` : null;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
/** The fetch an MCP transport uses: every redirect hop checked before it is followed. */
|
|
83
|
+
export const mcpFetch = (input, init) => fetchWithRedirectPolicy(input, init, MCP_REDIRECTS, (i, o) => fetch(i, o));
|
|
84
|
+
/**
|
|
85
|
+
* A remote MCP server is an untrusted tool vendor: its tool descriptions go
|
|
86
|
+
* into the prompt and its results into the conversation. Both are bounded,
|
|
87
|
+
* so a server cannot flood the context (or the bill) with either.
|
|
88
|
+
*/
|
|
89
|
+
export const MAX_MCP_DESCRIPTION_CHARS = 1_000;
|
|
90
|
+
export const MAX_MCP_RESULT_CHARS = 20_000;
|
|
91
|
+
const bounded = (text, max, what) => text.length <= max ? text : `${text.slice(0, max)}… [${what} cut at ${max} characters]`;
|
|
92
|
+
/** Every MCP connection opened and still in use, so a shutdown can close them. */
|
|
93
|
+
const openTransports = new Set();
|
|
94
|
+
/**
|
|
95
|
+
* Close every open MCP connection. A connection otherwise lives as long as
|
|
96
|
+
* the process (its tools are called for the life of the compiled agent), and
|
|
97
|
+
* its stream reconnects when the server restarts; an embedding app that
|
|
98
|
+
* stops cleanly, or a test, calls this.
|
|
99
|
+
*/
|
|
100
|
+
export async function closeMcpConnections() {
|
|
101
|
+
const all = [...openTransports];
|
|
102
|
+
openTransports.clear();
|
|
103
|
+
await Promise.all(all.map((t) => t.close().catch(() => { })));
|
|
104
|
+
}
|
|
56
105
|
export async function createMcpTools(mcpServerUrl) {
|
|
106
|
+
let transport;
|
|
57
107
|
try {
|
|
58
108
|
const url = await assertSafeMcpUrl(mcpServerUrl);
|
|
59
|
-
|
|
60
|
-
requestInit: { headers: mcpAuthHeaders(url) }
|
|
109
|
+
transport = new SSEClientTransport(url, {
|
|
110
|
+
requestInit: { headers: mcpAuthHeaders(url) },
|
|
111
|
+
fetch: mcpFetch,
|
|
61
112
|
});
|
|
62
113
|
const client = new Client({
|
|
63
114
|
name: 'melchizedek-a2a-client',
|
|
@@ -66,6 +117,7 @@ export async function createMcpTools(mcpServerUrl) {
|
|
|
66
117
|
capabilities: {}
|
|
67
118
|
});
|
|
68
119
|
await client.connect(transport);
|
|
120
|
+
openTransports.add(transport);
|
|
69
121
|
// Fetch available tools from the MCP server
|
|
70
122
|
const toolsResponse = await client.listTools();
|
|
71
123
|
// MCP servers describe tools in standard lowercase JSON Schema; the ADK
|
|
@@ -106,7 +158,7 @@ export async function createMcpTools(mcpServerUrl) {
|
|
|
106
158
|
}
|
|
107
159
|
return new FunctionTool({
|
|
108
160
|
name: tool.name,
|
|
109
|
-
description: tool.description || `MCP Tool: ${tool.name}`,
|
|
161
|
+
description: bounded(tool.description || `MCP Tool: ${tool.name}`, MAX_MCP_DESCRIPTION_CHARS, 'description'),
|
|
110
162
|
parameters: {
|
|
111
163
|
type: 'OBJECT',
|
|
112
164
|
properties,
|
|
@@ -120,7 +172,7 @@ export async function createMcpTools(mcpServerUrl) {
|
|
|
120
172
|
});
|
|
121
173
|
const content = result.content;
|
|
122
174
|
const texts = content.filter(c => c.type === 'text').map(c => c.text ?? '');
|
|
123
|
-
return texts.join('\n');
|
|
175
|
+
return bounded(texts.join('\n'), MAX_MCP_RESULT_CHARS, 'result');
|
|
124
176
|
}
|
|
125
177
|
catch (error) {
|
|
126
178
|
return `[MCP ERROR] Tool ${tool.name} failed: ${error.message}`;
|
|
@@ -131,6 +183,11 @@ export async function createMcpTools(mcpServerUrl) {
|
|
|
131
183
|
}
|
|
132
184
|
catch (error) {
|
|
133
185
|
console.warn(`[MCP] Failed to connect or load tools from ${mcpServerUrl}`, error);
|
|
186
|
+
// A failed connect leaves the SSE stream's reconnect timer running; close
|
|
187
|
+
// it, or every unreachable server keeps retrying for the process's life.
|
|
188
|
+
if (transport)
|
|
189
|
+
openTransports.delete(transport);
|
|
190
|
+
await transport?.close().catch(() => { });
|
|
134
191
|
return [];
|
|
135
192
|
}
|
|
136
193
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BaseTool } from '@google/adk';
|
|
2
|
+
export interface OpenApiAuthConfig {
|
|
3
|
+
/** Environment variable holding a bearer token (`Authorization: Bearer …`). */
|
|
4
|
+
bearer_env?: string;
|
|
5
|
+
/** An API key from an environment variable, sent in a header or the query. */
|
|
6
|
+
api_key?: {
|
|
7
|
+
env: string;
|
|
8
|
+
in: 'header' | 'query';
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** One entry of an agent's `openapi:` list. */
|
|
13
|
+
export interface OpenApiConfig {
|
|
14
|
+
/** Path to an OpenAPI 3 spec (.yaml, .yml or .json). Relative paths resolve beside the syndicate file. */
|
|
15
|
+
spec: string;
|
|
16
|
+
/** Operations to expose, by operationId or tool name. Omitted: every GET operation. */
|
|
17
|
+
operations?: string[];
|
|
18
|
+
/** Overrides the spec's `servers[0].url`. */
|
|
19
|
+
base_url?: string;
|
|
20
|
+
/** Prepended to every tool name (`<prefix>_<tool>`), to keep two APIs apart. */
|
|
21
|
+
prefix?: string;
|
|
22
|
+
auth?: OpenApiAuthConfig;
|
|
23
|
+
}
|
|
24
|
+
/** Characters a single API result may carry into the conversation. */
|
|
25
|
+
export declare const MAX_RESULT_CHARS = 20000;
|
|
26
|
+
/** Marks a tool built here, so require_approval may gate it (lib/compile.ts). */
|
|
27
|
+
export declare const OPENAPI_TOOL: unique symbol;
|
|
28
|
+
/** snake_case, the way ADK names a tool from an operationId. */
|
|
29
|
+
export declare function toSnake(name: string): string;
|
|
30
|
+
/** True when a configured operation name (an operationId or a tool name) names this tool. */
|
|
31
|
+
export declare function namesTool(configured: string, tool: {
|
|
32
|
+
name: string;
|
|
33
|
+
operation?: {
|
|
34
|
+
operationId?: string;
|
|
35
|
+
};
|
|
36
|
+
}): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Why an `auth` may not read this variable, or null. With
|
|
39
|
+
* OPENAPI_CREDENTIAL_ENVS set, only the names it lists are allowed; without
|
|
40
|
+
* it, anything but the framework's own variables is.
|
|
41
|
+
*/
|
|
42
|
+
export declare function credentialEnvProblem(name: string, env?: NodeJS.ProcessEnv): string | null;
|
|
43
|
+
/** Cut an API result to MAX_RESULT_CHARS, saying so. */
|
|
44
|
+
export declare function boundResult(result: unknown): unknown;
|
|
45
|
+
/**
|
|
46
|
+
* The tools one `openapi:` entry gives an agent. Throws (failing the compile)
|
|
47
|
+
* on a missing spec, an unset auth variable, an operation the spec does not
|
|
48
|
+
* have, or a server the guard refuses.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildOpenApiTools(entry: OpenApiConfig, baseDir?: string): Promise<BaseTool[]>;
|
|
51
|
+
/** True for a tool built from an `openapi:` entry. */
|
|
52
|
+
export declare function isOpenApiTool(tool: unknown): boolean;
|