melchizedek-agents 1.0.3 → 1.2.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 +29 -2
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +524 -0
- package/DOCUMENTATION.md +178 -43
- package/ONBOARDING.md +339 -0
- package/QUICKSTART.md +15 -2
- package/README.md +6 -2
- package/config/agents/README.md +2 -2
- package/config/agents/examples/ares.yaml +6 -10
- package/config/agents/examples/assistant.yaml +6 -6
- package/config/agents/examples/augustin.yaml +9 -15
- package/config/agents/examples/cartographers.yaml +6 -6
- package/config/agents/examples/council.yaml +7 -7
- package/config/agents/examples/critic.yaml +24 -24
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +37 -35
- package/config/agents/examples/librarian.yaml +4 -4
- package/config/agents/examples/patient_advocate.yaml +6 -10
- package/config/agents/examples/pipeline.yaml +20 -19
- package/config/agents/examples/research.yaml +36 -43
- package/config/agents/examples/scribe.yaml +31 -32
- package/config/agents/examples/scriptorium.yaml +6 -6
- package/config/agents/examples/structured_critic.yaml +82 -0
- package/config/agents/examples/style_council.yaml +9 -11
- package/config/agents/examples/syndicate.yaml +3 -5
- package/config/agents/examples/tutor.yaml +2 -2
- package/config/agents/examples/weather.yaml +2 -2
- package/config/agents/syndicate.schema.json +859 -13
- package/config/agents/syndicateSchema.yaml +122 -73
- package/config/agents/templates/README.md +1 -1
- package/config/agents/templates/account_memory.yaml +4 -4
- package/config/agents/templates/case_desk.yaml +6 -6
- package/config/agents/templates/conversational.yaml +2 -2
- package/config/agents/templates/draft_review.yaml +6 -6
- package/config/agents/templates/intake_extractor.yaml +64 -62
- package/config/agents/templates/research_brief.yaml +6 -6
- package/config/agents/templates/research_desk.yaml +6 -6
- package/config/agents/templates/review_panel.yaml +8 -8
- package/config/agents/templates/support_triage.yaml +30 -29
- package/config/agents/templates/systems_operator.yaml +24 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +64 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +43 -5
- package/dist/lib/a2a/oauthSetup.d.ts +106 -0
- package/dist/lib/a2a/oauthSetup.js +259 -0
- package/dist/lib/agentDialect.d.ts +76 -0
- package/dist/lib/agentDialect.js +151 -0
- package/dist/lib/compile.d.ts +42 -6
- package/dist/lib/compile.js +97 -45
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +20 -1
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +110 -1
- package/dist/lib/doctor.js +205 -9
- package/dist/lib/loadSyndicate.d.ts +54 -2
- package/dist/lib/loadSyndicate.js +22 -2
- package/dist/lib/memory/namespace.d.ts +6 -5
- package/dist/lib/memory/namespace.js +6 -5
- package/dist/lib/memory/providers.js +3 -4
- package/dist/lib/models/capabilities.d.ts +25 -1
- package/dist/lib/models/capabilities.js +79 -1
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +2 -1
- package/dist/lib/models/gateway.js +4 -2
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/onboarding.d.ts +134 -0
- package/dist/lib/onboarding.js +539 -0
- package/dist/lib/runtime/approvals.d.ts +8 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
- package/dist/lib/runtime/native/agentLoop.js +107 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
- package/dist/lib/runtime/native/checkpoint.js +359 -0
- package/dist/lib/runtime/native/delegate.d.ts +222 -18
- package/dist/lib/runtime/native/delegate.js +390 -39
- package/dist/lib/runtime/native/interrupts.d.ts +184 -2
- package/dist/lib/runtime/native/interrupts.js +391 -1
- package/dist/lib/runtime/native/request.d.ts +13 -3
- package/dist/lib/runtime/native/request.js +28 -4
- package/dist/lib/runtime/native/telemetry.js +10 -15
- package/dist/lib/runtime/questions.d.ts +7 -4
- package/dist/lib/runtime/sessions.d.ts +1 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
- package/dist/lib/runtime/syndicateTurn.js +353 -68
- package/dist/lib/runtime/turnControl.d.ts +56 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +12 -2
- package/dist/lib/storage/postgres/index.js +8 -3
- package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
- package/dist/lib/storage/postgres/taskQueue.js +29 -5
- package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
- package/dist/lib/storage/postgres/taskStore.js +74 -8
- package/dist/lib/syndicateSchema.d.ts +316 -1
- package/dist/lib/syndicateSchema.js +203 -29
- package/dist/lib/tools/auth.d.ts +20 -6
- package/dist/lib/tools/auth.js +21 -5
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/credentialUses.d.ts +92 -0
- package/dist/lib/tools/credentialUses.js +112 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthConsent.js +8 -1
- package/dist/lib/tools/oauthHosts.d.ts +73 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +242 -0
- package/dist/lib/tools/oauthTools.js +447 -0
- package/dist/lib/tools/openapiTools.d.ts +11 -6
- package/dist/lib/tools/openapiTools.js +88 -38
- package/dist/lib/tools/taskTools.d.ts +61 -6
- package/dist/lib/tools/taskTools.js +145 -21
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +14 -7
- package/dist/lib/workflow.js +14 -7
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +131 -1
- package/dist/scripts/assistant_worker.d.ts +12 -0
- package/dist/scripts/assistant_worker.js +81 -22
- package/dist/scripts/doctor.d.ts +7 -2
- package/dist/scripts/doctor.js +9 -4
- package/dist/scripts/setup.d.ts +23 -0
- package/dist/scripts/setup.js +154 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/yaml_codemod.d.ts +62 -0
- package/dist/scripts/yaml_codemod.js +652 -0
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +7 -4
- package/skills/briefs/melchizedek.md +1 -0
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek/SKILL.md +1 -0
- package/skills/melchizedek-author/SKILL.md +7 -4
- package/skills/melchizedek-author/assets/minimal.yaml +13 -13
- package/skills/melchizedek-models/SKILL.md +11 -4
- package/skills/melchizedek-onboard/SKILL.md +49 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -13,12 +13,21 @@
|
|
|
13
13
|
*
|
|
14
14
|
* ONE CALL, AS ADK'S AgentTool RAN IT:
|
|
15
15
|
* 1. The message: `{ role: 'user', parts: [{ text: args.request }] }`.
|
|
16
|
-
* 2. The subagent's session:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
16
|
+
* 2. The subagent's session: filed under the agent path (childAppName,
|
|
17
|
+
* ADR 0111), with the caller's user id and session id: the caller's
|
|
18
|
+
* app name, the caller's name and the subagent's at the top
|
|
19
|
+
* (`<app>/<caller>/<subagent>`), the caller's app name and the
|
|
20
|
+
* subagent's below it (`<app>/<caller>/<subagent>/<inner>`). It is
|
|
21
|
+
* created on the first call, with the caller's state as it stands (the
|
|
22
|
+
* session's, then this call's writes; `temp:` keys dropped), and kept:
|
|
23
|
+
* a later call, in this turn or another, continues it. This is not a
|
|
24
|
+
* branch of the caller's session; ADK's AgentTool runs a Runner of its
|
|
25
|
+
* own. ADK filed it under the subagent's name alone, so two syndicates
|
|
26
|
+
* with a same-named subagent on one conversation shared it. A session
|
|
27
|
+
* stored under that old key is still read (legacyChild): when no
|
|
28
|
+
* session exists under the path and the caller has called the
|
|
29
|
+
* subagent before (or is resuming a pause found there), the old one is
|
|
30
|
+
* continued, so a stored conversation resumes where it stopped.
|
|
22
31
|
* 3. A turn already stopped answers an empty text, before or after the
|
|
23
32
|
* message is stored.
|
|
24
33
|
* 4. The message is stored as a user event under a fresh invocation id
|
|
@@ -34,13 +43,34 @@
|
|
|
34
43
|
* schema it is parsed as JSON; text that does not parse fails the call
|
|
35
44
|
* with the parser's message, as on ADK.
|
|
36
45
|
*
|
|
37
|
-
* A PAUSE INSIDE A SUBAGENT (an ask_user call, an approval request
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* A PAUSE INSIDE A SUBAGENT (an ask_user call, an approval request, or a
|
|
47
|
+
* pause inside its own delegated call) reaches the caller (ADR 0110, which
|
|
48
|
+
* lifts ADR 0028's refusal). The child run ends paused; the call resolves to
|
|
49
|
+
* a SubagentPause naming the ids it waits on, and the caller stores no
|
|
50
|
+
* response to it: the call stays open, and the caller's run ends paused too
|
|
51
|
+
* (agentLoop.ts). The turn finds the pause by walking down the open calls
|
|
52
|
+
* (interrupts.ts delegatedPauses) and reports it with the agent path. The
|
|
53
|
+
* answer comes back as the caller's next user message; the caller resumes
|
|
54
|
+
* the open call (resumeSubagent), which stores the same answer parts as
|
|
55
|
+
* the child's next message, under a fresh invocation id, and runs the
|
|
56
|
+
* child's loop on: it reads the answer from its own session as a top-level
|
|
57
|
+
* agent reads it (an approval runs or refuses its pinned call, a question's
|
|
58
|
+
* answer is the call's response), finishes, and its answer is the open
|
|
59
|
+
* call's response. A child that pauses again leaves the call open again.
|
|
60
|
+
* A skill script's approval is the same pause, and so is an OAuth consent
|
|
61
|
+
* request (ADR 0118): the child gets the caller's consent step, its flows
|
|
62
|
+
* bound to the caller's app (consentPinnedTo), and a grant's answer travels
|
|
63
|
+
* down the open call as a decision does.
|
|
41
64
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
65
|
+
* CALLS TO SUBAGENTS IN ONE STEP RUN CONCURRENTLY (ADR 0116), under the
|
|
66
|
+
* caller's `max_concurrency` (the syndicate's root key, default
|
|
67
|
+
* DEFAULT_MAX_CONCURRENCY): agentLoop.ts runs each delegated call through
|
|
68
|
+
* the step's DelegationGate, which starts them in call order as slots free
|
|
69
|
+
* up, and runs two calls to the same subagent (one child session) one after
|
|
70
|
+
* the other. The step's responses are stored once every call has answered,
|
|
71
|
+
* in call order, whatever order the children finished in; a child that
|
|
72
|
+
* pauses leaves its call open while the others finish and are stored. ADK
|
|
73
|
+
* ran them one after another; `max_concurrency: 1` keeps that order.
|
|
44
74
|
*
|
|
45
75
|
* NESTED SYNDICATES: a `yaml_reference` subagent is the nested syndicate's
|
|
46
76
|
* orchestrator, named as the entry, listing its own subagent tools. It runs
|
|
@@ -49,6 +79,13 @@
|
|
|
49
79
|
*
|
|
50
80
|
* A NESTED WORKFLOW (ADR 0098): a `yaml_reference` to a workflow syndicate
|
|
51
81
|
* is a workflowSubagentTool, holding the whole graph as a WorkflowSubagent.
|
|
82
|
+
* A walk that ends paused (an ask_user node's question, an agent node's
|
|
83
|
+
* approval request) leaves the call open as a child loop's pause does
|
|
84
|
+
* (ADR 0111): the call resolves to a SubagentPause naming the walk's open
|
|
85
|
+
* interrupts, and resumeWorkflowSubagent walks the graph again on the
|
|
86
|
+
* answer, which the walk's own resume (lib/workflow/resume.ts) reads as it
|
|
87
|
+
* reads a top-level workflow's. An `ask_user` answer reaches the walk as
|
|
88
|
+
* ADK's explicit reply to its `adk_request_input` call.
|
|
52
89
|
* A call runs as steps 1 to 6 say, with the walk in place of the loop: the
|
|
53
90
|
* graph's walk (lib/workflow/turn.ts, runNativeWorkflow, which
|
|
54
91
|
* lib/compileNative.ts hands over) stores the message itself and yields the
|
|
@@ -69,7 +106,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
69
106
|
import { resolveAdapter } from '../../models/registry.js';
|
|
70
107
|
import { createTurnEvent } from '../events.js';
|
|
71
108
|
import { TEMP_STATE_PREFIX } from '../sessions.js';
|
|
109
|
+
import { REQUEST_INPUT_CALL } from './history.js';
|
|
72
110
|
import { SelfCorrection } from './selfCorrection.js';
|
|
111
|
+
import { currentTurnControl } from '../turnControl.js';
|
|
73
112
|
// ── The subagent tool ────────────────────────────────────────────────────────
|
|
74
113
|
/** Where a subagent tool keeps the agent it runs. A global symbol, so a second copy of this module still finds it. */
|
|
75
114
|
export const SUBAGENT = Symbol.for('melchizedek.subagent');
|
|
@@ -128,13 +167,91 @@ export function subagentOf(tool) {
|
|
|
128
167
|
return agent;
|
|
129
168
|
return undefined;
|
|
130
169
|
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
// ── Running one call ─────────────────────────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* A delegated call whose child run ended paused (ADR 0110): the ids the
|
|
173
|
+
* child waits on. The caller stores no response to the call and ends its
|
|
174
|
+
* own run paused.
|
|
175
|
+
*/
|
|
176
|
+
export class SubagentPause {
|
|
177
|
+
pending;
|
|
178
|
+
constructor(pending) {
|
|
179
|
+
this.pending = pending;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Delegated calls one step runs at once when the syndicate sets no `max_concurrency` (ADR 0116). */
|
|
183
|
+
export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
184
|
+
/**
|
|
185
|
+
* The delegated calls of one step (ADR 0116): at most `limit` run at once,
|
|
186
|
+
* started in the order they entered (agentLoop.ts enters them in call
|
|
187
|
+
* order, synchronously, before any of them runs), and calls sharing a key
|
|
188
|
+
* (the subagent's name, so one child session) run one after another in
|
|
189
|
+
* that order. A slot is held from the call's start to its release, which
|
|
190
|
+
* comes as soon as the child's run ends: what the step does after (self-
|
|
191
|
+
* correction's bookkeeping, which waits on earlier calls) holds no slot. A
|
|
192
|
+
* call waiting on an earlier same-key call holds its slot meanwhile: the
|
|
193
|
+
* earlier call already holds one, so the wait always ends.
|
|
194
|
+
*/
|
|
195
|
+
export class DelegationGate {
|
|
196
|
+
limit;
|
|
197
|
+
active = 0;
|
|
198
|
+
waiting = [];
|
|
199
|
+
lanes = new Map();
|
|
200
|
+
constructor(limit = DEFAULT_MAX_CONCURRENCY) {
|
|
201
|
+
this.limit = Number.isInteger(limit) && limit > 0 ? limit : DEFAULT_MAX_CONCURRENCY;
|
|
202
|
+
}
|
|
203
|
+
/** Enters one call under `key`, behind every call entered before it. */
|
|
204
|
+
enter(key) {
|
|
205
|
+
let granted = false;
|
|
206
|
+
let released = false;
|
|
207
|
+
let freeLane;
|
|
208
|
+
const laneDone = new Promise((resolve) => (freeLane = resolve));
|
|
209
|
+
const slot = new Promise((resolve) => {
|
|
210
|
+
this.waiting.push(() => {
|
|
211
|
+
granted = true;
|
|
212
|
+
resolve();
|
|
213
|
+
});
|
|
214
|
+
this.pump();
|
|
215
|
+
});
|
|
216
|
+
const before = this.lanes.get(key);
|
|
217
|
+
this.lanes.set(key, laneDone);
|
|
218
|
+
const ready = Promise.all([slot, before]).then(() => undefined);
|
|
219
|
+
const release = () => {
|
|
220
|
+
if (released)
|
|
221
|
+
return;
|
|
222
|
+
released = true;
|
|
223
|
+
freeLane();
|
|
224
|
+
if (granted) {
|
|
225
|
+
this.active -= 1;
|
|
226
|
+
this.pump();
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Never started (a step that failed before reaching the child): the slot it would have taken is not held, and it never will be.
|
|
230
|
+
void slot.then(() => {
|
|
231
|
+
this.active -= 1;
|
|
232
|
+
this.pump();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
return { ready, release };
|
|
237
|
+
}
|
|
238
|
+
/** Runs `run` under a ticket for `key`: once ready, the ticket released when it settles. */
|
|
239
|
+
async run(key, run) {
|
|
240
|
+
const ticket = this.enter(key);
|
|
241
|
+
try {
|
|
242
|
+
await ticket.ready;
|
|
243
|
+
return await run();
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
ticket.release();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
pump() {
|
|
250
|
+
while (this.active < this.limit && this.waiting.length > 0) {
|
|
251
|
+
this.active += 1;
|
|
252
|
+
this.waiting.shift()();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
138
255
|
}
|
|
139
256
|
/** ADK's AgentTool answer: the last event's non-thought text, joined by a newline; parsed under an output schema. */
|
|
140
257
|
function answerOf(agent, last) {
|
|
@@ -148,11 +265,107 @@ function answerOf(agent, last) {
|
|
|
148
265
|
.join('\n');
|
|
149
266
|
return agent.outputSchema ? JSON.parse(text) : text;
|
|
150
267
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
268
|
+
/**
|
|
269
|
+
* The app name a delegated call's child session is filed under (ADR 0111):
|
|
270
|
+
* the agent path. Below a top-level agent, its app name, its name and the
|
|
271
|
+
* subagent's (`<app>/<caller>/<subagent>`); below a delegated subagent,
|
|
272
|
+
* whose own session is already filed under its path, that app name and the
|
|
273
|
+
* subagent's. Agent names are identifiers (lib/syndicateSchema.ts), so
|
|
274
|
+
* never hold a `/`.
|
|
275
|
+
*/
|
|
276
|
+
export function childAppName(parent, caller, name) {
|
|
277
|
+
return parent.delegated ? `${parent.appName}/${name}` : `${parent.appName}/${caller}/${name}`;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The app name a nested syndicate run as a dispatch route or a workflow
|
|
281
|
+
* node is filed under (ADR 0119, ADR 0120): the session that runs it, and
|
|
282
|
+
* the entry's name behind its kind (`<app>/route:<route>`,
|
|
283
|
+
* `<walk's app>/node:<node>`). Agent names are identifiers, so a `:` never
|
|
284
|
+
* occurs in one, and a delegated call's key (childAppName: names only)
|
|
285
|
+
* never equals a route's or a node's: a delegate orchestrator `X` calling
|
|
286
|
+
* `Y` files `<app>/X/Y`, a route `X` with a nested node `Y` files
|
|
287
|
+
* `<app>/route:X/node:Y`. F3 (ADR 0119) filed it under `<app>/<entry>`
|
|
288
|
+
* (entryAppNameF3) and ADR 0106 under the entry's name alone; entrySession
|
|
289
|
+
* and the pause walk still read both.
|
|
290
|
+
*/
|
|
291
|
+
export function entryAppName(parentAppName, name, kind) {
|
|
292
|
+
return `${parentAppName}/${kind}:${name}`;
|
|
293
|
+
}
|
|
294
|
+
/** Where ADR 0119 filed a route's or a node's session before the kind segment (ADR 0120): `<app>/<entry>`. */
|
|
295
|
+
export function entryAppNameF3(parentAppName, name) {
|
|
296
|
+
return `${parentAppName}/${name}`;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The session a delegated call ran in before ADR 0111, filed under the
|
|
300
|
+
* subagent's name alone, when it is the one to continue (`continues`): the
|
|
301
|
+
* caller of a stored conversation called the subagent there before, or a
|
|
302
|
+
* pause was found waiting in it. Read only when no session exists under the
|
|
303
|
+
* agent path.
|
|
304
|
+
*/
|
|
305
|
+
export async function legacyChild(sessions, key, name, continues) {
|
|
306
|
+
if (!continues)
|
|
307
|
+
return undefined;
|
|
308
|
+
return sessions.get({ appName: name, userId: key.userId, sessionId: key.sessionId });
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The session a route's or a node's entry ran in before the kind segment
|
|
312
|
+
* (ADR 0120), when it is the one to continue (`continues`): the one F3
|
|
313
|
+
* filed under `<app>/<entry>`, else the one ADR 0106 filed under the
|
|
314
|
+
* entry's name alone. Read only when none exists under entryAppName.
|
|
315
|
+
*/
|
|
316
|
+
export async function legacyEntry(sessions, key, name, continues) {
|
|
317
|
+
if (!continues)
|
|
318
|
+
return undefined;
|
|
319
|
+
return (await sessions.get({ appName: entryAppNameF3(key.appName, name), userId: key.userId, sessionId: key.sessionId })) ?? (await legacyChild(sessions, key, name, true));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* The child session a nested syndicate run as a dispatch route or a
|
|
323
|
+
* workflow node runs on (ADR 0119, ADR 0120): the one filed under
|
|
324
|
+
* entryAppName, else, when the entry ran in `parent` before (an event it
|
|
325
|
+
* authored there) or a pause was found waiting below it (`resuming`), the
|
|
326
|
+
* one filed under an older key (legacyEntry), else a new one from `state`.
|
|
327
|
+
*/
|
|
328
|
+
export async function entrySession(sessions, parent, name, kind, state, resuming = false) {
|
|
329
|
+
const key = { appName: entryAppName(parent.appName, name, kind), userId: parent.userId, sessionId: parent.sessionId };
|
|
330
|
+
const own = await sessions.get(key);
|
|
331
|
+
if (own)
|
|
332
|
+
return own;
|
|
333
|
+
const legacy = await legacyEntry(sessions, parent, name, resuming || parent.events.some((e) => e.author === name));
|
|
334
|
+
return legacy ?? (await sessions.create({ ...key, state }));
|
|
335
|
+
}
|
|
336
|
+
/** Whether `caller` called `name` in `events` in a call other than `callId`: the subagent ran for it before. */
|
|
337
|
+
function calledBefore(events, caller, name, callId) {
|
|
338
|
+
return events.some((e) => e.author === caller && (e.content?.parts ?? []).some((p) => p.functionCall?.name === name && p.functionCall.id !== callId));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The child's session, as ADK's AgentTool gets or creates it: filed under
|
|
342
|
+
* the agent path (childAppName), from the caller's state the first time. A
|
|
343
|
+
* session under the old key is continued when the caller called the
|
|
344
|
+
* subagent before, or when `resuming` a pause found there (legacyChild):
|
|
345
|
+
* a syndicate that never called it, another syndicate's caller included,
|
|
346
|
+
* starts its own.
|
|
347
|
+
*/
|
|
348
|
+
async function childSession(name, context, scope, resuming) {
|
|
153
349
|
const { sessions, session: parent } = scope.ctx;
|
|
154
|
-
const key = { appName: name, userId: parent.userId, sessionId: parent.id };
|
|
155
|
-
|
|
350
|
+
const key = { appName: childAppName({ appName: parent.appName, delegated: scope.ctx.delegated === true }, scope.caller, name), userId: parent.userId, sessionId: parent.id };
|
|
351
|
+
const own = await sessions.get(key);
|
|
352
|
+
if (own)
|
|
353
|
+
return own;
|
|
354
|
+
const legacy = await legacyChild(sessions, key, name, resuming || calledBefore(parent.events, scope.caller, name, context.functionCallId));
|
|
355
|
+
return legacy ?? (await sessions.create({ ...key, state: { ...scope.stateBase, ...context.stateDelta } }));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The consent step as a child run reaches it (ADR 0118): a flow it begins
|
|
359
|
+
* is bound to `appName`, the caller's app, so the callback stores the grant
|
|
360
|
+
* under the app the run's pinned credentials read (the root's), never under
|
|
361
|
+
* the child session's agent path. A grandchild pins its caller's pinned
|
|
362
|
+
* step again; the first pin is applied last, so the root's app wins.
|
|
363
|
+
*/
|
|
364
|
+
export function consentPinnedTo(consent, appName) {
|
|
365
|
+
return {
|
|
366
|
+
has: (provider) => consent.has(provider),
|
|
367
|
+
begin: (binding) => consent.begin({ ...binding, appName }),
|
|
368
|
+
};
|
|
156
369
|
}
|
|
157
370
|
/** Step 5: an event's state writes, `temp:` keys aside, into the call's state delta. */
|
|
158
371
|
function recordState(event, context) {
|
|
@@ -168,13 +381,22 @@ function recordState(event, context) {
|
|
|
168
381
|
* throws, and for an output-schema answer that does not parse.
|
|
169
382
|
*/
|
|
170
383
|
export function runSubagent(agent, args, context, scope) {
|
|
171
|
-
return
|
|
384
|
+
return runChild(agent, { role: 'user', parts: [{ text: args.request }] }, context, scope, false);
|
|
172
385
|
}
|
|
173
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Resumes a delegated call the child left paused (ADR 0110): `answer` (the
|
|
388
|
+
* caller's user message parts that answer what waits below the call) is
|
|
389
|
+
* stored as the child's next message and the child's loop runs on, as
|
|
390
|
+
* runSubagent runs it. Resolves as runSubagent does, a SubagentPause again
|
|
391
|
+
* when the child pauses again.
|
|
392
|
+
*/
|
|
393
|
+
export function resumeSubagent(agent, answer, context, scope) {
|
|
394
|
+
return runChild(agent, { role: 'user', parts: answer }, context, scope, true);
|
|
395
|
+
}
|
|
396
|
+
async function runChild(agent, content, context, scope, resuming) {
|
|
174
397
|
const { ctx, signal } = scope;
|
|
175
398
|
const { sessions } = ctx;
|
|
176
|
-
const
|
|
177
|
-
const session = await childSession(agent.name, context, scope);
|
|
399
|
+
const session = await childSession(agent.name, context, scope, resuming);
|
|
178
400
|
if (signal?.aborted)
|
|
179
401
|
return '';
|
|
180
402
|
const invocationId = `e-${randomUUID()}`;
|
|
@@ -187,24 +409,36 @@ async function runChild(agent, args, context, scope) {
|
|
|
187
409
|
invocationId,
|
|
188
410
|
userContent: content,
|
|
189
411
|
stream: false,
|
|
412
|
+
// Its own delegations are filed below its path (childAppName, ADR 0111).
|
|
413
|
+
delegated: true,
|
|
190
414
|
// ADK's AgentTool builds its sub-runner without the reflect-and-retry
|
|
191
415
|
// plugins, so a subagent's own errors are not retried (ADR 0075).
|
|
192
416
|
selfCorrection: new SelfCorrection({ model_errors: 0, tool_errors: 0 }),
|
|
193
417
|
...(ctx.memory ? { memory: ctx.memory } : {}),
|
|
194
|
-
// The parent's grants, pinned to the root's app (ADR 0072)
|
|
195
|
-
//
|
|
418
|
+
// The parent's grants, pinned to the root's app (ADR 0072), and its consent step: a call that
|
|
419
|
+
// needs a grant pauses the child, and the pause reaches the turn through the open call (ADR 0118).
|
|
196
420
|
...(ctx.credentials ? { credentials: ctx.credentials } : {}),
|
|
421
|
+
...(ctx.consent ? { consent: consentPinnedTo(ctx.consent, ctx.session.appName) } : {}),
|
|
197
422
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
198
423
|
...(ctx.adapterFor ? { adapterFor: ctx.adapterFor } : {}),
|
|
199
424
|
...(ctx.log ? { log: ctx.log } : {}),
|
|
200
425
|
});
|
|
201
426
|
let last;
|
|
202
|
-
for
|
|
427
|
+
for (;;) {
|
|
428
|
+
const next = await loop.next();
|
|
429
|
+
if (next.done) {
|
|
430
|
+
// A child that ended waiting on a person leaves the call open (ADR 0110).
|
|
431
|
+
if (next.value.reason === 'paused' && !signal?.aborted)
|
|
432
|
+
return new SubagentPause(next.value.pending ?? []);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
203
435
|
// ADK's Runner stops yielding once the turn stopped: the answer is the last event it yielded.
|
|
204
|
-
if (signal?.aborted)
|
|
436
|
+
if (signal?.aborted) {
|
|
437
|
+
await loop.return(undefined);
|
|
205
438
|
break;
|
|
206
|
-
|
|
207
|
-
|
|
439
|
+
}
|
|
440
|
+
recordState(next.value, context);
|
|
441
|
+
last = next.value;
|
|
208
442
|
}
|
|
209
443
|
return answerOf(agent, last);
|
|
210
444
|
}
|
|
@@ -215,11 +449,26 @@ async function runChild(agent, args, context, scope) {
|
|
|
215
449
|
* yielded event's text, '' for none. Throws what the walk throws.
|
|
216
450
|
*/
|
|
217
451
|
export function runWorkflowSubagent(workflow, args, context, scope) {
|
|
218
|
-
return
|
|
452
|
+
return walkChild(workflow, [{ text: args.request }], context, scope, false);
|
|
453
|
+
}
|
|
454
|
+
/** The engine's ask_user tool name (lib/runtime/questions.ts ASK_USER). */
|
|
455
|
+
const ASK_USER_CALL = 'ask_user';
|
|
456
|
+
/**
|
|
457
|
+
* Resumes a delegated call whose nested workflow ended paused (ADR 0111):
|
|
458
|
+
* the graph is walked again on `answer` (the caller's user message parts
|
|
459
|
+
* that answer the walk's open interrupt), which the walk's resume reads as
|
|
460
|
+
* it reads a top-level workflow's. An approval decision goes as it came; an
|
|
461
|
+
* `ask_user` answer goes as ADK's explicit reply to the `adk_request_input`
|
|
462
|
+
* call it answers, under that call's name. Resolves as runWorkflowSubagent
|
|
463
|
+
* does, a SubagentPause again when the walk pauses again.
|
|
464
|
+
*/
|
|
465
|
+
export function resumeWorkflowSubagent(workflow, answer, context, scope) {
|
|
466
|
+
const parts = answer.map((p) => (p.functionResponse?.name === ASK_USER_CALL ? { functionResponse: { ...p.functionResponse, name: REQUEST_INPUT_CALL } } : p));
|
|
467
|
+
return walkChild(workflow, parts, context, scope, true);
|
|
219
468
|
}
|
|
220
|
-
async function walkChild(workflow,
|
|
469
|
+
async function walkChild(workflow, userParts, context, scope, resuming) {
|
|
221
470
|
const { ctx, signal } = scope;
|
|
222
|
-
const session = await childSession(workflow.name, context, scope);
|
|
471
|
+
const session = await childSession(workflow.name, context, scope, resuming);
|
|
223
472
|
if (signal?.aborted)
|
|
224
473
|
return '';
|
|
225
474
|
const walk = workflow.walk({
|
|
@@ -227,7 +476,7 @@ async function walkChild(workflow, args, context, scope) {
|
|
|
227
476
|
appName: session.appName,
|
|
228
477
|
userId: session.userId,
|
|
229
478
|
sessionId: session.id,
|
|
230
|
-
userParts
|
|
479
|
+
userParts,
|
|
231
480
|
adapterFor: ctx.adapterFor ?? resolveAdapter,
|
|
232
481
|
// As runChild: ADK's AgentTool runs its sub-runner without the reflect-and-retry plugins (ADR 0075).
|
|
233
482
|
selfCorrection: new SelfCorrection({ model_errors: 0, tool_errors: 0 }),
|
|
@@ -237,9 +486,20 @@ async function walkChild(workflow, args, context, scope) {
|
|
|
237
486
|
...(ctx.credentials ? { credentials: ctx.credentials } : {}),
|
|
238
487
|
});
|
|
239
488
|
let last;
|
|
240
|
-
for
|
|
241
|
-
|
|
489
|
+
for (;;) {
|
|
490
|
+
const next = await walk.next();
|
|
491
|
+
if (next.done) {
|
|
492
|
+
// A walk that ended waiting on a person leaves the call open (ADR 0111).
|
|
493
|
+
const paused = next.value?.run?.interruptIds ?? [];
|
|
494
|
+
if (paused.length > 0 && !signal?.aborted)
|
|
495
|
+
return new SubagentPause(paused);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
if (signal?.aborted) {
|
|
499
|
+
await walk.return(undefined);
|
|
242
500
|
break;
|
|
501
|
+
}
|
|
502
|
+
const event = next.value;
|
|
243
503
|
if (event.partial)
|
|
244
504
|
continue;
|
|
245
505
|
recordState(event, context);
|
|
@@ -247,3 +507,94 @@ async function walkChild(workflow, args, context, scope) {
|
|
|
247
507
|
}
|
|
248
508
|
return answerOf({}, last);
|
|
249
509
|
}
|
|
510
|
+
/** The engine's approval request call (lib/runtime/approvals.ts APPROVAL_REQUEST). */
|
|
511
|
+
const APPROVAL_REQUEST_CALL = 'adk_request_confirmation';
|
|
512
|
+
/**
|
|
513
|
+
* The message a nested dispatch syndicate's turn reads (ADR 0120): the
|
|
514
|
+
* parts as they came, except an answer to a question, which comes down as
|
|
515
|
+
* an `ask_user` reply (a delegated call's resume) or an `adk_request_input`
|
|
516
|
+
* reply (a workflow node's), and goes in as the plain text it was, as a
|
|
517
|
+
* person's answer reaches a turn at the top.
|
|
518
|
+
*/
|
|
519
|
+
function dispatchParts(parts) {
|
|
520
|
+
return parts.map((p) => {
|
|
521
|
+
const r = p.functionResponse;
|
|
522
|
+
if (!r || (r.name !== ASK_USER_CALL && r.name !== REQUEST_INPUT_CALL))
|
|
523
|
+
return p;
|
|
524
|
+
const result = r.response?.result;
|
|
525
|
+
return { text: typeof result === 'string' ? result : JSON.stringify(result ?? '') };
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* The request call a paused nested dispatch syndicate's turn waits on, as a
|
|
530
|
+
* workflow node raises it again on its caller's walk: an approval request
|
|
531
|
+
* pinned to the gated call, or an input request carrying the question.
|
|
532
|
+
* Undefined for a consent request, which a walk cannot raise (ADR 0119).
|
|
533
|
+
*/
|
|
534
|
+
function requestCallOf(end) {
|
|
535
|
+
const id = end.interruptId;
|
|
536
|
+
if (!id)
|
|
537
|
+
return undefined;
|
|
538
|
+
const approval = end.approval;
|
|
539
|
+
if (approval) {
|
|
540
|
+
return {
|
|
541
|
+
id,
|
|
542
|
+
name: APPROVAL_REQUEST_CALL,
|
|
543
|
+
args: { originalFunctionCall: { name: approval.tool ?? '', args: approval.args ?? {}, ...(approval.callId ? { id: approval.callId } : {}) }, toolConfirmation: { confirmed: false } },
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const input = end.input;
|
|
547
|
+
if (input) {
|
|
548
|
+
return {
|
|
549
|
+
id,
|
|
550
|
+
name: REQUEST_INPUT_CALL,
|
|
551
|
+
args: {
|
|
552
|
+
interruptId: id,
|
|
553
|
+
message: input.message ?? '',
|
|
554
|
+
...(input.payload !== undefined ? { payload: input.payload } : {}),
|
|
555
|
+
...(input.schema !== undefined ? { response_schema: input.schema } : {}),
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Runs a nested dispatch syndicate as a walk (ADR 0120): its own turn, on
|
|
563
|
+
* the conversation its caller opened (`run`), through the turn runner the
|
|
564
|
+
* current turn carries (TurnControl.nestedDispatch), under the turn's
|
|
565
|
+
* controls. Completed, it yields one event, never stored, that carries the
|
|
566
|
+
* route's final text and the conversation's state writes: the caller reads
|
|
567
|
+
* the answer and records the writes from it, as from a walk's last event.
|
|
568
|
+
* Paused, it ends with the request's id, and the request call itself for a
|
|
569
|
+
* workflow node to raise again. Failed, it throws the turn's error. A
|
|
570
|
+
* delegated call, a dispatch route and a workflow node all run it.
|
|
571
|
+
*/
|
|
572
|
+
export async function* nestedDispatchWalk(entry, run) {
|
|
573
|
+
const runner = currentTurnControl()?.nestedDispatch;
|
|
574
|
+
if (!runner)
|
|
575
|
+
throw new Error(`'${entry.name}' is a nested dispatch syndicate, which runs only inside a syndicate turn (runSyndicateTurn).`);
|
|
576
|
+
const end = await runner({
|
|
577
|
+
config: entry.config,
|
|
578
|
+
compile: entry.compile,
|
|
579
|
+
name: entry.name,
|
|
580
|
+
sessions: run.sessions,
|
|
581
|
+
appName: run.appName,
|
|
582
|
+
userId: run.userId,
|
|
583
|
+
sessionId: run.sessionId,
|
|
584
|
+
parts: dispatchParts(run.userParts),
|
|
585
|
+
});
|
|
586
|
+
if (end.status === 'canceled' || run.signal?.aborted || currentTurnControl()?.stopReason)
|
|
587
|
+
return { stopped: true };
|
|
588
|
+
if (end.status === 'failed')
|
|
589
|
+
throw new Error(end.error?.message ?? `${entry.name} failed.`);
|
|
590
|
+
if (end.status === 'input-required' && end.interruptId) {
|
|
591
|
+
const request = requestCallOf(end);
|
|
592
|
+
return { run: { interruptIds: [end.interruptId] }, ...(request ? { requests: [request] } : {}) };
|
|
593
|
+
}
|
|
594
|
+
yield createTurnEvent({
|
|
595
|
+
author: entry.name,
|
|
596
|
+
content: { role: 'model', parts: [{ text: end.text }] },
|
|
597
|
+
...(Object.keys(end.stateDelta).length ? { actions: { stateDelta: end.stateDelta } } : {}),
|
|
598
|
+
});
|
|
599
|
+
return { run: { interruptIds: [] } };
|
|
600
|
+
}
|