melchizedek-agents 0.16.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.env.example +40 -3
  2. package/AGENT_SETUP.md +4 -2
  3. package/CHANGELOG.md +246 -0
  4. package/DOCUMENTATION.md +311 -15
  5. package/QUICKSTART.md +2 -2
  6. package/README.md +125 -149
  7. package/config/agents/examples/harness.yaml +135 -0
  8. package/config/agents/examples/model_zoo.yaml +23 -9
  9. package/config/agents/examples/pipeline.yaml +120 -0
  10. package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
  11. package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
  12. package/config/agents/examples/weather.yaml +59 -0
  13. package/config/agents/syndicateSchema.yaml +125 -1
  14. package/db/hardening.sql +4 -3
  15. package/db/migrations/0011_erase_expired.sql +198 -0
  16. package/db/migrations/0012_audit_log.sql +98 -0
  17. package/dist/lib/a2a/app.d.ts +27 -0
  18. package/dist/lib/a2a/app.js +101 -5
  19. package/dist/lib/a2a/executor.d.ts +34 -4
  20. package/dist/lib/a2a/executor.js +109 -9
  21. package/dist/lib/compile.d.ts +41 -3
  22. package/dist/lib/compile.js +162 -17
  23. package/dist/lib/config.d.ts +24 -0
  24. package/dist/lib/config.js +24 -0
  25. package/dist/lib/doctor.js +17 -4
  26. package/dist/lib/index.d.ts +2 -0
  27. package/dist/lib/index.js +1 -0
  28. package/dist/lib/loadEnv.d.ts +5 -0
  29. package/dist/lib/loadEnv.js +7 -0
  30. package/dist/lib/loadSyndicate.d.ts +50 -0
  31. package/dist/lib/loadSyndicate.js +19 -3
  32. package/dist/lib/memory/erase.d.ts +5 -0
  33. package/dist/lib/memory/erase.js +5 -0
  34. package/dist/lib/memory/providers.d.ts +27 -0
  35. package/dist/lib/memory/providers.js +28 -0
  36. package/dist/lib/models/capabilities.js +10 -0
  37. package/dist/lib/models/endpoints.d.ts +1 -1
  38. package/dist/lib/models/endpoints.js +4 -1
  39. package/dist/lib/models/fallback.d.ts +41 -0
  40. package/dist/lib/models/fallback.js +105 -0
  41. package/dist/lib/models/gateway.d.ts +2 -2
  42. package/dist/lib/models/gateway.js +4 -4
  43. package/dist/lib/models/kimiLlm.d.ts +123 -0
  44. package/dist/lib/models/kimiLlm.js +171 -0
  45. package/dist/lib/models/providerMap.d.ts +2 -2
  46. package/dist/lib/models/providerMap.js +4 -1
  47. package/dist/lib/models/registry.d.ts +1 -1
  48. package/dist/lib/models/registry.js +11 -3
  49. package/dist/lib/net/redirects.d.ts +14 -0
  50. package/dist/lib/net/redirects.js +94 -0
  51. package/dist/lib/observability/audit.d.ts +21 -0
  52. package/dist/lib/observability/audit.js +39 -0
  53. package/dist/lib/observability/metrics.d.ts +6 -0
  54. package/dist/lib/observability/tracer.d.ts +14 -1
  55. package/dist/lib/observability/tracer.js +16 -2
  56. package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
  57. package/dist/lib/persistence/supabaseProvider.js +2 -31
  58. package/dist/lib/runtime/questions.d.ts +55 -0
  59. package/dist/lib/runtime/questions.js +107 -0
  60. package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
  61. package/dist/lib/runtime/syndicateTurn.js +165 -17
  62. package/dist/lib/storage/postgres/index.d.ts +9 -0
  63. package/dist/lib/storage/postgres/index.js +13 -0
  64. package/dist/lib/storage/rlsStatus.d.ts +45 -0
  65. package/dist/lib/storage/rlsStatus.js +69 -0
  66. package/dist/lib/syndicateSchema.d.ts +177 -0
  67. package/dist/lib/syndicateSchema.js +343 -3
  68. package/dist/lib/toolRegistry.js +9 -0
  69. package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
  70. package/dist/lib/tools/mcpToolFactory.js +61 -4
  71. package/dist/lib/tools/openapiTools.d.ts +52 -0
  72. package/dist/lib/tools/openapiTools.js +225 -0
  73. package/dist/lib/tools/skillToolset.d.ts +135 -0
  74. package/dist/lib/tools/skillToolset.js +247 -0
  75. package/dist/lib/tools/urlContextTool.d.ts +29 -0
  76. package/dist/lib/tools/urlContextTool.js +39 -0
  77. package/dist/lib/workflow.d.ts +72 -0
  78. package/dist/lib/workflow.js +180 -0
  79. package/dist/lib/workflowConfig.d.ts +95 -0
  80. package/dist/lib/workflowConfig.js +93 -0
  81. package/dist/scripts/a2a_server.js +51 -8
  82. package/dist/scripts/db.d.ts +9 -0
  83. package/dist/scripts/db.js +30 -15
  84. package/dist/scripts/syndicate_chat.js +39 -5
  85. package/package.json +6 -3
  86. package/skills/briefs/_shared.md +1 -1
  87. package/skills/briefs/melchizedek-author.md +8 -4
  88. package/skills/briefs/melchizedek-memory.md +1 -1
  89. package/skills/briefs/melchizedek-models.md +1 -1
  90. package/skills/briefs/melchizedek-scribe.md +4 -4
  91. package/skills/briefs/melchizedek-serve.md +1 -1
  92. package/skills/briefs/melchizedek.md +4 -1
  93. package/skills/melchizedek/SKILL.md +4 -1
  94. package/skills/melchizedek-author/SKILL.md +7 -3
  95. package/skills/melchizedek-memory/SKILL.md +1 -1
  96. package/skills/melchizedek-models/SKILL.md +1 -1
  97. package/skills/melchizedek-scribe/SKILL.md +3 -3
  98. package/skills/melchizedek-serve/SKILL.md +1 -1
  99. /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
  100. /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
@@ -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
  /**
@@ -118,28 +122,64 @@ export function a2aPartsToMessage(rawParts) {
118
122
  return { parts };
119
123
  }
120
124
  const NO_USAGE = { llmCalls: 0, inputTokens: 0, outputTokens: 0, thinkingTokens: 0 };
121
- /** Concurrency cap and in-flight registry shared by all executors. */
125
+ /** Concurrency caps and in-flight registry shared by all executors. */
122
126
  export class TaskLimiter {
123
127
  max;
128
+ perScope;
129
+ perCaller;
124
130
  running = new Map();
131
+ /** taskId → the keys it counts against, so release frees exactly those. */
132
+ keysOf = new Map();
133
+ counts = new Map();
125
134
  draining = false;
126
135
  idleWaiters = [];
127
- constructor(max) {
136
+ constructor(max, limits = {}) {
128
137
  this.max = max > 0 ? max : Infinity;
138
+ this.perScope = limits.perScope && limits.perScope > 0 ? limits.perScope : Infinity;
139
+ this.perCaller = limits.perCaller && limits.perCaller > 0 ? limits.perCaller : Infinity;
140
+ }
141
+ /**
142
+ * Why a task for this scope and caller may not start now, or undefined.
143
+ * Checked before acquire() so the refusal can name the cap that was hit.
144
+ */
145
+ refusal(who = {}) {
146
+ if (this.draining)
147
+ return 'The server is shutting down; retry shortly.';
148
+ if (this.running.size >= this.max)
149
+ return `The server is at its limit of ${this.max} concurrent tasks; retry shortly.`;
150
+ if (who.scopeKey && (this.counts.get(`s:${who.scopeKey}`) ?? 0) >= this.perScope) {
151
+ return `This user already has ${this.perScope} tasks running, the most allowed at once; retry when one finishes.`;
152
+ }
153
+ if (who.caller && (this.counts.get(`c:${who.caller}`) ?? 0) >= this.perCaller) {
154
+ return `This caller already has ${this.perCaller} tasks running, the most allowed at once; retry when one finishes.`;
155
+ }
156
+ return undefined;
129
157
  }
130
158
  get inFlight() {
131
159
  return this.running.size;
132
160
  }
133
- /** Reserve a slot. Returns undefined when saturated or shutting down. */
134
- acquire(taskId) {
135
- if (this.draining || this.running.size >= this.max)
161
+ /** Reserve a slot. Returns undefined when any cap is reached or the server is shutting down. */
162
+ acquire(taskId, who = {}) {
163
+ if (this.refusal(who))
136
164
  return undefined;
137
165
  const controller = new AbortController();
138
166
  this.running.set(taskId, controller);
167
+ const keys = [who.scopeKey && `s:${who.scopeKey}`, who.caller && `c:${who.caller}`].filter((k) => !!k);
168
+ for (const k of keys)
169
+ this.counts.set(k, (this.counts.get(k) ?? 0) + 1);
170
+ this.keysOf.set(taskId, keys);
139
171
  return controller;
140
172
  }
141
173
  release(taskId) {
142
174
  this.running.delete(taskId);
175
+ for (const k of this.keysOf.get(taskId) ?? []) {
176
+ const n = (this.counts.get(k) ?? 1) - 1;
177
+ if (n > 0)
178
+ this.counts.set(k, n);
179
+ else
180
+ this.counts.delete(k);
181
+ }
182
+ this.keysOf.delete(taskId);
143
183
  if (this.running.size === 0)
144
184
  for (const w of this.idleWaiters.splice(0))
145
185
  w();
@@ -209,6 +249,37 @@ function publishWorking(eventBus, taskId, contextId, text) {
209
249
  metadata: undefined,
210
250
  }));
211
251
  }
252
+ /**
253
+ * A workflow paused on an `ask_user` node (lib/workflow.ts): the task ends
254
+ * input-required with the question, and a data part carrying it. The
255
+ * answer is simply the next message on the conversation; its text becomes
256
+ * the node's output and the graph resumes where it waited.
257
+ */
258
+ function publishInputRequest(eventBus, taskId, contextId, input) {
259
+ const message = statusMessage(taskId, contextId, `Input needed: ${describeInput(input)}`);
260
+ message.parts.push({
261
+ content: {
262
+ $case: 'data',
263
+ value: {
264
+ type: 'input_request',
265
+ interrupt_id: input.id,
266
+ node: input.node,
267
+ message: input.message,
268
+ ...(input.payload !== undefined ? { payload: input.payload } : {}),
269
+ ...(input.schema !== undefined ? { schema: input.schema } : {}),
270
+ },
271
+ },
272
+ metadata: undefined,
273
+ filename: '',
274
+ mediaType: 'application/json',
275
+ });
276
+ eventBus.publish(AgentEvent.statusUpdate({
277
+ taskId,
278
+ contextId,
279
+ status: { state: TaskState.TASK_STATE_INPUT_REQUIRED, message, timestamp: new Date().toISOString() },
280
+ metadata: undefined,
281
+ }));
282
+ }
212
283
  /**
213
284
  * The turn paused on a gated tool call (ADR 0028): the task ends
214
285
  * input-required, saying what is to be approved, with a data part a client
@@ -332,6 +403,8 @@ export class SyndicateExecutor {
332
403
  // Reports the task once: to the task log and metrics, and its spend to
333
404
  // the policy. Never throws into the task.
334
405
  let reported = false;
406
+ // The turn's trace (the last root span started: a dispatch turn's answer).
407
+ let traceId;
335
408
  const report = async (ctx, status, reason, usage = NO_USAGE) => {
336
409
  if (reported)
337
410
  return;
@@ -347,6 +420,9 @@ export class SyndicateExecutor {
347
420
  }
348
421
  try {
349
422
  this.opts.onTaskEnd?.({
423
+ taskId,
424
+ traceId,
425
+ ...(ctx?.traceparent ? { callerTraceId: ctx.traceparent.split('-')[1] } : {}),
350
426
  agentId,
351
427
  syndicate: config.syndicate_name,
352
428
  caller: ctx?.caller,
@@ -360,6 +436,21 @@ export class SyndicateExecutor {
360
436
  catch {
361
437
  /* a broken log sink must not fail the task */
362
438
  }
439
+ try {
440
+ this.opts.onAudit?.({
441
+ event: 'task.end',
442
+ outcome: status,
443
+ caller: ctx?.caller,
444
+ scopeHash: scopeKey ? createHash('sha256').update(scopeKey).digest('hex').slice(0, 12) : undefined,
445
+ sourceIp: ctx?.sourceIp,
446
+ agentId,
447
+ taskId,
448
+ detail: { syndicate: config.syndicate_name, ...(reason ? { reason } : {}), ...(traceId ? { traceId } : {}), durationMs: Date.now() - started, llmCalls: usage.llmCalls },
449
+ });
450
+ }
451
+ catch {
452
+ /* nor a broken audit sink */
453
+ }
363
454
  };
364
455
  let ctxForReport;
365
456
  try {
@@ -408,11 +499,10 @@ export class SyndicateExecutor {
408
499
  return;
409
500
  }
410
501
  }
411
- slot = this.opts.limiter.acquire(taskId);
502
+ const who = { scopeKey: ctx.scopeKey, caller: ctx.caller };
503
+ slot = this.opts.limiter.acquire(taskId, who);
412
504
  if (!slot) {
413
- const why = this.opts.limiter.isDraining
414
- ? 'The server is shutting down; retry shortly.'
415
- : `The server is at its limit of ${this.opts.limiter.max} concurrent tasks; retry shortly.`;
505
+ const why = this.opts.limiter.refusal(who) ?? 'The server is at capacity; retry shortly.';
416
506
  warn(`Task ${short} rejected — ${why}`);
417
507
  publishFinal(eventBus, taskId, contextId, 'rejected', why);
418
508
  await report(ctx, 'rejected', 'capacity');
@@ -471,6 +561,10 @@ export class SyndicateExecutor {
471
561
  taskId,
472
562
  configHash: this.configHashFor(),
473
563
  attributes: surfaceAttributes(ctx),
564
+ traceparent: ctx.traceparent,
565
+ onSpanStart: (ids) => {
566
+ traceId = ids.traceId;
567
+ },
474
568
  },
475
569
  events: {
476
570
  onProgress: (text) => publishWorking(eventBus, taskId, contextId, text),
@@ -497,6 +591,12 @@ export class SyndicateExecutor {
497
591
  await report(ctx, 'input-required', 'approval', u);
498
592
  return;
499
593
  }
594
+ if (result.status === 'input-required' && result.input) {
595
+ log(`⏸ Task ${short} waiting for input from ${result.input.node} after ${spent}`);
596
+ publishInputRequest(eventBus, taskId, contextId, result.input);
597
+ await report(ctx, 'input-required', 'input', u);
598
+ return;
599
+ }
500
600
  if (result.status === 'failed') {
501
601
  warn(`✗ Task ${short} failed [${result.error?.code}] after ${spent}`);
502
602
  publishFinal(eventBus, taskId, contextId, 'failed', describeFailedTurn(result));
@@ -27,9 +27,10 @@
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 { FunctionTool, LlmAgent } from '@google/adk';
31
- import type { BaseLlm } from '@google/adk';
30
+ import { BaseLlm, FunctionTool, LlmAgent } from '@google/adk';
31
+ import type { BaseTool } from '@google/adk';
32
32
  import type { SubagentYamlConfig, SyndicateYamlConfig } from './loadSyndicate.ts';
33
+ import type { SkillsConfig } from './tools/skillToolset.ts';
33
34
  export interface CompileOptions {
34
35
  /**
35
36
  * Turns the YAML `model` string into what LlmAgent receives. Default:
@@ -48,7 +49,23 @@ export interface CompileOptions {
48
49
  onUnknownTool?: (name: string) => void;
49
50
  /** Progress/diagnostic line sink (nested loads, MCP discovery). */
50
51
  log?: (message: string) => void;
52
+ /**
53
+ * Node settings for an agent compiled as a workflow node (lib/workflow.ts):
54
+ * ADK's `retryConfig` and `timeout`, which an LlmAgent takes only at
55
+ * construction. Called with the agent's name; undefined means none.
56
+ */
57
+ nodeConfig?: (agentName: string) => Record<string, unknown> | undefined;
51
58
  }
59
+ /** An agent's `context:` block: compact a long conversation into a summary (ADR 0033). */
60
+ export interface ContextConfig {
61
+ /** Compact when the last request's prompt passed this many tokens. */
62
+ compact_after_tokens: number;
63
+ /** Events kept verbatim after the summary. Default 6. */
64
+ keep_recent_events?: number;
65
+ /** The model that writes the summary. Default: the agent's own. */
66
+ summary_model?: string;
67
+ }
68
+ export declare const DEFAULT_KEEP_RECENT_EVENTS = 6;
52
69
  /**
53
70
  * A copy of a FunctionTool that runs only after a person approves the call
54
71
  * (ADR 0028). ADK's own gate does the work: the call raises an
@@ -57,8 +74,29 @@ export interface CompileOptions {
57
74
  * ungated for every other agent that lists the same tool.
58
75
  */
59
76
  export declare function requireApprovalOn(tool: FunctionTool): FunctionTool;
60
- /** True when any agent of a syndicate declares an approval gate. */
77
+ /**
78
+ * A copy of an OpenAPI tool (lib/tools/openapiTools.ts) that runs only after
79
+ * a person approves the call: the same confirmation interrupt FunctionTool's
80
+ * own gate raises, so the turn pauses and resumes exactly as ADR 0028 says.
81
+ */
82
+ export declare function requireApprovalOnBaseTool<T extends BaseTool>(tool: T): T;
83
+ /** True when any agent of a syndicate may pause for approval: a gated tool, or skill scripts. */
61
84
  export declare function declaresApprovals(config: SyndicateYamlConfig): boolean;
85
+ /** An agent's `examples:` entry: one exchange the model should imitate. */
86
+ export interface ExampleConfig {
87
+ input: string;
88
+ output: string;
89
+ }
90
+ /**
91
+ * ADK's ExampleTool from YAML pairs: never called by the model, it adds the
92
+ * exchanges to every request's instruction as few-shot examples.
93
+ */
94
+ export declare function examplesTool(examples: ExampleConfig[] | undefined): unknown[];
95
+ /** True when a turn running this agent directly may pause for a person (ADR 0028). */
96
+ export declare function agentGates(agent: {
97
+ require_approval?: string[];
98
+ skills?: SkillsConfig;
99
+ } | undefined): boolean;
62
100
  /**
63
101
  * Builds ONE runnable agent from a subagent entry. A `yaml_reference` entry
64
102
  * compiles the nested syndicate's whole graph under this entry's name and
@@ -27,13 +27,18 @@
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, BaseLlm, 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';
37
+ import { FallbackLlm } from './models/fallback.js';
38
+ import { resolveModel as resolveRegistryModel } from './models/registry.js';
36
39
  import { remoteAgentTool } from './a2a/remoteAgent.js';
40
+ import { buildSkillHarness } from './tools/skillToolset.js';
41
+ import { buildOpenApiTools, isOpenApiTool, namesTool } from './tools/openapiTools.js';
37
42
  /** generateContentConfig as every entrypoint has always sent it to ADK. */
38
43
  function withServerSideToolInvocations(generateContentConfig) {
39
44
  const cfg = (generateContentConfig ?? {});
@@ -68,6 +73,36 @@ function passthroughFields(cfg) {
68
73
  out.disallowTransferToPeers = cfg.disallowTransferToPeers;
69
74
  return out;
70
75
  }
76
+ export const DEFAULT_KEEP_RECENT_EVENTS = 6;
77
+ /**
78
+ * The LlmAgent fields the engine builds from YAML rather than passing
79
+ * through (ADR 0033): `code_execution: gemini` (Gemini's server-side
80
+ * sandbox runs the model's Python; nothing runs on this host), `context:`
81
+ * (ADK's token-based compactor with an LLM summarizer, so a long
82
+ * conversation is summarized instead of overflowing the window), and
83
+ * `mode: task` (the agent works until it calls finish_task; on a workflow
84
+ * node, its arguments become the node's output).
85
+ */
86
+ function executionFields(cfg, opts) {
87
+ const out = {};
88
+ if (cfg.code_execution === 'gemini')
89
+ out.codeExecutor = new BuiltInCodeExecutor();
90
+ if (cfg.mode)
91
+ out.mode = cfg.mode;
92
+ if (cfg.context) {
93
+ const resolve = opts.resolveModel ?? ((m) => m);
94
+ const summaryModel = resolve(cfg.context.summary_model ?? cfg.model);
95
+ const llm = typeof summaryModel === 'string' || !summaryModel ? LLMRegistry.newLlm(String(summaryModel ?? cfg.model)) : summaryModel;
96
+ out.contextCompactors = [
97
+ new TokenBasedContextCompactor({
98
+ tokenThreshold: cfg.context.compact_after_tokens,
99
+ eventRetentionSize: cfg.context.keep_recent_events ?? DEFAULT_KEEP_RECENT_EVENTS,
100
+ summarizer: new LlmSummarizer({ llm }),
101
+ }),
102
+ ];
103
+ }
104
+ return out;
105
+ }
71
106
  /**
72
107
  * Says, once per compiled agent, what its resolved path cannot honour — a
73
108
  * dropped server-side tool, a gateway stand-in, or no route at all. Quiet
@@ -95,17 +130,46 @@ export function requireApprovalOn(tool) {
95
130
  Object.defineProperty(gated, 'requireConfirmation', { value: true, enumerable: false });
96
131
  return gated;
97
132
  }
133
+ /**
134
+ * A copy of an OpenAPI tool (lib/tools/openapiTools.ts) that runs only after
135
+ * a person approves the call: the same confirmation interrupt FunctionTool's
136
+ * own gate raises, so the turn pauses and resumes exactly as ADR 0028 says.
137
+ */
138
+ export function requireApprovalOnBaseTool(tool) {
139
+ const gated = Object.create(tool);
140
+ Object.defineProperty(gated, 'runAsync', {
141
+ value: async (request) => {
142
+ const ctx = request.toolContext;
143
+ if (!ctx.toolConfirmation) {
144
+ ctx.requestConfirmation({ hint: `Approval is required before ${tool.name} runs.`, payload: request.args });
145
+ ctx.actions.skipSummarization = true;
146
+ return { error: 'This call requires approval, please approve or reject.' };
147
+ }
148
+ if (!ctx.toolConfirmation.confirmed)
149
+ return { error: 'This call was rejected.' };
150
+ return tool.runAsync(request);
151
+ },
152
+ });
153
+ return gated;
154
+ }
98
155
  function gateTools(tools, names, agentName) {
99
156
  if (!names?.length)
100
157
  return tools;
101
158
  const wanted = new Set(names);
102
159
  const out = tools.map((t) => {
103
- const name = t?.name;
104
- if (!name || !wanted.has(name))
160
+ const tool = t;
161
+ const name = tool?.name;
162
+ if (!name)
163
+ return t;
164
+ // An OpenAPI operation may be named as the YAML named it (its operationId).
165
+ const match = [...wanted].find((w) => w === name || (isOpenApiTool(t) && namesTool(w, { name, operation: tool.operation })));
166
+ if (!match)
105
167
  return t;
106
- wanted.delete(name);
168
+ wanted.delete(match);
169
+ if (isOpenApiTool(t))
170
+ return requireApprovalOnBaseTool(t);
107
171
  if (!(t instanceof FunctionTool)) {
108
- throw new Error(`${agentName}: '${name}' cannot require approval — only function tools from the registry can be gated (ADR 0028).`);
172
+ throw new Error(`${agentName}: '${name}' cannot require approval — only function tools from the registry and OpenAPI operations can be gated (ADR 0028).`);
109
173
  }
110
174
  return requireApprovalOn(t);
111
175
  });
@@ -116,15 +180,61 @@ function gateTools(tools, names, agentName) {
116
180
  }
117
181
  return out;
118
182
  }
119
- /** True when any agent of a syndicate declares an approval gate. */
183
+ /** True when any agent of a syndicate may pause for approval: a gated tool, or skill scripts. */
120
184
  export function declaresApprovals(config) {
121
- return !!config.orchestrator?.require_approval?.length || (config.subagents ?? []).some((s) => !!s.require_approval?.length);
185
+ return agentGates(config.orchestrator) || (config.subagents ?? []).some((s) => agentGates(s));
122
186
  }
123
- async function resolveAgentTools(toolNames, mcpServerUrl, opts) {
124
- const tools = resolveNamedTools(toolNames, opts.onUnknownTool);
187
+ /**
188
+ * ADK's ExampleTool from YAML pairs: never called by the model, it adds the
189
+ * exchanges to every request's instruction as few-shot examples.
190
+ */
191
+ export function examplesTool(examples) {
192
+ if (!examples?.length)
193
+ return [];
194
+ return [
195
+ new ExampleTool(examples.map((e) => ({ input: { role: 'user', parts: [{ text: e.input }] }, output: [{ role: 'model', parts: [{ text: e.output }] }] }))),
196
+ ];
197
+ }
198
+ /**
199
+ * An agent with `fallback_model:` gets its model wrapped (lib/models/fallback.ts):
200
+ * the fallback answers a provider-side failure, and a provider that keeps
201
+ * failing is skipped for a cooldown. A model id string is resolved to its
202
+ * adapter first, so the wrapper always holds two adapters.
203
+ */
204
+ function withFallback(primary, fallbackId, resolve, opts) {
205
+ if (!fallbackId)
206
+ return primary;
207
+ const asLlm = (m) => (m instanceof BaseLlm ? m : resolveRegistryModel(String(m)));
208
+ return new FallbackLlm(asLlm(primary), asLlm(resolve(fallbackId)), opts.log ?? ((m) => console.warn(m)));
209
+ }
210
+ async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, examples, mcpAllowed) {
211
+ const tools = [...resolveNamedTools(toolNames, opts.onUnknownTool), ...examplesTool(examples)];
212
+ // OpenAPI operations become tools here, so require_approval can name them.
213
+ for (const entry of openapi ?? []) {
214
+ const built = await buildOpenApiTools(entry);
215
+ opts.log?.(`openapi · ${relative(process.cwd(), entry.spec) || entry.spec}: ${built.map((t) => t.name).join(', ') || '(no operations)'}`);
216
+ for (const t of built) {
217
+ if (tools.some((existing) => existing?.name === t.name))
218
+ throw new Error(`openapi ${entry.spec}: tool '${t.name}' collides with another tool; set a prefix`);
219
+ tools.push(t);
220
+ }
221
+ }
125
222
  if (mcpServerUrl) {
126
223
  opts.log?.(`Loading MCP tools: ${mcpServerUrl}`);
127
- const mcpTools = await createMcpTools(mcpServerUrl);
224
+ const offered = await createMcpTools(mcpServerUrl);
225
+ // mcp_tools: only the named tools are exposed (a server's list is its
226
+ // own to change); a name the server does not offer is reported, and a
227
+ // gate on it fails the compile in gateTools.
228
+ const allowed = mcpAllowed ? new Set(mcpAllowed) : undefined;
229
+ const mcpTools = allowed ? offered.filter((t) => allowed.has(t.name)) : offered;
230
+ if (allowed) {
231
+ const missing = [...allowed].filter((n) => !offered.some((t) => t.name === n));
232
+ if (missing.length)
233
+ opts.log?.(`MCP ${mcpServerUrl} does not offer ${missing.map((n) => `'${n}'`).join(', ')} (mcp_tools)`);
234
+ const hidden = offered.length - mcpTools.length;
235
+ if (hidden)
236
+ opts.log?.(`MCP ${mcpServerUrl}: ${hidden} tool(s) not in mcp_tools are not exposed`);
237
+ }
128
238
  for (const mcpTool of mcpTools) {
129
239
  if (!tools.some((t) => t.name === mcpTool.name))
130
240
  tools.push(mcpTool);
@@ -132,6 +242,28 @@ async function resolveAgentTools(toolNames, mcpServerUrl, opts) {
132
242
  }
133
243
  return tools;
134
244
  }
245
+ /**
246
+ * An agent's `skills:` block becomes one toolset among its tools and one
247
+ * block appended to its instruction (the frontmatter index, always in view).
248
+ * The registry tools a skill may unlock (`skills.tools`) are resolved here,
249
+ * by name, and handed to the toolset; they reach the model only after a
250
+ * skill naming them is loaded (lib/tools/skillToolset.ts).
251
+ */
252
+ async function withSkills(instruction, tools, skills, agentName, opts) {
253
+ if (!skills)
254
+ return { instruction, tools };
255
+ const unlockable = resolveNamedTools(skills.tools, opts.onUnknownTool);
256
+ const harness = await buildSkillHarness(skills, unlockable);
257
+ const count = Object.keys(harness.skills).length;
258
+ opts.log?.(`skills · ${agentName}: ${count} skill${count === 1 ? '' : 's'} from ${skills.dir}${skills.scripts === 'local' ? ' · scripts run after approval' : ''}`);
259
+ for (const problem of harness.problems)
260
+ opts.log?.(`⚠ skills · ${skills.dir}: not loaded — ${problem}`);
261
+ return { instruction: `${instruction.trimEnd()}\n\n${harness.instruction}`, tools: [...tools, harness.toolset] };
262
+ }
263
+ /** True when a turn running this agent directly may pause for a person (ADR 0028). */
264
+ export function agentGates(agent) {
265
+ return !!agent?.require_approval?.length || agent?.skills?.scripts === 'local';
266
+ }
135
267
  /**
136
268
  * Builds ONE runnable agent from a subagent entry. A `yaml_reference` entry
137
269
  * compiles the nested syndicate's whole graph under this entry's name and
@@ -154,18 +286,21 @@ export async function compileSubagent(subCfg, opts = {}) {
154
286
  }
155
287
  return compileGraph(nested, opts, subCfg.name, subCfg.description);
156
288
  }
157
- const tools = gateTools(await resolveAgentTools(subCfg.tools, subCfg.mcp_server_url, opts), subCfg.require_approval, subCfg.name);
289
+ const gated = gateTools(await resolveAgentTools(subCfg.tools, subCfg.mcp_server_url, opts, subCfg.openapi, subCfg.examples, subCfg.mcp_tools), subCfg.require_approval, subCfg.name);
290
+ const { instruction, tools } = await withSkills(subCfg.instruction ?? '', gated, subCfg.skills, subCfg.name, opts);
158
291
  const resolveModel = opts.resolveModel ?? ((m) => m);
159
292
  logCapabilities(opts, subCfg.name, subCfg.model, subCfg.tools);
160
293
  return new LlmAgent({
161
294
  name: subCfg.name,
162
295
  description: subCfg.description,
163
- model: resolveModel(subCfg.model),
164
- instruction: subCfg.instruction,
296
+ model: withFallback(resolveModel(subCfg.model), subCfg.fallback_model, resolveModel, opts),
297
+ instruction,
165
298
  tools: tools.length > 0 ? tools : undefined,
166
299
  outputSchema: subCfg.outputSchema,
167
300
  generateContentConfig: withServerSideToolInvocations(subCfg.generateContentConfig),
168
301
  ...passthroughFields(subCfg),
302
+ ...executionFields(subCfg, opts),
303
+ ...(opts.nodeConfig?.(subCfg.name) ?? {}),
169
304
  });
170
305
  }
171
306
  /**
@@ -178,6 +313,14 @@ export async function compileGraph(config, opts = {}, overrideName, overrideDesc
178
313
  // A registry definition carries its nested syndicates (ADR 0018 item 6).
179
314
  if (config.bundled_references && !opts.loadNested)
180
315
  opts = { ...opts, loadNested: nestedLoader(config) };
316
+ // A graph has no orchestrator-with-tools shape to build: its agents are
317
+ // nodes (lib/workflow.ts). Nested as a yaml_reference, only its
318
+ // orchestrator runs, since ADK cannot yet make a Workflow a subagent.
319
+ if (config.workflow && !overrideName) {
320
+ throw new Error(`${config.syndicate_name}: a workflow syndicate is compiled with compileWorkflow (lib/workflow.ts), not compileGraph`);
321
+ }
322
+ if (config.workflow)
323
+ opts.log?.(`${config.syndicate_name}: nested as a subagent, so only its orchestrator runs (a Workflow cannot be a subagent yet)`);
181
324
  const compiledTools = isDispatchSyndicate(config)
182
325
  ? []
183
326
  : await Promise.all((config.subagents ?? []).map(async (subCfg) => {
@@ -190,17 +333,19 @@ export async function compileGraph(config, opts = {}, overrideName, overrideDesc
190
333
  // Orchestrator tools are registry names only — no entrypoint has ever
191
334
  // attached an MCP server to an orchestrator, and this compiler preserves
192
335
  // 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));
336
+ compiledTools.push(...gateTools(await resolveAgentTools(config.orchestrator.tools, undefined, opts, config.orchestrator.openapi, config.orchestrator.examples), config.orchestrator.require_approval, overrideName || config.orchestrator.name));
337
+ const { instruction, tools: orchestratorTools } = await withSkills(config.orchestrator.instruction, compiledTools, config.orchestrator.skills, overrideName || config.orchestrator.name, opts);
194
338
  const resolveModel = opts.resolveModel ?? ((m) => m);
195
339
  logCapabilities(opts, overrideName || config.orchestrator.name, config.orchestrator.model, config.orchestrator.tools);
196
340
  return new LlmAgent({
197
341
  name: overrideName || config.orchestrator.name,
198
342
  description: overrideDescription || config.orchestrator.description,
199
- model: resolveModel(config.orchestrator.model),
200
- instruction: config.orchestrator.instruction,
201
- tools: compiledTools.length > 0 ? compiledTools : undefined,
343
+ model: withFallback(resolveModel(config.orchestrator.model), config.orchestrator.fallback_model, resolveModel, opts),
344
+ instruction,
345
+ tools: orchestratorTools.length > 0 ? orchestratorTools : undefined,
202
346
  outputSchema: config.orchestrator.outputSchema,
203
347
  generateContentConfig: withServerSideToolInvocations(config.orchestrator.generateContentConfig),
204
348
  ...passthroughFields(config.orchestrator),
349
+ ...executionFields(config.orchestrator, opts),
205
350
  });
206
351
  }
@@ -59,6 +59,30 @@ 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";
79
+ /**
80
+ * Model calls one turn may make, subagents included, when the syndicate sets
81
+ * no `max_steps`. A runaway tool loop stops here instead of at the provider's
82
+ * bill. Every shipped example that sets a cap sets 12 to 30; a syndicate that
83
+ * needs more raises its own `max_steps` (ADR 0039).
84
+ */
85
+ export declare const DEFAULT_MAX_STEPS = 50;
62
86
  /**
63
87
  * Default open-weight model, served locally by Ollama (lib/models/ollamaLlm.ts).
64
88
  * qwen3:8b is the smallest pulled model that supports tool calling — the
@@ -60,6 +60,30 @@ 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';
80
+ /**
81
+ * Model calls one turn may make, subagents included, when the syndicate sets
82
+ * no `max_steps`. A runaway tool loop stops here instead of at the provider's
83
+ * bill. Every shipped example that sets a cap sets 12 to 30; a syndicate that
84
+ * needs more raises its own `max_steps` (ADR 0039).
85
+ */
86
+ export const DEFAULT_MAX_STEPS = 50;
63
87
  /**
64
88
  * Default open-weight model, served locally by Ollama (lib/models/ollamaLlm.ts).
65
89
  * 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';