melchizedek-agents 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_SETUP.md +182 -0
- package/DOCUMENTATION.md +614 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +164 -0
- package/README.md +151 -0
- package/config/agents/README.md +27 -0
- package/config/agents/examples/ares.yaml +74 -0
- package/config/agents/examples/cartographers.yaml +115 -0
- package/config/agents/examples/claude.yaml +17 -0
- package/config/agents/examples/council.yaml +65 -0
- package/config/agents/examples/critic.yaml +74 -0
- package/config/agents/examples/delegation.yaml +29 -0
- package/config/agents/examples/hierarchical.yaml +26 -0
- package/config/agents/examples/image_production.yaml +112 -0
- package/config/agents/examples/librarian.yaml +75 -0
- package/config/agents/examples/model_zoo.yaml +81 -0
- package/config/agents/examples/patient_advocate.yaml +107 -0
- package/config/agents/examples/scriptorium.yaml +96 -0
- package/config/agents/examples/style_council.yaml +83 -0
- package/config/agents/examples/syndicate.yaml +34 -0
- package/config/agents/examples/tutor.yaml +66 -0
- package/config/agents/syndicateSchema.yaml +291 -0
- package/dist/lib/config.d.ts +108 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/dispatch.d.ts +133 -0
- package/dist/lib/dispatch.js +182 -0
- package/dist/lib/grounding.d.ts +28 -0
- package/dist/lib/grounding.js +61 -0
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/loadEnv.d.ts +21 -0
- package/dist/lib/loadEnv.js +58 -0
- package/dist/lib/loadSyndicate.d.ts +177 -0
- package/dist/lib/loadSyndicate.js +264 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
- package/dist/lib/memory/supabaseMemoryService.js +499 -0
- package/dist/lib/models/claudeLlm.d.ts +93 -0
- package/dist/lib/models/claudeLlm.js +332 -0
- package/dist/lib/models/gptLlm.d.ts +110 -0
- package/dist/lib/models/gptLlm.js +497 -0
- package/dist/lib/models/grokLlm.d.ts +64 -0
- package/dist/lib/models/grokLlm.js +86 -0
- package/dist/lib/models/ollamaLlm.d.ts +105 -0
- package/dist/lib/models/ollamaLlm.js +134 -0
- package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
- package/dist/lib/models/openAiCompatibleLlm.js +331 -0
- package/dist/lib/models/providerMap.d.ts +28 -0
- package/dist/lib/models/providerMap.js +45 -0
- package/dist/lib/models/registry.d.ts +71 -0
- package/dist/lib/models/registry.js +159 -0
- package/dist/lib/models/schemaNormalize.d.ts +22 -0
- package/dist/lib/models/schemaNormalize.js +58 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
- package/dist/lib/observability/supabaseSpanExporter.js +115 -0
- package/dist/lib/observability/tracer.d.ts +44 -0
- package/dist/lib/observability/tracer.js +242 -0
- package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
- package/dist/lib/persistence/supabaseProvider.js +88 -0
- package/dist/lib/session/supabaseSessionService.d.ts +26 -0
- package/dist/lib/session/supabaseSessionService.js +193 -0
- package/dist/lib/session/transcript.d.ts +193 -0
- package/dist/lib/session/transcript.js +309 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +58 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
- package/dist/lib/tools/collectionsSearchTool.js +86 -0
- package/dist/lib/tools/generateImageTool.d.ts +16 -0
- package/dist/lib/tools/generateImageTool.js +119 -0
- package/dist/lib/tools/inspectImageTool.d.ts +21 -0
- package/dist/lib/tools/inspectImageTool.js +97 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +116 -0
- package/dist/lib/tools/toolContract.d.ts +78 -0
- package/dist/lib/tools/toolContract.js +129 -0
- package/dist/lib/tools/webExtractTool.d.ts +70 -0
- package/dist/lib/tools/webExtractTool.js +461 -0
- package/dist/lib/tools/webSearchTool.d.ts +53 -0
- package/dist/lib/tools/webSearchTool.js +126 -0
- package/dist/lib/tools/wikiTools.d.ts +112 -0
- package/dist/lib/tools/wikiTools.js +697 -0
- package/dist/lib/tools/xSearchTool.d.ts +61 -0
- package/dist/lib/tools/xSearchTool.js +125 -0
- package/dist/lib/wiki/agentRun.d.ts +39 -0
- package/dist/lib/wiki/agentRun.js +79 -0
- package/dist/lib/wiki/builder.d.ts +71 -0
- package/dist/lib/wiki/builder.js +165 -0
- package/dist/lib/wiki/dive.d.ts +56 -0
- package/dist/lib/wiki/dive.js +208 -0
- package/dist/lib/wiki/entities.d.ts +220 -0
- package/dist/lib/wiki/entities.js +661 -0
- package/dist/lib/wiki/extract.d.ts +97 -0
- package/dist/lib/wiki/extract.js +305 -0
- package/dist/lib/wiki/fill.d.ts +37 -0
- package/dist/lib/wiki/fill.js +175 -0
- package/dist/lib/wiki/format.d.ts +132 -0
- package/dist/lib/wiki/format.js +177 -0
- package/dist/lib/wiki/graph.d.ts +80 -0
- package/dist/lib/wiki/graph.js +147 -0
- package/dist/lib/wiki/lint.d.ts +58 -0
- package/dist/lib/wiki/lint.js +267 -0
- package/dist/lib/wiki/markdown.d.ts +127 -0
- package/dist/lib/wiki/markdown.js +336 -0
- package/dist/lib/wiki/pipeline.d.ts +69 -0
- package/dist/lib/wiki/pipeline.js +119 -0
- package/dist/lib/wiki/vault.d.ts +84 -0
- package/dist/lib/wiki/vault.js +194 -0
- package/dist/scripts/a2a_server.d.ts +2 -0
- package/dist/scripts/a2a_server.js +997 -0
- package/dist/scripts/syndicate_chat.d.ts +2 -0
- package/dist/scripts/syndicate_chat.js +485 -0
- package/package.json +96 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inspectImageTool — native ADK FunctionTool for BLIND visual inventory.
|
|
3
|
+
*
|
|
4
|
+
* WHY a FunctionTool instead of an AgentTool subagent:
|
|
5
|
+
* Same constraint as generateImageTool in reverse — AgentTool converts all
|
|
6
|
+
* traffic to text, so image bytes can't ride into a subagent's context.
|
|
7
|
+
* A FunctionTool reads the saved file directly and calls the vision model
|
|
8
|
+
* itself.
|
|
9
|
+
*
|
|
10
|
+
* WHY the inventory prompt is hard-coded here and takes ONLY a file path:
|
|
11
|
+
* The whole point of the inventory is that it is blind. An agent that knows
|
|
12
|
+
* the spec asked for three amphorae will helpfully "see" three (expectation
|
|
13
|
+
* bias). Because this tool's signature accepts nothing but a path, the
|
|
14
|
+
* orchestrator CANNOT leak the spec into the inventory even by accident —
|
|
15
|
+
* the separation of perception from judgment is enforced by the type
|
|
16
|
+
* system, not by good intentions. Judgment happens downstream in the
|
|
17
|
+
* SpecAuditor subagent, which sees spec + inventory but never the image.
|
|
18
|
+
*/
|
|
19
|
+
import { FunctionTool } from '@google/adk';
|
|
20
|
+
import { type Schema } from '@google/genai';
|
|
21
|
+
export declare const inspectImageTool: FunctionTool<Schema>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inspectImageTool — native ADK FunctionTool for BLIND visual inventory.
|
|
3
|
+
*
|
|
4
|
+
* WHY a FunctionTool instead of an AgentTool subagent:
|
|
5
|
+
* Same constraint as generateImageTool in reverse — AgentTool converts all
|
|
6
|
+
* traffic to text, so image bytes can't ride into a subagent's context.
|
|
7
|
+
* A FunctionTool reads the saved file directly and calls the vision model
|
|
8
|
+
* itself.
|
|
9
|
+
*
|
|
10
|
+
* WHY the inventory prompt is hard-coded here and takes ONLY a file path:
|
|
11
|
+
* The whole point of the inventory is that it is blind. An agent that knows
|
|
12
|
+
* the spec asked for three amphorae will helpfully "see" three (expectation
|
|
13
|
+
* bias). Because this tool's signature accepts nothing but a path, the
|
|
14
|
+
* orchestrator CANNOT leak the spec into the inventory even by accident —
|
|
15
|
+
* the separation of perception from judgment is enforced by the type
|
|
16
|
+
* system, not by good intentions. Judgment happens downstream in the
|
|
17
|
+
* SpecAuditor subagent, which sees spec + inventory but never the image.
|
|
18
|
+
*/
|
|
19
|
+
import { FunctionTool } from '@google/adk';
|
|
20
|
+
import { GoogleGenAI } from '@google/genai';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { resolve, extname, sep } from 'node:path';
|
|
23
|
+
const VISION_MODEL = 'gemini-3.7-flash';
|
|
24
|
+
const INVENTORY_PROTOCOL = `You are a blind visual inventory agent. You know NOTHING about what this image was supposed to be, who requested it, or why. Enumerate ONLY what is actually present:
|
|
25
|
+
|
|
26
|
+
1. SUBJECTS — every distinct subject and object, with EXACT counts.
|
|
27
|
+
2. COMPOSITION — framing, perspective, and placement (foreground / midground / background).
|
|
28
|
+
3. LIGHT — direction, quality, and any notable lighting phenomena.
|
|
29
|
+
4. PALETTE — dominant colors and tonal range.
|
|
30
|
+
5. MEDIUM CUES — photographic, painterly, or rendered qualities; grain, texture, brushwork.
|
|
31
|
+
6. TEXT & ARTIFACTS — any lettering, watermarks, or generation artifacts.
|
|
32
|
+
|
|
33
|
+
RULES:
|
|
34
|
+
- Counts are exact numbers, never "several".
|
|
35
|
+
- ZERO quality judgments: no "beautiful", no "well-composed", no "striking".
|
|
36
|
+
- ZERO guesses about intent or purpose.
|
|
37
|
+
- If something is genuinely ambiguous, write "uncertain:" and describe what is visible.
|
|
38
|
+
Return a plain, factual, numbered inventory.`;
|
|
39
|
+
export const inspectImageTool = new FunctionTool({
|
|
40
|
+
name: 'inspect_image',
|
|
41
|
+
description: 'Performs a BLIND visual inventory of a previously saved image (subjects with exact counts, ' +
|
|
42
|
+
'composition, light, palette, medium cues, artifacts). Pass ONLY the file path returned by ' +
|
|
43
|
+
'generate_image — never describe the expected content, or the inventory will be primed.',
|
|
44
|
+
parameters: {
|
|
45
|
+
type: 'OBJECT',
|
|
46
|
+
properties: {
|
|
47
|
+
image_path: {
|
|
48
|
+
type: 'STRING',
|
|
49
|
+
description: 'Path to the saved image, e.g. "outputs/image_1234.png".',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['image_path'],
|
|
53
|
+
},
|
|
54
|
+
execute: async (input) => {
|
|
55
|
+
const { image_path } = input;
|
|
56
|
+
console.log(`[InspectTool] Blind inventory requested for: ${image_path}`);
|
|
57
|
+
// Confine reads to the outputs/ directory — this tool inventories
|
|
58
|
+
// generated artifacts, not the filesystem. OUTPUTS_DIR env overrides
|
|
59
|
+
// the cwd default (must match generateImageTool's).
|
|
60
|
+
const outputsDir = process.env.OUTPUTS_DIR
|
|
61
|
+
? resolve(process.env.OUTPUTS_DIR)
|
|
62
|
+
: resolve(process.cwd(), 'outputs');
|
|
63
|
+
const fullPath = resolve(process.cwd(), image_path);
|
|
64
|
+
if (!fullPath.startsWith(outputsDir + sep)) {
|
|
65
|
+
throw new Error('inspect_image only reads files under outputs/');
|
|
66
|
+
}
|
|
67
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_GENAI_API_KEY;
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
throw new Error('Gemini API Key is not configured (neither GEMINI_API_KEY nor GOOGLE_GENAI_API_KEY was found in environment).');
|
|
70
|
+
}
|
|
71
|
+
const bytes = readFileSync(fullPath);
|
|
72
|
+
const ext = extname(fullPath).slice(1).toLowerCase() || 'png';
|
|
73
|
+
const mimeType = `image/${ext === 'jpg' ? 'jpeg' : ext}`;
|
|
74
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
75
|
+
console.log(`[InspectTool] Sending image (${bytes.length} bytes, ${mimeType}) to ${VISION_MODEL}...`);
|
|
76
|
+
const response = await ai.models.generateContent({
|
|
77
|
+
model: VISION_MODEL,
|
|
78
|
+
contents: [
|
|
79
|
+
{
|
|
80
|
+
role: 'user',
|
|
81
|
+
parts: [
|
|
82
|
+
{ inlineData: { mimeType, data: bytes.toString('base64') } },
|
|
83
|
+
{ text: INVENTORY_PROTOCOL },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const text = response.candidates?.[0]?.content?.parts
|
|
89
|
+
?.map((p) => p.text ?? '')
|
|
90
|
+
.join('') ?? '';
|
|
91
|
+
if (!text) {
|
|
92
|
+
throw new Error('inspect_image: vision model returned no text inventory.');
|
|
93
|
+
}
|
|
94
|
+
console.log(`[InspectTool] Inventory received (${text.length} chars).`);
|
|
95
|
+
return `BLIND INVENTORY of ${image_path}:\n${text}`;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { FunctionTool } from '@google/adk';
|
|
2
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
4
|
+
// Security (SSRF): mcp_server_url can arrive from a registry-stored syndicate
|
|
5
|
+
// config. Restrict the schemes we'll dial and block private/loopback/link-local
|
|
6
|
+
// hosts so a malicious config can't reach internal services or the cloud
|
|
7
|
+
// metadata endpoint. Set ALLOW_PRIVATE_MCP=true to permit private hosts in
|
|
8
|
+
// local development (where MCP servers commonly run on localhost).
|
|
9
|
+
function assertSafeMcpUrl(raw) {
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error(`Invalid MCP server URL: ${raw}`);
|
|
16
|
+
}
|
|
17
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
18
|
+
throw new Error(`Unsupported MCP URL scheme: ${url.protocol}`);
|
|
19
|
+
}
|
|
20
|
+
if (process.env.ALLOW_PRIVATE_MCP === 'true')
|
|
21
|
+
return url;
|
|
22
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
23
|
+
const isPrivate = host === 'localhost' ||
|
|
24
|
+
host === '0.0.0.0' ||
|
|
25
|
+
host === '::1' ||
|
|
26
|
+
host.endsWith('.localhost') ||
|
|
27
|
+
host.endsWith('.internal') ||
|
|
28
|
+
/^127\./.test(host) ||
|
|
29
|
+
/^10\./.test(host) ||
|
|
30
|
+
/^192\.168\./.test(host) ||
|
|
31
|
+
/^169\.254\./.test(host) || // link-local incl. 169.254.169.254 metadata
|
|
32
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
|
|
33
|
+
/^fc/.test(host) || /^fd/.test(host) || /^fe80:/.test(host); // IPv6 ULA/link-local
|
|
34
|
+
if (isPrivate) {
|
|
35
|
+
throw new Error(`Refusing to connect to private/loopback MCP host: ${host} (set ALLOW_PRIVATE_MCP=true for local dev)`);
|
|
36
|
+
}
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
export async function createMcpTools(mcpServerUrl) {
|
|
40
|
+
try {
|
|
41
|
+
const transport = new SSEClientTransport(assertSafeMcpUrl(mcpServerUrl));
|
|
42
|
+
const client = new Client({
|
|
43
|
+
name: 'melchizedek-a2a-client',
|
|
44
|
+
version: '1.0.0'
|
|
45
|
+
}, {
|
|
46
|
+
capabilities: {}
|
|
47
|
+
});
|
|
48
|
+
await client.connect(transport);
|
|
49
|
+
// Fetch available tools from the MCP server
|
|
50
|
+
const toolsResponse = await client.listTools();
|
|
51
|
+
// MCP servers describe tools in standard lowercase JSON Schema; the ADK
|
|
52
|
+
// is Gemini-native and expects UPPERCASE type names. Uppercase deeply,
|
|
53
|
+
// preserving enum/description/required and nested properties/items —
|
|
54
|
+
// non-Gemini adapters normalize back to lowercase at request-build time
|
|
55
|
+
// via lib/models/schemaNormalize.ts.
|
|
56
|
+
const toGeminiSchema = (node) => {
|
|
57
|
+
if (Array.isArray(node))
|
|
58
|
+
return node.map(toGeminiSchema);
|
|
59
|
+
if (!node || typeof node !== 'object')
|
|
60
|
+
return node;
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [key, value] of Object.entries(node)) {
|
|
63
|
+
if (key === 'type') {
|
|
64
|
+
out[key] = typeof value === 'string' ? value.toUpperCase() : value;
|
|
65
|
+
}
|
|
66
|
+
else if (key === 'enum' || key === 'required') {
|
|
67
|
+
out[key] = value; // value lists, not schema nodes
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
out[key] = toGeminiSchema(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
return toolsResponse.tools.map(tool => {
|
|
76
|
+
const properties = {};
|
|
77
|
+
const required = tool.inputSchema?.required || [];
|
|
78
|
+
if (tool.inputSchema?.properties) {
|
|
79
|
+
for (const [key, prop] of Object.entries(tool.inputSchema.properties)) {
|
|
80
|
+
properties[key] = toGeminiSchema({
|
|
81
|
+
...prop,
|
|
82
|
+
type: prop.type ?? 'string',
|
|
83
|
+
description: prop.description || ''
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return new FunctionTool({
|
|
88
|
+
name: tool.name,
|
|
89
|
+
description: tool.description || `MCP Tool: ${tool.name}`,
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'OBJECT',
|
|
92
|
+
properties,
|
|
93
|
+
required
|
|
94
|
+
},
|
|
95
|
+
execute: async (input) => {
|
|
96
|
+
try {
|
|
97
|
+
const result = await client.callTool({
|
|
98
|
+
name: tool.name,
|
|
99
|
+
arguments: input
|
|
100
|
+
});
|
|
101
|
+
const content = result.content;
|
|
102
|
+
const texts = content.filter(c => c.type === 'text').map(c => c.text ?? '');
|
|
103
|
+
return texts.join('\n');
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return `[MCP ERROR] Tool ${tool.name} failed: ${error.message}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.warn(`[MCP] Failed to connect or load tools from ${mcpServerUrl}`, error);
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/toolContract.ts — define a tool ONCE, serve it anywhere.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* A native tool that should be reachable both by our own agents (ADK
|
|
6
|
+
* FunctionTool, Gemini-dialect Schema) and by outside MCP clients
|
|
7
|
+
* (standard JSON Schema in tools/list) previously needed its schema
|
|
8
|
+
* hand-written twice — two dialects of the same contract, guaranteed to
|
|
9
|
+
* drift. Here a tool is one object — name, description, zod schema,
|
|
10
|
+
* execute — and thin adapters DERIVE each deployment surface:
|
|
11
|
+
*
|
|
12
|
+
* defineTool(...) ──► toFunctionTool() ADK / syndicate agents
|
|
13
|
+
* └──► toMcpToolDefinition() MCP tools/list entry
|
|
14
|
+
*
|
|
15
|
+
* The zod schema is the single source of truth. zod v4's native
|
|
16
|
+
* z.toJSONSchema() emits standard JSON Schema (what MCP and every
|
|
17
|
+
* non-Gemini provider natively want); toGeminiSchema() below derives the
|
|
18
|
+
* ADK dialect from it — the exact inverse of the request-time bridge in
|
|
19
|
+
* lib/models/schemaNormalize.ts, which keeps lowercasing FunctionTool
|
|
20
|
+
* schemas for Anthropic/OpenAI/xAI/Ollama and is unaffected by this file.
|
|
21
|
+
*
|
|
22
|
+
* DELIBERATELY NOT HERE: exposure. Defining a contract publishes nothing.
|
|
23
|
+
* An agent sees the tool only when its name is added to TOOL_MAP in
|
|
24
|
+
* lib/toolRegistry.ts AND declared in the syndicate YAML; an MCP client
|
|
25
|
+
* sees it only when a server script explicitly lists it (see
|
|
26
|
+
* scripts/financial_mcp_server.ts). Both remain deliberate acts.
|
|
27
|
+
*/
|
|
28
|
+
import { FunctionTool } from '@google/adk';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
export interface ToolContract<S extends z.ZodType = z.ZodType> {
|
|
31
|
+
name: string;
|
|
32
|
+
/** LLM-facing prompt text, not developer docs — it steers when agents call the tool. */
|
|
33
|
+
description: string;
|
|
34
|
+
/** Single source of truth for the input shape; both wire dialects derive from it. */
|
|
35
|
+
schema: S;
|
|
36
|
+
execute: (input: z.infer<S>) => Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/** Identity helper whose only job is inferring the execute() input type from the schema. */
|
|
39
|
+
export declare function defineTool<S extends z.ZodType>(contract: ToolContract<S>): ToolContract<S>;
|
|
40
|
+
/**
|
|
41
|
+
* Validate args against the contract's schema and run it. Validation failure
|
|
42
|
+
* returns an error STRING (never throws) so the calling model sees what to
|
|
43
|
+
* fix and can retry — the same convention the hand-written tools used for
|
|
44
|
+
* upstream API failures. Unknown keys are stripped by zod's default object
|
|
45
|
+
* behavior.
|
|
46
|
+
*/
|
|
47
|
+
export declare function executeContract(contract: ToolContract<any>, args: unknown): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Standard JSON Schema for the contract's input — the canonical dialect
|
|
50
|
+
* (MCP inputSchema; also what four of our five providers natively accept).
|
|
51
|
+
* `$schema` is dropped: it's metadata about the document, noise on the wire.
|
|
52
|
+
*/
|
|
53
|
+
export declare function toStandardJsonSchema(contract: ToolContract<any>): Record<string, unknown>;
|
|
54
|
+
/**
|
|
55
|
+
* Standard JSON Schema → Gemini/ADK dialect. The inverse of
|
|
56
|
+
* schemaNormalize.toLowercaseJsonSchema(): every `type` value is UPPERCASED
|
|
57
|
+
* ('object' → 'OBJECT'). Also drops keywords the Gemini API rejects or
|
|
58
|
+
* ignores: `additionalProperties` (zod emits it on every object) and
|
|
59
|
+
* `default` (defaults are applied by zod at parse time, not by the model).
|
|
60
|
+
* `enum` and `required` are value lists, copied verbatim.
|
|
61
|
+
*/
|
|
62
|
+
export declare function toGeminiSchema(jsonSchema: unknown): Record<string, unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* ADK surface: a live FunctionTool whose Gemini-dialect parameters are
|
|
65
|
+
* derived from the zod schema and whose execute() zod-validates its input.
|
|
66
|
+
* Non-Gemini providers keep receiving this schema through
|
|
67
|
+
* schemaNormalize.toLowercaseJsonSchema() at request-build time, unchanged.
|
|
68
|
+
*/
|
|
69
|
+
export declare function toFunctionTool(contract: ToolContract<any>): FunctionTool;
|
|
70
|
+
/**
|
|
71
|
+
* MCP surface: a tools/list entry. Pair with executeContract() in the
|
|
72
|
+
* server's CallTool handler — see scripts/financial_mcp_server.ts.
|
|
73
|
+
*/
|
|
74
|
+
export declare function toMcpToolDefinition(contract: ToolContract<any>): {
|
|
75
|
+
name: string;
|
|
76
|
+
description: string;
|
|
77
|
+
inputSchema: Record<string, unknown>;
|
|
78
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/toolContract.ts — define a tool ONCE, serve it anywhere.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* A native tool that should be reachable both by our own agents (ADK
|
|
6
|
+
* FunctionTool, Gemini-dialect Schema) and by outside MCP clients
|
|
7
|
+
* (standard JSON Schema in tools/list) previously needed its schema
|
|
8
|
+
* hand-written twice — two dialects of the same contract, guaranteed to
|
|
9
|
+
* drift. Here a tool is one object — name, description, zod schema,
|
|
10
|
+
* execute — and thin adapters DERIVE each deployment surface:
|
|
11
|
+
*
|
|
12
|
+
* defineTool(...) ──► toFunctionTool() ADK / syndicate agents
|
|
13
|
+
* └──► toMcpToolDefinition() MCP tools/list entry
|
|
14
|
+
*
|
|
15
|
+
* The zod schema is the single source of truth. zod v4's native
|
|
16
|
+
* z.toJSONSchema() emits standard JSON Schema (what MCP and every
|
|
17
|
+
* non-Gemini provider natively want); toGeminiSchema() below derives the
|
|
18
|
+
* ADK dialect from it — the exact inverse of the request-time bridge in
|
|
19
|
+
* lib/models/schemaNormalize.ts, which keeps lowercasing FunctionTool
|
|
20
|
+
* schemas for Anthropic/OpenAI/xAI/Ollama and is unaffected by this file.
|
|
21
|
+
*
|
|
22
|
+
* DELIBERATELY NOT HERE: exposure. Defining a contract publishes nothing.
|
|
23
|
+
* An agent sees the tool only when its name is added to TOOL_MAP in
|
|
24
|
+
* lib/toolRegistry.ts AND declared in the syndicate YAML; an MCP client
|
|
25
|
+
* sees it only when a server script explicitly lists it (see
|
|
26
|
+
* scripts/financial_mcp_server.ts). Both remain deliberate acts.
|
|
27
|
+
*/
|
|
28
|
+
import { FunctionTool } from '@google/adk';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
/** Identity helper whose only job is inferring the execute() input type from the schema. */
|
|
31
|
+
export function defineTool(contract) {
|
|
32
|
+
return contract;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate args against the contract's schema and run it. Validation failure
|
|
36
|
+
* returns an error STRING (never throws) so the calling model sees what to
|
|
37
|
+
* fix and can retry — the same convention the hand-written tools used for
|
|
38
|
+
* upstream API failures. Unknown keys are stripped by zod's default object
|
|
39
|
+
* behavior.
|
|
40
|
+
*/
|
|
41
|
+
export async function executeContract(contract, args) {
|
|
42
|
+
const parsed = contract.schema.safeParse(args ?? {});
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
const issues = parsed.error.issues
|
|
45
|
+
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
46
|
+
.join('; ');
|
|
47
|
+
return `Error: invalid arguments for ${contract.name}: ${issues}`;
|
|
48
|
+
}
|
|
49
|
+
return contract.execute(parsed.data);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Standard JSON Schema for the contract's input — the canonical dialect
|
|
53
|
+
* (MCP inputSchema; also what four of our five providers natively accept).
|
|
54
|
+
* `$schema` is dropped: it's metadata about the document, noise on the wire.
|
|
55
|
+
*/
|
|
56
|
+
export function toStandardJsonSchema(contract) {
|
|
57
|
+
const { $schema: _drop, ...schema } = z.toJSONSchema(contract.schema);
|
|
58
|
+
return schema;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Standard JSON Schema → Gemini/ADK dialect. The inverse of
|
|
62
|
+
* schemaNormalize.toLowercaseJsonSchema(): every `type` value is UPPERCASED
|
|
63
|
+
* ('object' → 'OBJECT'). Also drops keywords the Gemini API rejects or
|
|
64
|
+
* ignores: `additionalProperties` (zod emits it on every object) and
|
|
65
|
+
* `default` (defaults are applied by zod at parse time, not by the model).
|
|
66
|
+
* `enum` and `required` are value lists, copied verbatim.
|
|
67
|
+
*/
|
|
68
|
+
export function toGeminiSchema(jsonSchema) {
|
|
69
|
+
const result = geminiNode(jsonSchema);
|
|
70
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
return { type: 'OBJECT', properties: {} };
|
|
74
|
+
}
|
|
75
|
+
function geminiNode(node) {
|
|
76
|
+
if (Array.isArray(node))
|
|
77
|
+
return node.map(geminiNode);
|
|
78
|
+
if (!node || typeof node !== 'object')
|
|
79
|
+
return node;
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [key, value] of Object.entries(node)) {
|
|
82
|
+
if (key === 'additionalProperties' || key === 'default' || key === '$schema') {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
else if (key === 'type') {
|
|
86
|
+
if (typeof value === 'string') {
|
|
87
|
+
out[key] = value.toUpperCase();
|
|
88
|
+
}
|
|
89
|
+
else if (Array.isArray(value)) {
|
|
90
|
+
out[key] = value.map((v) => (typeof v === 'string' ? v.toUpperCase() : v));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
out[key] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (key === 'enum' || key === 'required') {
|
|
97
|
+
out[key] = Array.isArray(value) ? [...value] : value;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
out[key] = geminiNode(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* ADK surface: a live FunctionTool whose Gemini-dialect parameters are
|
|
107
|
+
* derived from the zod schema and whose execute() zod-validates its input.
|
|
108
|
+
* Non-Gemini providers keep receiving this schema through
|
|
109
|
+
* schemaNormalize.toLowercaseJsonSchema() at request-build time, unchanged.
|
|
110
|
+
*/
|
|
111
|
+
export function toFunctionTool(contract) {
|
|
112
|
+
return new FunctionTool({
|
|
113
|
+
name: contract.name,
|
|
114
|
+
description: contract.description,
|
|
115
|
+
parameters: toGeminiSchema(toStandardJsonSchema(contract)),
|
|
116
|
+
execute: (input) => executeContract(contract, input),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* MCP surface: a tools/list entry. Pair with executeContract() in the
|
|
121
|
+
* server's CallTool handler — see scripts/financial_mcp_server.ts.
|
|
122
|
+
*/
|
|
123
|
+
export function toMcpToolDefinition(contract) {
|
|
124
|
+
return {
|
|
125
|
+
name: contract.name,
|
|
126
|
+
description: contract.description,
|
|
127
|
+
inputSchema: toStandardJsonSchema(contract),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/webExtractTool.ts — deterministic URL → clean-text reading.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* `web_search` routes to each provider's SERVER-SIDE search (§7.5): the
|
|
6
|
+
* provider decides which pages to fetch and which snippets to quote. An
|
|
7
|
+
* agent could never say "now read THIS article in full" — for a news
|
|
8
|
+
* agent that is the difference between arbitrating what a search chose
|
|
9
|
+
* to quote and actually reading the five articles being compared.
|
|
10
|
+
* `web_extract` closes that gap: a plain client-side function tool that
|
|
11
|
+
* fetches given URLs and returns the page's clean text. Deterministic,
|
|
12
|
+
* no LLM summarization, and KEYLESS — it also works on local Ollama
|
|
13
|
+
* agents, the first web capability local mode gets.
|
|
14
|
+
*
|
|
15
|
+
* DESIGN (cribbed from NousResearch/hermes-agent `web_extract`, adapted):
|
|
16
|
+
* - Up to 5 URLs per call; per-page character budget (default 15k,
|
|
17
|
+
* WEB_EXTRACT_CHAR_LIMIT env, clamped 2k–500k — deployment config,
|
|
18
|
+
* never YAML, the XAI_COLLECTION_IDS doctrine).
|
|
19
|
+
* - Over-budget pages return a head+tail window (~75%/25%) with an
|
|
20
|
+
* omission marker. Hermes spills full text to disk and tells the
|
|
21
|
+
* agent to page with read_file; melchizedek agents have no filesystem
|
|
22
|
+
* tools, so paging is an `offset` param instead: the full text is
|
|
23
|
+
* held in a 15-minute in-process cache and re-calling with
|
|
24
|
+
* `{urls:[same], offset:N}` continues reading without refetching.
|
|
25
|
+
* - Zero new dependencies: native fetch + a heuristic HTML→markdown
|
|
26
|
+
* pipeline (strip script/style/nav chrome, prefer <article>/<main>,
|
|
27
|
+
* headings/lists/links/blockquotes to markdown, entities decoded,
|
|
28
|
+
* base64 images never survive because <img> collapses to alt text).
|
|
29
|
+
*
|
|
30
|
+
* SECURITY (SSRF):
|
|
31
|
+
* Agents pass arbitrary URLs and this process may run server-side
|
|
32
|
+
* (A2A on Heroku), so http(s)-only plus a literal-host guard: loopback,
|
|
33
|
+
* RFC-1918, link-local/metadata (169.254.*), CGNAT, ULA/IPv6-local and
|
|
34
|
+
* localhost-ish hostnames are refused, and redirects are followed
|
|
35
|
+
* manually so every hop is re-checked. Deliberate v1 limit: the guard
|
|
36
|
+
* inspects literals only — it does not resolve DNS, so a public
|
|
37
|
+
* hostname that resolves privately can still slip; acceptable for a
|
|
38
|
+
* tool that runs with no ambient credentials, revisit if that changes.
|
|
39
|
+
*
|
|
40
|
+
* FAILURE CONTRACT: never throws. Each URL yields either its content
|
|
41
|
+
* block or an inline `Error:` block; one bad URL never sinks the rest.
|
|
42
|
+
*/
|
|
43
|
+
import { z } from 'zod';
|
|
44
|
+
export declare const WEB_EXTRACT_TOOL_NAME = "web_extract";
|
|
45
|
+
/** Per-page char budget from the environment; malformed values warn and fall back. */
|
|
46
|
+
export declare function extractCharLimit(): number;
|
|
47
|
+
/** Reason the host is refused, or null when it looks routable. Literals only — see header. */
|
|
48
|
+
export declare function blockedHostReason(hostname: string): string | null;
|
|
49
|
+
export declare function decodeEntities(text: string): string;
|
|
50
|
+
export interface ExtractedPage {
|
|
51
|
+
title: string | null;
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function extractHtml(html: string): ExtractedPage;
|
|
55
|
+
/**
|
|
56
|
+
* Reason the extraction looks like a block page rather than content, or
|
|
57
|
+
* null when it looks legitimate. `htmlBytes` is the raw document size —
|
|
58
|
+
* the thinness check needs it to tell "big page, no text" (a JS shell)
|
|
59
|
+
* from a genuinely small page, which stays valid.
|
|
60
|
+
*/
|
|
61
|
+
export declare function blockedPageReason(text: string, htmlBytes: number): string | null;
|
|
62
|
+
export declare function windowContent(text: string, url: string, limit: number, offset?: number): string;
|
|
63
|
+
/** Test hook — the cache is process-global. */
|
|
64
|
+
export declare function clearWebExtractCache(): void;
|
|
65
|
+
export declare const webExtractContract: import("./toolContract.ts").ToolContract<z.ZodObject<{
|
|
66
|
+
urls: z.ZodArray<z.ZodString>;
|
|
67
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
68
|
+
}, z.core.$strip>>;
|
|
69
|
+
/** ADK surface, ready for the registry. */
|
|
70
|
+
export declare const webExtractTool: import("@google/adk").FunctionTool<undefined>;
|