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
|
@@ -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';
|
|
@@ -63,10 +63,17 @@ import { isDispatchSyndicate, matchRouteOverride, resolveRoute } from '../dispat
|
|
|
63
63
|
import { collectGrounding, describeGrounding, newGroundingState, webSourcesLine } from '../grounding.js';
|
|
64
64
|
import { resolveGuards } from '../guards/index.js';
|
|
65
65
|
import { collectGuards, nestedLoader } from '../loadSyndicate.js';
|
|
66
|
+
import { context } from '@opentelemetry/api';
|
|
67
|
+
import core from '@opentelemetry/core';
|
|
68
|
+
const { suppressTracing } = core;
|
|
66
69
|
import { traceAgentRun } from '../observability/tracer.js';
|
|
67
70
|
import { ProjectedSessionService, renderTranscriptDigest } from '../session/transcript.js';
|
|
68
71
|
import { approvalDecisionIn, describeApproval, interruptedTurnStart, pendingApproval } from './approvals.js';
|
|
69
72
|
import { pendingQuestion, questionAnswerPart, questionFrom, turnStartOfCall } from './questions.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';
|
|
70
77
|
export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.js';
|
|
71
78
|
export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.js';
|
|
72
79
|
import { RemoteA2AAgent, remoteContextId, remoteToolOutput } from '../a2a/remoteAgent.js';
|
|
@@ -274,9 +281,17 @@ export async function runSyndicateTurn(opts) {
|
|
|
274
281
|
maxLlmCalls: opts.maxLlmCalls ?? config.max_steps ?? DEFAULT_MAX_STEPS,
|
|
275
282
|
deadlineMs: opts.deadlineMs,
|
|
276
283
|
signal: opts.signal,
|
|
284
|
+
// `trace: false`: no span from anything the turn runs, to any sink.
|
|
285
|
+
untraced: opts.trace === false,
|
|
277
286
|
});
|
|
287
|
+
// A nested dispatch syndicate, wherever the turn reaches one, runs as a turn of its own under these controls (ADR 0120).
|
|
288
|
+
control.nestedDispatch = (run) => runNestedDispatch(opts, control, runtime, run);
|
|
278
289
|
try {
|
|
279
|
-
|
|
290
|
+
const run = () => runWithTurnControl(control, () => runTurnInner(opts, control, runtime));
|
|
291
|
+
// A span a library opens through OpenTelemetry's own API (not the
|
|
292
|
+
// engine's, which read control.untraced) is suppressed too, while a
|
|
293
|
+
// registered tracer is there to read the suppression.
|
|
294
|
+
return await (control.untraced ? context.with(suppressTracing(context.active()), run) : run());
|
|
280
295
|
}
|
|
281
296
|
finally {
|
|
282
297
|
control.dispose();
|
|
@@ -297,7 +312,50 @@ function nativeWorkflowAgent(spec, opts) {
|
|
|
297
312
|
const { graph, agents, resolveTool, workflows } = compileNativeWorkflow(spec);
|
|
298
313
|
return { runtime: 'native-workflow', graph, agents, adapterFor: nativeAdapterFor(opts, workflowAgentSpecs(spec)), resolveTool, workflows };
|
|
299
314
|
}
|
|
300
|
-
|
|
315
|
+
/**
|
|
316
|
+
* A nested dispatch syndicate's own turn (ADR 0120): runTurnInner on its
|
|
317
|
+
* conversation (`run`, opened by the caller), under the turn's controls,
|
|
318
|
+
* with the turn's store, memory, credentials and log, never streamed, its
|
|
319
|
+
* guards left to the turn at the top (collectGuards already lists them
|
|
320
|
+
* there). Its result is read as a walk's end: the route's final text, the
|
|
321
|
+
* conversation's state writes, and what it waits on, by id.
|
|
322
|
+
*/
|
|
323
|
+
async function runNestedDispatch(opts, control, runtime, run) {
|
|
324
|
+
const sessions = run.sessions;
|
|
325
|
+
const key = { appName: run.appName, userId: run.userId, sessionId: run.sessionId };
|
|
326
|
+
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
327
|
+
const before = new Map(Object.entries(kept((await sessions.get(key))?.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
328
|
+
const config = run.config;
|
|
329
|
+
const trace = opts.trace === false ? false : { ...(opts.trace ?? {}), syndicateName: config.syndicate_name ?? run.name };
|
|
330
|
+
const nested = await runTurnInner({
|
|
331
|
+
config,
|
|
332
|
+
parts: run.parts,
|
|
333
|
+
appName: run.appName,
|
|
334
|
+
userId: run.userId,
|
|
335
|
+
sessionId: run.sessionId,
|
|
336
|
+
sessionService: sessions,
|
|
337
|
+
...(opts.memoryService ? { memoryService: opts.memoryService } : {}),
|
|
338
|
+
compile: run.compile,
|
|
339
|
+
...(opts.toolCredentials ? { toolCredentials: opts.toolCredentials } : {}),
|
|
340
|
+
streaming: false,
|
|
341
|
+
trace,
|
|
342
|
+
events: { ...(opts.events?.log ? { log: opts.events.log } : {}), ...(opts.events?.warn ? { warn: opts.events.warn } : {}) },
|
|
343
|
+
}, control, runtime, { credentialApp: opts.appName });
|
|
344
|
+
const after = kept((await sessions.get(key))?.state);
|
|
345
|
+
const stateDelta = Object.fromEntries(Object.entries(after).filter(([k, v]) => before.get(k) !== JSON.stringify(v)));
|
|
346
|
+
const interruptId = nested.approval?.id ?? nested.input?.id ?? nested.consent?.id;
|
|
347
|
+
return {
|
|
348
|
+
status: nested.status,
|
|
349
|
+
text: nested.text,
|
|
350
|
+
stateDelta,
|
|
351
|
+
...(interruptId ? { interruptId } : {}),
|
|
352
|
+
...(nested.approval ? { approval: nested.approval } : {}),
|
|
353
|
+
...(nested.input ? { input: nested.input } : {}),
|
|
354
|
+
...(nested.consent ? { consent: nested.consent } : {}),
|
|
355
|
+
...(nested.error ? { error: nested.error } : {}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
301
359
|
const { config, appName, userId, sessionId, sessionService } = opts;
|
|
302
360
|
const ev = opts.events ?? {};
|
|
303
361
|
// Nested references load from the definition's bundle when it has one
|
|
@@ -312,11 +370,13 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
312
370
|
const nativeOf = (spec) => ({ runtime: 'native', agent: compileNative(spec), adapterFor: nativeAdapterFor(compileOpts, spec) });
|
|
313
371
|
/** The orchestrator (a DELEGATE root, or a dispatch classifier). */
|
|
314
372
|
const compileRoot = async () => nativeOf(await compileSpec(config, compileOpts));
|
|
315
|
-
/** A dispatch route: one agent,
|
|
373
|
+
/** A dispatch route: one agent, a nested workflow's whole graph (ADR 0106), or a nested dispatch syndicate's own turn (ADR 0120). */
|
|
316
374
|
const compileRoute = async (routeCfg) => {
|
|
317
375
|
const entry = await compileEntrySpec(routeCfg, compileOpts);
|
|
318
376
|
if (entry.kind === 'workflow')
|
|
319
377
|
return { ...nativeWorkflowAgent(entry.workflow, compileOpts), workflow: entry.workflow };
|
|
378
|
+
if (entry.kind === 'dispatch')
|
|
379
|
+
return { dispatch: entry.dispatch };
|
|
320
380
|
return nativeOf(entry.spec);
|
|
321
381
|
};
|
|
322
382
|
// Self-correction (ADR 0075): one per turn, from the YAML's retries:.
|
|
@@ -360,32 +420,75 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
360
420
|
// A message answering an approval resumes the agent that asked; the answer
|
|
361
421
|
// must name the request still open in this conversation.
|
|
362
422
|
const decision = approvalDecisionIn(parts);
|
|
423
|
+
// Pauses inside delegated subagents (ADR 0110): what waits below a call an agent of this conversation left open.
|
|
424
|
+
const delegationKey = { sessions: sessionService, userId, sessionId, appName };
|
|
425
|
+
/** The first pause waiting below an open delegated call in this conversation, `author`'s calls only when given. */
|
|
426
|
+
const pauseBelow = async (events, author) => isWorkflowSyndicate(config) ? undefined : (await delegatedPauses(delegationKey, events, author))[0];
|
|
427
|
+
// Pauses inside nested workflows run as a dispatch route or a workflow node (ADR 0119).
|
|
428
|
+
const pauseKey = { ...delegationKey, appName };
|
|
429
|
+
/** 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. */
|
|
430
|
+
const deepen = (pending) => deepestPause(pauseKey, pending);
|
|
431
|
+
/** A dispatch route that is a nested workflow, its walk paused: the conversation's last event is its pause record. */
|
|
432
|
+
const routePaused = isDispatchSyndicate(config) ? await routePause(pauseKey, existing?.events ?? [], subagentNames) : undefined;
|
|
433
|
+
let resumingBelow;
|
|
434
|
+
let resumingRoute;
|
|
363
435
|
let resuming;
|
|
364
436
|
if (decision) {
|
|
365
437
|
resuming = pendingApproval(existing?.events ?? []);
|
|
438
|
+
if (resuming && isWorkflowSyndicate(config))
|
|
439
|
+
resuming = await deepen(resuming);
|
|
440
|
+
if (!resuming) {
|
|
441
|
+
resumingBelow = await pauseBelow(existing?.events ?? []);
|
|
442
|
+
resuming = resumingBelow?.approval;
|
|
443
|
+
}
|
|
444
|
+
if (!resuming && routePaused?.approval) {
|
|
445
|
+
resumingRoute = routePaused;
|
|
446
|
+
resuming = routePaused.approval;
|
|
447
|
+
}
|
|
366
448
|
if (!resuming || resuming.id !== decision.id) {
|
|
367
449
|
result.status = 'failed';
|
|
368
450
|
result.error = { code: 'NO_PENDING_APPROVAL', message: `No approval ${decision.id} is waiting in this conversation.` };
|
|
369
451
|
return finish();
|
|
370
452
|
}
|
|
371
453
|
// The log names the call, not its arguments: they are user content.
|
|
372
|
-
ev.log?.(`✓ Approval ${decision.approved ? 'granted' : 'refused'}: ${resuming.agent} → ${resuming.tool}`);
|
|
454
|
+
ev.log?.(`✓ Approval ${decision.approved ? 'granted' : 'refused'}: ${(resuming.path ?? [resuming.agent]).join(' → ')} → ${resuming.tool}`);
|
|
373
455
|
}
|
|
374
456
|
// ── Consent (lib/runtime/credentials.ts, ADR 0085) ────────────────────────
|
|
375
457
|
// While a call waits for an OAuth grant, the next message resumes it once
|
|
376
458
|
// the callback has stored the grant; until then it repeats the request and
|
|
377
459
|
// runs nothing.
|
|
378
|
-
|
|
460
|
+
// A nested dispatch syndicate's turn reads and stores grants under the root's app, as a delegated subagent does (ADR 0118, ADR 0120).
|
|
461
|
+
const credentialApp = nestedTurn?.credentialApp ?? appName;
|
|
462
|
+
const credentialStore = opts.toolCredentials ? pinnedCredentialStore(opts.toolCredentials.store, credentialApp) : undefined;
|
|
463
|
+
const consentStep = opts.toolCredentials?.consent
|
|
464
|
+
? nestedTurn
|
|
465
|
+
? consentPinnedTo(opts.toolCredentials.consent, credentialApp)
|
|
466
|
+
: opts.toolCredentials.consent
|
|
467
|
+
: undefined;
|
|
379
468
|
const consentPause = (pending) => {
|
|
380
469
|
result.status = 'input-required';
|
|
381
470
|
result.consent = pending;
|
|
382
471
|
result.text = `Authorization needed: ${describeConsent(pending)}. Open the authorization link, then send any message to continue.`;
|
|
383
|
-
ev.log?.(`⏸ Authorization needed: ${pending.agent} → ${pending.provider}`);
|
|
472
|
+
ev.log?.(`⏸ Authorization needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.provider}`);
|
|
384
473
|
return finish();
|
|
385
474
|
};
|
|
386
475
|
let granting;
|
|
476
|
+
// 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.
|
|
477
|
+
let grantingBelow;
|
|
478
|
+
// 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.
|
|
479
|
+
let consentRoute;
|
|
387
480
|
if (!decision && !isWorkflowSyndicate(config)) {
|
|
388
|
-
|
|
481
|
+
let open = pendingConsent(existing?.events ?? []);
|
|
482
|
+
if (!open && config.subagents?.length) {
|
|
483
|
+
const below = await pauseBelow(existing?.events ?? []);
|
|
484
|
+
if (below?.consent) {
|
|
485
|
+
grantingBelow = below;
|
|
486
|
+
open = below.consent;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const routeWaits = !open && !!routePaused?.consent;
|
|
490
|
+
if (routeWaits)
|
|
491
|
+
open = routePaused.consent;
|
|
389
492
|
if (open) {
|
|
390
493
|
if (!credentialStore) {
|
|
391
494
|
result.status = 'failed';
|
|
@@ -394,16 +497,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
394
497
|
}
|
|
395
498
|
let granted = false;
|
|
396
499
|
try {
|
|
397
|
-
granted = !!open.provider && !!(await credentialStore.get({ appName, userId, provider: open.provider }));
|
|
500
|
+
granted = !!open.provider && !!(await credentialStore.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
398
501
|
}
|
|
399
502
|
catch {
|
|
400
503
|
// Expired or unreadable: not granted. The request stands.
|
|
401
504
|
}
|
|
402
505
|
if (!granted)
|
|
403
506
|
return consentPause(open);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
507
|
+
// The route's own turn carries the grant's answer down to its call; the message goes to it as it came.
|
|
508
|
+
if (routeWaits)
|
|
509
|
+
consentRoute = routePaused;
|
|
510
|
+
else {
|
|
511
|
+
granting = open;
|
|
512
|
+
parts = [credentialResponsePart(open.id, open.provider)];
|
|
513
|
+
}
|
|
514
|
+
ev.log?.(`✓ Authorization granted: ${(open.path ?? [open.agent]).join(' → ')} → ${open.provider}`);
|
|
407
515
|
}
|
|
408
516
|
}
|
|
409
517
|
// ── Questions (lib/runtime/questions.ts) ───────────────────────────────────
|
|
@@ -411,24 +519,45 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
411
519
|
// answer: it becomes that call's response, and the agent that asked
|
|
412
520
|
// resumes its own tool loop (the loop reads the response from its history;
|
|
413
521
|
// ADR 0079). A workflow's pauses are its walk's own business.
|
|
522
|
+
// A question asked inside a delegated subagent (ADR 0110) is answered the same way: the answer is stored here, and the
|
|
523
|
+
// open call that leads to the asker carries it down; `answering` names that call and the agent that made it.
|
|
414
524
|
let answering;
|
|
415
|
-
|
|
416
|
-
|
|
525
|
+
/** 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). */
|
|
526
|
+
let routeAnswer;
|
|
527
|
+
if (!decision && !granting && !consentRoute && !isWorkflowSyndicate(config)) {
|
|
417
528
|
const plainText = parts.length > 0 && parts.every((p) => typeof p.text === 'string');
|
|
418
|
-
|
|
419
|
-
|
|
529
|
+
const own = plainText ? pendingQuestion(existing?.events ?? []) : undefined;
|
|
530
|
+
const below = plainText && !own ? await pauseBelow(existing?.events ?? []) : undefined;
|
|
531
|
+
const question = own ?? below?.question;
|
|
532
|
+
if (!question && plainText && routePaused?.question) {
|
|
533
|
+
// The conversation stores the person's words; the route's walk reads them as the reply to the node that asked.
|
|
534
|
+
const waiting = routePaused.question;
|
|
535
|
+
answering = { agent: routePaused.route, id: waiting.id };
|
|
536
|
+
routeAnswer = [{ functionResponse: { id: waiting.id, name: INPUT_REQUEST, response: { result: messageText } } }];
|
|
537
|
+
ev.log?.(`✓ Answer to ${(waiting.path ?? [waiting.node]).join(' → ')}'s question`);
|
|
538
|
+
}
|
|
539
|
+
else if (question) {
|
|
540
|
+
answering = below ? { agent: below.path[0], id: below.callIds[0] } : { agent: question.node, id: question.id };
|
|
420
541
|
parts = [questionAnswerPart(question.id, messageText)];
|
|
421
|
-
ev.log?.(`✓ Answer to ${question.node}'s question`);
|
|
542
|
+
ev.log?.(`✓ Answer to ${(question.path ?? [question.node]).join(' → ')}'s question`);
|
|
422
543
|
}
|
|
423
544
|
}
|
|
424
545
|
/** The agent a dispatch turn must resume, and the call its interrupted turn holds. */
|
|
425
|
-
const resumeTarget =
|
|
426
|
-
? { agent:
|
|
427
|
-
:
|
|
428
|
-
? { agent:
|
|
429
|
-
:
|
|
430
|
-
? {
|
|
431
|
-
:
|
|
546
|
+
const resumeTarget = resumingBelow
|
|
547
|
+
? { agent: resumingBelow.path[0], id: resumingBelow.callIds[0], why: 'resuming an approval' }
|
|
548
|
+
: resumingRoute
|
|
549
|
+
? { agent: resumingRoute.route, id: resuming.id, why: 'resuming an approval' }
|
|
550
|
+
: consentRoute
|
|
551
|
+
? { agent: consentRoute.route, id: consentRoute.consent.id, why: 'resuming after an authorization' }
|
|
552
|
+
: grantingBelow
|
|
553
|
+
? { agent: grantingBelow.path[0], id: grantingBelow.callIds[0], why: 'resuming after an authorization' }
|
|
554
|
+
: resuming
|
|
555
|
+
? { agent: resuming.agent, id: resuming.id, why: 'resuming an approval' }
|
|
556
|
+
: granting
|
|
557
|
+
? { agent: granting.agent, id: granting.id, why: 'resuming after an authorization' }
|
|
558
|
+
: answering
|
|
559
|
+
? { ...answering, why: 'answering its question' }
|
|
560
|
+
: undefined;
|
|
432
561
|
/** After the answering run: is a call now waiting for a grant? */
|
|
433
562
|
const awaitingConsent = async (agentName) => {
|
|
434
563
|
if (!opts.toolCredentials?.consent)
|
|
@@ -454,13 +583,40 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
454
583
|
ev.log?.(`⏸ Input needed: ${describeInput(input)}`);
|
|
455
584
|
return finish();
|
|
456
585
|
};
|
|
586
|
+
/** After the answering run: does a pause wait inside a delegated call `agentName` left open (ADR 0110)? */
|
|
587
|
+
const pausedBelow = async (agentName) => {
|
|
588
|
+
const after = await sessionService.get({ appName, userId, sessionId });
|
|
589
|
+
const below = await pauseBelow(after?.events ?? [], agentName);
|
|
590
|
+
if (below?.approval)
|
|
591
|
+
return pause(below.approval);
|
|
592
|
+
if (below?.consent)
|
|
593
|
+
return consentPause(below.consent);
|
|
594
|
+
if (below?.question) {
|
|
595
|
+
result.status = 'input-required';
|
|
596
|
+
result.input = below.question;
|
|
597
|
+
result.text = below.question.message;
|
|
598
|
+
ev.log?.(`⏸ Input needed: ${describeInput(below.question)} (${below.path.join(' → ')})`);
|
|
599
|
+
return finish();
|
|
600
|
+
}
|
|
601
|
+
return undefined;
|
|
602
|
+
};
|
|
457
603
|
const pause = (pending) => {
|
|
458
604
|
result.status = 'input-required';
|
|
459
605
|
result.approval = pending;
|
|
460
606
|
result.text = `Approval needed: ${describeApproval(pending)}.`;
|
|
461
|
-
ev.log?.(`⏸ Approval needed: ${pending.agent} → ${pending.tool}`);
|
|
607
|
+
ev.log?.(`⏸ Approval needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.tool}`);
|
|
608
|
+
return finish();
|
|
609
|
+
};
|
|
610
|
+
/** The turn waits on what a question asks. */
|
|
611
|
+
const question = (input) => {
|
|
612
|
+
result.status = 'input-required';
|
|
613
|
+
result.input = input;
|
|
614
|
+
result.text = input.message;
|
|
615
|
+
ev.log?.(`⏸ Input needed: ${describeInput(input)}${input.path ? ` (${input.path.join(' → ')})` : ''}`);
|
|
462
616
|
return finish();
|
|
463
617
|
};
|
|
618
|
+
/** 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. */
|
|
619
|
+
const pausedWalk = (paused) => paused.approval ? pause(paused.approval) : paused.consent ? consentPause(paused.consent) : question(paused.question);
|
|
464
620
|
/** Run ONE agent against ONE session, under the turn's controls. */
|
|
465
621
|
const runAgent = async (params) => {
|
|
466
622
|
let stream;
|
|
@@ -501,7 +657,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
501
657
|
...(selfCorrection ? { selfCorrection } : {}),
|
|
502
658
|
// Tool credentials and the consent step (ADR 0072, ADR 0085); the classifier's lane lists no tools.
|
|
503
659
|
...(credentialStore ? { credentials: credentialStore } : {}),
|
|
504
|
-
...(
|
|
660
|
+
...(consentStep && params.stage !== 'classify' ? { consent: consentStep } : {}),
|
|
505
661
|
...(compileOpts.log ? { log: compileOpts.log } : {}),
|
|
506
662
|
});
|
|
507
663
|
}
|
|
@@ -548,15 +704,27 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
548
704
|
};
|
|
549
705
|
/**
|
|
550
706
|
* A dispatch route that is a workflow syndicate (ADR 0106): its whole graph
|
|
551
|
-
* walked on the child session filed under the
|
|
552
|
-
* userId, sessionId }`,
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
* by the
|
|
558
|
-
* the
|
|
559
|
-
*
|
|
707
|
+
* walked on the child session filed under the agent path and its kind
|
|
708
|
+
* (`{ <app>/route:<route>, userId, sessionId }`, ADR 0119, ADR 0120; the
|
|
709
|
+
* one filed under an older key is continued when the route ran in the
|
|
710
|
+
* conversation before, entrySession),
|
|
711
|
+
* created from the conversation's state the first time, `temp:` keys
|
|
712
|
+
* dropped, and kept, as a delegated nested workflow walks (ADR 0098), and
|
|
713
|
+
* drained by the reader a workflow turn uses, so the route's answer is what
|
|
714
|
+
* the workflow would answer as its own syndicate. The conversation stores
|
|
715
|
+
* the message and the answer, one event authored by the route that carries
|
|
716
|
+
* the walk's state writes, so the classifier and the next route read the
|
|
717
|
+
* exchange as they read any route's. A node that gave up fails the turn
|
|
718
|
+
* NODE_FAILED, as a workflow turn does.
|
|
719
|
+
*
|
|
720
|
+
* A walk that ends paused (ADR 0119) stores the route's pause record in
|
|
721
|
+
* place of the answer: authored by the route at its own path, no content,
|
|
722
|
+
* the walk's open interrupts in `longRunningToolIds`, its state writes so
|
|
723
|
+
* far. While it is the conversation's last event the route waits: the
|
|
724
|
+
* turn reports the request or question with the path from the route down
|
|
725
|
+
* to the node that asked (interrupts.ts routePause), a decision or a
|
|
726
|
+
* plain-text answer resumes the route without classifying, and the walk
|
|
727
|
+
* reads the answer as its explicit reply (`routeAnswer`).
|
|
560
728
|
*/
|
|
561
729
|
const runWorkflowRoute = async (route, agent, resolution) => {
|
|
562
730
|
const store = sessionService;
|
|
@@ -565,14 +733,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
565
733
|
if (!shared)
|
|
566
734
|
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
567
735
|
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
568
|
-
|
|
569
|
-
const
|
|
736
|
+
// Filed under the agent path (ADR 0119), else continued under the route's name alone where ADR 0106 filed it.
|
|
737
|
+
const resumingWalk = resumingRoute !== undefined || routeAnswer !== undefined;
|
|
738
|
+
const child = await entrySession(store, { appName, userId, sessionId, events: shared.events }, route, 'route', kept(shared.state), resumingWalk);
|
|
739
|
+
const childKey = { appName: child.appName, userId, sessionId };
|
|
570
740
|
const before = new Map(Object.entries(kept(child.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
571
741
|
const invocationId = `e-${randomUUID()}`;
|
|
572
742
|
await store.append(shared, createTurnEvent({ invocationId, author: 'user', content: { role: 'user', parts } }));
|
|
743
|
+
/** The walk's state writes this turn, `temp:` keys aside: what the conversation's event for the route carries. */
|
|
744
|
+
const written = async () => {
|
|
745
|
+
const after = kept((await store.get(childKey))?.state);
|
|
746
|
+
return Object.fromEntries(Object.entries(after).filter(([k, v]) => before.get(k) !== JSON.stringify(v)));
|
|
747
|
+
};
|
|
573
748
|
let walked;
|
|
574
749
|
try {
|
|
575
|
-
walked = await runAgent({ agent, sid: sessionId, appName:
|
|
750
|
+
walked = await runAgent({ agent, sid: sessionId, appName: child.appName, userParts: routeAnswer ?? parts, sessions: sessionService, stage: 'dispatch', route: resolution, publishToolStatus: true, errorPolicy: 'collect' });
|
|
576
751
|
}
|
|
577
752
|
catch (err) {
|
|
578
753
|
const last = control.stopReason ? undefined : err;
|
|
@@ -583,9 +758,22 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
583
758
|
result.error = { code: last instanceof UnsupportedWorkflowResumeError ? 'RESUME_UNSUPPORTED' : last instanceof NodeRunLimitError ? NODE_RUN_LIMIT : 'NODE_FAILED', message: last.message };
|
|
584
759
|
return { failed: finish() };
|
|
585
760
|
}
|
|
761
|
+
if (!walked.error && !control.stopReason) {
|
|
762
|
+
// A walk that ended paused (ADR 0119): the conversation stores the route's pause record, which names the walk's
|
|
763
|
+
// open interrupts and carries its state writes so far; the next message finds the walk through it.
|
|
764
|
+
const walkEnd = (await store.get(childKey))?.events.at(-1);
|
|
765
|
+
const paused = walkEnd?.author === route && walkEnd.nodeInfo?.path === route && !walkEnd.content?.parts?.length ? await nestedWorkflowPause(pauseKey, route, 'route') : undefined;
|
|
766
|
+
if (paused) {
|
|
767
|
+
const record = workflowPauseEvent({ name: route, invocationId, input: null, interruptIds: [...(walkEnd.longRunningToolIds ?? [])] });
|
|
768
|
+
const stateDelta = await written();
|
|
769
|
+
if (Object.keys(stateDelta).length)
|
|
770
|
+
record.actions.stateDelta = stateDelta;
|
|
771
|
+
await store.append((await store.get(key)) ?? shared, record);
|
|
772
|
+
return { answer: walked, paused };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
586
775
|
if (walked.text && !walked.error && !control.stopReason) {
|
|
587
|
-
const
|
|
588
|
-
const stateDelta = Object.fromEntries(Object.entries(after).filter(([k, v]) => before.get(k) !== JSON.stringify(v)));
|
|
776
|
+
const stateDelta = await written();
|
|
589
777
|
const answered = createTurnEvent({
|
|
590
778
|
invocationId,
|
|
591
779
|
author: route,
|
|
@@ -596,6 +784,77 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
596
784
|
}
|
|
597
785
|
return { answer: walked };
|
|
598
786
|
};
|
|
787
|
+
/**
|
|
788
|
+
* A dispatch route that is a nested dispatch syndicate (ADR 0120): its own
|
|
789
|
+
* turn (runNestedDispatch: its classifier picks one of its routes, the
|
|
790
|
+
* route answers), on the child session filed under the route's kind
|
|
791
|
+
* (`<app>/route:<route>`, entrySession), created from the conversation's
|
|
792
|
+
* state the first time, `temp:` keys dropped, and kept. The conversation
|
|
793
|
+
* stores the message and, as for a workflow route, one event authored by
|
|
794
|
+
* the route: its answer (the inner route's final text) with the state
|
|
795
|
+
* writes, or, when its turn ends paused, the route's pause record naming
|
|
796
|
+
* the one request it waits on. The turn reports that request with the
|
|
797
|
+
* path from the route down (routePause, which follows the record into the
|
|
798
|
+
* nested conversation), and the next message that answers it resumes the
|
|
799
|
+
* route without classifying: the nested turn reads it as a message at the
|
|
800
|
+
* top reads an answer (an approval decision as it came, a question's
|
|
801
|
+
* answer as the person's text, any message once a grant is stored).
|
|
802
|
+
*/
|
|
803
|
+
const runDispatchRoute = async (route, dispatch) => {
|
|
804
|
+
const store = sessionService;
|
|
805
|
+
const key = { appName, userId, sessionId };
|
|
806
|
+
const shared = await store.get(key);
|
|
807
|
+
if (!shared)
|
|
808
|
+
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
809
|
+
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
810
|
+
const resumingTurn = resumingRoute !== undefined || routeAnswer !== undefined || consentRoute !== undefined;
|
|
811
|
+
const child = await entrySession(store, { appName, userId, sessionId, events: shared.events }, route, 'route', kept(shared.state), resumingTurn);
|
|
812
|
+
const invocationId = `e-${randomUUID()}`;
|
|
813
|
+
await store.append(shared, createTurnEvent({ invocationId, author: 'user', content: { role: 'user', parts } }));
|
|
814
|
+
// A question's answer goes in as the person's words; anything else as it came.
|
|
815
|
+
const nestedParts = routeAnswer ? [{ text: messageText }] : parts;
|
|
816
|
+
const end = await runNestedDispatch(opts, control, _runtime, {
|
|
817
|
+
config: dispatch.config,
|
|
818
|
+
compile: dispatch.compile,
|
|
819
|
+
name: route,
|
|
820
|
+
sessions: store,
|
|
821
|
+
appName: child.appName,
|
|
822
|
+
userId,
|
|
823
|
+
sessionId,
|
|
824
|
+
parts: nestedParts,
|
|
825
|
+
});
|
|
826
|
+
if (control.stopReason)
|
|
827
|
+
return { failed: finish() };
|
|
828
|
+
if (end.status === 'failed') {
|
|
829
|
+
result.status = 'failed';
|
|
830
|
+
result.failedStage = 'dispatch';
|
|
831
|
+
result.error = end.error ?? { code: 'ROUTE_FAILED', message: `${route} failed.` };
|
|
832
|
+
return { failed: finish() };
|
|
833
|
+
}
|
|
834
|
+
const delta = Object.keys(end.stateDelta).length ? { actions: { stateDelta: end.stateDelta } } : {};
|
|
835
|
+
const answer = emptyRun();
|
|
836
|
+
answer.agents.add(route);
|
|
837
|
+
if (end.status === 'input-required' && end.interruptId) {
|
|
838
|
+
const record = workflowPauseEvent({ name: route, invocationId, input: null, interruptIds: [end.interruptId] });
|
|
839
|
+
if (delta.actions)
|
|
840
|
+
record.actions.stateDelta = delta.actions.stateDelta;
|
|
841
|
+
await store.append((await store.get(key)) ?? shared, record);
|
|
842
|
+
const stored = (await store.get(key))?.events ?? [];
|
|
843
|
+
const paused = await routePause(pauseKey, stored, subagentNames);
|
|
844
|
+
if (!paused)
|
|
845
|
+
throw new Error(`${route}: its turn paused on ${end.interruptId}, which its conversation does not hold.`);
|
|
846
|
+
return { answer, paused };
|
|
847
|
+
}
|
|
848
|
+
answer.text = end.text;
|
|
849
|
+
if (end.text) {
|
|
850
|
+
await store.append((await store.get(key)) ?? shared, createTurnEvent({ invocationId, author: route, content: { role: 'model', parts: [{ text: end.text }] }, ...delta }));
|
|
851
|
+
}
|
|
852
|
+
return { answer };
|
|
853
|
+
};
|
|
854
|
+
// While a route's walk waits on a gated call (ADR 0119), a message that is not its decision repeats the request: the
|
|
855
|
+
// walk cannot move past the waiting node, so nothing is stored and nothing runs, as a workflow turn answers it.
|
|
856
|
+
if (!decision && routePaused?.approval)
|
|
857
|
+
return pause(routePaused.approval);
|
|
599
858
|
let answer;
|
|
600
859
|
if (isDispatchSyndicate(config)) {
|
|
601
860
|
// ══ PLAN ═════════════════════════════════════════════════════════════
|
|
@@ -611,7 +870,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
611
870
|
throw new Error(`Call ${resumeTarget.id} was raised by '${resumeTarget.agent}', which is not a route of this syndicate.`);
|
|
612
871
|
}
|
|
613
872
|
resolution = { route: resumeTarget.agent, reason: resumeTarget.why, fellBack: false, fallbackReason: '', viaOverride: false };
|
|
614
|
-
decidedBy = resuming ? 'approval' : granting ? 'consent' : 'answer';
|
|
873
|
+
decidedBy = resuming ? 'approval' : granting || consentRoute ? 'consent' : 'answer';
|
|
615
874
|
}
|
|
616
875
|
else if (resolution) {
|
|
617
876
|
ev.log?.(`⇄ Route pinned by override: ${resolution.route}`);
|
|
@@ -676,12 +935,29 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
676
935
|
// projection: other agents' turns would otherwise read as user speech
|
|
677
936
|
// mixed with their tool payloads (lib/session/transcript.ts).
|
|
678
937
|
const routeAgent = await compileRoute(routeCfg);
|
|
679
|
-
if (routeAgent
|
|
938
|
+
if ('dispatch' in routeAgent) {
|
|
939
|
+
// A nested dispatch syndicate runs as its own turn on its own conversation (ADR 0120).
|
|
940
|
+
const ran = await runDispatchRoute(routeCfg.name, routeAgent.dispatch);
|
|
941
|
+
if ('failed' in ran)
|
|
942
|
+
return ran.failed;
|
|
943
|
+
answer = ran.answer;
|
|
944
|
+
// Its turn paused: the turn waits on what it waits on, with the path from the route down.
|
|
945
|
+
if (ran.paused) {
|
|
946
|
+
result.answer = answer;
|
|
947
|
+
return pausedWalk(ran.paused);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
else if (routeAgent.workflow) {
|
|
680
951
|
// A workflow route walks its whole graph on its own child session (ADR 0106).
|
|
681
952
|
const walked = await runWorkflowRoute(routeCfg.name, routeAgent, resolution);
|
|
682
953
|
if ('failed' in walked)
|
|
683
954
|
return walked.failed;
|
|
684
955
|
answer = walked.answer;
|
|
956
|
+
// Its walk paused (ADR 0119): the turn waits on the node that asked, with the path from the route down to it.
|
|
957
|
+
if (walked.paused) {
|
|
958
|
+
result.answer = answer;
|
|
959
|
+
return pausedWalk(walked.paused);
|
|
960
|
+
}
|
|
685
961
|
}
|
|
686
962
|
else
|
|
687
963
|
answer = await runAgent({
|
|
@@ -690,13 +966,17 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
690
966
|
userParts: parts,
|
|
691
967
|
// Resuming an approval or answering a question: the interrupted turn
|
|
692
968
|
// is replayed raw, so the loop finds the call the message answers.
|
|
693
|
-
sessions: new ProjectedSessionService(sessionService, routeCfg.name,
|
|
694
|
-
? { rawFrom: (events) =>
|
|
695
|
-
:
|
|
696
|
-
? { rawFrom: (events) => turnStartOfCall(events,
|
|
697
|
-
:
|
|
698
|
-
? { rawFrom: (events) =>
|
|
699
|
-
:
|
|
969
|
+
sessions: new ProjectedSessionService(sessionService, routeCfg.name, resumingBelow
|
|
970
|
+
? { rawFrom: (events) => turnStartOfCall(events, resumingBelow.callIds[0]) }
|
|
971
|
+
: grantingBelow
|
|
972
|
+
? { rawFrom: (events) => turnStartOfCall(events, grantingBelow.callIds[0]) }
|
|
973
|
+
: resuming
|
|
974
|
+
? { rawFrom: (events) => interruptedTurnStart(events, resuming.id) }
|
|
975
|
+
: answering
|
|
976
|
+
? { rawFrom: (events) => turnStartOfCall(events, answering.id) }
|
|
977
|
+
: granting
|
|
978
|
+
? { rawFrom: (events) => turnStartOfCall(events, granting.id) }
|
|
979
|
+
: {}),
|
|
700
980
|
stage: 'dispatch',
|
|
701
981
|
route: resolution,
|
|
702
982
|
publishToolStatus: true,
|
|
@@ -709,11 +989,16 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
709
989
|
result.error = answer.error;
|
|
710
990
|
return finish();
|
|
711
991
|
}
|
|
712
|
-
if (!routeCfg.a2a_agent_url && agentGates(routeCfg)) {
|
|
992
|
+
if (!routeCfg.a2a_agent_url && (agentGates(routeCfg) || routeCfg.yaml_reference)) {
|
|
713
993
|
const pending = await awaitingApproval(routeCfg.name);
|
|
714
994
|
if (pending)
|
|
715
995
|
return pause(pending);
|
|
716
996
|
}
|
|
997
|
+
if (!routeCfg.a2a_agent_url) {
|
|
998
|
+
const below = await pausedBelow(routeCfg.name);
|
|
999
|
+
if (below)
|
|
1000
|
+
return below;
|
|
1001
|
+
}
|
|
717
1002
|
if (!routeCfg.a2a_agent_url) {
|
|
718
1003
|
const consent = await awaitingConsent(routeCfg.name);
|
|
719
1004
|
if (consent)
|
|
@@ -741,10 +1026,11 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
741
1026
|
// resumes the graph where it waited.
|
|
742
1027
|
// While a gated call waits, a message that is not its decision repeats the request: the walk cannot move past
|
|
743
1028
|
// the waiting node, so nothing is stored and nothing runs, as the A2A server answers it (ADR 0098).
|
|
744
|
-
|
|
1029
|
+
// 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.
|
|
1030
|
+
if (!decision) {
|
|
745
1031
|
const open = pendingApproval(existing?.events ?? []);
|
|
746
1032
|
if (open)
|
|
747
|
-
return pause(open);
|
|
1033
|
+
return pause(await deepen(open));
|
|
748
1034
|
}
|
|
749
1035
|
const workflowAgent = await compileNativeWorkflowAgent(config, compileOpts);
|
|
750
1036
|
try {
|
|
@@ -781,21 +1067,14 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
781
1067
|
result.error = answer.error;
|
|
782
1068
|
return finish();
|
|
783
1069
|
}
|
|
784
|
-
// A gated call paused its node, and the walk with it (ADR 0098): the next message answers it.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const input = answer.inputRequests[answer.inputRequests.length - 1];
|
|
793
|
-
result.status = 'input-required';
|
|
794
|
-
result.input = input;
|
|
795
|
-
result.text = input.message;
|
|
796
|
-
ev.log?.(`⏸ Input needed: ${describeInput(input)}`);
|
|
797
|
-
return finish();
|
|
798
|
-
}
|
|
1070
|
+
// A gated call paused its node, and the walk with it (ADR 0098): the next message answers it. A node that is a
|
|
1071
|
+
// nested workflow pauses on its walk's request, reported with the path down to the node that asked (ADR 0119).
|
|
1072
|
+
const after = await sessionService.get({ appName, userId, sessionId });
|
|
1073
|
+
const pending = pendingApproval(after?.events ?? []);
|
|
1074
|
+
if (pending)
|
|
1075
|
+
return pause(await deepen(pending));
|
|
1076
|
+
if (answer.inputRequests.length)
|
|
1077
|
+
return question(await deepen(answer.inputRequests[answer.inputRequests.length - 1]));
|
|
799
1078
|
result.text = answer.text;
|
|
800
1079
|
if (!result.text) {
|
|
801
1080
|
const failed = answer.nodeErrors[answer.nodeErrors.length - 1];
|
|
@@ -835,6 +1114,11 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
835
1114
|
const consent = await awaitingConsent(config.orchestrator.name);
|
|
836
1115
|
if (consent)
|
|
837
1116
|
return consentPause(consent);
|
|
1117
|
+
if (config.subagents?.length) {
|
|
1118
|
+
const below = await pausedBelow(config.orchestrator.name);
|
|
1119
|
+
if (below)
|
|
1120
|
+
return below;
|
|
1121
|
+
}
|
|
838
1122
|
const askedOrchestrator = asked(answer);
|
|
839
1123
|
if (askedOrchestrator)
|
|
840
1124
|
return askedOrchestrator;
|
|
@@ -855,7 +1139,8 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
855
1139
|
// Named in the syndicate's `guards:` list (and any nested syndicate's). They
|
|
856
1140
|
// REWRITE rather than retry, on the answering turn's text with every tool
|
|
857
1141
|
// result it produced. The classifier never reaches here.
|
|
858
|
-
|
|
1142
|
+
// A nested dispatch syndicate's guards run once, on the turn's answer at the top, which lists them (ADR 0120).
|
|
1143
|
+
const guardNames = nestedTurn ? [] : collectGuards(config, compileOpts.loadNested);
|
|
859
1144
|
if (result.text && guardNames.length) {
|
|
860
1145
|
const inputs = answer.toolResultTexts;
|
|
861
1146
|
for (const guard of resolveGuards(guardNames, (n) => ev.warn?.(`Unknown guard '${n}' — ignored`))) {
|