melchizedek-agents 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -2
- package/CHANGELOG.md +333 -0
- package/DOCUMENTATION.md +99 -39
- package/README.md +1 -0
- 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 +6 -6
- package/config/agents/examples/critic.yaml +16 -16
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +33 -32
- 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 +1979 -0
- package/config/agents/syndicateSchema.yaml +109 -72
- 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 +61 -60
- 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 +21 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +15 -0
- package/dist/lib/a2a/app.js +53 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +34 -4
- package/dist/lib/a2a/oauthSetup.d.ts +90 -0
- package/dist/lib/a2a/oauthSetup.js +205 -0
- package/dist/lib/agentDialect.d.ts +66 -0
- package/dist/lib/agentDialect.js +134 -0
- package/dist/lib/compile.d.ts +10 -5
- package/dist/lib/compile.js +69 -37
- package/dist/lib/compileNative.js +2 -0
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +34 -1
- package/dist/lib/doctor.js +47 -3
- package/dist/lib/loadSyndicate.d.ts +45 -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 +16 -1
- package/dist/lib/models/capabilities.js +55 -1
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/runtime/approvals.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
- package/dist/lib/runtime/native/agentLoop.js +105 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
- package/dist/lib/runtime/native/checkpoint.js +365 -0
- package/dist/lib/runtime/native/delegate.d.ts +141 -18
- package/dist/lib/runtime/native/delegate.js +235 -39
- package/dist/lib/runtime/native/interrupts.d.ts +101 -2
- package/dist/lib/runtime/native/interrupts.js +201 -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 +7 -1
- package/dist/lib/runtime/syndicateTurn.js +74 -22
- package/dist/lib/runtime/turnControl.d.ts +15 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/storage/postgres/index.d.ts +6 -1
- package/dist/lib/storage/postgres/index.js +7 -2
- package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
- package/dist/lib/storage/postgres/taskQueue.js +20 -3
- 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 +278 -0
- package/dist/lib/syndicateSchema.js +182 -15
- package/dist/lib/tools/auth.d.ts +7 -5
- package/dist/lib/tools/auth.js +12 -4
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthHosts.d.ts +71 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +233 -0
- package/dist/lib/tools/oauthTools.js +421 -0
- package/dist/lib/tools/openapiTools.d.ts +4 -6
- package/dist/lib/tools/openapiTools.js +40 -36
- package/dist/lib/tools/taskTools.d.ts +30 -5
- package/dist/lib/tools/taskTools.js +74 -10
- package/dist/lib/workflow.d.ts +8 -4
- package/dist/lib/workflow.js +8 -4
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +122 -1
- package/dist/scripts/assistant_worker.d.ts +10 -0
- package/dist/scripts/assistant_worker.js +69 -19
- package/dist/scripts/doctor.d.ts +4 -1
- package/dist/scripts/doctor.js +5 -2
- package/dist/scripts/yaml_codemod.d.ts +52 -0
- package/dist/scripts/yaml_codemod.js +570 -0
- package/package.json +11 -7
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +4 -3
- package/skills/briefs/melchizedek.md +1 -0
- 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 +6 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { trace, context, defaultTextMapGetter, ROOT_CONTEXT } from '@opentelemetry/api';
|
|
1
|
+
import { trace, context, defaultTextMapGetter, ROOT_CONTEXT, INVALID_SPAN_CONTEXT } from '@opentelemetry/api';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { ADK_SPAN_SCOPE, RUNTIME_SPAN_SCOPE, TELEMETRY_SCHEMA_VERSION, agentOfSpanName, engineVersion, isModelCallSpan, isToolSpanName } from './lineage.js';
|
|
4
4
|
import { FilteringSpanExporter, otlpContentMode } from './otlpFilter.js';
|
|
5
5
|
import { telemetryRedactor } from './redact.js';
|
|
6
|
-
import { chargeLlmCall, chargeTokens } from '../runtime/turnControl.js';
|
|
6
|
+
import { chargeLlmCall, chargeTokens, turnUntraced } from '../runtime/turnControl.js';
|
|
7
7
|
import sdkNode from '@opentelemetry/sdk-trace-node';
|
|
8
8
|
const { NodeTracerProvider } = sdkNode;
|
|
9
9
|
import sdkBase from '@opentelemetry/sdk-trace-base';
|
|
@@ -275,6 +275,26 @@ export function initializeTracing() {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
const tracer = trace.getTracer('melchizedek-tracer');
|
|
278
|
+
/**
|
|
279
|
+
* A span that records nothing, for code running in a turn that opted out of
|
|
280
|
+
* tracing (`runSyndicateTurn({ trace: false })`, turnUntraced): its
|
|
281
|
+
* attributes, events and end go nowhere, and no tracer is started for it.
|
|
282
|
+
*/
|
|
283
|
+
function nonRecordingSpan() {
|
|
284
|
+
return trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Starts a span on the named tracer, starting the tracer first; in a turn
|
|
288
|
+
* that opted out of tracing, a non-recording span and no tracer. Every span
|
|
289
|
+
* the engine opens inside a turn goes through here or checks turnUntraced
|
|
290
|
+
* itself.
|
|
291
|
+
*/
|
|
292
|
+
export function startEngineSpan(tracerName, name, options) {
|
|
293
|
+
if (turnUntraced())
|
|
294
|
+
return nonRecordingSpan();
|
|
295
|
+
initializeTracing();
|
|
296
|
+
return trace.getTracer(tracerName).startSpan(name, options);
|
|
297
|
+
}
|
|
278
298
|
/** Turn identity by trace id, so llm.request children can carry it too. */
|
|
279
299
|
const turnContexts = new Map();
|
|
280
300
|
/**
|
|
@@ -328,6 +348,11 @@ export function callerSpanContext(traceparent) {
|
|
|
328
348
|
return trace.getSpanContext(ctx);
|
|
329
349
|
}
|
|
330
350
|
export async function* traceAgentRun(stream, metadata) {
|
|
351
|
+
// A turn that opted out of tracing gets its events untouched and no span.
|
|
352
|
+
if (turnUntraced()) {
|
|
353
|
+
yield* stream;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
331
356
|
initializeTracing();
|
|
332
357
|
const caller = callerSpanContext(metadata.traceparent);
|
|
333
358
|
const span = tracer.startSpan(`Syndicate Execution: ${metadata.syndicateName}`, caller ? { links: [{ context: caller }], attributes: { 'caller.trace_id': caller.traceId } } : undefined);
|
|
@@ -551,7 +576,10 @@ function refineErrorCode(code, message) {
|
|
|
551
576
|
* (e.g. llm.web_search.native) via setLlmSpanAttribute below.
|
|
552
577
|
*/
|
|
553
578
|
export async function* traceLlmGeneration(meta, inner) {
|
|
554
|
-
|
|
579
|
+
// An untraced turn still charges the call and its tokens; it opens no span.
|
|
580
|
+
const untraced = turnUntraced();
|
|
581
|
+
if (!untraced)
|
|
582
|
+
initializeTracing();
|
|
555
583
|
// Every model call on every provider passes here, so this is where the
|
|
556
584
|
// turn's step budget and cancellation are enforced (lib/runtime/
|
|
557
585
|
// turnControl.ts). A refused call never reaches the provider.
|
|
@@ -561,7 +589,7 @@ export async function* traceLlmGeneration(meta, inner) {
|
|
|
561
589
|
yield { errorCode: charge.code, errorMessage: charge.message };
|
|
562
590
|
return;
|
|
563
591
|
}
|
|
564
|
-
const span = tracer.startSpan('llm.request');
|
|
592
|
+
const span = untraced ? nonRecordingSpan() : tracer.startSpan('llm.request');
|
|
565
593
|
span.setAttribute('llm.provider', meta.provider);
|
|
566
594
|
span.setAttribute('llm.model', meta.model);
|
|
567
595
|
// OpenTelemetry GenAI semantic conventions alongside the llm.* names, so
|
|
@@ -633,7 +661,7 @@ export async function* traceLlmGeneration(meta, inner) {
|
|
|
633
661
|
if (thinkingPreview) {
|
|
634
662
|
span.addEvent('llm.thinking', { 'thinking.preview': thinkingPreview });
|
|
635
663
|
}
|
|
636
|
-
if (errorCode) {
|
|
664
|
+
if (errorCode && span.isRecording()) {
|
|
637
665
|
span.setAttribute('llm.error_code', errorCode);
|
|
638
666
|
span.setAttribute('llm.error_message', errorMessage.slice(0, ERROR_MESSAGE_MAX_CHARS));
|
|
639
667
|
// Make this span a payload candidate (see supabaseSpanExporter's
|
|
@@ -24,6 +24,12 @@ export interface PendingApproval {
|
|
|
24
24
|
args: Record<string, unknown>;
|
|
25
25
|
/** The original call's id. */
|
|
26
26
|
callId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* For a call gated inside a delegated subagent (ADR 0110): the agents from
|
|
29
|
+
* the turn's own agent down to the one that asked, `agent` last. Absent
|
|
30
|
+
* when the turn's own agent asked.
|
|
31
|
+
*/
|
|
32
|
+
path?: string[];
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
35
|
* The approval request still waiting for an answer, or undefined. A request
|
|
@@ -72,6 +72,17 @@
|
|
|
72
72
|
*
|
|
73
73
|
* DELEGATION: a call to a subagent tool runs the subagent as its own child
|
|
74
74
|
* loop, as ADK's AgentTool runs it (lib/runtime/native/delegate.ts, ADR 0074).
|
|
75
|
+
* A step's delegated calls run at once through the step's DelegationGate,
|
|
76
|
+
* at most the agent's `maxConcurrency` (the syndicate's `max_concurrency`)
|
|
77
|
+
* at a time, entered in call order, their responses stored in call order
|
|
78
|
+
* (ADR 0116).
|
|
79
|
+
* A child that ends paused leaves the call open (ADR 0110): no response is
|
|
80
|
+
* stored for it, the step's other responses are, and the run ends paused on
|
|
81
|
+
* the call's id. Before each step, after the approval resume, a delegated
|
|
82
|
+
* call whose pause the latest user message answers is resumed with that
|
|
83
|
+
* answer (interrupts.ts resumedDelegations, delegate.ts resumeSubagent) and
|
|
84
|
+
* its response stored; while another open call still waits, the run ends
|
|
85
|
+
* paused again on it without a model step.
|
|
75
86
|
*
|
|
76
87
|
* SELF-CORRECTION (ADR 0034, ADR 0075) is ADK's reflect-and-retry plugins,
|
|
77
88
|
* ported to lib/runtime/native/selfCorrection.ts: the step declares the
|
|
@@ -162,6 +173,12 @@ export interface AgentLoopContext extends Omit<ModelStepOptions, 'agent' | 'adap
|
|
|
162
173
|
* a delegated subagent's loop.
|
|
163
174
|
*/
|
|
164
175
|
consent?: Pick<OAuthConsent, 'has' | 'begin'>;
|
|
176
|
+
/**
|
|
177
|
+
* The run is a delegated subagent's, on its own child session filed under
|
|
178
|
+
* its agent path (lib/runtime/native/delegate.ts childAppName, ADR 0111):
|
|
179
|
+
* its own delegations are filed below that path. Set by runChild only.
|
|
180
|
+
*/
|
|
181
|
+
delegated?: boolean;
|
|
165
182
|
}
|
|
166
183
|
/** How the run ended. */
|
|
167
184
|
export type AgentLoopEndReason =
|
|
@@ -181,7 +198,7 @@ export interface AgentLoopEnd {
|
|
|
181
198
|
steps: number;
|
|
182
199
|
/** The last event stored. */
|
|
183
200
|
lastEvent?: TurnEvent;
|
|
184
|
-
/** For `paused`: the ids waiting on a person (the ask_user call ids,
|
|
201
|
+
/** For `paused`: the ids waiting on a person (the ask_user call ids, the adk_request_confirmation call ids, or the delegated calls whose child waits; ADR 0110). */
|
|
185
202
|
pending?: string[];
|
|
186
203
|
/** For `stopped`: the turn's code and message. */
|
|
187
204
|
stop?: StepStop;
|
|
@@ -72,6 +72,17 @@
|
|
|
72
72
|
*
|
|
73
73
|
* DELEGATION: a call to a subagent tool runs the subagent as its own child
|
|
74
74
|
* loop, as ADK's AgentTool runs it (lib/runtime/native/delegate.ts, ADR 0074).
|
|
75
|
+
* A step's delegated calls run at once through the step's DelegationGate,
|
|
76
|
+
* at most the agent's `maxConcurrency` (the syndicate's `max_concurrency`)
|
|
77
|
+
* at a time, entered in call order, their responses stored in call order
|
|
78
|
+
* (ADR 0116).
|
|
79
|
+
* A child that ends paused leaves the call open (ADR 0110): no response is
|
|
80
|
+
* stored for it, the step's other responses are, and the run ends paused on
|
|
81
|
+
* the call's id. Before each step, after the approval resume, a delegated
|
|
82
|
+
* call whose pause the latest user message answers is resumed with that
|
|
83
|
+
* answer (interrupts.ts resumedDelegations, delegate.ts resumeSubagent) and
|
|
84
|
+
* its response stored; while another open call still waits, the run ends
|
|
85
|
+
* paused again on it without a model step.
|
|
75
86
|
*
|
|
76
87
|
* SELF-CORRECTION (ADR 0034, ADR 0075) is ADK's reflect-and-retry plugins,
|
|
77
88
|
* ported to lib/runtime/native/selfCorrection.ts: the step declares the
|
|
@@ -125,8 +136,8 @@ import { MAX_VALUE_DEPTH, nestedDeeperThan } from '../valueDepth.js';
|
|
|
125
136
|
import { currentTurnSignal } from '../turnControl.js';
|
|
126
137
|
import { compactBeforeStep } from './compaction.js';
|
|
127
138
|
import { ADK_CALL_ID_PREFIX } from './history.js';
|
|
128
|
-
import { runSubagent, runWorkflowSubagent, subagentOf, workflowSubagentOf } from './delegate.js';
|
|
129
|
-
import { approvedCalls, grantedCalls } from './interrupts.js';
|
|
139
|
+
import { DEFAULT_MAX_CONCURRENCY, DelegationGate, SubagentPause, resumeSubagent, resumeWorkflowSubagent, runSubagent, runWorkflowSubagent, subagentOf, workflowSubagentOf } from './delegate.js';
|
|
140
|
+
import { approvedCalls, grantedCalls, resumedDelegations } from './interrupts.js';
|
|
130
141
|
import { SelfCorrection } from './selfCorrection.js';
|
|
131
142
|
import { eitherSignal, runModelStep, stopOf } from './step.js';
|
|
132
143
|
import { taskNodeRun } from './taskMode.js';
|
|
@@ -342,7 +353,7 @@ async function runOwnTool(tool, args, context) {
|
|
|
342
353
|
throw new Error(`Error in tool '${tool.name}': ${message}`);
|
|
343
354
|
}
|
|
344
355
|
}
|
|
345
|
-
async function runCall(scope, call, tools, correction, confirmation) {
|
|
356
|
+
async function runCall(scope, call, tools, correction, confirmation, resume, ticket) {
|
|
346
357
|
const context = callContext(scope, call.id || undefined, confirmation);
|
|
347
358
|
const name = call.name ?? '';
|
|
348
359
|
const tool = name && tools.has(name) ? tools.get(name) : undefined;
|
|
@@ -363,17 +374,26 @@ async function runCall(scope, call, tools, correction, confirmation) {
|
|
|
363
374
|
// Delegation (WS2-6): a subagent runs as its own child loop, a nested workflow as its own walk (ADR 0098), before the generic path (delegate.ts).
|
|
364
375
|
const subagent = subagentOf(tool);
|
|
365
376
|
const workflow = subagent ? undefined : workflowSubagentOf(tool);
|
|
366
|
-
const delegation = { ctx: scope.ctx, stateBase: scope.stateBase, signal: scope.signal, runLoop: runAgentLoop
|
|
377
|
+
const delegation = { ctx: scope.ctx, caller: scope.agent.name, stateBase: scope.stateBase, signal: scope.signal, runLoop: runAgentLoop };
|
|
367
378
|
let response;
|
|
368
379
|
let failure;
|
|
369
380
|
try {
|
|
381
|
+
// ADR 0116: a delegated call waits for its turn in the step's gate, and frees it as soon as the child's run ends.
|
|
382
|
+
if (ticket && (subagent || workflow))
|
|
383
|
+
await ticket.ready;
|
|
370
384
|
response = subagent
|
|
371
|
-
?
|
|
385
|
+
? resume
|
|
386
|
+
? await resumeSubagent(subagent, resume, context, delegation)
|
|
387
|
+
: await runSubagent(subagent, args, context, delegation)
|
|
372
388
|
: workflow
|
|
373
|
-
?
|
|
389
|
+
? resume
|
|
390
|
+
? await resumeWorkflowSubagent(workflow, resume, context, delegation)
|
|
391
|
+
: await runWorkflowSubagent(workflow, args, context, delegation)
|
|
374
392
|
: await runOwnTool(tool, args, context);
|
|
393
|
+
ticket?.release();
|
|
375
394
|
}
|
|
376
395
|
catch (e) {
|
|
396
|
+
ticket?.release();
|
|
377
397
|
failure = e instanceof Error ? e.message : e;
|
|
378
398
|
// Self-correction answers a thrown Error with reflection guidance in its place; a call waiting on a grant is not a failure.
|
|
379
399
|
const guided = isEmptyRecord(context.actions.requestedAuthConfigs) ? await correction?.failed(toolName, args, e) : undefined;
|
|
@@ -386,6 +406,12 @@ async function runCall(scope, call, tools, correction, confirmation) {
|
|
|
386
406
|
const provider = String(asked[0]?.credentialKey ?? '');
|
|
387
407
|
return { part: { functionResponse: { id: context.functionCallId, name: toolName, response: { result: CONSENT_TEXTS.pending(provider) } } }, actions: context.actions };
|
|
388
408
|
}
|
|
409
|
+
// A child that ended paused (ADR 0110): the call stays open, with no response, and the run ends paused on it.
|
|
410
|
+
if (response instanceof SubagentPause) {
|
|
411
|
+
if (context.functionCallId)
|
|
412
|
+
(scope.paused ??= []).push(context.functionCallId);
|
|
413
|
+
return isDefaultActions(context.actions) ? undefined : { actions: context.actions };
|
|
414
|
+
}
|
|
389
415
|
if (failure === undefined)
|
|
390
416
|
await correction?.answered(toolName, response);
|
|
391
417
|
// As ADK: a long-running call with no response answers nothing, even when it threw.
|
|
@@ -401,10 +427,33 @@ async function runCall(scope, call, tools, correction, confirmation) {
|
|
|
401
427
|
* The calls run, in parallel, as the one response event ADK stores for them
|
|
402
428
|
* (not yet stored); undefined when none answered. `confirmations` holds the
|
|
403
429
|
* person's answer for a pinned call an approval resumes, by call id.
|
|
430
|
+
*
|
|
431
|
+
* Delegated calls (a subagent, a nested workflow) run through the step's
|
|
432
|
+
* DelegationGate (ADR 0116): at most the agent's `maxConcurrency` at once,
|
|
433
|
+
* entered in call order, two calls to one subagent one after the other.
|
|
434
|
+
* Other calls start at once, as before. The event is built once every call
|
|
435
|
+
* has answered, its parts in call order, and the open calls a paused child
|
|
436
|
+
* left (scope.paused) are listed in call order too: neither depends on
|
|
437
|
+
* which call finished first.
|
|
404
438
|
*/
|
|
405
|
-
async function runCalls(scope, calls, tools, confirmations) {
|
|
439
|
+
async function runCalls(scope, calls, tools, confirmations, resumes) {
|
|
406
440
|
const order = scope.selfCorrection?.forCalls(scope.ctx.invocationId, calls.length);
|
|
407
|
-
const
|
|
441
|
+
const gate = new DelegationGate(scope.agent.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
|
|
442
|
+
const outcomes = (await Promise.all(calls.map((call, i) => {
|
|
443
|
+
const tool = tools.get(call.name ?? '');
|
|
444
|
+
// Entered here, synchronously and in call order, before any call runs (ADR 0116).
|
|
445
|
+
const delegated = subagentOf(tool) ?? workflowSubagentOf(tool);
|
|
446
|
+
const ticket = delegated ? gate.enter(delegated.name) : undefined;
|
|
447
|
+
return traceToolCall(call, tool, () => runCall(scope, call, tools, order?.call(i), call.id ? confirmations?.get(call.id) : undefined, call.id ? resumes?.get(call.id) : undefined, ticket)).finally(() => {
|
|
448
|
+
ticket?.release();
|
|
449
|
+
order?.release(i);
|
|
450
|
+
});
|
|
451
|
+
}))).filter((o) => !!o);
|
|
452
|
+
// The open calls, in call order rather than the order their children paused in.
|
|
453
|
+
if (scope.paused && scope.paused.length > 1) {
|
|
454
|
+
const at = (id) => calls.findIndex((c) => c.id === id);
|
|
455
|
+
scope.paused.sort((a, b) => at(a) - at(b));
|
|
456
|
+
}
|
|
408
457
|
if (outcomes.length === 0)
|
|
409
458
|
return undefined;
|
|
410
459
|
const base = { invocationId: scope.ctx.invocationId, author: scope.agent.name };
|
|
@@ -503,6 +552,32 @@ async function resumeGrants(agent, ctx, stateBase, selfCorrection) {
|
|
|
503
552
|
const auth = authEvent(scope, response);
|
|
504
553
|
return { response, ...(auth ? { auth } : {}) };
|
|
505
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Delegated calls the latest user message resumes (ADR 0110), before a step
|
|
557
|
+
* and after the approval resume: each open call whose child waits on what
|
|
558
|
+
* the message answers runs the child on with that answer, and their
|
|
559
|
+
* response is returned to be stored, with the open calls still paused:
|
|
560
|
+
* those the child paused again, and those the message did not answer.
|
|
561
|
+
* Undefined when the message answers none; 'stopped' when the turn stopped
|
|
562
|
+
* while they ran (nothing is stored).
|
|
563
|
+
*/
|
|
564
|
+
async function resumeDelegations(agent, ctx, stateBase, selfCorrection) {
|
|
565
|
+
const resumed = await resumedDelegations(agent, ctx, ctx.sessions);
|
|
566
|
+
if (!resumed)
|
|
567
|
+
return undefined;
|
|
568
|
+
const calls = resumed.calls.filter((c) => {
|
|
569
|
+
const tool = resumed.tools.get(c.name ?? '');
|
|
570
|
+
return subagentOf(tool) || workflowSubagentOf(tool);
|
|
571
|
+
});
|
|
572
|
+
if (calls.length === 0)
|
|
573
|
+
return undefined;
|
|
574
|
+
const signal = eitherSignal(ctx.signal, currentTurnSignal());
|
|
575
|
+
const scope = { agent, ctx, stateBase, selfCorrection, ...(signal ? { signal } : {}) };
|
|
576
|
+
const response = await runCalls(scope, calls, resumed.tools, undefined, resumed.answers);
|
|
577
|
+
if (signal?.aborted)
|
|
578
|
+
return 'stopped';
|
|
579
|
+
return { ...(response ? { response } : {}), paused: [...(scope.paused ?? []), ...resumed.waiting] };
|
|
580
|
+
}
|
|
506
581
|
// ── The model step, with partials and the fallback ───────────────────────────
|
|
507
582
|
/** Runs a step, yielding its partial events as they arrive; returns the step's result. */
|
|
508
583
|
async function* withPartials(start) {
|
|
@@ -640,6 +715,18 @@ async function* agentLoop(agent, ctx) {
|
|
|
640
715
|
lastEvent = await store(resumed);
|
|
641
716
|
yield lastEvent;
|
|
642
717
|
}
|
|
718
|
+
// Delegation hook (WS6-2a, ADR 0110): an answer to a pause inside a delegated call resumes that call before the step.
|
|
719
|
+
const redelegated = await resumeDelegations(agent, ctx, withStateOverlay(session.state, runTemp.values()), selfCorrection);
|
|
720
|
+
if (redelegated === 'stopped')
|
|
721
|
+
return { reason: 'stopped', steps, lastEvent, stop: stopOf() };
|
|
722
|
+
if (redelegated) {
|
|
723
|
+
if (redelegated.response) {
|
|
724
|
+
lastEvent = await store(redelegated.response);
|
|
725
|
+
yield lastEvent;
|
|
726
|
+
}
|
|
727
|
+
if (redelegated.paused.length > 0)
|
|
728
|
+
return { reason: 'paused', steps, lastEvent, pending: redelegated.paused };
|
|
729
|
+
}
|
|
643
730
|
// Compaction hook (WS2-9, lib/runtime/native/compaction.ts): with `context:`, the summary is stored before the step reads the history,
|
|
644
731
|
// before the step budget, as ADK's request processors run before its call count.
|
|
645
732
|
const compacted = await compactBeforeStep(agent, {
|
|
@@ -679,6 +766,8 @@ async function* agentLoop(agent, ctx) {
|
|
|
679
766
|
// As ADK: a turn that stopped while the calls ran (a long subagent run, say) stores no response.
|
|
680
767
|
if (scope.signal?.aborted)
|
|
681
768
|
return { reason: 'stopped', steps, lastEvent, stop: stopOf() };
|
|
769
|
+
// Delegated calls whose child ended paused (ADR 0110): they stay open, and the run ends paused on them once the step's responses are stored.
|
|
770
|
+
const delegated = scope.paused ?? [];
|
|
682
771
|
if (response) {
|
|
683
772
|
const auth = authEvent(scope, response);
|
|
684
773
|
if (auth) {
|
|
@@ -690,17 +779,22 @@ async function* agentLoop(agent, ctx) {
|
|
|
690
779
|
if (confirmation) {
|
|
691
780
|
lastEvent = await store(confirmation);
|
|
692
781
|
yield lastEvent;
|
|
693
|
-
return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? [])] };
|
|
782
|
+
return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? []), ...delegated] };
|
|
694
783
|
}
|
|
695
784
|
lastEvent = await store(response);
|
|
696
785
|
yield lastEvent;
|
|
697
786
|
// As ADK: the response asking for auth is final, and the run waits on its credential request (ADR 0085).
|
|
698
787
|
if (authIds.length > 0)
|
|
699
|
-
return { reason: 'paused', steps, lastEvent, pending: authIds };
|
|
788
|
+
return { reason: 'paused', steps, lastEvent, pending: [...authIds, ...delegated] };
|
|
700
789
|
stepEnd = lastEvent;
|
|
701
|
-
if (task?.finished)
|
|
790
|
+
if (task?.finished && delegated.length === 0)
|
|
702
791
|
return { reason: 'final', steps, lastEvent, output: task.output };
|
|
703
792
|
}
|
|
793
|
+
if (delegated.length > 0) {
|
|
794
|
+
const answered = new Set(getFunctionResponses(stepEnd).map((r) => r.id));
|
|
795
|
+
const longRunning = (modelEvent.longRunningToolIds ?? []).filter((id) => !answered.has(id));
|
|
796
|
+
return { reason: 'paused', steps, lastEvent, pending: [...longRunning, ...delegated] };
|
|
797
|
+
}
|
|
704
798
|
}
|
|
705
799
|
// ADK's runAsyncImpl: stop on a final event, unless it is an empty metadata event after tool calls.
|
|
706
800
|
const emptyMetadata = stepEnd.author === agent.name && !stepEnd.partial && !hasParts(stepEnd) && isDefaultActions(stepEnd.actions);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/runtime/native/checkpoint.ts — durable long runs: the run's sessions
|
|
3
|
+
* snapshotted at every step boundary, and a run restored from the last
|
|
4
|
+
* snapshot taking the next step (ADR 0113, ADR 0015).
|
|
5
|
+
*
|
|
6
|
+
* WHY this file exists:
|
|
7
|
+
* A background job (ADR 0015) runs as one turn on an in-process session
|
|
8
|
+
* store. A worker that stopped mid-job lost everything the job had done,
|
|
9
|
+
* and the next claim started again from the first model call. The turn
|
|
10
|
+
* runner's signature is pinned by every surface and the boundary suite
|
|
11
|
+
* (ADR 0024), so durability lives at the one seam the runner already
|
|
12
|
+
* takes: the SessionService. checkpointingSessions wraps the in-process
|
|
13
|
+
* store a run writes, hands a snapshot of every session to a sink at each
|
|
14
|
+
* step boundary, and restores a snapshot into a fresh store so that the
|
|
15
|
+
* runner, unchanged, continues where the snapshot stood. runDurableTurn
|
|
16
|
+
* is runSyndicateTurn with that store and a sink.
|
|
17
|
+
*
|
|
18
|
+
* A STEP BOUNDARY is an append that carries function responses after which
|
|
19
|
+
* every function call any session of the run holds has its response (calls
|
|
20
|
+
* listed in their event's longRunningToolIds, which wait on a person, aside).
|
|
21
|
+
* So a parent waiting on a delegated subagent is never at a boundary, the
|
|
22
|
+
* subagent's own steps are not either, and a final answer never is: a
|
|
23
|
+
* resumed run redoes the step that was in flight, and the last one.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IT GUARANTEES:
|
|
26
|
+
* - A resumed run's stored events equal an uninterrupted run's, ids and
|
|
27
|
+
* times of the steps run after the resume aside: restored events keep
|
|
28
|
+
* their ids, times and invocation ids, and their state deltas are
|
|
29
|
+
* applied again in order (InProcessSessionService.append, applyEvent).
|
|
30
|
+
* - The opening message is stored once. A session whose first event is
|
|
31
|
+
* the run's own message (the run's session, a workflow route's child)
|
|
32
|
+
* is held back on restore: it does not exist until the runner creates
|
|
33
|
+
* it, so `resumedSession` reads as on a fresh run, and the runner's
|
|
34
|
+
* append of the message is answered with the stored events, the
|
|
35
|
+
* opening one returned in its place, onto the caller's own session
|
|
36
|
+
* object. Every other session (a delegated subagent's) is restored at
|
|
37
|
+
* once, before the runner starts.
|
|
38
|
+
* - A checkpoint is used only by the run that wrote it, for the message
|
|
39
|
+
* it was written for: its runId and the sha256 of the message's parts
|
|
40
|
+
* must match, and its events must parse (parseTurnEvents). Otherwise
|
|
41
|
+
* the run starts fresh.
|
|
42
|
+
* - The sink's save resolving false means the run no longer holds its
|
|
43
|
+
* claim (the job was cancelled or leased elsewhere): runDurableTurn
|
|
44
|
+
* aborts the run and reports lostClaim. A save that throws is reported
|
|
45
|
+
* to onSaveError and the run goes on; a later attempt resumes from the
|
|
46
|
+
* older checkpoint.
|
|
47
|
+
*
|
|
48
|
+
* LIMITS:
|
|
49
|
+
* - A dispatch syndicate's route classifier runs in a throwaway store, so
|
|
50
|
+
* its model call is not checkpointed and runs again on resume.
|
|
51
|
+
* - A dispatch route that is an agent answers through the transcript
|
|
52
|
+
* projection (ProjectedSessionService, lib/session/transcript.ts), which
|
|
53
|
+
* copies the session when the route opens it and applies only the
|
|
54
|
+
* route's own appends to that copy. Restored events cannot reach that
|
|
55
|
+
* copy through the store, so a checkpoint taken inside such a route is
|
|
56
|
+
* not resumed: runDurableTurn starts that run fresh. A workflow route
|
|
57
|
+
* writes its own child session and resumes.
|
|
58
|
+
* - Each resumed attempt has its own step budget (max_steps, the loop's
|
|
59
|
+
* call ceiling); attempts are bounded by the queue's MAX_ATTEMPTS.
|
|
60
|
+
* - `temp:` state lasts one invocation and is never stored, so it does not
|
|
61
|
+
* survive a resume.
|
|
62
|
+
* - A workflow walk resumes through its own rules (ADR 0094); its
|
|
63
|
+
* checkpoint is the sessions it walks.
|
|
64
|
+
* - A checkpoint holds the run's sessions, so it carries the user's text,
|
|
65
|
+
* model output and tool results: it lives only where the run's record
|
|
66
|
+
* lives, under the same owner, and is deleted with it. This module never
|
|
67
|
+
* clears one; the caller does when the run finishes.
|
|
68
|
+
*/
|
|
69
|
+
import type { TurnEvent } from '../events.ts';
|
|
70
|
+
import { InProcessSessionService } from '../sessions.ts';
|
|
71
|
+
import type { SessionService } from '../sessions.ts';
|
|
72
|
+
import type { MessagePart, SyndicateTurnOptions, SyndicateTurnResult } from '../syndicateTurn.ts';
|
|
73
|
+
export declare const CHECKPOINT_VERSION = 1;
|
|
74
|
+
/** One session of a run, as it stood at a step boundary. */
|
|
75
|
+
export interface CheckpointSession {
|
|
76
|
+
appName: string;
|
|
77
|
+
userId: string;
|
|
78
|
+
id: string;
|
|
79
|
+
/** The state given to create(), before any event. */
|
|
80
|
+
createdState: Record<string, unknown>;
|
|
81
|
+
events: TurnEvent[];
|
|
82
|
+
/** The first event is the run's own opening user message: restore holds the session back until the runner stores that message. */
|
|
83
|
+
opening: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** A run's sessions at its latest step boundary. */
|
|
86
|
+
export interface RunCheckpoint {
|
|
87
|
+
version: 1;
|
|
88
|
+
runId: string;
|
|
89
|
+
/** The run's own session id: a resumed run uses it again. */
|
|
90
|
+
sessionId: string;
|
|
91
|
+
/** sha256 hex of JSON.stringify(parts) of the run's message (strings as `{ text }`). */
|
|
92
|
+
openingHash: string;
|
|
93
|
+
/** Step boundaries recorded so far, across every attempt. */
|
|
94
|
+
steps: number;
|
|
95
|
+
/** ISO time of the save. */
|
|
96
|
+
savedAt: string;
|
|
97
|
+
sessions: CheckpointSession[];
|
|
98
|
+
}
|
|
99
|
+
/** Where a run's checkpoints go. save resolves false when the run no longer holds its claim (cancelled, or re-leased): the run must stop. */
|
|
100
|
+
export interface CheckpointSink {
|
|
101
|
+
load(): Promise<RunCheckpoint | null>;
|
|
102
|
+
save(checkpoint: RunCheckpoint): Promise<boolean>;
|
|
103
|
+
}
|
|
104
|
+
export interface CheckpointingOptions {
|
|
105
|
+
runId: string;
|
|
106
|
+
/** The run's own session id (the one runSyndicateTurn is given). */
|
|
107
|
+
sessionId: string;
|
|
108
|
+
/** The run's message. */
|
|
109
|
+
parts: MessagePart[];
|
|
110
|
+
/** The checkpoint to restore; ignored unless its runId and openingHash match and it parses. */
|
|
111
|
+
from?: RunCheckpoint | null;
|
|
112
|
+
save: (checkpoint: RunCheckpoint) => Promise<boolean>;
|
|
113
|
+
/** Called once, when a save resolves false. */
|
|
114
|
+
onLost?: () => void;
|
|
115
|
+
onSaveError?: (err: unknown) => void;
|
|
116
|
+
/** The store wrapped. Default a fresh InProcessSessionService. */
|
|
117
|
+
inner?: InProcessSessionService;
|
|
118
|
+
}
|
|
119
|
+
export type CheckpointingSessionService = SessionService & {
|
|
120
|
+
/** The checkpoint given was restored. */
|
|
121
|
+
readonly restored: boolean;
|
|
122
|
+
/** Step boundaries recorded, the restored checkpoint's included. */
|
|
123
|
+
readonly steps: number;
|
|
124
|
+
};
|
|
125
|
+
/** sha256 hex of the message's parts, as a checkpoint names its run's message. */
|
|
126
|
+
export declare function openingHashOf(parts: readonly MessagePart[]): string;
|
|
127
|
+
/**
|
|
128
|
+
* The checkpoint, checked, when it belongs to this run and this message;
|
|
129
|
+
* null otherwise. A stored checkpoint is data read back from a table or a
|
|
130
|
+
* file, so every field the restore reads is checked before it is trusted.
|
|
131
|
+
*/
|
|
132
|
+
export declare function usableCheckpoint(cp: unknown, runId: string, parts: readonly MessagePart[]): RunCheckpoint | null;
|
|
133
|
+
/**
|
|
134
|
+
* A session store that checkpoints a run: the in-process store `inner`,
|
|
135
|
+
* restored from `from` when it is this run's checkpoint for this message,
|
|
136
|
+
* saving a snapshot of every session at each step boundary (see the header).
|
|
137
|
+
*/
|
|
138
|
+
export declare function checkpointingSessions(opts: CheckpointingOptions): CheckpointingSessionService;
|
|
139
|
+
export type DurableTurnOptions = Omit<SyndicateTurnOptions, 'sessionService' | 'sessionId'> & {
|
|
140
|
+
/** Names the run: a checkpoint is restored only by the run that wrote it. */
|
|
141
|
+
runId: string;
|
|
142
|
+
checkpoints: CheckpointSink;
|
|
143
|
+
/** Called once when a save resolves false; the run is then aborted. */
|
|
144
|
+
onLost?: () => void;
|
|
145
|
+
onSaveError?: (err: unknown) => void;
|
|
146
|
+
/** The in-process store the run writes. Default a fresh one; a test passes its own to read the sessions afterwards. */
|
|
147
|
+
sessions?: InProcessSessionService;
|
|
148
|
+
};
|
|
149
|
+
export type DurableTurnResult = SyndicateTurnResult & {
|
|
150
|
+
/** The step boundaries the restored checkpoint had recorded; 0 on a fresh run. */
|
|
151
|
+
resumedFromStep: number;
|
|
152
|
+
/** A save resolved false: the run no longer held its claim and was aborted. */
|
|
153
|
+
lostClaim: boolean;
|
|
154
|
+
/** The run's own session id: the checkpoint's on a resume, a new UUID otherwise. */
|
|
155
|
+
sessionId: string;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* runSyndicateTurn, durable: the run's sessions live in a fresh in-process
|
|
159
|
+
* store, restored from the sink's checkpoint when it is this run's for this
|
|
160
|
+
* message, and checkpointed at every step boundary. A save that resolves
|
|
161
|
+
* false aborts the run (`lostClaim`). The caller clears the checkpoint when
|
|
162
|
+
* the run finishes; this never does.
|
|
163
|
+
*/
|
|
164
|
+
export declare function runDurableTurn(opts: DurableTurnOptions): Promise<DurableTurnResult>;
|