melchizedek-agents 1.1.0 → 1.3.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 +19 -0
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +373 -0
- package/DOCUMENTATION.md +193 -21
- package/ONBOARDING.md +378 -0
- package/QUICKSTART.md +15 -2
- package/README.md +26 -3
- package/config/agents/examples/connectors.yaml +82 -0
- package/config/agents/examples/council.yaml +1 -1
- package/config/agents/examples/critic.yaml +8 -8
- package/config/agents/examples/image_production.yaml +4 -3
- package/config/agents/syndicate.schema.json +389 -33
- package/config/agents/syndicateSchema.yaml +18 -6
- package/config/agents/templates/intake_extractor.yaml +3 -2
- package/config/agents/templates/systems_operator.yaml +5 -2
- package/dist/lib/a2a/app.d.ts +12 -0
- package/dist/lib/a2a/app.js +25 -11
- package/dist/lib/a2a/executor.js +14 -6
- package/dist/lib/a2a/oauthSetup.d.ts +16 -0
- package/dist/lib/a2a/oauthSetup.js +77 -8
- package/dist/lib/agentDialect.d.ts +18 -8
- package/dist/lib/agentDialect.js +36 -19
- package/dist/lib/chatgpt/adapter.d.ts +50 -0
- package/dist/lib/chatgpt/adapter.js +107 -0
- package/dist/lib/chatgpt/cli.d.ts +23 -0
- package/dist/lib/chatgpt/cli.js +77 -0
- package/dist/lib/chatgpt/oauth.d.ts +103 -0
- package/dist/lib/chatgpt/oauth.js +448 -0
- package/dist/lib/chatgpt/state.d.ts +98 -0
- package/dist/lib/chatgpt/state.js +145 -0
- package/dist/lib/chatgpt/store.d.ts +24 -0
- package/dist/lib/chatgpt/store.js +94 -0
- package/dist/lib/compile.d.ts +38 -7
- package/dist/lib/compile.js +97 -62
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +18 -1
- package/dist/lib/dispatch.js +6 -2
- package/dist/lib/doctor.d.ts +107 -0
- package/dist/lib/doctor.js +217 -7
- package/dist/lib/loadSyndicate.d.ts +15 -0
- package/dist/lib/mcp/server.d.ts +195 -0
- package/dist/lib/mcp/server.js +564 -0
- package/dist/lib/memory/supabaseMemoryService.js +2 -2
- package/dist/lib/models/adapterResolver.js +6 -1
- package/dist/lib/models/capabilities.d.ts +9 -0
- package/dist/lib/models/capabilities.js +24 -0
- 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 +3 -2
- package/dist/lib/models/gateway.js +8 -4
- 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/onboarding.d.ts +135 -0
- package/dist/lib/onboarding.js +578 -0
- package/dist/lib/runtime/approvals.d.ts +2 -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 +5 -2
- package/dist/lib/runtime/native/agentLoop.js +2 -0
- package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
- package/dist/lib/runtime/native/checkpoint.js +13 -19
- package/dist/lib/runtime/native/delegate.d.ts +82 -1
- package/dist/lib/runtime/native/delegate.js +157 -2
- package/dist/lib/runtime/native/interrupts.d.ts +88 -5
- package/dist/lib/runtime/native/interrupts.js +220 -30
- package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
- package/dist/lib/runtime/syndicateTurn.js +324 -77
- package/dist/lib/runtime/turnControl.d.ts +41 -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 +6 -1
- package/dist/lib/storage/postgres/index.js +1 -1
- package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
- package/dist/lib/storage/postgres/taskQueue.js +10 -3
- package/dist/lib/syndicateSchema.d.ts +184 -9
- package/dist/lib/syndicateSchema.js +143 -29
- package/dist/lib/tools/auth.d.ts +13 -1
- package/dist/lib/tools/auth.js +10 -2
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialUses.d.ts +98 -0
- package/dist/lib/tools/credentialUses.js +120 -0
- package/dist/lib/tools/generateImageTool.js +2 -1
- package/dist/lib/tools/inspectImageTool.js +1 -1
- package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
- package/dist/lib/tools/mcpToolFactory.js +72 -26
- package/dist/lib/tools/mcpTransports.d.ts +12 -0
- package/dist/lib/tools/mcpTransports.js +11 -0
- package/dist/lib/tools/oauthConsent.d.ts +18 -3
- package/dist/lib/tools/oauthConsent.js +57 -15
- package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
- package/dist/lib/tools/oauthDiscovery.js +616 -0
- package/dist/lib/tools/oauthHosts.d.ts +2 -0
- package/dist/lib/tools/oauthHosts.js +2 -2
- package/dist/lib/tools/oauthTools.d.ts +54 -13
- package/dist/lib/tools/oauthTools.js +106 -23
- package/dist/lib/tools/openapiTools.d.ts +7 -0
- package/dist/lib/tools/openapiTools.js +49 -3
- package/dist/lib/tools/taskTools.d.ts +32 -2
- package/dist/lib/tools/taskTools.js +93 -33
- package/dist/lib/tools/xApiSearchTool.js +2 -2
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +10 -7
- package/dist/lib/workflow.js +10 -7
- package/dist/scripts/a2a_server.d.ts +2 -2
- package/dist/scripts/a2a_server.js +19 -2
- package/dist/scripts/assistant_worker.d.ts +2 -0
- package/dist/scripts/assistant_worker.js +24 -5
- package/dist/scripts/doctor.d.ts +4 -2
- package/dist/scripts/doctor.js +6 -4
- package/dist/scripts/mcp_server.d.ts +12 -0
- package/dist/scripts/mcp_server.js +345 -0
- package/dist/scripts/setup.d.ts +25 -0
- package/dist/scripts/setup.js +181 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/syndicate_chat.js +12 -3
- package/dist/scripts/yaml_codemod.d.ts +12 -2
- package/dist/scripts/yaml_codemod.js +115 -33
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-models.md +4 -2
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek-models/SKILL.md +6 -2
- package/skills/melchizedek-onboard/SKILL.md +71 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import { randomUUID } from 'node:crypto';
|
|
43
43
|
import { DEFAULT_MAX_STEPS } from '../config.js';
|
|
44
|
-
import { agentGates, compileEntrySpec, compileSpec, compileWorkflowSpec,
|
|
44
|
+
import { agentGates, compileEntrySpec, compileSpec, compileWorkflowSpec, workflowAgentSpecs } from '../compile.js';
|
|
45
45
|
import { compileNative, compileNativeWorkflow, nativeAdapterFor } from '../compileNative.js';
|
|
46
46
|
import { nativeMemory, refuseOnNative, runNativeAgent } from './nativeTurn.js';
|
|
47
47
|
import { createTurnEvent, getFunctionCalls, getFunctionResponses } from './events.js';
|
|
@@ -57,7 +57,7 @@ export { CREDENTIAL_REQUEST, credentialResponsePart, describeConsent, pendingCon
|
|
|
57
57
|
export { chooseRuntime, describeRuntime, runtimeSetting, DEFAULT_RUNTIME, RUNTIMES, RuntimeRemovedError, UnsupportedOnRuntimeError } from './runtimeFlag.js';
|
|
58
58
|
// The session store a turn takes when nothing durable is configured, and the engine's interfaces (ADR 0107).
|
|
59
59
|
export { InProcessSessionService } from './sessions.js';
|
|
60
|
-
import { ROUTE_STEP_SUFFIX,
|
|
60
|
+
import { ROUTE_STEP_SUFFIX, inputRequestFrom, isWorkflowSyndicate } from '../workflow.js';
|
|
61
61
|
export { describeInput } from '../workflow.js';
|
|
62
62
|
import { isDispatchSyndicate, matchRouteOverride, resolveRoute } from '../dispatch.js';
|
|
63
63
|
import { collectGrounding, describeGrounding, newGroundingState, webSourcesLine } from '../grounding.js';
|
|
@@ -70,7 +70,10 @@ import { traceAgentRun } from '../observability/tracer.js';
|
|
|
70
70
|
import { ProjectedSessionService, renderTranscriptDigest } from '../session/transcript.js';
|
|
71
71
|
import { approvalDecisionIn, describeApproval, interruptedTurnStart, pendingApproval } from './approvals.js';
|
|
72
72
|
import { pendingQuestion, questionAnswerPart, questionFrom, turnStartOfCall } from './questions.js';
|
|
73
|
-
import { delegatedPauses } from './native/interrupts.js';
|
|
73
|
+
import { deepestPause, delegatedPauses, nestedWorkflowPause, routePause } from './native/interrupts.js';
|
|
74
|
+
import { consentPinnedTo, entrySession } from './native/delegate.js';
|
|
75
|
+
import { workflowPauseEvent } from '../workflow/pause.js';
|
|
76
|
+
import { INPUT_REQUEST } from '../workflowConfig.js';
|
|
74
77
|
export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.js';
|
|
75
78
|
export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.js';
|
|
76
79
|
import { RemoteA2AAgent, remoteContextId, remoteToolOutput } from '../a2a/remoteAgent.js';
|
|
@@ -83,15 +86,28 @@ const CLASSIFIER_PREAMBLE = `RECENT CONVERSATION, oldest first — context for y
|
|
|
83
86
|
// The defaults live with the loop's self-correction (lib/runtime/native/selfCorrection.ts).
|
|
84
87
|
export { DEFAULT_MODEL_ERROR_RETRIES, DEFAULT_TOOL_ERROR_RETRIES } from './native/selfCorrection.js';
|
|
85
88
|
// ── Draining one agent's stream ──────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The shape of a tool call's arguments for a log line: the key names and the
|
|
91
|
+
* size, never a value. Arguments are model-chosen and routinely carry the
|
|
92
|
+
* person's words, and a server log is not where those belong (the ledger is
|
|
93
|
+
* the deliberate record, with its own retention).
|
|
94
|
+
*/
|
|
95
|
+
function describeToolArgs(args) {
|
|
96
|
+
const keys = args ? Object.keys(args) : [];
|
|
97
|
+
if (!keys.length)
|
|
98
|
+
return 'no args';
|
|
99
|
+
let bytes;
|
|
100
|
+
try {
|
|
101
|
+
bytes = Buffer.byteLength(JSON.stringify(args), 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return `args: ${keys.join(', ')}`;
|
|
105
|
+
}
|
|
106
|
+
return `args: ${keys.join(', ')} (${bytes.toLocaleString()} bytes)`;
|
|
107
|
+
}
|
|
108
|
+
/** A pending question for a log line: who asks, never what. */
|
|
109
|
+
function describeInputForLog(input) {
|
|
110
|
+
return `${input.node} asks a question${input.path?.length ? ` (${input.path.join(' → ')})` : ''}`;
|
|
95
111
|
}
|
|
96
112
|
/**
|
|
97
113
|
* Drains one agent's event stream into a DrainedRun. The answer is the LAST
|
|
@@ -185,7 +201,7 @@ export async function drainAgentStream(stream, opts = {}) {
|
|
|
185
201
|
if (inputRequest) {
|
|
186
202
|
// An ask_user node: the workflow waits for the person (lib/workflow.ts).
|
|
187
203
|
d.inputRequests.push(inputRequest);
|
|
188
|
-
ev.log?.(`⏸ ${
|
|
204
|
+
ev.log?.(`⏸ ${describeInputForLog(inputRequest)}`);
|
|
189
205
|
continue;
|
|
190
206
|
}
|
|
191
207
|
if (!name)
|
|
@@ -194,7 +210,7 @@ export async function drainAgentStream(stream, opts = {}) {
|
|
|
194
210
|
if (opts.subagentNames?.has(name))
|
|
195
211
|
d.delegations.push(name);
|
|
196
212
|
d.toolCalls.push({ agent: e.author ?? 'unknown', name, args });
|
|
197
|
-
ev.log?.(`→ Tool: ${name}
|
|
213
|
+
ev.log?.(`→ Tool: ${name} by ${e.author ?? 'unknown'} — ${describeToolArgs(args)}`);
|
|
198
214
|
if (opts.publishToolStatus)
|
|
199
215
|
ev.onProgress?.(`Invoking tool: ${name}`);
|
|
200
216
|
}
|
|
@@ -281,6 +297,8 @@ export async function runSyndicateTurn(opts) {
|
|
|
281
297
|
// `trace: false`: no span from anything the turn runs, to any sink.
|
|
282
298
|
untraced: opts.trace === false,
|
|
283
299
|
});
|
|
300
|
+
// A nested dispatch syndicate, wherever the turn reaches one, runs as a turn of its own under these controls (ADR 0120).
|
|
301
|
+
control.nestedDispatch = (run) => runNestedDispatch(opts, control, runtime, run);
|
|
284
302
|
try {
|
|
285
303
|
const run = () => runWithTurnControl(control, () => runTurnInner(opts, control, runtime));
|
|
286
304
|
// A span a library opens through OpenTelemetry's own API (not the
|
|
@@ -307,7 +325,50 @@ function nativeWorkflowAgent(spec, opts) {
|
|
|
307
325
|
const { graph, agents, resolveTool, workflows } = compileNativeWorkflow(spec);
|
|
308
326
|
return { runtime: 'native-workflow', graph, agents, adapterFor: nativeAdapterFor(opts, workflowAgentSpecs(spec)), resolveTool, workflows };
|
|
309
327
|
}
|
|
310
|
-
|
|
328
|
+
/**
|
|
329
|
+
* A nested dispatch syndicate's own turn (ADR 0120): runTurnInner on its
|
|
330
|
+
* conversation (`run`, opened by the caller), under the turn's controls,
|
|
331
|
+
* with the turn's store, memory, credentials and log, never streamed, its
|
|
332
|
+
* guards left to the turn at the top (collectGuards already lists them
|
|
333
|
+
* there). Its result is read as a walk's end: the route's final text, the
|
|
334
|
+
* conversation's state writes, and what it waits on, by id.
|
|
335
|
+
*/
|
|
336
|
+
async function runNestedDispatch(opts, control, runtime, run) {
|
|
337
|
+
const sessions = run.sessions;
|
|
338
|
+
const key = { appName: run.appName, userId: run.userId, sessionId: run.sessionId };
|
|
339
|
+
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
340
|
+
const before = new Map(Object.entries(kept((await sessions.get(key))?.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
341
|
+
const config = run.config;
|
|
342
|
+
const trace = opts.trace === false ? false : { ...(opts.trace ?? {}), syndicateName: config.syndicate_name ?? run.name };
|
|
343
|
+
const nested = await runTurnInner({
|
|
344
|
+
config,
|
|
345
|
+
parts: run.parts,
|
|
346
|
+
appName: run.appName,
|
|
347
|
+
userId: run.userId,
|
|
348
|
+
sessionId: run.sessionId,
|
|
349
|
+
sessionService: sessions,
|
|
350
|
+
...(opts.memoryService ? { memoryService: opts.memoryService } : {}),
|
|
351
|
+
compile: run.compile,
|
|
352
|
+
...(opts.toolCredentials ? { toolCredentials: opts.toolCredentials } : {}),
|
|
353
|
+
streaming: false,
|
|
354
|
+
trace,
|
|
355
|
+
events: { ...(opts.events?.log ? { log: opts.events.log } : {}), ...(opts.events?.warn ? { warn: opts.events.warn } : {}) },
|
|
356
|
+
}, control, runtime, { credentialApp: opts.appName });
|
|
357
|
+
const after = kept((await sessions.get(key))?.state);
|
|
358
|
+
const stateDelta = Object.fromEntries(Object.entries(after).filter(([k, v]) => before.get(k) !== JSON.stringify(v)));
|
|
359
|
+
const interruptId = nested.approval?.id ?? nested.input?.id ?? nested.consent?.id;
|
|
360
|
+
return {
|
|
361
|
+
status: nested.status,
|
|
362
|
+
text: nested.text,
|
|
363
|
+
stateDelta,
|
|
364
|
+
...(interruptId ? { interruptId } : {}),
|
|
365
|
+
...(nested.approval ? { approval: nested.approval } : {}),
|
|
366
|
+
...(nested.input ? { input: nested.input } : {}),
|
|
367
|
+
...(nested.consent ? { consent: nested.consent } : {}),
|
|
368
|
+
...(nested.error ? { error: nested.error } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
311
372
|
const { config, appName, userId, sessionId, sessionService } = opts;
|
|
312
373
|
const ev = opts.events ?? {};
|
|
313
374
|
// Nested references load from the definition's bundle when it has one
|
|
@@ -322,11 +383,13 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
322
383
|
const nativeOf = (spec) => ({ runtime: 'native', agent: compileNative(spec), adapterFor: nativeAdapterFor(compileOpts, spec) });
|
|
323
384
|
/** The orchestrator (a DELEGATE root, or a dispatch classifier). */
|
|
324
385
|
const compileRoot = async () => nativeOf(await compileSpec(config, compileOpts));
|
|
325
|
-
/** A dispatch route: one agent,
|
|
386
|
+
/** A dispatch route: one agent, a nested workflow's whole graph (ADR 0106), or a nested dispatch syndicate's own turn (ADR 0120). */
|
|
326
387
|
const compileRoute = async (routeCfg) => {
|
|
327
388
|
const entry = await compileEntrySpec(routeCfg, compileOpts);
|
|
328
389
|
if (entry.kind === 'workflow')
|
|
329
390
|
return { ...nativeWorkflowAgent(entry.workflow, compileOpts), workflow: entry.workflow };
|
|
391
|
+
if (entry.kind === 'dispatch')
|
|
392
|
+
return { dispatch: entry.dispatch };
|
|
330
393
|
return nativeOf(entry.spec);
|
|
331
394
|
};
|
|
332
395
|
// Self-correction (ADR 0075): one per turn, from the YAML's retries:.
|
|
@@ -374,14 +437,27 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
374
437
|
const delegationKey = { sessions: sessionService, userId, sessionId, appName };
|
|
375
438
|
/** The first pause waiting below an open delegated call in this conversation, `author`'s calls only when given. */
|
|
376
439
|
const pauseBelow = async (events, author) => isWorkflowSyndicate(config) ? undefined : (await delegatedPauses(delegationKey, events, author))[0];
|
|
440
|
+
// Pauses inside nested workflows run as a dispatch route or a workflow node (ADR 0119).
|
|
441
|
+
const pauseKey = { ...delegationKey, appName };
|
|
442
|
+
/** A request a workflow's own session holds, raised by a node that is a nested workflow: the one its walk waits on, with the path. */
|
|
443
|
+
const deepen = (pending) => deepestPause(pauseKey, pending);
|
|
444
|
+
/** A dispatch route that is a nested workflow, its walk paused: the conversation's last event is its pause record. */
|
|
445
|
+
const routePaused = isDispatchSyndicate(config) ? await routePause(pauseKey, existing?.events ?? [], subagentNames) : undefined;
|
|
377
446
|
let resumingBelow;
|
|
447
|
+
let resumingRoute;
|
|
378
448
|
let resuming;
|
|
379
449
|
if (decision) {
|
|
380
450
|
resuming = pendingApproval(existing?.events ?? []);
|
|
451
|
+
if (resuming && isWorkflowSyndicate(config))
|
|
452
|
+
resuming = await deepen(resuming);
|
|
381
453
|
if (!resuming) {
|
|
382
454
|
resumingBelow = await pauseBelow(existing?.events ?? []);
|
|
383
455
|
resuming = resumingBelow?.approval;
|
|
384
456
|
}
|
|
457
|
+
if (!resuming && routePaused?.approval) {
|
|
458
|
+
resumingRoute = routePaused;
|
|
459
|
+
resuming = routePaused.approval;
|
|
460
|
+
}
|
|
385
461
|
if (!resuming || resuming.id !== decision.id) {
|
|
386
462
|
result.status = 'failed';
|
|
387
463
|
result.error = { code: 'NO_PENDING_APPROVAL', message: `No approval ${decision.id} is waiting in this conversation.` };
|
|
@@ -394,17 +470,38 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
394
470
|
// While a call waits for an OAuth grant, the next message resumes it once
|
|
395
471
|
// the callback has stored the grant; until then it repeats the request and
|
|
396
472
|
// runs nothing.
|
|
397
|
-
|
|
473
|
+
// A nested dispatch syndicate's turn reads and stores grants under the root's app, as a delegated subagent does (ADR 0118, ADR 0120).
|
|
474
|
+
const credentialApp = nestedTurn?.credentialApp ?? appName;
|
|
475
|
+
const credentialStore = opts.toolCredentials ? pinnedCredentialStore(opts.toolCredentials.store, credentialApp) : undefined;
|
|
476
|
+
const consentStep = opts.toolCredentials?.consent
|
|
477
|
+
? nestedTurn
|
|
478
|
+
? consentPinnedTo(opts.toolCredentials.consent, credentialApp)
|
|
479
|
+
: opts.toolCredentials.consent
|
|
480
|
+
: undefined;
|
|
398
481
|
const consentPause = (pending) => {
|
|
399
482
|
result.status = 'input-required';
|
|
400
483
|
result.consent = pending;
|
|
401
484
|
result.text = `Authorization needed: ${describeConsent(pending)}. Open the authorization link, then send any message to continue.`;
|
|
402
|
-
ev.log?.(`⏸ Authorization needed: ${pending.agent} → ${pending.provider}`);
|
|
485
|
+
ev.log?.(`⏸ Authorization needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.provider}`);
|
|
403
486
|
return finish();
|
|
404
487
|
};
|
|
405
488
|
let granting;
|
|
489
|
+
// A call waiting for a grant inside a delegated subagent (ADR 0118): the grant's answer is stored here, and the open call carries it down.
|
|
490
|
+
let grantingBelow;
|
|
491
|
+
// A dispatch route that is a nested dispatch syndicate, waiting for a grant (ADR 0120): its own turn checks the grant, then resumes or asks again.
|
|
492
|
+
let consentRoute;
|
|
406
493
|
if (!decision && !isWorkflowSyndicate(config)) {
|
|
407
|
-
|
|
494
|
+
let open = pendingConsent(existing?.events ?? []);
|
|
495
|
+
if (!open && config.subagents?.length) {
|
|
496
|
+
const below = await pauseBelow(existing?.events ?? []);
|
|
497
|
+
if (below?.consent) {
|
|
498
|
+
grantingBelow = below;
|
|
499
|
+
open = below.consent;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const routeWaits = !open && !!routePaused?.consent;
|
|
503
|
+
if (routeWaits)
|
|
504
|
+
open = routePaused.consent;
|
|
408
505
|
if (open) {
|
|
409
506
|
if (!credentialStore) {
|
|
410
507
|
result.status = 'failed';
|
|
@@ -413,16 +510,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
413
510
|
}
|
|
414
511
|
let granted = false;
|
|
415
512
|
try {
|
|
416
|
-
granted = !!open.provider && !!(await credentialStore.get({ appName, userId, provider: open.provider }));
|
|
513
|
+
granted = !!open.provider && !!(await credentialStore.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
417
514
|
}
|
|
418
515
|
catch {
|
|
419
516
|
// Expired or unreadable: not granted. The request stands.
|
|
420
517
|
}
|
|
421
518
|
if (!granted)
|
|
422
519
|
return consentPause(open);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
520
|
+
// The route's own turn carries the grant's answer down to its call; the message goes to it as it came.
|
|
521
|
+
if (routeWaits)
|
|
522
|
+
consentRoute = routePaused;
|
|
523
|
+
else {
|
|
524
|
+
granting = open;
|
|
525
|
+
parts = [credentialResponsePart(open.id, open.provider)];
|
|
526
|
+
}
|
|
527
|
+
ev.log?.(`✓ Authorization granted: ${(open.path ?? [open.agent]).join(' → ')} → ${open.provider}`);
|
|
426
528
|
}
|
|
427
529
|
}
|
|
428
530
|
// ── Questions (lib/runtime/questions.ts) ───────────────────────────────────
|
|
@@ -433,12 +535,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
433
535
|
// A question asked inside a delegated subagent (ADR 0110) is answered the same way: the answer is stored here, and the
|
|
434
536
|
// open call that leads to the asker carries it down; `answering` names that call and the agent that made it.
|
|
435
537
|
let answering;
|
|
436
|
-
|
|
538
|
+
/** The parts a paused workflow route's walk resumes on, when the message answers its question: the answer as the explicit reply to its `adk_request_input` call (ADR 0119). */
|
|
539
|
+
let routeAnswer;
|
|
540
|
+
if (!decision && !granting && !consentRoute && !isWorkflowSyndicate(config)) {
|
|
437
541
|
const plainText = parts.length > 0 && parts.every((p) => typeof p.text === 'string');
|
|
438
542
|
const own = plainText ? pendingQuestion(existing?.events ?? []) : undefined;
|
|
439
543
|
const below = plainText && !own ? await pauseBelow(existing?.events ?? []) : undefined;
|
|
440
544
|
const question = own ?? below?.question;
|
|
441
|
-
if (question) {
|
|
545
|
+
if (!question && plainText && routePaused?.question) {
|
|
546
|
+
// The conversation stores the person's words; the route's walk reads them as the reply to the node that asked.
|
|
547
|
+
const waiting = routePaused.question;
|
|
548
|
+
answering = { agent: routePaused.route, id: waiting.id };
|
|
549
|
+
routeAnswer = [{ functionResponse: { id: waiting.id, name: INPUT_REQUEST, response: { result: messageText } } }];
|
|
550
|
+
ev.log?.(`✓ Answer to ${(waiting.path ?? [waiting.node]).join(' → ')}'s question`);
|
|
551
|
+
}
|
|
552
|
+
else if (question) {
|
|
442
553
|
answering = below ? { agent: below.path[0], id: below.callIds[0] } : { agent: question.node, id: question.id };
|
|
443
554
|
parts = [questionAnswerPart(question.id, messageText)];
|
|
444
555
|
ev.log?.(`✓ Answer to ${(question.path ?? [question.node]).join(' → ')}'s question`);
|
|
@@ -447,13 +558,19 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
447
558
|
/** The agent a dispatch turn must resume, and the call its interrupted turn holds. */
|
|
448
559
|
const resumeTarget = resumingBelow
|
|
449
560
|
? { agent: resumingBelow.path[0], id: resumingBelow.callIds[0], why: 'resuming an approval' }
|
|
450
|
-
:
|
|
451
|
-
? { agent:
|
|
452
|
-
:
|
|
453
|
-
? { agent:
|
|
454
|
-
:
|
|
455
|
-
? {
|
|
456
|
-
:
|
|
561
|
+
: resumingRoute
|
|
562
|
+
? { agent: resumingRoute.route, id: resuming.id, why: 'resuming an approval' }
|
|
563
|
+
: consentRoute
|
|
564
|
+
? { agent: consentRoute.route, id: consentRoute.consent.id, why: 'resuming after an authorization' }
|
|
565
|
+
: grantingBelow
|
|
566
|
+
? { agent: grantingBelow.path[0], id: grantingBelow.callIds[0], why: 'resuming after an authorization' }
|
|
567
|
+
: resuming
|
|
568
|
+
? { agent: resuming.agent, id: resuming.id, why: 'resuming an approval' }
|
|
569
|
+
: granting
|
|
570
|
+
? { agent: granting.agent, id: granting.id, why: 'resuming after an authorization' }
|
|
571
|
+
: answering
|
|
572
|
+
? { ...answering, why: 'answering its question' }
|
|
573
|
+
: undefined;
|
|
457
574
|
/** After the answering run: is a call now waiting for a grant? */
|
|
458
575
|
const awaitingConsent = async (agentName) => {
|
|
459
576
|
if (!opts.toolCredentials?.consent)
|
|
@@ -476,7 +593,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
476
593
|
result.status = 'input-required';
|
|
477
594
|
result.input = input;
|
|
478
595
|
result.text = input.message;
|
|
479
|
-
ev.log?.(`⏸ Input needed: ${
|
|
596
|
+
ev.log?.(`⏸ Input needed: ${describeInputForLog(input)}`);
|
|
480
597
|
return finish();
|
|
481
598
|
};
|
|
482
599
|
/** After the answering run: does a pause wait inside a delegated call `agentName` left open (ADR 0110)? */
|
|
@@ -485,11 +602,13 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
485
602
|
const below = await pauseBelow(after?.events ?? [], agentName);
|
|
486
603
|
if (below?.approval)
|
|
487
604
|
return pause(below.approval);
|
|
605
|
+
if (below?.consent)
|
|
606
|
+
return consentPause(below.consent);
|
|
488
607
|
if (below?.question) {
|
|
489
608
|
result.status = 'input-required';
|
|
490
609
|
result.input = below.question;
|
|
491
610
|
result.text = below.question.message;
|
|
492
|
-
ev.log?.(`⏸ Input needed: ${
|
|
611
|
+
ev.log?.(`⏸ Input needed: ${describeInputForLog({ node: below.question.node, path: below.path })}`);
|
|
493
612
|
return finish();
|
|
494
613
|
}
|
|
495
614
|
return undefined;
|
|
@@ -501,6 +620,16 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
501
620
|
ev.log?.(`⏸ Approval needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.tool}`);
|
|
502
621
|
return finish();
|
|
503
622
|
};
|
|
623
|
+
/** The turn waits on what a question asks. */
|
|
624
|
+
const question = (input) => {
|
|
625
|
+
result.status = 'input-required';
|
|
626
|
+
result.input = input;
|
|
627
|
+
result.text = input.message;
|
|
628
|
+
ev.log?.(`⏸ Input needed: ${describeInputForLog(input)}`);
|
|
629
|
+
return finish();
|
|
630
|
+
};
|
|
631
|
+
/** A nested workflow's walk, or a nested dispatch syndicate's turn, paused below a route (ADR 0119, ADR 0120): its approval request, its consent request, else its question. */
|
|
632
|
+
const pausedWalk = (paused) => paused.approval ? pause(paused.approval) : paused.consent ? consentPause(paused.consent) : question(paused.question);
|
|
504
633
|
/** Run ONE agent against ONE session, under the turn's controls. */
|
|
505
634
|
const runAgent = async (params) => {
|
|
506
635
|
let stream;
|
|
@@ -541,7 +670,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
541
670
|
...(selfCorrection ? { selfCorrection } : {}),
|
|
542
671
|
// Tool credentials and the consent step (ADR 0072, ADR 0085); the classifier's lane lists no tools.
|
|
543
672
|
...(credentialStore ? { credentials: credentialStore } : {}),
|
|
544
|
-
...(
|
|
673
|
+
...(consentStep && params.stage !== 'classify' ? { consent: consentStep } : {}),
|
|
545
674
|
...(compileOpts.log ? { log: compileOpts.log } : {}),
|
|
546
675
|
});
|
|
547
676
|
}
|
|
@@ -588,15 +717,27 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
588
717
|
};
|
|
589
718
|
/**
|
|
590
719
|
* A dispatch route that is a workflow syndicate (ADR 0106): its whole graph
|
|
591
|
-
* walked on the child session filed under the
|
|
592
|
-
* userId, sessionId }`,
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* by the
|
|
598
|
-
* the
|
|
599
|
-
*
|
|
720
|
+
* walked on the child session filed under the agent path and its kind
|
|
721
|
+
* (`{ <app>/route:<route>, userId, sessionId }`, ADR 0119, ADR 0120; the
|
|
722
|
+
* one filed under an older key is continued when the route ran in the
|
|
723
|
+
* conversation before, entrySession),
|
|
724
|
+
* created from the conversation's state the first time, `temp:` keys
|
|
725
|
+
* dropped, and kept, as a delegated nested workflow walks (ADR 0098), and
|
|
726
|
+
* drained by the reader a workflow turn uses, so the route's answer is what
|
|
727
|
+
* the workflow would answer as its own syndicate. The conversation stores
|
|
728
|
+
* the message and the answer, one event authored by the route that carries
|
|
729
|
+
* the walk's state writes, so the classifier and the next route read the
|
|
730
|
+
* exchange as they read any route's. A node that gave up fails the turn
|
|
731
|
+
* NODE_FAILED, as a workflow turn does.
|
|
732
|
+
*
|
|
733
|
+
* A walk that ends paused (ADR 0119) stores the route's pause record in
|
|
734
|
+
* place of the answer: authored by the route at its own path, no content,
|
|
735
|
+
* the walk's open interrupts in `longRunningToolIds`, its state writes so
|
|
736
|
+
* far. While it is the conversation's last event the route waits: the
|
|
737
|
+
* turn reports the request or question with the path from the route down
|
|
738
|
+
* to the node that asked (interrupts.ts routePause), a decision or a
|
|
739
|
+
* plain-text answer resumes the route without classifying, and the walk
|
|
740
|
+
* reads the answer as its explicit reply (`routeAnswer`).
|
|
600
741
|
*/
|
|
601
742
|
const runWorkflowRoute = async (route, agent, resolution) => {
|
|
602
743
|
const store = sessionService;
|
|
@@ -605,14 +746,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
605
746
|
if (!shared)
|
|
606
747
|
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
607
748
|
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
608
|
-
|
|
609
|
-
const
|
|
749
|
+
// Filed under the agent path (ADR 0119), else continued under the route's name alone where ADR 0106 filed it.
|
|
750
|
+
const resumingWalk = resumingRoute !== undefined || routeAnswer !== undefined;
|
|
751
|
+
const child = await entrySession(store, { appName, userId, sessionId, events: shared.events }, route, 'route', kept(shared.state), resumingWalk);
|
|
752
|
+
const childKey = { appName: child.appName, userId, sessionId };
|
|
610
753
|
const before = new Map(Object.entries(kept(child.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
611
754
|
const invocationId = `e-${randomUUID()}`;
|
|
612
755
|
await store.append(shared, createTurnEvent({ invocationId, author: 'user', content: { role: 'user', parts } }));
|
|
756
|
+
/** The walk's state writes this turn, `temp:` keys aside: what the conversation's event for the route carries. */
|
|
757
|
+
const written = async () => {
|
|
758
|
+
const after = kept((await store.get(childKey))?.state);
|
|
759
|
+
return Object.fromEntries(Object.entries(after).filter(([k, v]) => before.get(k) !== JSON.stringify(v)));
|
|
760
|
+
};
|
|
613
761
|
let walked;
|
|
614
762
|
try {
|
|
615
|
-
walked = await runAgent({ agent, sid: sessionId, appName:
|
|
763
|
+
walked = await runAgent({ agent, sid: sessionId, appName: child.appName, userParts: routeAnswer ?? parts, sessions: sessionService, stage: 'dispatch', route: resolution, publishToolStatus: true, errorPolicy: 'collect' });
|
|
616
764
|
}
|
|
617
765
|
catch (err) {
|
|
618
766
|
const last = control.stopReason ? undefined : err;
|
|
@@ -623,9 +771,22 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
623
771
|
result.error = { code: last instanceof UnsupportedWorkflowResumeError ? 'RESUME_UNSUPPORTED' : last instanceof NodeRunLimitError ? NODE_RUN_LIMIT : 'NODE_FAILED', message: last.message };
|
|
624
772
|
return { failed: finish() };
|
|
625
773
|
}
|
|
774
|
+
if (!walked.error && !control.stopReason) {
|
|
775
|
+
// A walk that ended paused (ADR 0119): the conversation stores the route's pause record, which names the walk's
|
|
776
|
+
// open interrupts and carries its state writes so far; the next message finds the walk through it.
|
|
777
|
+
const walkEnd = (await store.get(childKey))?.events.at(-1);
|
|
778
|
+
const paused = walkEnd?.author === route && walkEnd.nodeInfo?.path === route && !walkEnd.content?.parts?.length ? await nestedWorkflowPause(pauseKey, route, 'route') : undefined;
|
|
779
|
+
if (paused) {
|
|
780
|
+
const record = workflowPauseEvent({ name: route, invocationId, input: null, interruptIds: [...(walkEnd.longRunningToolIds ?? [])] });
|
|
781
|
+
const stateDelta = await written();
|
|
782
|
+
if (Object.keys(stateDelta).length)
|
|
783
|
+
record.actions.stateDelta = stateDelta;
|
|
784
|
+
await store.append((await store.get(key)) ?? shared, record);
|
|
785
|
+
return { answer: walked, paused };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
626
788
|
if (walked.text && !walked.error && !control.stopReason) {
|
|
627
|
-
const
|
|
628
|
-
const stateDelta = Object.fromEntries(Object.entries(after).filter(([k, v]) => before.get(k) !== JSON.stringify(v)));
|
|
789
|
+
const stateDelta = await written();
|
|
629
790
|
const answered = createTurnEvent({
|
|
630
791
|
invocationId,
|
|
631
792
|
author: route,
|
|
@@ -636,6 +797,77 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
636
797
|
}
|
|
637
798
|
return { answer: walked };
|
|
638
799
|
};
|
|
800
|
+
/**
|
|
801
|
+
* A dispatch route that is a nested dispatch syndicate (ADR 0120): its own
|
|
802
|
+
* turn (runNestedDispatch: its classifier picks one of its routes, the
|
|
803
|
+
* route answers), on the child session filed under the route's kind
|
|
804
|
+
* (`<app>/route:<route>`, entrySession), created from the conversation's
|
|
805
|
+
* state the first time, `temp:` keys dropped, and kept. The conversation
|
|
806
|
+
* stores the message and, as for a workflow route, one event authored by
|
|
807
|
+
* the route: its answer (the inner route's final text) with the state
|
|
808
|
+
* writes, or, when its turn ends paused, the route's pause record naming
|
|
809
|
+
* the one request it waits on. The turn reports that request with the
|
|
810
|
+
* path from the route down (routePause, which follows the record into the
|
|
811
|
+
* nested conversation), and the next message that answers it resumes the
|
|
812
|
+
* route without classifying: the nested turn reads it as a message at the
|
|
813
|
+
* top reads an answer (an approval decision as it came, a question's
|
|
814
|
+
* answer as the person's text, any message once a grant is stored).
|
|
815
|
+
*/
|
|
816
|
+
const runDispatchRoute = async (route, dispatch) => {
|
|
817
|
+
const store = sessionService;
|
|
818
|
+
const key = { appName, userId, sessionId };
|
|
819
|
+
const shared = await store.get(key);
|
|
820
|
+
if (!shared)
|
|
821
|
+
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
822
|
+
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
823
|
+
const resumingTurn = resumingRoute !== undefined || routeAnswer !== undefined || consentRoute !== undefined;
|
|
824
|
+
const child = await entrySession(store, { appName, userId, sessionId, events: shared.events }, route, 'route', kept(shared.state), resumingTurn);
|
|
825
|
+
const invocationId = `e-${randomUUID()}`;
|
|
826
|
+
await store.append(shared, createTurnEvent({ invocationId, author: 'user', content: { role: 'user', parts } }));
|
|
827
|
+
// A question's answer goes in as the person's words; anything else as it came.
|
|
828
|
+
const nestedParts = routeAnswer ? [{ text: messageText }] : parts;
|
|
829
|
+
const end = await runNestedDispatch(opts, control, _runtime, {
|
|
830
|
+
config: dispatch.config,
|
|
831
|
+
compile: dispatch.compile,
|
|
832
|
+
name: route,
|
|
833
|
+
sessions: store,
|
|
834
|
+
appName: child.appName,
|
|
835
|
+
userId,
|
|
836
|
+
sessionId,
|
|
837
|
+
parts: nestedParts,
|
|
838
|
+
});
|
|
839
|
+
if (control.stopReason)
|
|
840
|
+
return { failed: finish() };
|
|
841
|
+
if (end.status === 'failed') {
|
|
842
|
+
result.status = 'failed';
|
|
843
|
+
result.failedStage = 'dispatch';
|
|
844
|
+
result.error = end.error ?? { code: 'ROUTE_FAILED', message: `${route} failed.` };
|
|
845
|
+
return { failed: finish() };
|
|
846
|
+
}
|
|
847
|
+
const delta = Object.keys(end.stateDelta).length ? { actions: { stateDelta: end.stateDelta } } : {};
|
|
848
|
+
const answer = emptyRun();
|
|
849
|
+
answer.agents.add(route);
|
|
850
|
+
if (end.status === 'input-required' && end.interruptId) {
|
|
851
|
+
const record = workflowPauseEvent({ name: route, invocationId, input: null, interruptIds: [end.interruptId] });
|
|
852
|
+
if (delta.actions)
|
|
853
|
+
record.actions.stateDelta = delta.actions.stateDelta;
|
|
854
|
+
await store.append((await store.get(key)) ?? shared, record);
|
|
855
|
+
const stored = (await store.get(key))?.events ?? [];
|
|
856
|
+
const paused = await routePause(pauseKey, stored, subagentNames);
|
|
857
|
+
if (!paused)
|
|
858
|
+
throw new Error(`${route}: its turn paused on ${end.interruptId}, which its conversation does not hold.`);
|
|
859
|
+
return { answer, paused };
|
|
860
|
+
}
|
|
861
|
+
answer.text = end.text;
|
|
862
|
+
if (end.text) {
|
|
863
|
+
await store.append((await store.get(key)) ?? shared, createTurnEvent({ invocationId, author: route, content: { role: 'model', parts: [{ text: end.text }] }, ...delta }));
|
|
864
|
+
}
|
|
865
|
+
return { answer };
|
|
866
|
+
};
|
|
867
|
+
// While a route's walk waits on a gated call (ADR 0119), a message that is not its decision repeats the request: the
|
|
868
|
+
// walk cannot move past the waiting node, so nothing is stored and nothing runs, as a workflow turn answers it.
|
|
869
|
+
if (!decision && routePaused?.approval)
|
|
870
|
+
return pause(routePaused.approval);
|
|
639
871
|
let answer;
|
|
640
872
|
if (isDispatchSyndicate(config)) {
|
|
641
873
|
// ══ PLAN ═════════════════════════════════════════════════════════════
|
|
@@ -651,7 +883,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
651
883
|
throw new Error(`Call ${resumeTarget.id} was raised by '${resumeTarget.agent}', which is not a route of this syndicate.`);
|
|
652
884
|
}
|
|
653
885
|
resolution = { route: resumeTarget.agent, reason: resumeTarget.why, fellBack: false, fallbackReason: '', viaOverride: false };
|
|
654
|
-
decidedBy = resuming ? 'approval' : granting ? 'consent' : 'answer';
|
|
886
|
+
decidedBy = resuming ? 'approval' : granting || consentRoute ? 'consent' : 'answer';
|
|
655
887
|
}
|
|
656
888
|
else if (resolution) {
|
|
657
889
|
ev.log?.(`⇄ Route pinned by override: ${resolution.route}`);
|
|
@@ -703,7 +935,8 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
703
935
|
throw new Error(`Dispatch failed: no subagent named '${resolution.route}' is declared.`);
|
|
704
936
|
result.route = { ...resolution, decidedBy };
|
|
705
937
|
const routeNote = resolution.reason ? ` — ${resolution.reason}` : '';
|
|
706
|
-
|
|
938
|
+
// The classifier's reason can paraphrase the person: progress (to the caller) carries it, the log does not.
|
|
939
|
+
ev.log?.(`⇄ Route: ${resolution.route}`);
|
|
707
940
|
ev.onProgress?.(`Routed to ${resolution.route}${routeNote}`);
|
|
708
941
|
// ══ DISPATCH ═════════════════════════════════════════════════════════
|
|
709
942
|
if (routeCfg.a2a_agent_url) {
|
|
@@ -716,12 +949,29 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
716
949
|
// projection: other agents' turns would otherwise read as user speech
|
|
717
950
|
// mixed with their tool payloads (lib/session/transcript.ts).
|
|
718
951
|
const routeAgent = await compileRoute(routeCfg);
|
|
719
|
-
if (routeAgent
|
|
952
|
+
if ('dispatch' in routeAgent) {
|
|
953
|
+
// A nested dispatch syndicate runs as its own turn on its own conversation (ADR 0120).
|
|
954
|
+
const ran = await runDispatchRoute(routeCfg.name, routeAgent.dispatch);
|
|
955
|
+
if ('failed' in ran)
|
|
956
|
+
return ran.failed;
|
|
957
|
+
answer = ran.answer;
|
|
958
|
+
// Its turn paused: the turn waits on what it waits on, with the path from the route down.
|
|
959
|
+
if (ran.paused) {
|
|
960
|
+
result.answer = answer;
|
|
961
|
+
return pausedWalk(ran.paused);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else if (routeAgent.workflow) {
|
|
720
965
|
// A workflow route walks its whole graph on its own child session (ADR 0106).
|
|
721
966
|
const walked = await runWorkflowRoute(routeCfg.name, routeAgent, resolution);
|
|
722
967
|
if ('failed' in walked)
|
|
723
968
|
return walked.failed;
|
|
724
969
|
answer = walked.answer;
|
|
970
|
+
// Its walk paused (ADR 0119): the turn waits on the node that asked, with the path from the route down to it.
|
|
971
|
+
if (walked.paused) {
|
|
972
|
+
result.answer = answer;
|
|
973
|
+
return pausedWalk(walked.paused);
|
|
974
|
+
}
|
|
725
975
|
}
|
|
726
976
|
else
|
|
727
977
|
answer = await runAgent({
|
|
@@ -732,13 +982,15 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
732
982
|
// is replayed raw, so the loop finds the call the message answers.
|
|
733
983
|
sessions: new ProjectedSessionService(sessionService, routeCfg.name, resumingBelow
|
|
734
984
|
? { rawFrom: (events) => turnStartOfCall(events, resumingBelow.callIds[0]) }
|
|
735
|
-
:
|
|
736
|
-
? { rawFrom: (events) =>
|
|
737
|
-
:
|
|
738
|
-
? { rawFrom: (events) =>
|
|
739
|
-
:
|
|
740
|
-
? { rawFrom: (events) => turnStartOfCall(events,
|
|
741
|
-
:
|
|
985
|
+
: grantingBelow
|
|
986
|
+
? { rawFrom: (events) => turnStartOfCall(events, grantingBelow.callIds[0]) }
|
|
987
|
+
: resuming
|
|
988
|
+
? { rawFrom: (events) => interruptedTurnStart(events, resuming.id) }
|
|
989
|
+
: answering
|
|
990
|
+
? { rawFrom: (events) => turnStartOfCall(events, answering.id) }
|
|
991
|
+
: granting
|
|
992
|
+
? { rawFrom: (events) => turnStartOfCall(events, granting.id) }
|
|
993
|
+
: {}),
|
|
742
994
|
stage: 'dispatch',
|
|
743
995
|
route: resolution,
|
|
744
996
|
publishToolStatus: true,
|
|
@@ -788,10 +1040,11 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
788
1040
|
// resumes the graph where it waited.
|
|
789
1041
|
// While a gated call waits, a message that is not its decision repeats the request: the walk cannot move past
|
|
790
1042
|
// the waiting node, so nothing is stored and nothing runs, as the A2A server answers it (ADR 0098).
|
|
791
|
-
|
|
1043
|
+
// A node that is a nested workflow raised its walk's request again on the walk's own session (ADR 0119), wherever the gate is declared.
|
|
1044
|
+
if (!decision) {
|
|
792
1045
|
const open = pendingApproval(existing?.events ?? []);
|
|
793
1046
|
if (open)
|
|
794
|
-
return pause(open);
|
|
1047
|
+
return pause(await deepen(open));
|
|
795
1048
|
}
|
|
796
1049
|
const workflowAgent = await compileNativeWorkflowAgent(config, compileOpts);
|
|
797
1050
|
try {
|
|
@@ -828,21 +1081,14 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
828
1081
|
result.error = answer.error;
|
|
829
1082
|
return finish();
|
|
830
1083
|
}
|
|
831
|
-
// A gated call paused its node, and the walk with it (ADR 0098): the next message answers it.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const input = answer.inputRequests[answer.inputRequests.length - 1];
|
|
840
|
-
result.status = 'input-required';
|
|
841
|
-
result.input = input;
|
|
842
|
-
result.text = input.message;
|
|
843
|
-
ev.log?.(`⏸ Input needed: ${describeInput(input)}`);
|
|
844
|
-
return finish();
|
|
845
|
-
}
|
|
1084
|
+
// A gated call paused its node, and the walk with it (ADR 0098): the next message answers it. A node that is a
|
|
1085
|
+
// nested workflow pauses on its walk's request, reported with the path down to the node that asked (ADR 0119).
|
|
1086
|
+
const after = await sessionService.get({ appName, userId, sessionId });
|
|
1087
|
+
const pending = pendingApproval(after?.events ?? []);
|
|
1088
|
+
if (pending)
|
|
1089
|
+
return pause(await deepen(pending));
|
|
1090
|
+
if (answer.inputRequests.length)
|
|
1091
|
+
return question(await deepen(answer.inputRequests[answer.inputRequests.length - 1]));
|
|
846
1092
|
result.text = answer.text;
|
|
847
1093
|
if (!result.text) {
|
|
848
1094
|
const failed = answer.nodeErrors[answer.nodeErrors.length - 1];
|
|
@@ -907,7 +1153,8 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
907
1153
|
// Named in the syndicate's `guards:` list (and any nested syndicate's). They
|
|
908
1154
|
// REWRITE rather than retry, on the answering turn's text with every tool
|
|
909
1155
|
// result it produced. The classifier never reaches here.
|
|
910
|
-
|
|
1156
|
+
// A nested dispatch syndicate's guards run once, on the turn's answer at the top, which lists them (ADR 0120).
|
|
1157
|
+
const guardNames = nestedTurn ? [] : collectGuards(config, compileOpts.loadNested);
|
|
911
1158
|
if (result.text && guardNames.length) {
|
|
912
1159
|
const inputs = answer.toolResultTexts;
|
|
913
1160
|
for (const guard of resolveGuards(guardNames, (n) => ev.warn?.(`Unknown guard '${n}' — ignored`))) {
|