melchizedek-agents 0.16.2 → 0.17.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 +17 -0
- package/AGENT_SETUP.md +2 -1
- package/CHANGELOG.md +142 -0
- package/DOCUMENTATION.md +237 -7
- 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 +112 -1
- package/db/migrations/0011_erase_expired.sql +198 -0
- package/dist/lib/a2a/app.d.ts +7 -0
- package/dist/lib/a2a/app.js +7 -1
- package/dist/lib/a2a/executor.js +41 -0
- package/dist/lib/compile.d.ts +40 -1
- package/dist/lib/compile.js +132 -14
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +17 -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/loadSyndicate.d.ts +46 -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/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/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/tracer.d.ts +1 -1
- 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 +42 -4
- package/dist/lib/runtime/syndicateTurn.js +162 -16
- package/dist/lib/storage/postgres/index.d.ts +6 -0
- package/dist/lib/storage/postgres/index.js +11 -0
- package/dist/lib/storage/rlsStatus.d.ts +45 -0
- package/dist/lib/storage/rlsStatus.js +69 -0
- package/dist/lib/syndicateSchema.d.ts +169 -0
- package/dist/lib/syndicateSchema.js +327 -2
- package/dist/lib/toolRegistry.js +9 -0
- package/dist/lib/tools/openapiTools.d.ts +46 -0
- package/dist/lib/tools/openapiTools.js +195 -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/syndicate_chat.js +39 -5
- package/package.json +4 -1
- 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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/skillToolset.ts — Agent Skills for a syndicate agent, the way a
|
|
3
|
+
* coding harness holds them.
|
|
4
|
+
*
|
|
5
|
+
* WHY this file exists:
|
|
6
|
+
* A coding harness (Claude Code, Codex, Gemini CLI) keeps every installed
|
|
7
|
+
* skill's frontmatter in its system prompt, reads a SKILL.md in full only
|
|
8
|
+
* when a request matches its description, opens the files a skill ships
|
|
9
|
+
* only when the procedure reaches them, and runs the scripts a skill
|
|
10
|
+
* ships. ADK 2.2 carries the mechanism (`SkillToolset`, `loadAllSkillsInDir`,
|
|
11
|
+
* the open SKILL.md standard's layout: SKILL.md + references/, assets/,
|
|
12
|
+
* scripts/). Three things it does not do, this file does:
|
|
13
|
+
*
|
|
14
|
+
* 1. INJECT THE INDEX. ADK's TypeScript port never calls the toolset's own
|
|
15
|
+
* request hook, so `<available_skills>` would reach the model only
|
|
16
|
+
* through a `list_skills` call. `skillsInstruction()` renders the index
|
|
17
|
+
* once at compile time and lib/compile.ts appends it to the agent's
|
|
18
|
+
* instruction: no round trip, always in view.
|
|
19
|
+
* 2. KEEP DISCLOSURE PROGRESSIVE. ADK's `load_skill` returns every
|
|
20
|
+
* resource's full content with the instructions. `LeanLoadSkillTool`
|
|
21
|
+
* returns the instructions and the resources' NAMES; `load_skill_resource`
|
|
22
|
+
* reads one when the procedure asks.
|
|
23
|
+
* 3. GATE EVERY SCRIPT RUN. ADK's `run_skill_script` runs when called.
|
|
24
|
+
* `GatedRunSkillScriptTool` asks for a person's approval first through
|
|
25
|
+
* the same confirmation interrupt `require_approval` uses (ADR 0028),
|
|
26
|
+
* so the turn pauses `input-required` and resumes on approve or reject.
|
|
27
|
+
*
|
|
28
|
+
* `allowed-tools` in a skill's frontmatter is honoured through ADK's
|
|
29
|
+
* activation mechanism: once the skill is loaded, the tools it names
|
|
30
|
+
* become callable, provided the agent's YAML listed them under
|
|
31
|
+
* `skills.tools`. Exposure stays two deliberate acts (the skill names the
|
|
32
|
+
* tool, the YAML permits it); a skill cannot grant itself a tool the YAML
|
|
33
|
+
* withheld.
|
|
34
|
+
*
|
|
35
|
+
* WHAT RUNS: only scripts shipped in a skill's `scripts/` directory, through
|
|
36
|
+
* ADK's local executor (`skills.scripts: local`), after approval. The
|
|
37
|
+
* model's own code never executes: the executor is handed to the toolset,
|
|
38
|
+
* never to the agent, so ADK's code-block execution stays off. ADK's
|
|
39
|
+
* container executor cannot stage a skill's files yet, so it is not offered.
|
|
40
|
+
*
|
|
41
|
+
* FAILURE CONTRACT: a missing skills directory fails the compile (an agent
|
|
42
|
+
* running without the skills it was declared with would be a silent
|
|
43
|
+
* defect). Inside a turn, the ADK tools return `{ error }` objects the
|
|
44
|
+
* model can read, never throw.
|
|
45
|
+
*/
|
|
46
|
+
import type { FunctionDeclaration } from '@google/genai';
|
|
47
|
+
import { BaseTool, RunSkillScriptTool, SkillToolset } from '@google/adk';
|
|
48
|
+
import type { ReadonlyContext, RunAsyncToolRequest } from '@google/adk';
|
|
49
|
+
/** What an agent's YAML `skills:` block may say. */
|
|
50
|
+
export interface SkillsConfig {
|
|
51
|
+
/** A directory of skills, one subdirectory each holding a SKILL.md. Relative to the working directory. */
|
|
52
|
+
dir: string;
|
|
53
|
+
/** Whether a skill's `scripts/` may run. `local` runs them on this host, each after a person approves. */
|
|
54
|
+
scripts?: 'none' | 'local';
|
|
55
|
+
/** Registry tool names a skill may unlock through its `allowed-tools` frontmatter, once loaded. */
|
|
56
|
+
tools?: string[];
|
|
57
|
+
}
|
|
58
|
+
/** ADK's Skill, as this module reads it (the type is not exported from the package index). */
|
|
59
|
+
export interface LoadedSkill {
|
|
60
|
+
frontmatter: {
|
|
61
|
+
name: string;
|
|
62
|
+
description: string;
|
|
63
|
+
allowedTools?: string;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
instructions: string;
|
|
68
|
+
resources?: {
|
|
69
|
+
references?: Record<string, unknown>;
|
|
70
|
+
assets?: Record<string, unknown>;
|
|
71
|
+
scripts?: Record<string, unknown>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Seconds a skill script may run before the local executor kills it. */
|
|
75
|
+
export declare const SCRIPT_TIMEOUT_SECONDS = 120;
|
|
76
|
+
/**
|
|
77
|
+
* Load every skill under `dir` (ADK's loader: a skill is a directory whose
|
|
78
|
+
* name equals its frontmatter `name`, holding a SKILL.md with a description;
|
|
79
|
+
* an invalid skill is skipped with ADK's warning). A directory that does
|
|
80
|
+
* not exist is an error: the YAML declared skills this agent cannot have.
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadSkillSuite(dir: string): Promise<Record<string, LoadedSkill>>;
|
|
83
|
+
/**
|
|
84
|
+
* Why a skill directory under `dir` was NOT loaded: ADK skips an invalid
|
|
85
|
+
* skill with a log line the operator may never see, so the compiler reports
|
|
86
|
+
* each one. A candidate is a subdirectory holding a SKILL.md.
|
|
87
|
+
*/
|
|
88
|
+
export declare function skillSuiteProblems(dir: string): Promise<string[]>;
|
|
89
|
+
/**
|
|
90
|
+
* The block lib/compile.ts appends to an agent's instruction: the frontmatter
|
|
91
|
+
* index every request sees, and the names of the tools that read and run a
|
|
92
|
+
* skill. The ORDER of use (read before following, cite, when to run a
|
|
93
|
+
* script) is the YAML instruction's to state; this block only says what
|
|
94
|
+
* exists.
|
|
95
|
+
*/
|
|
96
|
+
export declare function skillsInstruction(skills: Record<string, LoadedSkill>, config: SkillsConfig): string;
|
|
97
|
+
/**
|
|
98
|
+
* ADK's `load_skill`, minus the resource dump: the procedure in full, the
|
|
99
|
+
* frontmatter, and the names of the files the skill ships. Loading also
|
|
100
|
+
* activates the skill for the agent (ADK's state key), which is what unlocks
|
|
101
|
+
* its `allowed-tools`.
|
|
102
|
+
*/
|
|
103
|
+
export declare class LeanLoadSkillTool extends BaseTool {
|
|
104
|
+
private readonly toolset;
|
|
105
|
+
constructor(toolset: SkillToolset);
|
|
106
|
+
_getDeclaration(): FunctionDeclaration;
|
|
107
|
+
runAsync({ args, toolContext }: RunAsyncToolRequest): Promise<unknown>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* ADK's `run_skill_script` behind a person's approval: the first call raises
|
|
111
|
+
* the confirmation interrupt (the hint names the skill, the script and the
|
|
112
|
+
* arguments) and returns without running; the resumed call runs only when
|
|
113
|
+
* the answer confirmed it. Mirrors FunctionTool's own gate, so the turn
|
|
114
|
+
* runner and the A2A server treat it exactly as `require_approval` (ADR 0028).
|
|
115
|
+
*/
|
|
116
|
+
export declare class GatedRunSkillScriptTool extends RunSkillScriptTool {
|
|
117
|
+
runAsync(request: RunAsyncToolRequest): Promise<unknown>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The toolset an agent with `skills:` carries. `list_skills` is left out
|
|
121
|
+
* (the index is in the instruction); `load_skill` is the lean one;
|
|
122
|
+
* `run_skill_script` is present, gated, only when scripts may run.
|
|
123
|
+
*/
|
|
124
|
+
export declare class HarnessSkillToolset extends SkillToolset {
|
|
125
|
+
private readonly harnessTools;
|
|
126
|
+
constructor(skills: Record<string, LoadedSkill>, config: SkillsConfig, additionalTools?: unknown[]);
|
|
127
|
+
getTools(context?: ReadonlyContext): Promise<BaseTool[]>;
|
|
128
|
+
}
|
|
129
|
+
/** Build the toolset and its instruction block for one agent. */
|
|
130
|
+
export declare function buildSkillHarness(config: SkillsConfig, additionalTools: unknown[]): Promise<{
|
|
131
|
+
toolset: HarnessSkillToolset;
|
|
132
|
+
instruction: string;
|
|
133
|
+
skills: Record<string, LoadedSkill>;
|
|
134
|
+
problems: string[];
|
|
135
|
+
}>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/skillToolset.ts — Agent Skills for a syndicate agent, the way a
|
|
3
|
+
* coding harness holds them.
|
|
4
|
+
*
|
|
5
|
+
* WHY this file exists:
|
|
6
|
+
* A coding harness (Claude Code, Codex, Gemini CLI) keeps every installed
|
|
7
|
+
* skill's frontmatter in its system prompt, reads a SKILL.md in full only
|
|
8
|
+
* when a request matches its description, opens the files a skill ships
|
|
9
|
+
* only when the procedure reaches them, and runs the scripts a skill
|
|
10
|
+
* ships. ADK 2.2 carries the mechanism (`SkillToolset`, `loadAllSkillsInDir`,
|
|
11
|
+
* the open SKILL.md standard's layout: SKILL.md + references/, assets/,
|
|
12
|
+
* scripts/). Three things it does not do, this file does:
|
|
13
|
+
*
|
|
14
|
+
* 1. INJECT THE INDEX. ADK's TypeScript port never calls the toolset's own
|
|
15
|
+
* request hook, so `<available_skills>` would reach the model only
|
|
16
|
+
* through a `list_skills` call. `skillsInstruction()` renders the index
|
|
17
|
+
* once at compile time and lib/compile.ts appends it to the agent's
|
|
18
|
+
* instruction: no round trip, always in view.
|
|
19
|
+
* 2. KEEP DISCLOSURE PROGRESSIVE. ADK's `load_skill` returns every
|
|
20
|
+
* resource's full content with the instructions. `LeanLoadSkillTool`
|
|
21
|
+
* returns the instructions and the resources' NAMES; `load_skill_resource`
|
|
22
|
+
* reads one when the procedure asks.
|
|
23
|
+
* 3. GATE EVERY SCRIPT RUN. ADK's `run_skill_script` runs when called.
|
|
24
|
+
* `GatedRunSkillScriptTool` asks for a person's approval first through
|
|
25
|
+
* the same confirmation interrupt `require_approval` uses (ADR 0028),
|
|
26
|
+
* so the turn pauses `input-required` and resumes on approve or reject.
|
|
27
|
+
*
|
|
28
|
+
* `allowed-tools` in a skill's frontmatter is honoured through ADK's
|
|
29
|
+
* activation mechanism: once the skill is loaded, the tools it names
|
|
30
|
+
* become callable, provided the agent's YAML listed them under
|
|
31
|
+
* `skills.tools`. Exposure stays two deliberate acts (the skill names the
|
|
32
|
+
* tool, the YAML permits it); a skill cannot grant itself a tool the YAML
|
|
33
|
+
* withheld.
|
|
34
|
+
*
|
|
35
|
+
* WHAT RUNS: only scripts shipped in a skill's `scripts/` directory, through
|
|
36
|
+
* ADK's local executor (`skills.scripts: local`), after approval. The
|
|
37
|
+
* model's own code never executes: the executor is handed to the toolset,
|
|
38
|
+
* never to the agent, so ADK's code-block execution stays off. ADK's
|
|
39
|
+
* container executor cannot stage a skill's files yet, so it is not offered.
|
|
40
|
+
*
|
|
41
|
+
* FAILURE CONTRACT: a missing skills directory fails the compile (an agent
|
|
42
|
+
* running without the skills it was declared with would be a silent
|
|
43
|
+
* defect). Inside a turn, the ADK tools return `{ error }` objects the
|
|
44
|
+
* model can read, never throw.
|
|
45
|
+
*/
|
|
46
|
+
import { readdirSync, realpathSync } from 'node:fs';
|
|
47
|
+
import { resolve } from 'node:path';
|
|
48
|
+
import { Type } from '@google/genai';
|
|
49
|
+
import { BaseTool, LoadSkillResourceTool, RunSkillScriptTool, SkillToolset, UnsafeLocalCodeExecutor, loadAllSkillsInDir, validateSkillDir, } from '@google/adk';
|
|
50
|
+
/** Seconds a skill script may run before the local executor kills it. */
|
|
51
|
+
export const SCRIPT_TIMEOUT_SECONDS = 120;
|
|
52
|
+
/**
|
|
53
|
+
* Load every skill under `dir` (ADK's loader: a skill is a directory whose
|
|
54
|
+
* name equals its frontmatter `name`, holding a SKILL.md with a description;
|
|
55
|
+
* an invalid skill is skipped with ADK's warning). A directory that does
|
|
56
|
+
* not exist is an error: the YAML declared skills this agent cannot have.
|
|
57
|
+
*/
|
|
58
|
+
export async function loadSkillSuite(dir) {
|
|
59
|
+
const abs = resolve(dir);
|
|
60
|
+
let real;
|
|
61
|
+
try {
|
|
62
|
+
real = realpathSync(abs);
|
|
63
|
+
readdirSync(real);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(`skills: directory not found or unreadable: ${dir}`);
|
|
67
|
+
}
|
|
68
|
+
const skills = (await loadAllSkillsInDir(real));
|
|
69
|
+
for (const skill of Object.values(skills))
|
|
70
|
+
honourAllowedTools(skill);
|
|
71
|
+
return skills;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Why a skill directory under `dir` was NOT loaded: ADK skips an invalid
|
|
75
|
+
* skill with a log line the operator may never see, so the compiler reports
|
|
76
|
+
* each one. A candidate is a subdirectory holding a SKILL.md.
|
|
77
|
+
*/
|
|
78
|
+
export async function skillSuiteProblems(dir) {
|
|
79
|
+
const real = realpathSync(resolve(dir));
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const entry of readdirSync(real, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.isDirectory())
|
|
83
|
+
continue;
|
|
84
|
+
const skillDir = resolve(real, entry.name);
|
|
85
|
+
if (!readdirSync(skillDir).some((f) => f.toLowerCase() === 'skill.md'))
|
|
86
|
+
continue;
|
|
87
|
+
for (const problem of await validateSkillDir(skillDir))
|
|
88
|
+
out.push(`${entry.name}: ${problem.split('\n')[0]}`);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The open standard's `allowed-tools` (a space- or comma-separated list of
|
|
94
|
+
* tool names) becomes ADK's `metadata.adk_additional_tools`, which is what
|
|
95
|
+
* the toolset reads when the skill is loaded. A skill that already set the
|
|
96
|
+
* ADK field keeps it.
|
|
97
|
+
*/
|
|
98
|
+
function honourAllowedTools(skill) {
|
|
99
|
+
const fm = skill.frontmatter;
|
|
100
|
+
const declared = typeof fm.allowedTools === 'string' ? fm.allowedTools : typeof fm['allowed-tools'] === 'string' ? fm['allowed-tools'] : '';
|
|
101
|
+
const names = declared.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
102
|
+
if (names.length === 0)
|
|
103
|
+
return;
|
|
104
|
+
fm.metadata = { ...(fm.metadata ?? {}) };
|
|
105
|
+
if (!Array.isArray(fm.metadata.adk_additional_tools))
|
|
106
|
+
fm.metadata.adk_additional_tools = names;
|
|
107
|
+
}
|
|
108
|
+
function escapeXml(s) {
|
|
109
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The block lib/compile.ts appends to an agent's instruction: the frontmatter
|
|
113
|
+
* index every request sees, and the names of the tools that read and run a
|
|
114
|
+
* skill. The ORDER of use (read before following, cite, when to run a
|
|
115
|
+
* script) is the YAML instruction's to state; this block only says what
|
|
116
|
+
* exists.
|
|
117
|
+
*/
|
|
118
|
+
export function skillsInstruction(skills, config) {
|
|
119
|
+
const entries = Object.values(skills).sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
|
|
120
|
+
const index = entries.length === 0
|
|
121
|
+
? '<available_skills>\n</available_skills>'
|
|
122
|
+
: [
|
|
123
|
+
'<available_skills>',
|
|
124
|
+
...entries.flatMap((s) => [
|
|
125
|
+
' <skill>',
|
|
126
|
+
` <name>${escapeXml(s.frontmatter.name)}</name>`,
|
|
127
|
+
` <description>${escapeXml(s.frontmatter.description.trim())}</description>`,
|
|
128
|
+
' </skill>',
|
|
129
|
+
]),
|
|
130
|
+
'</available_skills>',
|
|
131
|
+
].join('\n');
|
|
132
|
+
const scripts = config.scripts === 'local';
|
|
133
|
+
const lines = [
|
|
134
|
+
'Skills are installed procedures, one directory each: a SKILL.md with the procedure, and optionally references/, assets/ and scripts/. The index below lists every installed skill by name and description; it is complete, so never call a tool to list skills.',
|
|
135
|
+
'load_skill(name) returns a skill\'s full SKILL.md and the names of the files it ships. load_skill_resource(skill_name, path) returns one of those files ("references/x.md", "assets/x.yaml", "scripts/x.sh").',
|
|
136
|
+
scripts
|
|
137
|
+
? 'run_skill_script(skill_name, script_path, args) runs a script from a skill\'s scripts/ directory on this machine. Every run waits for the user\'s approval before it executes; when a run is refused, say so and do not retry it.'
|
|
138
|
+
: 'Scripts in a skill\'s scripts/ directory cannot run here; when a procedure says to run one, give the user the exact command instead.',
|
|
139
|
+
index,
|
|
140
|
+
];
|
|
141
|
+
return lines.join('\n\n');
|
|
142
|
+
}
|
|
143
|
+
function resourceNames(skill) {
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const [dir, files] of Object.entries(skill.resources ?? {})) {
|
|
146
|
+
for (const name of Object.keys(files ?? {}))
|
|
147
|
+
out.push(`${dir}/${name}`);
|
|
148
|
+
}
|
|
149
|
+
return out.sort();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* ADK's `load_skill`, minus the resource dump: the procedure in full, the
|
|
153
|
+
* frontmatter, and the names of the files the skill ships. Loading also
|
|
154
|
+
* activates the skill for the agent (ADK's state key), which is what unlocks
|
|
155
|
+
* its `allowed-tools`.
|
|
156
|
+
*/
|
|
157
|
+
export class LeanLoadSkillTool extends BaseTool {
|
|
158
|
+
toolset;
|
|
159
|
+
constructor(toolset) {
|
|
160
|
+
super({
|
|
161
|
+
name: 'load_skill',
|
|
162
|
+
description: 'Read one installed skill in full: its SKILL.md procedure, its frontmatter, and the names of the files it ships. Read a skill before you follow it.',
|
|
163
|
+
});
|
|
164
|
+
this.toolset = toolset;
|
|
165
|
+
}
|
|
166
|
+
_getDeclaration() {
|
|
167
|
+
return {
|
|
168
|
+
name: this.name,
|
|
169
|
+
description: this.description,
|
|
170
|
+
parameters: {
|
|
171
|
+
type: Type.OBJECT,
|
|
172
|
+
properties: { name: { type: Type.STRING, description: 'The skill\'s name, exactly as the index lists it.' } },
|
|
173
|
+
required: ['name'],
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async runAsync({ args, toolContext }) {
|
|
178
|
+
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
179
|
+
if (!name)
|
|
180
|
+
return { error: 'Skill name is required.', error_code: 'MISSING_SKILL_NAME' };
|
|
181
|
+
const skill = this.toolset.getSkill(name);
|
|
182
|
+
if (!skill) {
|
|
183
|
+
return { error: `Skill '${name}' not found. Installed: ${Object.keys(this.toolset.skills).sort().join(', ') || '(none)'}.`, error_code: 'SKILL_NOT_FOUND' };
|
|
184
|
+
}
|
|
185
|
+
// The same state key ADK's own load_skill sets: SkillToolset reads it to
|
|
186
|
+
// resolve the skill's additional tools on the next request.
|
|
187
|
+
const agentName = toolContext.invocationContext.agent?.name ?? toolContext.agentName;
|
|
188
|
+
const stateKey = `_adk_activated_skill_${agentName}`;
|
|
189
|
+
const activated = toolContext.state.get(stateKey) ?? [];
|
|
190
|
+
if (!activated.includes(name))
|
|
191
|
+
toolContext.state.set(stateKey, [...activated, name]);
|
|
192
|
+
return { skill_name: name, frontmatter: skill.frontmatter, instructions: skill.instructions, files: resourceNames(skill) };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* ADK's `run_skill_script` behind a person's approval: the first call raises
|
|
197
|
+
* the confirmation interrupt (the hint names the skill, the script and the
|
|
198
|
+
* arguments) and returns without running; the resumed call runs only when
|
|
199
|
+
* the answer confirmed it. Mirrors FunctionTool's own gate, so the turn
|
|
200
|
+
* runner and the A2A server treat it exactly as `require_approval` (ADR 0028).
|
|
201
|
+
*/
|
|
202
|
+
export class GatedRunSkillScriptTool extends RunSkillScriptTool {
|
|
203
|
+
async runAsync(request) {
|
|
204
|
+
const { args, toolContext } = request;
|
|
205
|
+
const ctx = toolContext;
|
|
206
|
+
if (!ctx.toolConfirmation) {
|
|
207
|
+
const summary = `${String(args.skill_name ?? '?')}: ${String(args.script_path ?? '?')}${args.args ? ` ${JSON.stringify(args.args)}` : ''}`;
|
|
208
|
+
ctx.requestConfirmation({
|
|
209
|
+
hint: `Approval is required before a skill script runs on this machine. The agent asks to run ${summary}.`,
|
|
210
|
+
payload: { skill_name: args.skill_name, script_path: args.script_path, args: args.args ?? {} },
|
|
211
|
+
});
|
|
212
|
+
ctx.actions.skipSummarization = true;
|
|
213
|
+
return { error: 'This script run requires approval, please approve or reject.' };
|
|
214
|
+
}
|
|
215
|
+
if (!ctx.toolConfirmation.confirmed)
|
|
216
|
+
return { error: 'This script run was rejected.' };
|
|
217
|
+
return super.runAsync(request);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The toolset an agent with `skills:` carries. `list_skills` is left out
|
|
222
|
+
* (the index is in the instruction); `load_skill` is the lean one;
|
|
223
|
+
* `run_skill_script` is present, gated, only when scripts may run.
|
|
224
|
+
*/
|
|
225
|
+
export class HarnessSkillToolset extends SkillToolset {
|
|
226
|
+
harnessTools;
|
|
227
|
+
constructor(skills, config, additionalTools = []) {
|
|
228
|
+
const codeExecutor = config.scripts === 'local' ? new UnsafeLocalCodeExecutor({ timeoutSeconds: SCRIPT_TIMEOUT_SECONDS }) : undefined;
|
|
229
|
+
super(skills, { codeExecutor, additionalTools: additionalTools });
|
|
230
|
+
this.harnessTools = [new LeanLoadSkillTool(this), new LoadSkillResourceTool(this)];
|
|
231
|
+
if (codeExecutor)
|
|
232
|
+
this.harnessTools.push(new GatedRunSkillScriptTool(this));
|
|
233
|
+
}
|
|
234
|
+
async getTools(context) {
|
|
235
|
+
// The parent resolves the skills' additional tools from the activation
|
|
236
|
+
// state; its own fixed tools are replaced by the harness set.
|
|
237
|
+
const parentTools = await super.getTools(context);
|
|
238
|
+
const fixed = new Set(['list_skills', 'load_skill', 'load_skill_resource', 'run_skill_script', 'run_skill_inline_script', 'search_skills']);
|
|
239
|
+
return [...this.harnessTools, ...parentTools.filter((t) => !fixed.has(t.name))];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Build the toolset and its instruction block for one agent. */
|
|
243
|
+
export async function buildSkillHarness(config, additionalTools) {
|
|
244
|
+
const skills = await loadSkillSuite(config.dir);
|
|
245
|
+
const toolset = new HarnessSkillToolset(skills, config, additionalTools);
|
|
246
|
+
return { toolset, instruction: skillsInstruction(skills, config), skills, problems: await skillSuiteProblems(config.dir) };
|
|
247
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 type { LlmRequest } from '@google/adk';
|
|
19
|
+
export declare const URL_CONTEXT_TOOL_NAME = "url_context";
|
|
20
|
+
export declare class UrlContextTool extends BaseTool {
|
|
21
|
+
constructor();
|
|
22
|
+
/** Never a client-side function tool. */
|
|
23
|
+
_getDeclaration(): undefined;
|
|
24
|
+
runAsync(): Promise<unknown>;
|
|
25
|
+
processLlmRequest({ llmRequest }: {
|
|
26
|
+
llmRequest: LlmRequest;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const URL_CONTEXT: UrlContextTool;
|
|
@@ -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>;
|