melchizedek-agents 1.1.0 → 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 +10 -0
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +232 -0
- package/DOCUMENTATION.md +91 -16
- package/ONBOARDING.md +339 -0
- package/QUICKSTART.md +15 -2
- package/README.md +5 -2
- 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 +65 -7
- 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 +22 -11
- package/dist/lib/a2a/executor.js +14 -6
- package/dist/lib/a2a/oauthSetup.d.ts +16 -0
- package/dist/lib/a2a/oauthSetup.js +57 -3
- package/dist/lib/agentDialect.d.ts +18 -8
- package/dist/lib/agentDialect.js +36 -19
- package/dist/lib/compile.d.ts +38 -7
- package/dist/lib/compile.js +57 -37
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +18 -1
- package/dist/lib/doctor.d.ts +76 -0
- package/dist/lib/doctor.js +159 -7
- package/dist/lib/loadSyndicate.d.ts +9 -0
- 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 +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/onboarding.d.ts +134 -0
- package/dist/lib/onboarding.js +539 -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 +295 -62
- 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 +38 -1
- package/dist/lib/syndicateSchema.js +25 -18
- 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 +92 -0
- package/dist/lib/tools/credentialUses.js +112 -0
- package/dist/lib/tools/mcpToolFactory.js +1 -1
- package/dist/lib/tools/oauthConsent.js +8 -1
- package/dist/lib/tools/oauthHosts.d.ts +2 -0
- package/dist/lib/tools/oauthHosts.js +2 -2
- package/dist/lib/tools/oauthTools.d.ts +10 -1
- package/dist/lib/tools/oauthTools.js +29 -3
- 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/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 +11 -2
- package/dist/scripts/assistant_worker.d.ts +2 -0
- package/dist/scripts/assistant_worker.js +13 -4
- package/dist/scripts/doctor.d.ts +4 -2
- package/dist/scripts/doctor.js +6 -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 +12 -2
- package/dist/scripts/yaml_codemod.js +115 -33
- package/package.json +4 -1
- 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 +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';
|
|
@@ -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';
|
|
@@ -281,6 +284,8 @@ export async function runSyndicateTurn(opts) {
|
|
|
281
284
|
// `trace: false`: no span from anything the turn runs, to any sink.
|
|
282
285
|
untraced: opts.trace === false,
|
|
283
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);
|
|
284
289
|
try {
|
|
285
290
|
const run = () => runWithTurnControl(control, () => runTurnInner(opts, control, runtime));
|
|
286
291
|
// A span a library opens through OpenTelemetry's own API (not the
|
|
@@ -307,7 +312,50 @@ function nativeWorkflowAgent(spec, opts) {
|
|
|
307
312
|
const { graph, agents, resolveTool, workflows } = compileNativeWorkflow(spec);
|
|
308
313
|
return { runtime: 'native-workflow', graph, agents, adapterFor: nativeAdapterFor(opts, workflowAgentSpecs(spec)), resolveTool, workflows };
|
|
309
314
|
}
|
|
310
|
-
|
|
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) {
|
|
311
359
|
const { config, appName, userId, sessionId, sessionService } = opts;
|
|
312
360
|
const ev = opts.events ?? {};
|
|
313
361
|
// Nested references load from the definition's bundle when it has one
|
|
@@ -322,11 +370,13 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
322
370
|
const nativeOf = (spec) => ({ runtime: 'native', agent: compileNative(spec), adapterFor: nativeAdapterFor(compileOpts, spec) });
|
|
323
371
|
/** The orchestrator (a DELEGATE root, or a dispatch classifier). */
|
|
324
372
|
const compileRoot = async () => nativeOf(await compileSpec(config, compileOpts));
|
|
325
|
-
/** 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). */
|
|
326
374
|
const compileRoute = async (routeCfg) => {
|
|
327
375
|
const entry = await compileEntrySpec(routeCfg, compileOpts);
|
|
328
376
|
if (entry.kind === 'workflow')
|
|
329
377
|
return { ...nativeWorkflowAgent(entry.workflow, compileOpts), workflow: entry.workflow };
|
|
378
|
+
if (entry.kind === 'dispatch')
|
|
379
|
+
return { dispatch: entry.dispatch };
|
|
330
380
|
return nativeOf(entry.spec);
|
|
331
381
|
};
|
|
332
382
|
// Self-correction (ADR 0075): one per turn, from the YAML's retries:.
|
|
@@ -374,14 +424,27 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
374
424
|
const delegationKey = { sessions: sessionService, userId, sessionId, appName };
|
|
375
425
|
/** The first pause waiting below an open delegated call in this conversation, `author`'s calls only when given. */
|
|
376
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;
|
|
377
433
|
let resumingBelow;
|
|
434
|
+
let resumingRoute;
|
|
378
435
|
let resuming;
|
|
379
436
|
if (decision) {
|
|
380
437
|
resuming = pendingApproval(existing?.events ?? []);
|
|
438
|
+
if (resuming && isWorkflowSyndicate(config))
|
|
439
|
+
resuming = await deepen(resuming);
|
|
381
440
|
if (!resuming) {
|
|
382
441
|
resumingBelow = await pauseBelow(existing?.events ?? []);
|
|
383
442
|
resuming = resumingBelow?.approval;
|
|
384
443
|
}
|
|
444
|
+
if (!resuming && routePaused?.approval) {
|
|
445
|
+
resumingRoute = routePaused;
|
|
446
|
+
resuming = routePaused.approval;
|
|
447
|
+
}
|
|
385
448
|
if (!resuming || resuming.id !== decision.id) {
|
|
386
449
|
result.status = 'failed';
|
|
387
450
|
result.error = { code: 'NO_PENDING_APPROVAL', message: `No approval ${decision.id} is waiting in this conversation.` };
|
|
@@ -394,17 +457,38 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
394
457
|
// While a call waits for an OAuth grant, the next message resumes it once
|
|
395
458
|
// the callback has stored the grant; until then it repeats the request and
|
|
396
459
|
// runs nothing.
|
|
397
|
-
|
|
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;
|
|
398
468
|
const consentPause = (pending) => {
|
|
399
469
|
result.status = 'input-required';
|
|
400
470
|
result.consent = pending;
|
|
401
471
|
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}`);
|
|
472
|
+
ev.log?.(`⏸ Authorization needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.provider}`);
|
|
403
473
|
return finish();
|
|
404
474
|
};
|
|
405
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;
|
|
406
480
|
if (!decision && !isWorkflowSyndicate(config)) {
|
|
407
|
-
|
|
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;
|
|
408
492
|
if (open) {
|
|
409
493
|
if (!credentialStore) {
|
|
410
494
|
result.status = 'failed';
|
|
@@ -413,16 +497,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
413
497
|
}
|
|
414
498
|
let granted = false;
|
|
415
499
|
try {
|
|
416
|
-
granted = !!open.provider && !!(await credentialStore.get({ appName, userId, provider: open.provider }));
|
|
500
|
+
granted = !!open.provider && !!(await credentialStore.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
417
501
|
}
|
|
418
502
|
catch {
|
|
419
503
|
// Expired or unreadable: not granted. The request stands.
|
|
420
504
|
}
|
|
421
505
|
if (!granted)
|
|
422
506
|
return consentPause(open);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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}`);
|
|
426
515
|
}
|
|
427
516
|
}
|
|
428
517
|
// ── Questions (lib/runtime/questions.ts) ───────────────────────────────────
|
|
@@ -433,12 +522,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
433
522
|
// A question asked inside a delegated subagent (ADR 0110) is answered the same way: the answer is stored here, and the
|
|
434
523
|
// open call that leads to the asker carries it down; `answering` names that call and the agent that made it.
|
|
435
524
|
let answering;
|
|
436
|
-
|
|
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)) {
|
|
437
528
|
const plainText = parts.length > 0 && parts.every((p) => typeof p.text === 'string');
|
|
438
529
|
const own = plainText ? pendingQuestion(existing?.events ?? []) : undefined;
|
|
439
530
|
const below = plainText && !own ? await pauseBelow(existing?.events ?? []) : undefined;
|
|
440
531
|
const question = own ?? below?.question;
|
|
441
|
-
if (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) {
|
|
442
540
|
answering = below ? { agent: below.path[0], id: below.callIds[0] } : { agent: question.node, id: question.id };
|
|
443
541
|
parts = [questionAnswerPart(question.id, messageText)];
|
|
444
542
|
ev.log?.(`✓ Answer to ${(question.path ?? [question.node]).join(' → ')}'s question`);
|
|
@@ -447,13 +545,19 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
447
545
|
/** The agent a dispatch turn must resume, and the call its interrupted turn holds. */
|
|
448
546
|
const resumeTarget = resumingBelow
|
|
449
547
|
? { agent: resumingBelow.path[0], id: resumingBelow.callIds[0], why: 'resuming an approval' }
|
|
450
|
-
:
|
|
451
|
-
? { agent:
|
|
452
|
-
:
|
|
453
|
-
? { agent:
|
|
454
|
-
:
|
|
455
|
-
? {
|
|
456
|
-
:
|
|
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;
|
|
457
561
|
/** After the answering run: is a call now waiting for a grant? */
|
|
458
562
|
const awaitingConsent = async (agentName) => {
|
|
459
563
|
if (!opts.toolCredentials?.consent)
|
|
@@ -485,6 +589,8 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
485
589
|
const below = await pauseBelow(after?.events ?? [], agentName);
|
|
486
590
|
if (below?.approval)
|
|
487
591
|
return pause(below.approval);
|
|
592
|
+
if (below?.consent)
|
|
593
|
+
return consentPause(below.consent);
|
|
488
594
|
if (below?.question) {
|
|
489
595
|
result.status = 'input-required';
|
|
490
596
|
result.input = below.question;
|
|
@@ -501,6 +607,16 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
501
607
|
ev.log?.(`⏸ Approval needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.tool}`);
|
|
502
608
|
return finish();
|
|
503
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(' → ')})` : ''}`);
|
|
616
|
+
return finish();
|
|
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);
|
|
504
620
|
/** Run ONE agent against ONE session, under the turn's controls. */
|
|
505
621
|
const runAgent = async (params) => {
|
|
506
622
|
let stream;
|
|
@@ -541,7 +657,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
541
657
|
...(selfCorrection ? { selfCorrection } : {}),
|
|
542
658
|
// Tool credentials and the consent step (ADR 0072, ADR 0085); the classifier's lane lists no tools.
|
|
543
659
|
...(credentialStore ? { credentials: credentialStore } : {}),
|
|
544
|
-
...(
|
|
660
|
+
...(consentStep && params.stage !== 'classify' ? { consent: consentStep } : {}),
|
|
545
661
|
...(compileOpts.log ? { log: compileOpts.log } : {}),
|
|
546
662
|
});
|
|
547
663
|
}
|
|
@@ -588,15 +704,27 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
588
704
|
};
|
|
589
705
|
/**
|
|
590
706
|
* 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
|
-
*
|
|
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`).
|
|
600
728
|
*/
|
|
601
729
|
const runWorkflowRoute = async (route, agent, resolution) => {
|
|
602
730
|
const store = sessionService;
|
|
@@ -605,14 +733,21 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
605
733
|
if (!shared)
|
|
606
734
|
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
607
735
|
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
608
|
-
|
|
609
|
-
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 };
|
|
610
740
|
const before = new Map(Object.entries(kept(child.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
611
741
|
const invocationId = `e-${randomUUID()}`;
|
|
612
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
|
+
};
|
|
613
748
|
let walked;
|
|
614
749
|
try {
|
|
615
|
-
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' });
|
|
616
751
|
}
|
|
617
752
|
catch (err) {
|
|
618
753
|
const last = control.stopReason ? undefined : err;
|
|
@@ -623,9 +758,22 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
623
758
|
result.error = { code: last instanceof UnsupportedWorkflowResumeError ? 'RESUME_UNSUPPORTED' : last instanceof NodeRunLimitError ? NODE_RUN_LIMIT : 'NODE_FAILED', message: last.message };
|
|
624
759
|
return { failed: finish() };
|
|
625
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
|
+
}
|
|
626
775
|
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)));
|
|
776
|
+
const stateDelta = await written();
|
|
629
777
|
const answered = createTurnEvent({
|
|
630
778
|
invocationId,
|
|
631
779
|
author: route,
|
|
@@ -636,6 +784,77 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
636
784
|
}
|
|
637
785
|
return { answer: walked };
|
|
638
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);
|
|
639
858
|
let answer;
|
|
640
859
|
if (isDispatchSyndicate(config)) {
|
|
641
860
|
// ══ PLAN ═════════════════════════════════════════════════════════════
|
|
@@ -651,7 +870,7 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
651
870
|
throw new Error(`Call ${resumeTarget.id} was raised by '${resumeTarget.agent}', which is not a route of this syndicate.`);
|
|
652
871
|
}
|
|
653
872
|
resolution = { route: resumeTarget.agent, reason: resumeTarget.why, fellBack: false, fallbackReason: '', viaOverride: false };
|
|
654
|
-
decidedBy = resuming ? 'approval' : granting ? 'consent' : 'answer';
|
|
873
|
+
decidedBy = resuming ? 'approval' : granting || consentRoute ? 'consent' : 'answer';
|
|
655
874
|
}
|
|
656
875
|
else if (resolution) {
|
|
657
876
|
ev.log?.(`⇄ Route pinned by override: ${resolution.route}`);
|
|
@@ -716,12 +935,29 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
716
935
|
// projection: other agents' turns would otherwise read as user speech
|
|
717
936
|
// mixed with their tool payloads (lib/session/transcript.ts).
|
|
718
937
|
const routeAgent = await compileRoute(routeCfg);
|
|
719
|
-
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) {
|
|
720
951
|
// A workflow route walks its whole graph on its own child session (ADR 0106).
|
|
721
952
|
const walked = await runWorkflowRoute(routeCfg.name, routeAgent, resolution);
|
|
722
953
|
if ('failed' in walked)
|
|
723
954
|
return walked.failed;
|
|
724
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
|
+
}
|
|
725
961
|
}
|
|
726
962
|
else
|
|
727
963
|
answer = await runAgent({
|
|
@@ -732,13 +968,15 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
732
968
|
// is replayed raw, so the loop finds the call the message answers.
|
|
733
969
|
sessions: new ProjectedSessionService(sessionService, routeCfg.name, resumingBelow
|
|
734
970
|
? { rawFrom: (events) => turnStartOfCall(events, resumingBelow.callIds[0]) }
|
|
735
|
-
:
|
|
736
|
-
? { rawFrom: (events) =>
|
|
737
|
-
:
|
|
738
|
-
? { rawFrom: (events) =>
|
|
739
|
-
:
|
|
740
|
-
? { rawFrom: (events) => turnStartOfCall(events,
|
|
741
|
-
:
|
|
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
|
+
: {}),
|
|
742
980
|
stage: 'dispatch',
|
|
743
981
|
route: resolution,
|
|
744
982
|
publishToolStatus: true,
|
|
@@ -788,10 +1026,11 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
788
1026
|
// resumes the graph where it waited.
|
|
789
1027
|
// While a gated call waits, a message that is not its decision repeats the request: the walk cannot move past
|
|
790
1028
|
// the waiting node, so nothing is stored and nothing runs, as the A2A server answers it (ADR 0098).
|
|
791
|
-
|
|
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) {
|
|
792
1031
|
const open = pendingApproval(existing?.events ?? []);
|
|
793
1032
|
if (open)
|
|
794
|
-
return pause(open);
|
|
1033
|
+
return pause(await deepen(open));
|
|
795
1034
|
}
|
|
796
1035
|
const workflowAgent = await compileNativeWorkflowAgent(config, compileOpts);
|
|
797
1036
|
try {
|
|
@@ -828,21 +1067,14 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
828
1067
|
result.error = answer.error;
|
|
829
1068
|
return finish();
|
|
830
1069
|
}
|
|
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
|
-
}
|
|
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]));
|
|
846
1078
|
result.text = answer.text;
|
|
847
1079
|
if (!result.text) {
|
|
848
1080
|
const failed = answer.nodeErrors[answer.nodeErrors.length - 1];
|
|
@@ -907,7 +1139,8 @@ async function runTurnInner(opts, control, _runtime) {
|
|
|
907
1139
|
// Named in the syndicate's `guards:` list (and any nested syndicate's). They
|
|
908
1140
|
// REWRITE rather than retry, on the answering turn's text with every tool
|
|
909
1141
|
// result it produced. The classifier never reaches here.
|
|
910
|
-
|
|
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);
|
|
911
1144
|
if (result.text && guardNames.length) {
|
|
912
1145
|
const inputs = answer.toolResultTexts;
|
|
913
1146
|
for (const guard of resolveGuards(guardNames, (n) => ev.warn?.(`Unknown guard '${n}' — ignored`))) {
|
|
@@ -45,6 +45,47 @@ export interface TurnControl {
|
|
|
45
45
|
stopReason?: TurnStopReason;
|
|
46
46
|
/** Stop the turn. The first reason wins. */
|
|
47
47
|
stop(reason: TurnStopReason): void;
|
|
48
|
+
/**
|
|
49
|
+
* Runs a nested dispatch syndicate as its own turn on its own
|
|
50
|
+
* conversation, under this turn's controls (ADR 0120): set by the turn
|
|
51
|
+
* runner (lib/runtime/syndicateTurn.ts), read where a nested dispatch
|
|
52
|
+
* syndicate runs (a delegated call, a dispatch route, a workflow node),
|
|
53
|
+
* which sit below the turn runner and cannot import it.
|
|
54
|
+
*/
|
|
55
|
+
nestedDispatch?: (run: NestedDispatchRun) => Promise<NestedDispatchEnd>;
|
|
56
|
+
}
|
|
57
|
+
/** One run of a nested dispatch syndicate (ADR 0120): what it is, where its conversation is filed, and the message. */
|
|
58
|
+
export interface NestedDispatchRun {
|
|
59
|
+
/** The nested syndicate's config (a SyndicateYamlConfig with a `dispatch:` block). */
|
|
60
|
+
config: unknown;
|
|
61
|
+
/** The compile options it loads its routes with (CompileOptions, its nesting chain included). */
|
|
62
|
+
compile: unknown;
|
|
63
|
+
/** The entry's name: the nested syndicate as its caller lists it. */
|
|
64
|
+
name: string;
|
|
65
|
+
/** The store, and the key its conversation is filed under (already opened by the caller). */
|
|
66
|
+
sessions: unknown;
|
|
67
|
+
appName: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
/** The message's parts: the request, or the answer to what waits in it. */
|
|
71
|
+
parts: unknown[];
|
|
72
|
+
}
|
|
73
|
+
/** How a nested dispatch syndicate's turn ended (ADR 0120). */
|
|
74
|
+
export interface NestedDispatchEnd {
|
|
75
|
+
status: 'completed' | 'input-required' | 'failed' | 'canceled';
|
|
76
|
+
/** The route's final text: the nested syndicate's answer. */
|
|
77
|
+
text: string;
|
|
78
|
+
/** Its conversation's state writes this run, `temp:` keys aside. */
|
|
79
|
+
stateDelta: Record<string, unknown>;
|
|
80
|
+
/** What it waits on, when paused: the request's id, and the request as the turn reports it (a PendingApproval, a PendingInput, a PendingConsent). */
|
|
81
|
+
interruptId?: string;
|
|
82
|
+
approval?: unknown;
|
|
83
|
+
input?: unknown;
|
|
84
|
+
consent?: unknown;
|
|
85
|
+
error?: {
|
|
86
|
+
code: string;
|
|
87
|
+
message: string;
|
|
88
|
+
};
|
|
48
89
|
}
|
|
49
90
|
export interface TurnControlOptions {
|
|
50
91
|
maxLlmCalls?: number;
|
|
@@ -232,6 +232,15 @@ export declare class ProjectedSessionService implements SessionService {
|
|
|
232
232
|
* The event lands in the projected session the runtime holds and, through
|
|
233
233
|
* the store, in the real one `get` read. A session `get` did not hand out
|
|
234
234
|
* is the real one, and the store alone applies the event to it.
|
|
235
|
+
*
|
|
236
|
+
* A store may answer an append with events of its own: a durable run's
|
|
237
|
+
* store restoring a checkpoint (lib/runtime/native/checkpoint.ts, ADR 0113)
|
|
238
|
+
* answers the opening message with the run's stored events, the stored
|
|
239
|
+
* opening returned in place of the one given. The projected session then
|
|
240
|
+
* takes, in place of the event given, every event the real session gained,
|
|
241
|
+
* unprojected: they are the current turn, which is never projected. The
|
|
242
|
+
* agent loop thereby reads the restored steps, as it would on the store's
|
|
243
|
+
* own session.
|
|
235
244
|
*/
|
|
236
245
|
append(session: EngineSession, event: TurnEvent): Promise<TurnEvent>;
|
|
237
246
|
}
|