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.
Files changed (82) hide show
  1. package/.env.example +17 -0
  2. package/AGENT_SETUP.md +2 -1
  3. package/CHANGELOG.md +142 -0
  4. package/DOCUMENTATION.md +237 -7
  5. package/QUICKSTART.md +2 -2
  6. package/README.md +125 -149
  7. package/config/agents/examples/harness.yaml +135 -0
  8. package/config/agents/examples/model_zoo.yaml +23 -9
  9. package/config/agents/examples/pipeline.yaml +120 -0
  10. package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
  11. package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
  12. package/config/agents/examples/weather.yaml +59 -0
  13. package/config/agents/syndicateSchema.yaml +112 -1
  14. package/db/migrations/0011_erase_expired.sql +198 -0
  15. package/dist/lib/a2a/app.d.ts +7 -0
  16. package/dist/lib/a2a/app.js +7 -1
  17. package/dist/lib/a2a/executor.js +41 -0
  18. package/dist/lib/compile.d.ts +40 -1
  19. package/dist/lib/compile.js +132 -14
  20. package/dist/lib/config.d.ts +17 -0
  21. package/dist/lib/config.js +17 -0
  22. package/dist/lib/doctor.js +17 -4
  23. package/dist/lib/index.d.ts +2 -0
  24. package/dist/lib/index.js +1 -0
  25. package/dist/lib/loadSyndicate.d.ts +46 -0
  26. package/dist/lib/loadSyndicate.js +19 -3
  27. package/dist/lib/memory/erase.d.ts +5 -0
  28. package/dist/lib/memory/erase.js +5 -0
  29. package/dist/lib/models/capabilities.js +10 -0
  30. package/dist/lib/models/endpoints.d.ts +1 -1
  31. package/dist/lib/models/endpoints.js +4 -1
  32. package/dist/lib/models/gateway.d.ts +2 -2
  33. package/dist/lib/models/gateway.js +4 -4
  34. package/dist/lib/models/kimiLlm.d.ts +123 -0
  35. package/dist/lib/models/kimiLlm.js +171 -0
  36. package/dist/lib/models/providerMap.d.ts +2 -2
  37. package/dist/lib/models/providerMap.js +4 -1
  38. package/dist/lib/models/registry.d.ts +1 -1
  39. package/dist/lib/models/registry.js +11 -3
  40. package/dist/lib/net/redirects.d.ts +14 -0
  41. package/dist/lib/net/redirects.js +94 -0
  42. package/dist/lib/observability/tracer.d.ts +1 -1
  43. package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
  44. package/dist/lib/persistence/supabaseProvider.js +2 -31
  45. package/dist/lib/runtime/questions.d.ts +55 -0
  46. package/dist/lib/runtime/questions.js +107 -0
  47. package/dist/lib/runtime/syndicateTurn.d.ts +42 -4
  48. package/dist/lib/runtime/syndicateTurn.js +162 -16
  49. package/dist/lib/storage/postgres/index.d.ts +6 -0
  50. package/dist/lib/storage/postgres/index.js +11 -0
  51. package/dist/lib/storage/rlsStatus.d.ts +45 -0
  52. package/dist/lib/storage/rlsStatus.js +69 -0
  53. package/dist/lib/syndicateSchema.d.ts +169 -0
  54. package/dist/lib/syndicateSchema.js +327 -2
  55. package/dist/lib/toolRegistry.js +9 -0
  56. package/dist/lib/tools/openapiTools.d.ts +46 -0
  57. package/dist/lib/tools/openapiTools.js +195 -0
  58. package/dist/lib/tools/skillToolset.d.ts +135 -0
  59. package/dist/lib/tools/skillToolset.js +247 -0
  60. package/dist/lib/tools/urlContextTool.d.ts +29 -0
  61. package/dist/lib/tools/urlContextTool.js +39 -0
  62. package/dist/lib/workflow.d.ts +72 -0
  63. package/dist/lib/workflow.js +180 -0
  64. package/dist/lib/workflowConfig.d.ts +95 -0
  65. package/dist/lib/workflowConfig.js +93 -0
  66. package/dist/scripts/syndicate_chat.js +39 -5
  67. package/package.json +4 -1
  68. package/skills/briefs/_shared.md +1 -1
  69. package/skills/briefs/melchizedek-author.md +8 -4
  70. package/skills/briefs/melchizedek-memory.md +1 -1
  71. package/skills/briefs/melchizedek-models.md +1 -1
  72. package/skills/briefs/melchizedek-scribe.md +4 -4
  73. package/skills/briefs/melchizedek-serve.md +1 -1
  74. package/skills/briefs/melchizedek.md +4 -1
  75. package/skills/melchizedek/SKILL.md +4 -1
  76. package/skills/melchizedek-author/SKILL.md +7 -3
  77. package/skills/melchizedek-memory/SKILL.md +1 -1
  78. package/skills/melchizedek-models/SKILL.md +1 -1
  79. package/skills/melchizedek-scribe/SKILL.md +3 -3
  80. package/skills/melchizedek-serve/SKILL.md +1 -1
  81. /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
  82. /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
@@ -31,6 +31,7 @@ import type { BaseLlm, BaseMemoryService, BaseSessionService } from '@google/adk
31
31
  import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
32
32
  import type { ProviderEndpoint, ProviderId } from '../models/registry.ts';
33
33
  import type { TurnLock } from './turnLock.ts';
34
+ import type { RlsHardeningStatus } from '../storage/rlsStatus.ts';
34
35
  import type { EraseCounts } from '../memory/erase.ts';
35
36
  import type { Embedder, MemoryExtractor } from '../memory/providers.ts';
36
37
  import type { A2AContext } from './executor.ts';
@@ -68,6 +69,12 @@ export interface A2AAppOptions {
68
69
  * migrations this package ships (ADR 0021).
69
70
  */
70
71
  schemaVersion?: () => Promise<number | null>;
72
+ /**
73
+ * Whether db/hardening.sql is in force (postgresStorage supplies it).
74
+ * When given, the server checks it at boot like the supabase-js path:
75
+ * fatal with `requireHardenedDb`, a warning otherwise.
76
+ */
77
+ rlsHardening?: () => Promise<RlsHardeningStatus>;
71
78
  /**
72
79
  * One turn at a time per conversation across instances (postgresStorage
73
80
  * supplies an advisory lock). Default: a lock in this process.
@@ -187,11 +187,13 @@ export async function createA2AApp(options) {
187
187
  let memoryService;
188
188
  let erase;
189
189
  let readSchemaVersion;
190
+ let checkHardening;
190
191
  if (options.storage) {
191
192
  durableSessions = options.storage.sessionService;
192
193
  memoryService = options.storage.memoryService;
193
194
  erase = options.storage.erase;
194
195
  readSchemaVersion = options.storage.schemaVersion;
196
+ checkHardening = options.storage.rlsHardening;
195
197
  }
196
198
  else if (hasSupabaseCredentials()) {
197
199
  // ADR 0021 item 6: the supabase-js path stays for a transition period.
@@ -209,7 +211,11 @@ export async function createA2AApp(options) {
209
211
  memoryService = services.memoryService;
210
212
  erase = (scopeKey, eraseOpts) => eraseScope(services.rpcClient, scopeKey, eraseOpts);
211
213
  readSchemaVersion = services.schemaVersion;
212
- const rls = await services.checkRlsHardening();
214
+ checkHardening = services.checkRlsHardening;
215
+ }
216
+ // Both storage paths: refuse a public deployment whose tables an API key can read.
217
+ if (checkHardening) {
218
+ const rls = await checkHardening();
213
219
  if (rls.applied) {
214
220
  log(`✓ DB hardening verified — ${rls.detail}.`);
215
221
  }
@@ -19,6 +19,7 @@ import { turnLockKey } from './turnLock.js';
19
19
  import { ingestTurnMemory, runSyndicateTurn } from '../runtime/syndicateTurn.js';
20
20
  import { approvalResponsePart, describeApproval, pendingApproval } from '../runtime/approvals.js';
21
21
  import { declaresApprovals } from '../compile.js';
22
+ import { describeInput } from '../runtime/syndicateTurn.js';
22
23
  export const requestContextStorage = new AsyncLocalStorage();
23
24
  /** The app name every A2A session and memory silo is stored under. */
24
25
  export const A2A_APP_NAME = 'melchizedek-a2a';
@@ -72,6 +73,9 @@ export function describeFailedTurn(result) {
72
73
  if (result.failedStage === 'dispatch' && result.route) {
73
74
  return `Error: [${error.code}] ${result.route.route} failed to answer — ${describeTurnError(error)}`;
74
75
  }
76
+ if (result.failedStage === 'workflow') {
77
+ return `Error: [${error.code}] The workflow failed — ${describeTurnError(error)}`;
78
+ }
75
79
  return `Error: [${error.code}] The agent run failed — ${describeTurnError(error)}`;
76
80
  }
77
81
  /**
@@ -209,6 +213,37 @@ function publishWorking(eventBus, taskId, contextId, text) {
209
213
  metadata: undefined,
210
214
  }));
211
215
  }
216
+ /**
217
+ * A workflow paused on an `ask_user` node (lib/workflow.ts): the task ends
218
+ * input-required with the question, and a data part carrying it. The
219
+ * answer is simply the next message on the conversation; its text becomes
220
+ * the node's output and the graph resumes where it waited.
221
+ */
222
+ function publishInputRequest(eventBus, taskId, contextId, input) {
223
+ const message = statusMessage(taskId, contextId, `Input needed: ${describeInput(input)}`);
224
+ message.parts.push({
225
+ content: {
226
+ $case: 'data',
227
+ value: {
228
+ type: 'input_request',
229
+ interrupt_id: input.id,
230
+ node: input.node,
231
+ message: input.message,
232
+ ...(input.payload !== undefined ? { payload: input.payload } : {}),
233
+ ...(input.schema !== undefined ? { schema: input.schema } : {}),
234
+ },
235
+ },
236
+ metadata: undefined,
237
+ filename: '',
238
+ mediaType: 'application/json',
239
+ });
240
+ eventBus.publish(AgentEvent.statusUpdate({
241
+ taskId,
242
+ contextId,
243
+ status: { state: TaskState.TASK_STATE_INPUT_REQUIRED, message, timestamp: new Date().toISOString() },
244
+ metadata: undefined,
245
+ }));
246
+ }
212
247
  /**
213
248
  * The turn paused on a gated tool call (ADR 0028): the task ends
214
249
  * input-required, saying what is to be approved, with a data part a client
@@ -497,6 +532,12 @@ export class SyndicateExecutor {
497
532
  await report(ctx, 'input-required', 'approval', u);
498
533
  return;
499
534
  }
535
+ if (result.status === 'input-required' && result.input) {
536
+ log(`⏸ Task ${short} waiting for input from ${result.input.node} after ${spent}`);
537
+ publishInputRequest(eventBus, taskId, contextId, result.input);
538
+ await report(ctx, 'input-required', 'input', u);
539
+ return;
540
+ }
500
541
  if (result.status === 'failed') {
501
542
  warn(`✗ Task ${short} failed [${result.error?.code}] after ${spent}`);
502
543
  publishFinal(eventBus, taskId, contextId, 'failed', describeFailedTurn(result));
@@ -28,8 +28,10 @@
28
28
  * Contract and rationale: lib/dispatch.ts.
29
29
  */
30
30
  import { FunctionTool, LlmAgent } from '@google/adk';
31
+ import type { BaseTool } from '@google/adk';
31
32
  import type { BaseLlm } from '@google/adk';
32
33
  import type { SubagentYamlConfig, SyndicateYamlConfig } from './loadSyndicate.ts';
34
+ import type { SkillsConfig } from './tools/skillToolset.ts';
33
35
  export interface CompileOptions {
34
36
  /**
35
37
  * Turns the YAML `model` string into what LlmAgent receives. Default:
@@ -48,7 +50,23 @@ export interface CompileOptions {
48
50
  onUnknownTool?: (name: string) => void;
49
51
  /** Progress/diagnostic line sink (nested loads, MCP discovery). */
50
52
  log?: (message: string) => void;
53
+ /**
54
+ * Node settings for an agent compiled as a workflow node (lib/workflow.ts):
55
+ * ADK's `retryConfig` and `timeout`, which an LlmAgent takes only at
56
+ * construction. Called with the agent's name; undefined means none.
57
+ */
58
+ nodeConfig?: (agentName: string) => Record<string, unknown> | undefined;
51
59
  }
60
+ /** An agent's `context:` block: compact a long conversation into a summary (ADR 0033). */
61
+ export interface ContextConfig {
62
+ /** Compact when the last request's prompt passed this many tokens. */
63
+ compact_after_tokens: number;
64
+ /** Events kept verbatim after the summary. Default 6. */
65
+ keep_recent_events?: number;
66
+ /** The model that writes the summary. Default: the agent's own. */
67
+ summary_model?: string;
68
+ }
69
+ export declare const DEFAULT_KEEP_RECENT_EVENTS = 6;
52
70
  /**
53
71
  * A copy of a FunctionTool that runs only after a person approves the call
54
72
  * (ADR 0028). ADK's own gate does the work: the call raises an
@@ -57,8 +75,29 @@ export interface CompileOptions {
57
75
  * ungated for every other agent that lists the same tool.
58
76
  */
59
77
  export declare function requireApprovalOn(tool: FunctionTool): FunctionTool;
60
- /** True when any agent of a syndicate declares an approval gate. */
78
+ /**
79
+ * A copy of an OpenAPI tool (lib/tools/openapiTools.ts) that runs only after
80
+ * a person approves the call: the same confirmation interrupt FunctionTool's
81
+ * own gate raises, so the turn pauses and resumes exactly as ADR 0028 says.
82
+ */
83
+ export declare function requireApprovalOnBaseTool<T extends BaseTool>(tool: T): T;
84
+ /** True when any agent of a syndicate may pause for approval: a gated tool, or skill scripts. */
61
85
  export declare function declaresApprovals(config: SyndicateYamlConfig): boolean;
86
+ /** An agent's `examples:` entry: one exchange the model should imitate. */
87
+ export interface ExampleConfig {
88
+ input: string;
89
+ output: string;
90
+ }
91
+ /**
92
+ * ADK's ExampleTool from YAML pairs: never called by the model, it adds the
93
+ * exchanges to every request's instruction as few-shot examples.
94
+ */
95
+ export declare function examplesTool(examples: ExampleConfig[] | undefined): unknown[];
96
+ /** True when a turn running this agent directly may pause for a person (ADR 0028). */
97
+ export declare function agentGates(agent: {
98
+ require_approval?: string[];
99
+ skills?: SkillsConfig;
100
+ } | undefined): boolean;
62
101
  /**
63
102
  * Builds ONE runnable agent from a subagent entry. A `yaml_reference` entry
64
103
  * compiles the nested syndicate's whole graph under this entry's name and
@@ -27,13 +27,16 @@
27
27
  * dispatches to is identical to the one DELEGATE would have wrapped.
28
28
  * Contract and rationale: lib/dispatch.ts.
29
29
  */
30
- import { AgentTool, FunctionTool, LlmAgent } from '@google/adk';
30
+ import { AgentTool, BuiltInCodeExecutor, ExampleTool, FunctionTool, LLMRegistry, LlmAgent, LlmSummarizer, TokenBasedContextCompactor } from '@google/adk';
31
+ import { relative } from 'node:path';
31
32
  import { isDispatchSyndicate } from './dispatch.js';
32
33
  import { loadSyndicate, nestedLoader } from './loadSyndicate.js';
33
34
  import { resolveTools as resolveNamedTools } from './toolRegistry.js';
34
35
  import { createMcpTools } from './tools/mcpToolFactory.js';
35
36
  import { capabilitySummary, describeCapabilities } from './models/capabilities.js';
36
37
  import { remoteAgentTool } from './a2a/remoteAgent.js';
38
+ import { buildSkillHarness } from './tools/skillToolset.js';
39
+ import { buildOpenApiTools, isOpenApiTool, namesTool } from './tools/openapiTools.js';
37
40
  /** generateContentConfig as every entrypoint has always sent it to ADK. */
38
41
  function withServerSideToolInvocations(generateContentConfig) {
39
42
  const cfg = (generateContentConfig ?? {});
@@ -68,6 +71,36 @@ function passthroughFields(cfg) {
68
71
  out.disallowTransferToPeers = cfg.disallowTransferToPeers;
69
72
  return out;
70
73
  }
74
+ export const DEFAULT_KEEP_RECENT_EVENTS = 6;
75
+ /**
76
+ * The LlmAgent fields the engine builds from YAML rather than passing
77
+ * through (ADR 0033): `code_execution: gemini` (Gemini's server-side
78
+ * sandbox runs the model's Python; nothing runs on this host), `context:`
79
+ * (ADK's token-based compactor with an LLM summarizer, so a long
80
+ * conversation is summarized instead of overflowing the window), and
81
+ * `mode: task` (the agent works until it calls finish_task; on a workflow
82
+ * node, its arguments become the node's output).
83
+ */
84
+ function executionFields(cfg, opts) {
85
+ const out = {};
86
+ if (cfg.code_execution === 'gemini')
87
+ out.codeExecutor = new BuiltInCodeExecutor();
88
+ if (cfg.mode)
89
+ out.mode = cfg.mode;
90
+ if (cfg.context) {
91
+ const resolve = opts.resolveModel ?? ((m) => m);
92
+ const summaryModel = resolve(cfg.context.summary_model ?? cfg.model);
93
+ const llm = typeof summaryModel === 'string' || !summaryModel ? LLMRegistry.newLlm(String(summaryModel ?? cfg.model)) : summaryModel;
94
+ out.contextCompactors = [
95
+ new TokenBasedContextCompactor({
96
+ tokenThreshold: cfg.context.compact_after_tokens,
97
+ eventRetentionSize: cfg.context.keep_recent_events ?? DEFAULT_KEEP_RECENT_EVENTS,
98
+ summarizer: new LlmSummarizer({ llm }),
99
+ }),
100
+ ];
101
+ }
102
+ return out;
103
+ }
71
104
  /**
72
105
  * Says, once per compiled agent, what its resolved path cannot honour — a
73
106
  * dropped server-side tool, a gateway stand-in, or no route at all. Quiet
@@ -95,17 +128,46 @@ export function requireApprovalOn(tool) {
95
128
  Object.defineProperty(gated, 'requireConfirmation', { value: true, enumerable: false });
96
129
  return gated;
97
130
  }
131
+ /**
132
+ * A copy of an OpenAPI tool (lib/tools/openapiTools.ts) that runs only after
133
+ * a person approves the call: the same confirmation interrupt FunctionTool's
134
+ * own gate raises, so the turn pauses and resumes exactly as ADR 0028 says.
135
+ */
136
+ export function requireApprovalOnBaseTool(tool) {
137
+ const gated = Object.create(tool);
138
+ Object.defineProperty(gated, 'runAsync', {
139
+ value: async (request) => {
140
+ const ctx = request.toolContext;
141
+ if (!ctx.toolConfirmation) {
142
+ ctx.requestConfirmation({ hint: `Approval is required before ${tool.name} runs.`, payload: request.args });
143
+ ctx.actions.skipSummarization = true;
144
+ return { error: 'This call requires approval, please approve or reject.' };
145
+ }
146
+ if (!ctx.toolConfirmation.confirmed)
147
+ return { error: 'This call was rejected.' };
148
+ return tool.runAsync(request);
149
+ },
150
+ });
151
+ return gated;
152
+ }
98
153
  function gateTools(tools, names, agentName) {
99
154
  if (!names?.length)
100
155
  return tools;
101
156
  const wanted = new Set(names);
102
157
  const out = tools.map((t) => {
103
- const name = t?.name;
104
- if (!name || !wanted.has(name))
158
+ const tool = t;
159
+ const name = tool?.name;
160
+ if (!name)
161
+ return t;
162
+ // An OpenAPI operation may be named as the YAML named it (its operationId).
163
+ const match = [...wanted].find((w) => w === name || (isOpenApiTool(t) && namesTool(w, { name, operation: tool.operation })));
164
+ if (!match)
105
165
  return t;
106
- wanted.delete(name);
166
+ wanted.delete(match);
167
+ if (isOpenApiTool(t))
168
+ return requireApprovalOnBaseTool(t);
107
169
  if (!(t instanceof FunctionTool)) {
108
- throw new Error(`${agentName}: '${name}' cannot require approval — only function tools from the registry can be gated (ADR 0028).`);
170
+ throw new Error(`${agentName}: '${name}' cannot require approval — only function tools from the registry and OpenAPI operations can be gated (ADR 0028).`);
109
171
  }
110
172
  return requireApprovalOn(t);
111
173
  });
@@ -116,12 +178,33 @@ function gateTools(tools, names, agentName) {
116
178
  }
117
179
  return out;
118
180
  }
119
- /** True when any agent of a syndicate declares an approval gate. */
181
+ /** True when any agent of a syndicate may pause for approval: a gated tool, or skill scripts. */
120
182
  export function declaresApprovals(config) {
121
- return !!config.orchestrator?.require_approval?.length || (config.subagents ?? []).some((s) => !!s.require_approval?.length);
183
+ return agentGates(config.orchestrator) || (config.subagents ?? []).some((s) => agentGates(s));
184
+ }
185
+ /**
186
+ * ADK's ExampleTool from YAML pairs: never called by the model, it adds the
187
+ * exchanges to every request's instruction as few-shot examples.
188
+ */
189
+ export function examplesTool(examples) {
190
+ if (!examples?.length)
191
+ return [];
192
+ return [
193
+ new ExampleTool(examples.map((e) => ({ input: { role: 'user', parts: [{ text: e.input }] }, output: [{ role: 'model', parts: [{ text: e.output }] }] }))),
194
+ ];
122
195
  }
123
- async function resolveAgentTools(toolNames, mcpServerUrl, opts) {
124
- const tools = resolveNamedTools(toolNames, opts.onUnknownTool);
196
+ async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, examples) {
197
+ const tools = [...resolveNamedTools(toolNames, opts.onUnknownTool), ...examplesTool(examples)];
198
+ // OpenAPI operations become tools here, so require_approval can name them.
199
+ for (const entry of openapi ?? []) {
200
+ const built = await buildOpenApiTools(entry);
201
+ opts.log?.(`openapi · ${relative(process.cwd(), entry.spec) || entry.spec}: ${built.map((t) => t.name).join(', ') || '(no operations)'}`);
202
+ for (const t of built) {
203
+ if (tools.some((existing) => existing?.name === t.name))
204
+ throw new Error(`openapi ${entry.spec}: tool '${t.name}' collides with another tool; set a prefix`);
205
+ tools.push(t);
206
+ }
207
+ }
125
208
  if (mcpServerUrl) {
126
209
  opts.log?.(`Loading MCP tools: ${mcpServerUrl}`);
127
210
  const mcpTools = await createMcpTools(mcpServerUrl);
@@ -132,6 +215,28 @@ async function resolveAgentTools(toolNames, mcpServerUrl, opts) {
132
215
  }
133
216
  return tools;
134
217
  }
218
+ /**
219
+ * An agent's `skills:` block becomes one toolset among its tools and one
220
+ * block appended to its instruction (the frontmatter index, always in view).
221
+ * The registry tools a skill may unlock (`skills.tools`) are resolved here,
222
+ * by name, and handed to the toolset; they reach the model only after a
223
+ * skill naming them is loaded (lib/tools/skillToolset.ts).
224
+ */
225
+ async function withSkills(instruction, tools, skills, agentName, opts) {
226
+ if (!skills)
227
+ return { instruction, tools };
228
+ const unlockable = resolveNamedTools(skills.tools, opts.onUnknownTool);
229
+ const harness = await buildSkillHarness(skills, unlockable);
230
+ const count = Object.keys(harness.skills).length;
231
+ opts.log?.(`skills · ${agentName}: ${count} skill${count === 1 ? '' : 's'} from ${skills.dir}${skills.scripts === 'local' ? ' · scripts run after approval' : ''}`);
232
+ for (const problem of harness.problems)
233
+ opts.log?.(`⚠ skills · ${skills.dir}: not loaded — ${problem}`);
234
+ return { instruction: `${instruction.trimEnd()}\n\n${harness.instruction}`, tools: [...tools, harness.toolset] };
235
+ }
236
+ /** True when a turn running this agent directly may pause for a person (ADR 0028). */
237
+ export function agentGates(agent) {
238
+ return !!agent?.require_approval?.length || agent?.skills?.scripts === 'local';
239
+ }
135
240
  /**
136
241
  * Builds ONE runnable agent from a subagent entry. A `yaml_reference` entry
137
242
  * compiles the nested syndicate's whole graph under this entry's name and
@@ -154,18 +259,21 @@ export async function compileSubagent(subCfg, opts = {}) {
154
259
  }
155
260
  return compileGraph(nested, opts, subCfg.name, subCfg.description);
156
261
  }
157
- const tools = gateTools(await resolveAgentTools(subCfg.tools, subCfg.mcp_server_url, opts), subCfg.require_approval, subCfg.name);
262
+ const gated = gateTools(await resolveAgentTools(subCfg.tools, subCfg.mcp_server_url, opts, subCfg.openapi, subCfg.examples), subCfg.require_approval, subCfg.name);
263
+ const { instruction, tools } = await withSkills(subCfg.instruction ?? '', gated, subCfg.skills, subCfg.name, opts);
158
264
  const resolveModel = opts.resolveModel ?? ((m) => m);
159
265
  logCapabilities(opts, subCfg.name, subCfg.model, subCfg.tools);
160
266
  return new LlmAgent({
161
267
  name: subCfg.name,
162
268
  description: subCfg.description,
163
269
  model: resolveModel(subCfg.model),
164
- instruction: subCfg.instruction,
270
+ instruction,
165
271
  tools: tools.length > 0 ? tools : undefined,
166
272
  outputSchema: subCfg.outputSchema,
167
273
  generateContentConfig: withServerSideToolInvocations(subCfg.generateContentConfig),
168
274
  ...passthroughFields(subCfg),
275
+ ...executionFields(subCfg, opts),
276
+ ...(opts.nodeConfig?.(subCfg.name) ?? {}),
169
277
  });
170
278
  }
171
279
  /**
@@ -178,6 +286,14 @@ export async function compileGraph(config, opts = {}, overrideName, overrideDesc
178
286
  // A registry definition carries its nested syndicates (ADR 0018 item 6).
179
287
  if (config.bundled_references && !opts.loadNested)
180
288
  opts = { ...opts, loadNested: nestedLoader(config) };
289
+ // A graph has no orchestrator-with-tools shape to build: its agents are
290
+ // nodes (lib/workflow.ts). Nested as a yaml_reference, only its
291
+ // orchestrator runs, since ADK cannot yet make a Workflow a subagent.
292
+ if (config.workflow && !overrideName) {
293
+ throw new Error(`${config.syndicate_name}: a workflow syndicate is compiled with compileWorkflow (lib/workflow.ts), not compileGraph`);
294
+ }
295
+ if (config.workflow)
296
+ opts.log?.(`${config.syndicate_name}: nested as a subagent, so only its orchestrator runs (a Workflow cannot be a subagent yet)`);
181
297
  const compiledTools = isDispatchSyndicate(config)
182
298
  ? []
183
299
  : await Promise.all((config.subagents ?? []).map(async (subCfg) => {
@@ -190,17 +306,19 @@ export async function compileGraph(config, opts = {}, overrideName, overrideDesc
190
306
  // Orchestrator tools are registry names only — no entrypoint has ever
191
307
  // attached an MCP server to an orchestrator, and this compiler preserves
192
308
  // that exactly rather than widening the contract in passing.
193
- compiledTools.push(...gateTools(await resolveAgentTools(config.orchestrator.tools, undefined, opts), config.orchestrator.require_approval, overrideName || config.orchestrator.name));
309
+ compiledTools.push(...gateTools(await resolveAgentTools(config.orchestrator.tools, undefined, opts, config.orchestrator.openapi, config.orchestrator.examples), config.orchestrator.require_approval, overrideName || config.orchestrator.name));
310
+ const { instruction, tools: orchestratorTools } = await withSkills(config.orchestrator.instruction, compiledTools, config.orchestrator.skills, overrideName || config.orchestrator.name, opts);
194
311
  const resolveModel = opts.resolveModel ?? ((m) => m);
195
312
  logCapabilities(opts, overrideName || config.orchestrator.name, config.orchestrator.model, config.orchestrator.tools);
196
313
  return new LlmAgent({
197
314
  name: overrideName || config.orchestrator.name,
198
315
  description: overrideDescription || config.orchestrator.description,
199
316
  model: resolveModel(config.orchestrator.model),
200
- instruction: config.orchestrator.instruction,
201
- tools: compiledTools.length > 0 ? compiledTools : undefined,
317
+ instruction,
318
+ tools: orchestratorTools.length > 0 ? orchestratorTools : undefined,
202
319
  outputSchema: config.orchestrator.outputSchema,
203
320
  generateContentConfig: withServerSideToolInvocations(config.orchestrator.generateContentConfig),
204
321
  ...passthroughFields(config.orchestrator),
322
+ ...executionFields(config.orchestrator, opts),
205
323
  });
206
324
  }
@@ -59,6 +59,23 @@ export declare const DEFAULT_GROK_MODEL = "grok-4.7";
59
59
  * accept the param and never receive it (lib/models/grokLlm.ts).
60
60
  */
61
61
  export declare const DEFAULT_GROK_REASONING_EFFORT = "medium";
62
+ /**
63
+ * Default Moonshot model used when a kimi-* model is requested but no
64
+ * specific identifier is provided. Requires MOONSHOT_API_KEY
65
+ * (lib/models/kimiLlm.ts). kimi-k3 is the flagship, priced like a mid-tier
66
+ * closed model ($3 / $15, Claude Sonnet 4.6's price; Sonnet 5.5 is cheaper);
67
+ * kimi-k2.6 is the general tier at a quarter of that (the family table and
68
+ * the cost note are in the adapter's header).
69
+ */
70
+ export declare const DEFAULT_KIMI_MODEL = "kimi-k3";
71
+ /**
72
+ * Reasoning effort sent with kimi-k3 requests: 'low' | 'high' | 'max'
73
+ * (Moonshot's own default is 'max'; K3 always thinks and has no off switch).
74
+ * Pinned to HIGH — one step below max, which is the benchmark setting and
75
+ * the slowest; an agent sets generateContentConfig.reasoningEffort to
76
+ * override. Source: platform.moonshot.ai › Guides › Reasoning effort.
77
+ */
78
+ export declare const DEFAULT_KIMI_REASONING_EFFORT = "high";
62
79
  /**
63
80
  * Default open-weight model, served locally by Ollama (lib/models/ollamaLlm.ts).
64
81
  * qwen3:8b is the smallest pulled model that supports tool calling — the
@@ -60,6 +60,23 @@ export const DEFAULT_GROK_MODEL = 'grok-4.7';
60
60
  * accept the param and never receive it (lib/models/grokLlm.ts).
61
61
  */
62
62
  export const DEFAULT_GROK_REASONING_EFFORT = 'medium';
63
+ /**
64
+ * Default Moonshot model used when a kimi-* model is requested but no
65
+ * specific identifier is provided. Requires MOONSHOT_API_KEY
66
+ * (lib/models/kimiLlm.ts). kimi-k3 is the flagship, priced like a mid-tier
67
+ * closed model ($3 / $15, Claude Sonnet 4.6's price; Sonnet 5.5 is cheaper);
68
+ * kimi-k2.6 is the general tier at a quarter of that (the family table and
69
+ * the cost note are in the adapter's header).
70
+ */
71
+ export const DEFAULT_KIMI_MODEL = 'kimi-k3';
72
+ /**
73
+ * Reasoning effort sent with kimi-k3 requests: 'low' | 'high' | 'max'
74
+ * (Moonshot's own default is 'max'; K3 always thinks and has no off switch).
75
+ * Pinned to HIGH — one step below max, which is the benchmark setting and
76
+ * the slowest; an agent sets generateContentConfig.reasoningEffort to
77
+ * override. Source: platform.moonshot.ai › Guides › Reasoning effort.
78
+ */
79
+ export const DEFAULT_KIMI_REASONING_EFFORT = 'high';
63
80
  /**
64
81
  * Default open-weight model, served locally by Ollama (lib/models/ollamaLlm.ts).
65
82
  * qwen3:8b is the smallest pulled model that supports tool calling — the
@@ -84,6 +84,7 @@ export const CONSOLE_URLS = {
84
84
  anthropic: 'https://console.anthropic.com',
85
85
  openai: 'https://platform.openai.com/api-keys',
86
86
  xai: 'https://console.x.ai',
87
+ moonshot: 'https://platform.moonshot.ai',
87
88
  ollama: 'https://ollama.com',
88
89
  };
89
90
  // ── Discovery ────────────────────────────────────────────────────────────────
@@ -162,8 +163,9 @@ function walk(config, prefix, opts, rows) {
162
163
  const orchModel = orch?.model;
163
164
  if (orch && orchModel) {
164
165
  // In DELEGATE mode the orchestrator calls its subagents as tools; under
165
- // plan-dispatch it is a tool-less classifier and code runs the route.
166
- const delegates = !config.dispatch && (config.subagents ?? []).length > 0;
166
+ // plan-dispatch it is a tool-less classifier and code runs the route; in
167
+ // a workflow it is one node of the graph.
168
+ const delegates = !config.dispatch && !config.workflow && (config.subagents ?? []).length > 0;
167
169
  rows.push({
168
170
  agent: prefix + orch.name,
169
171
  role: 'orchestrator',
@@ -390,6 +392,15 @@ const ANSI = {
390
392
  red: '\x1b[31m',
391
393
  cyan: '\x1b[36m',
392
394
  };
395
+ /** One id per provider, in that provider's own prefix, for the providers line. */
396
+ const PROBE_ID = {
397
+ gemini: 'gemini-x',
398
+ anthropic: 'claude-x',
399
+ openai: 'gpt-x',
400
+ xai: 'grok-x',
401
+ moonshot: 'kimi-x',
402
+ ollama: 'ollama/x',
403
+ };
393
404
  function pad(s, n) {
394
405
  return s.length >= n ? s.slice(0, n - 1) + '…' : s.padEnd(n);
395
406
  }
@@ -405,9 +416,11 @@ export function renderDoctor(result, opts = {}) {
405
416
  : `${c.green}${text}${c.reset}`;
406
417
  lines.push(`${c.bold}melchizedek doctor${c.reset} ${c.dim}· ${result.agentsDir}${c.reset}`);
407
418
  lines.push('');
408
- // Provider line: what is funded, and how.
419
+ // Provider line: what is funded, and how. Each provider is probed with an
420
+ // id of its own prefix, so the line names ITS key (a fallthrough once put
421
+ // xAI's key beside Moonshot).
409
422
  const providerBits = Object.keys(PROVIDERS).map((p) => {
410
- const r = describeCapabilities(p === 'ollama' ? 'ollama/x' : p === 'gemini' ? 'gemini-x' : p === 'anthropic' ? 'claude-x' : p === 'openai' ? 'gpt-x' : 'grok-x');
423
+ const r = describeCapabilities(PROBE_ID[p]);
411
424
  const mark = !r.funded ? `${c.red}✗${c.reset}` : r.transport === 'gateway' ? `${c.yellow}◇${c.reset}` : `${c.green}✓${c.reset}`;
412
425
  const how = !r.funded ? `${c.dim}${r.keyEnv} not set${c.reset}` : r.transport === 'gateway' ? `${c.dim}via gateway:${r.gateway}${c.reset}` : p === 'ollama' ? `${c.dim}local${c.reset}` : `${c.dim}${r.platform ?? 'direct'}${c.reset}`;
413
426
  return `${mark} ${PROVIDERS[p].label} ${how}`;
@@ -19,6 +19,8 @@ export type { PendingApproval } from './runtime/approvals.ts';
19
19
  export type { DrainedRun, MessagePart, TurnUsage, RouteDecision, SyndicateTurnOptions, SyndicateTurnResult, TraceOptions, TurnEvents, TurnStage, } from './runtime/syndicateTurn.ts';
20
20
  export { compileGraph, compileSubagent } from './compile.ts';
21
21
  export type { CompileOptions } from './compile.ts';
22
+ export { compileWorkflow, isWorkflowSyndicate, describeInput } from './workflow.ts';
23
+ export type { WorkflowConfig, WorkflowNodeYaml, EdgeElement, PendingInput, CompiledWorkflow } from './workflow.ts';
22
24
  export { createA2AApp, compileAgentCard, currentRequestContext } from './a2a/app.ts';
23
25
  export type { A2AApp, A2AAppOptions, RequestIdentity } from './a2a/app.ts';
24
26
  export type { A2AContext } from './a2a/executor.ts';
package/dist/lib/index.js CHANGED
@@ -17,6 +17,7 @@ export { loadSyndicate, loadSyndicateFromRegistry, parseCliBindings, validateReg
17
17
  export { runSyndicateTurn, ingestTurnMemory } from './runtime/syndicateTurn.js';
18
18
  export { approvalResponsePart, pendingApproval, APPROVAL_REQUEST } from './runtime/approvals.js';
19
19
  export { compileGraph, compileSubagent } from './compile.js';
20
+ export { compileWorkflow, isWorkflowSyndicate, describeInput } from './workflow.js';
20
21
  // ── Serve it over A2A, or call a remote A2A agent ───────────────────────────
21
22
  export { createA2AApp, compileAgentCard, currentRequestContext } from './a2a/app.js';
22
23
  export { callerTokens, firstOf, hashCallerToken, jwtIdentity, parseCallers, scopeSegment, sharedSecret, trustedHeader, } from './a2a/identity.js';
@@ -1,5 +1,12 @@
1
1
  import type { DispatchConfig } from './dispatch.ts';
2
2
  export type { DispatchConfig } from './dispatch.ts';
3
+ import type { SkillsConfig } from './tools/skillToolset.ts';
4
+ import type { OpenApiConfig } from './tools/openapiTools.ts';
5
+ import type { ContextConfig, ExampleConfig } from './compile.ts';
6
+ export type { OpenApiConfig } from './tools/openapiTools.ts';
7
+ export type { SkillsConfig } from './tools/skillToolset.ts';
8
+ import type { WorkflowConfig } from './workflowConfig.ts';
9
+ export type { WorkflowConfig } from './workflowConfig.ts';
3
10
  export interface ThinkingConfig {
4
11
  /** Token budget for internal reasoning. 0 = off, -1 = dynamic. */
5
12
  thinkingBudget?: number;
@@ -81,6 +88,26 @@ export interface AgentYamlConfig {
81
88
  */
82
89
  outputSchema?: Record<string, unknown>;
83
90
  mcp_server_url?: string;
91
+ /**
92
+ * Agent Skills (SKILL.md directories) this agent holds the way a coding
93
+ * harness does: the index in its instruction, a skill read on demand, its
94
+ * scripts run only after approval. lib/tools/skillToolset.ts.
95
+ */
96
+ skills?: SkillsConfig;
97
+ /**
98
+ * HTTP APIs this agent may call, each from an OpenAPI 3 spec file: one tool
99
+ * per operation, the GET operations unless `operations` names others, auth
100
+ * from environment variables. lib/tools/openapiTools.ts.
101
+ */
102
+ openapi?: OpenApiConfig[];
103
+ /** `gemini`: the model's Python runs in Gemini's server-side sandbox (ADR 0033). Gemini models only. */
104
+ code_execution?: 'gemini';
105
+ /** Compact a long conversation into a summary past a token threshold (ADR 0033). Delegate orchestrator only. */
106
+ context?: ContextConfig;
107
+ /** `task`: the agent works until it calls finish_task; its arguments are the node's output. Workflow nodes only. */
108
+ mode?: 'task';
109
+ /** Few-shot exchanges added to every request's instruction (ADK's ExampleTool). */
110
+ examples?: ExampleConfig[];
84
111
  orchestration?: {
85
112
  role?: 'primary' | 'sub-agent';
86
113
  delegates?: string[];
@@ -121,6 +148,24 @@ export interface SyndicateYamlConfig {
121
148
  * See lib/dispatch.ts for the full contract.
122
149
  */
123
150
  dispatch?: DispatchConfig;
151
+ /**
152
+ * Self-correction (ADK's reflect-and-retry plugins), on by default:
153
+ * `model_errors` retries a model reply ADK marks malformed (default 2),
154
+ * `tool_errors` caps a tool's retries after it throws, with structured
155
+ * guidance to the model (default 3). 0 turns either off. ADR 0034.
156
+ */
157
+ retries?: {
158
+ model_errors?: number;
159
+ tool_errors?: number;
160
+ };
161
+ /**
162
+ * Opts this syndicate into a WORKFLOW: a graph whose nodes are its agents
163
+ * (plus join, map, tool and ask_user nodes) and whose edges say what runs
164
+ * after what and on which route. The orchestrator is a node like any
165
+ * other; nothing delegates. Cannot be combined with `dispatch`.
166
+ * Contract: lib/workflow.ts.
167
+ */
168
+ workflow?: WorkflowConfig;
124
169
  /** Optional default variable definitions for {{token}} interpolation. */
125
170
  variables?: VariableMap;
126
171
  /** Defines the persistence and semantic memory layer for the syndicate. */
@@ -237,6 +282,7 @@ export declare function loadSyndicate(filename: string, options?: LoadSyndicateO
237
282
  export declare function readSyndicateFile(filename: string, options?: Pick<LoadSyndicateOptions, 'agentsDir' | 'shippedFallback'>): {
238
283
  raw: unknown;
239
284
  label: string;
285
+ filePath: string;
240
286
  };
241
287
  /**
242
288
  * Every nested `yaml_reference:` a syndicate reaches, read raw from the agents
@@ -228,8 +228,24 @@ export function collectGuards(config, loadNested = nestedLoader(config), seen =
228
228
  }
229
229
  export function loadSyndicate(filename, options = {}) {
230
230
  const { bindings = {}, overrides } = options;
231
- const { raw, label } = readSyndicateFile(filename, options);
232
- return resolveAndValidate(raw, label, { bindings, overrides });
231
+ const { raw, label, filePath } = readSyndicateFile(filename, options);
232
+ return anchorSpecPaths(resolveAndValidate(raw, label, { bindings, overrides }), path.dirname(filePath));
233
+ }
234
+ /**
235
+ * An `openapi:` spec path is written relative to the syndicate file, so a
236
+ * file copied with its specs keeps working from any working directory. A
237
+ * definition from the registry has no file and resolves against the cwd.
238
+ */
239
+ function anchorSpecPaths(config, dir) {
240
+ const anchor = (agent) => {
241
+ for (const entry of agent?.openapi ?? [])
242
+ if (!path.isAbsolute(entry.spec))
243
+ entry.spec = path.resolve(dir, entry.spec);
244
+ };
245
+ anchor(config.orchestrator);
246
+ for (const sub of config.subagents ?? [])
247
+ anchor(sub);
248
+ return config;
233
249
  }
234
250
  /**
235
251
  * Read one syndicate file, unparsed values and all, from the agents root (the
@@ -266,7 +282,7 @@ export function readSyndicateFile(filename, options = {}) {
266
282
  // is under it), so a multi-file nested load says WHICH file is wrong.
267
283
  const rel = path.relative(process.cwd(), filePath);
268
284
  const label = rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : filePath;
269
- return { raw: parse(fileContent), label };
285
+ return { raw: parse(fileContent), label, filePath };
270
286
  }
271
287
  /**
272
288
  * Every nested `yaml_reference:` a syndicate reaches, read raw from the agents
@@ -8,6 +8,11 @@
8
8
  * markers, and (without a namespace) its task tools' list and jobs
9
9
  * (migration 0010), in one transaction. Returns what each store lost.
10
10
  *
11
+ * With a namespace, ledger turns and A2A tasks are kept only when their
12
+ * conversation is still live in another namespace (migration 0011): they do
13
+ * not record the namespace, and a conversation whose session expired must
14
+ * not survive an erase.
15
+ *
11
16
  * Throws on any failure: a deletion request that silently did nothing is
12
17
  * worse than an error the caller can surface and retry.
13
18
  */