melchizedek-agents 1.3.1 → 1.6.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 +15 -0
- package/CHANGELOG.md +166 -0
- package/DOCUMENTATION.md +84 -6
- package/ONBOARDING.md +3 -3
- package/README.md +2 -1
- package/config/agents/examples/connectors.yaml +4 -1
- package/config/agents/examples/event_assistant.yaml +82 -0
- package/config/agents/syndicate.schema.json +80 -6
- package/db/hardening.sql +43 -14
- package/db/migrations/0015_model_pricing.sql +107 -0
- package/db/telemetry.sql +79 -1
- package/dist/lib/a2a/app.js +2 -1
- package/dist/lib/chatgpt/state.js +4 -4
- package/dist/lib/compile.js +15 -2
- package/dist/lib/compileNative.js +1 -0
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/loadSyndicate.js +4 -0
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/models/claudeAdapter.js +2 -1
- package/dist/lib/models/credentialSource.d.ts +28 -0
- package/dist/lib/models/credentialSource.js +49 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.js +2 -1
- package/dist/lib/models/geminiAdapter.js +2 -1
- package/dist/lib/models/gptAdapter.js +2 -1
- package/dist/lib/models/grokAdapter.js +2 -1
- package/dist/lib/models/kimiAdapter.js +2 -1
- package/dist/lib/models/providerMap.js +3 -2
- package/dist/lib/observability/embeddings.js +2 -1
- package/dist/lib/observability/lineage.js +11 -3
- package/dist/lib/observability/supabaseSpanExporter.d.ts +19 -1
- package/dist/lib/observability/supabaseSpanExporter.js +37 -1
- package/dist/lib/observability/tracer.d.ts +18 -0
- package/dist/lib/observability/tracer.js +60 -2
- package/dist/lib/paths.d.ts +31 -0
- package/dist/lib/paths.js +76 -0
- package/dist/lib/runtime/credentials.d.ts +21 -0
- package/dist/lib/runtime/credentials.js +32 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +6 -0
- package/dist/lib/runtime/native/delegate.d.ts +2 -0
- package/dist/lib/runtime/native/delegate.js +39 -3
- package/dist/lib/runtime/native/interrupts.d.ts +33 -3
- package/dist/lib/runtime/native/interrupts.js +99 -9
- package/dist/lib/runtime/syndicateTurn.d.ts +14 -1
- package/dist/lib/runtime/syndicateTurn.js +45 -11
- package/dist/lib/runtime/turnControl.d.ts +27 -0
- package/dist/lib/storage/postgres/index.d.ts +25 -0
- package/dist/lib/storage/postgres/index.js +10 -1
- package/dist/lib/syndicateSchema.d.ts +26 -4
- package/dist/lib/syndicateSchema.js +63 -1
- package/dist/lib/tools/generateImageTool.js +6 -6
- package/dist/lib/tools/inspectImageTool.d.ts +2 -0
- package/dist/lib/tools/inspectImageTool.js +17 -10
- package/dist/lib/tools/mcpToolFactory.d.ts +34 -1
- package/dist/lib/tools/mcpToolFactory.js +200 -27
- package/dist/lib/tools/mcpTransports.d.ts +4 -2
- package/dist/lib/tools/mcpTransports.js +4 -2
- package/dist/lib/tools/taskTools.js +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/workflow/agentNode.d.ts +19 -3
- package/dist/lib/workflow/agentNode.js +106 -13
- package/dist/lib/workflow/resume.d.ts +8 -7
- package/dist/lib/workflow/resume.js +17 -10
- package/dist/lib/workflow/turn.d.ts +12 -2
- package/dist/lib/workflow/turn.js +22 -12
- package/dist/scripts/a2a_server.js +5 -0
- package/package.json +11 -4
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
*/
|
|
105
105
|
import { randomUUID } from 'node:crypto';
|
|
106
106
|
import { resolveAdapter } from '../../models/registry.js';
|
|
107
|
+
import { credentialRequestCall } from '../credentials.js';
|
|
107
108
|
import { createTurnEvent } from '../events.js';
|
|
108
109
|
import { TEMP_STATE_PREFIX } from '../sessions.js';
|
|
109
110
|
import { REQUEST_INPUT_CALL } from './history.js';
|
|
@@ -393,9 +394,31 @@ export function runSubagent(agent, args, context, scope) {
|
|
|
393
394
|
export function resumeSubagent(agent, answer, context, scope) {
|
|
394
395
|
return runChild(agent, { role: 'user', parts: answer }, context, scope, true);
|
|
395
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Where a delegated call's run sits (ADR 0135): the path below the caller's,
|
|
399
|
+
* `name` last, and the call's id. A top-level caller's path is its name.
|
|
400
|
+
*/
|
|
401
|
+
function branchOf(name, context, scope) {
|
|
402
|
+
const path = [...(scope.ctx.agentPath ?? [scope.caller]), name];
|
|
403
|
+
return { agent: name, path, ...(context.functionCallId ? { callId: context.functionCallId } : {}) };
|
|
404
|
+
}
|
|
405
|
+
/** Hands one event to the turn's observer (ADR 0135). An observer that throws never breaks the run. */
|
|
406
|
+
function observe(sink, event, branch) {
|
|
407
|
+
if (!sink)
|
|
408
|
+
return;
|
|
409
|
+
try {
|
|
410
|
+
sink(event, branch);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
/* the observer is the caller's; the turn goes on */
|
|
414
|
+
}
|
|
415
|
+
}
|
|
396
416
|
async function runChild(agent, content, context, scope, resuming) {
|
|
397
417
|
const { ctx, signal } = scope;
|
|
398
418
|
const { sessions } = ctx;
|
|
419
|
+
// With an observer the child streams, so its thinking and text reach it as partials (ADR 0135); partials are stored nowhere.
|
|
420
|
+
const sink = currentTurnControl()?.agentEvents;
|
|
421
|
+
const branch = branchOf(agent.name, context, scope);
|
|
399
422
|
const session = await childSession(agent.name, context, scope, resuming);
|
|
400
423
|
if (signal?.aborted)
|
|
401
424
|
return '';
|
|
@@ -408,9 +431,10 @@ async function runChild(agent, content, context, scope, resuming) {
|
|
|
408
431
|
sessions,
|
|
409
432
|
invocationId,
|
|
410
433
|
userContent: content,
|
|
411
|
-
stream:
|
|
434
|
+
stream: sink !== undefined,
|
|
412
435
|
// Its own delegations are filed below its path (childAppName, ADR 0111).
|
|
413
436
|
delegated: true,
|
|
437
|
+
agentPath: branch.path,
|
|
414
438
|
// ADK's AgentTool builds its sub-runner without the reflect-and-retry
|
|
415
439
|
// plugins, so a subagent's own errors are not retried (ADR 0075).
|
|
416
440
|
selfCorrection: new SelfCorrection({ model_errors: 0, tool_errors: 0 }),
|
|
@@ -437,6 +461,9 @@ async function runChild(agent, content, context, scope, resuming) {
|
|
|
437
461
|
await loop.return(undefined);
|
|
438
462
|
break;
|
|
439
463
|
}
|
|
464
|
+
observe(sink, next.value, branch);
|
|
465
|
+
if (next.value.partial)
|
|
466
|
+
continue;
|
|
440
467
|
recordState(next.value, context);
|
|
441
468
|
last = next.value;
|
|
442
469
|
}
|
|
@@ -468,6 +495,8 @@ export function resumeWorkflowSubagent(workflow, answer, context, scope) {
|
|
|
468
495
|
}
|
|
469
496
|
async function walkChild(workflow, userParts, context, scope, resuming) {
|
|
470
497
|
const { ctx, signal } = scope;
|
|
498
|
+
const sink = currentTurnControl()?.agentEvents;
|
|
499
|
+
const branch = branchOf(workflow.name, context, scope);
|
|
471
500
|
const session = await childSession(workflow.name, context, scope, resuming);
|
|
472
501
|
if (signal?.aborted)
|
|
473
502
|
return '';
|
|
@@ -484,6 +513,8 @@ async function walkChild(workflow, userParts, context, scope, resuming) {
|
|
|
484
513
|
...(ctx.memory ? { memory: ctx.memory } : {}),
|
|
485
514
|
...(ctx.log ? { log: ctx.log } : {}),
|
|
486
515
|
...(ctx.credentials ? { credentials: ctx.credentials } : {}),
|
|
516
|
+
// A node that needs a grant pauses the walk, and the pause reaches the turn through the open call (ADR 0128).
|
|
517
|
+
...(ctx.consent ? { consent: consentPinnedTo(ctx.consent, ctx.session.appName) } : {}),
|
|
487
518
|
});
|
|
488
519
|
let last;
|
|
489
520
|
for (;;) {
|
|
@@ -500,6 +531,7 @@ async function walkChild(workflow, userParts, context, scope, resuming) {
|
|
|
500
531
|
break;
|
|
501
532
|
}
|
|
502
533
|
const event = next.value;
|
|
534
|
+
observe(sink, event, branch);
|
|
503
535
|
if (event.partial)
|
|
504
536
|
continue;
|
|
505
537
|
recordState(event, context);
|
|
@@ -528,8 +560,9 @@ function dispatchParts(parts) {
|
|
|
528
560
|
/**
|
|
529
561
|
* The request call a paused nested dispatch syndicate's turn waits on, as a
|
|
530
562
|
* workflow node raises it again on its caller's walk: an approval request
|
|
531
|
-
* pinned to the gated call,
|
|
532
|
-
*
|
|
563
|
+
* pinned to the gated call, an input request carrying the question, or a
|
|
564
|
+
* credential request rebuilt from the consent request (credentialRequestCall,
|
|
565
|
+
* ADR 0128), under the nested request's id.
|
|
533
566
|
*/
|
|
534
567
|
function requestCallOf(end) {
|
|
535
568
|
const id = end.interruptId;
|
|
@@ -556,6 +589,9 @@ function requestCallOf(end) {
|
|
|
556
589
|
},
|
|
557
590
|
};
|
|
558
591
|
}
|
|
592
|
+
const consent = end.consent;
|
|
593
|
+
if (consent && consent.id === id)
|
|
594
|
+
return credentialRequestCall(consent);
|
|
559
595
|
return undefined;
|
|
560
596
|
}
|
|
561
597
|
/**
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
* grantedCalls is that preprocessor, check for check, with one difference
|
|
59
59
|
* in what the answer carries:
|
|
60
60
|
* 1. The answers: the function responses named `adk_request_credential`
|
|
61
|
-
* in the
|
|
61
|
+
* in the latest user event, provided this agent has stored nothing
|
|
62
|
+
* with content since (at the top that is the last event with content;
|
|
63
|
+
* in a walk another node's events may come between, ADR 0128). None:
|
|
62
64
|
* nothing happens.
|
|
63
65
|
* 2. The requests they answer: `adk_request_credential` calls this agent
|
|
64
66
|
* made, by id. An answer naming no such request is ignored, as ADK
|
|
@@ -102,7 +104,8 @@
|
|
|
102
104
|
* workflow under the call's name) makes an approval request an agent node
|
|
103
105
|
* raised, or an ask_user node's question, the pause, with the node that
|
|
104
106
|
* asked at the end of the path. The walk stops there: a pause inside a
|
|
105
|
-
* workflow node's own delegation does not run (lib/workflow/agentNode.ts)
|
|
107
|
+
* workflow node's own delegation does not run (lib/workflow/agentNode.ts),
|
|
108
|
+
* except an OAuth consent request, which does (ADR 0128, below).
|
|
106
109
|
*
|
|
107
110
|
* NESTED WORKFLOWS AS A ROUTE OR A NODE (ADR 0119). A workflow node that is
|
|
108
111
|
* a nested workflow raises its walk's open requests again on its caller's
|
|
@@ -126,6 +129,19 @@
|
|
|
126
129
|
* call, the route's pause record or the node's raised request, and the
|
|
127
130
|
* nested turn reads it as a message at the top.
|
|
128
131
|
*
|
|
132
|
+
* CONSENT INSIDE A WALK (ADR 0128). An agent node's own consent request
|
|
133
|
+
* is an interrupt of the walk like its approval request: the walk's pause
|
|
134
|
+
* record names it, and walkConsents finds it there for the turn. A node
|
|
135
|
+
* whose request waits below it (in a call its agent left open, in a nested
|
|
136
|
+
* walk, in a nested dispatch syndicate's conversation) raises a copy of it
|
|
137
|
+
* on the walk under the same id (lib/workflow/agentNode.ts,
|
|
138
|
+
* lib/workflow/turn.ts), and consentBelow follows the copy down to the
|
|
139
|
+
* agent that asked; workflowPause reads a credential request as it reads
|
|
140
|
+
* an approval or a question, so a walk nested below a route, a node or a
|
|
141
|
+
* delegated call is followed the same way. grantedCalls reads the latest
|
|
142
|
+
* user event unless the agent has written since, so another node's events
|
|
143
|
+
* between the grant's answer and the node's resume do not hide it.
|
|
144
|
+
*
|
|
129
145
|
* CONSENT AND SKILL SCRIPTS INSIDE DELEGATED SUBAGENTS (ADR 0118). A skill
|
|
130
146
|
* script run asks for approval through the same `adk_request_confirmation`
|
|
131
147
|
* call a gated tool stores, so the walk and the resume above carry it with
|
|
@@ -230,6 +246,20 @@ export declare function openCalls(events: readonly TurnEvent[], author?: string)
|
|
|
230
246
|
author: string;
|
|
231
247
|
call: TurnFunctionCall;
|
|
232
248
|
}>;
|
|
249
|
+
/**
|
|
250
|
+
* The OAuth consent requests a walk waits on (ADR 0128): the credential
|
|
251
|
+
* requests among the open interrupts its pause record names (the session's
|
|
252
|
+
* last event, authored by the workflow at its own path, no content), each
|
|
253
|
+
* followed down to the agent that asked. A node's own request comes back as
|
|
254
|
+
* it is, `agent` the node and no `path`, as a node's own approval request
|
|
255
|
+
* does; one a node raised again on the walk (a delegated subagent's, a
|
|
256
|
+
* nested walk's, a nested dispatch syndicate's) carries the `path` from the
|
|
257
|
+
* node down. `key.appName` is the walk session's app. Empty when the walk is
|
|
258
|
+
* not paused, or waits on no grant.
|
|
259
|
+
*/
|
|
260
|
+
export declare function walkConsents(key: DelegationKey & {
|
|
261
|
+
appName: string;
|
|
262
|
+
}, events: readonly TurnEvent[], depth?: number): Promise<PendingConsent[]>;
|
|
233
263
|
/** A pause a nested workflow's walk holds: the path from the workflow down to the node that asked, and what it asks. */
|
|
234
264
|
export interface NestedWorkflowPause {
|
|
235
265
|
/** From the workflow (the route, the node, the subagent) down to the node that asked: `[workflow, …, node]`. */
|
|
@@ -238,7 +268,7 @@ export interface NestedWorkflowPause {
|
|
|
238
268
|
approval?: PendingApproval;
|
|
239
269
|
/** The question, its `node` the node that asked and its `path` set. */
|
|
240
270
|
question?: PendingInput;
|
|
241
|
-
/** The OAuth consent request, inside a nested dispatch syndicate's route (ADR 0120); its `path` set. */
|
|
271
|
+
/** The OAuth consent request, inside a nested dispatch syndicate's route (ADR 0120) or a node of the walk (ADR 0128); its `path` set. */
|
|
242
272
|
consent?: PendingConsent;
|
|
243
273
|
}
|
|
244
274
|
/**
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
* grantedCalls is that preprocessor, check for check, with one difference
|
|
59
59
|
* in what the answer carries:
|
|
60
60
|
* 1. The answers: the function responses named `adk_request_credential`
|
|
61
|
-
* in the
|
|
61
|
+
* in the latest user event, provided this agent has stored nothing
|
|
62
|
+
* with content since (at the top that is the last event with content;
|
|
63
|
+
* in a walk another node's events may come between, ADR 0128). None:
|
|
62
64
|
* nothing happens.
|
|
63
65
|
* 2. The requests they answer: `adk_request_credential` calls this agent
|
|
64
66
|
* made, by id. An answer naming no such request is ignored, as ADK
|
|
@@ -102,7 +104,8 @@
|
|
|
102
104
|
* workflow under the call's name) makes an approval request an agent node
|
|
103
105
|
* raised, or an ask_user node's question, the pause, with the node that
|
|
104
106
|
* asked at the end of the path. The walk stops there: a pause inside a
|
|
105
|
-
* workflow node's own delegation does not run (lib/workflow/agentNode.ts)
|
|
107
|
+
* workflow node's own delegation does not run (lib/workflow/agentNode.ts),
|
|
108
|
+
* except an OAuth consent request, which does (ADR 0128, below).
|
|
106
109
|
*
|
|
107
110
|
* NESTED WORKFLOWS AS A ROUTE OR A NODE (ADR 0119). A workflow node that is
|
|
108
111
|
* a nested workflow raises its walk's open requests again on its caller's
|
|
@@ -126,6 +129,19 @@
|
|
|
126
129
|
* call, the route's pause record or the node's raised request, and the
|
|
127
130
|
* nested turn reads it as a message at the top.
|
|
128
131
|
*
|
|
132
|
+
* CONSENT INSIDE A WALK (ADR 0128). An agent node's own consent request
|
|
133
|
+
* is an interrupt of the walk like its approval request: the walk's pause
|
|
134
|
+
* record names it, and walkConsents finds it there for the turn. A node
|
|
135
|
+
* whose request waits below it (in a call its agent left open, in a nested
|
|
136
|
+
* walk, in a nested dispatch syndicate's conversation) raises a copy of it
|
|
137
|
+
* on the walk under the same id (lib/workflow/agentNode.ts,
|
|
138
|
+
* lib/workflow/turn.ts), and consentBelow follows the copy down to the
|
|
139
|
+
* agent that asked; workflowPause reads a credential request as it reads
|
|
140
|
+
* an approval or a question, so a walk nested below a route, a node or a
|
|
141
|
+
* delegated call is followed the same way. grantedCalls reads the latest
|
|
142
|
+
* user event unless the agent has written since, so another node's events
|
|
143
|
+
* between the grant's answer and the node's resume do not hide it.
|
|
144
|
+
*
|
|
129
145
|
* CONSENT AND SKILL SCRIPTS INSIDE DELEGATED SUBAGENTS (ADR 0118). A skill
|
|
130
146
|
* script run asks for approval through the same `adk_request_confirmation`
|
|
131
147
|
* call a gated tool stores, so the walk and the resume above carry it with
|
|
@@ -144,7 +160,7 @@ import { nativeToolOf } from '../../models/schemaNormalize.js';
|
|
|
144
160
|
import { instructionToolOf, isTool, toolOf } from '../../tools/tool.js';
|
|
145
161
|
import { inputRequestFrom } from '../../workflowConfig.js';
|
|
146
162
|
import { approvalRequestOf, pendingApproval } from '../approvals.js';
|
|
147
|
-
import { pendingConsent } from '../credentials.js';
|
|
163
|
+
import { consentFrom, pendingConsent } from '../credentials.js';
|
|
148
164
|
import { getFunctionCalls, getFunctionResponses } from '../events.js';
|
|
149
165
|
import { ASK_USER, pendingQuestion } from '../questions.js';
|
|
150
166
|
import { childAppName, entryAppName, legacyChild, legacyEntry } from './delegate.js';
|
|
@@ -308,11 +324,17 @@ export async function grantedCalls(agent, scope) {
|
|
|
308
324
|
const events = scope.session.events;
|
|
309
325
|
if (events.length === 0)
|
|
310
326
|
return undefined;
|
|
327
|
+
// The latest user event, unless this agent has written since (ADR 0128): in a walk, another node's events may
|
|
328
|
+
// follow the person's answer before this node reads it; at the top only this agent writes after the message.
|
|
311
329
|
let last;
|
|
312
|
-
for (let i = events.length - 1; i >= 0 && !last; i--)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
330
|
+
for (let i = events.length - 1; i >= 0 && !last; i--) {
|
|
331
|
+
const event = events[i];
|
|
332
|
+
if (event.author === agent.name && event.content !== undefined)
|
|
333
|
+
return undefined;
|
|
334
|
+
if (event.author === 'user' && event.content !== undefined)
|
|
335
|
+
last = event;
|
|
336
|
+
}
|
|
337
|
+
if (!last)
|
|
316
338
|
return undefined;
|
|
317
339
|
const answers = new Map();
|
|
318
340
|
for (const r of getFunctionResponses(last))
|
|
@@ -427,6 +449,7 @@ async function pauseBelow(key, caller, call, depth, seen) {
|
|
|
427
449
|
callIds: [id],
|
|
428
450
|
...(walked.approval ? { approval: { ...walked.approval, path } } : {}),
|
|
429
451
|
...(walked.question ? { question: { ...walked.question, path } } : {}),
|
|
452
|
+
...(walked.consent ? { consent: { ...walked.consent, path } } : {}),
|
|
430
453
|
};
|
|
431
454
|
}
|
|
432
455
|
const approval = pendingApproval(child.events);
|
|
@@ -540,8 +563,9 @@ function workflowPause(events, name, id) {
|
|
|
540
563
|
const open = new Set((record.longRunningToolIds ?? []).filter((x) => id === undefined || x === id));
|
|
541
564
|
if (open.size === 0)
|
|
542
565
|
return undefined;
|
|
543
|
-
// The latest open request the record names, an approval first
|
|
544
|
-
|
|
566
|
+
// The latest open request the record names, an approval first, then a question, then a consent request (ADR 0128):
|
|
567
|
+
// the walk stores node inputs as user turns, so a later one may follow it.
|
|
568
|
+
for (const kind of [REQUEST_CONFIRMATION_CALL, REQUEST_INPUT_CALL, REQUEST_CREDENTIAL_CALL]) {
|
|
545
569
|
for (let i = events.length - 2; i >= 0; i--) {
|
|
546
570
|
const event = events[i];
|
|
547
571
|
if (!event.author || event.author === 'user')
|
|
@@ -551,6 +575,12 @@ function workflowPause(events, name, id) {
|
|
|
551
575
|
continue;
|
|
552
576
|
if (kind === REQUEST_CONFIRMATION_CALL)
|
|
553
577
|
return { asker: event.author, approval: approvalRequestOf(event, call) };
|
|
578
|
+
if (kind === REQUEST_CREDENTIAL_CALL) {
|
|
579
|
+
const consent = consentFrom(event.author, call);
|
|
580
|
+
if (consent)
|
|
581
|
+
return { asker: event.author, consent };
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
554
584
|
const question = inputRequestFrom(event.author, call);
|
|
555
585
|
if (question && question.id === call.id)
|
|
556
586
|
return { asker: event.author, question };
|
|
@@ -559,6 +589,58 @@ function workflowPause(events, name, id) {
|
|
|
559
589
|
}
|
|
560
590
|
return undefined;
|
|
561
591
|
}
|
|
592
|
+
/** Whether `asker` made the call `functionCallId` itself in `events`: its consent request is its own, not one it raised again. */
|
|
593
|
+
function madeCall(events, asker, functionCallId) {
|
|
594
|
+
return events.some((event) => event.author === asker && getFunctionCalls(event).some((call) => call.id === functionCallId && call.name !== REQUEST_CREDENTIAL_CALL));
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The consent request `id` waiting below the node `asker` of a walk whose
|
|
598
|
+
* session holds `events` (filed under `key.appName`), when the node raised
|
|
599
|
+
* it again on the walk (ADR 0128): inside the node's own child session (a
|
|
600
|
+
* nested workflow, or a nested dispatch syndicate's conversation,
|
|
601
|
+
* nestedWorkflowPause), else inside a call the node left open (a delegated
|
|
602
|
+
* subagent, delegatedPauses). Its `path` runs from `asker` down to the
|
|
603
|
+
* agent that asked. Undefined when nothing below holds it.
|
|
604
|
+
*/
|
|
605
|
+
async function consentBelow(key, events, asker, id, depth) {
|
|
606
|
+
if (depth >= MAX_DELEGATION_DEPTH)
|
|
607
|
+
return undefined;
|
|
608
|
+
const nested = await nestedWorkflowPause(key, asker, 'node', id, depth + 1);
|
|
609
|
+
if (nested?.consent?.id === id)
|
|
610
|
+
return nested.consent;
|
|
611
|
+
const below = await delegatedPauses({ ...key, delegated: false }, events, asker, depth + 1);
|
|
612
|
+
return below.find((pause) => pause.consent?.id === id)?.consent;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* The OAuth consent requests a walk waits on (ADR 0128): the credential
|
|
616
|
+
* requests among the open interrupts its pause record names (the session's
|
|
617
|
+
* last event, authored by the workflow at its own path, no content), each
|
|
618
|
+
* followed down to the agent that asked. A node's own request comes back as
|
|
619
|
+
* it is, `agent` the node and no `path`, as a node's own approval request
|
|
620
|
+
* does; one a node raised again on the walk (a delegated subagent's, a
|
|
621
|
+
* nested walk's, a nested dispatch syndicate's) carries the `path` from the
|
|
622
|
+
* node down. `key.appName` is the walk session's app. Empty when the walk is
|
|
623
|
+
* not paused, or waits on no grant.
|
|
624
|
+
*/
|
|
625
|
+
export async function walkConsents(key, events, depth = 0) {
|
|
626
|
+
const record = events[events.length - 1];
|
|
627
|
+
const name = record?.author;
|
|
628
|
+
if (!record || !name || name === 'user' || (record.content?.parts ?? []).length > 0 || record.nodeInfo?.path !== name)
|
|
629
|
+
return [];
|
|
630
|
+
const out = [];
|
|
631
|
+
for (const id of record.longRunningToolIds ?? []) {
|
|
632
|
+
const walked = workflowPause(events, name, id);
|
|
633
|
+
const consent = walked?.consent;
|
|
634
|
+
if (!walked || !consent)
|
|
635
|
+
continue;
|
|
636
|
+
if (madeCall(events, walked.asker, consent.functionCallId)) {
|
|
637
|
+
out.push(consent);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
out.push((await consentBelow(key, events, walked.asker, id, depth)) ?? consent);
|
|
641
|
+
}
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
562
644
|
/**
|
|
563
645
|
* The pause the walk of `name` holds in `child` (its child session), with
|
|
564
646
|
* `id` the only interrupt that counts when given: the node that asked, or,
|
|
@@ -569,6 +651,14 @@ async function workflowPauseIn(key, child, name, id, depth = 0) {
|
|
|
569
651
|
const walked = workflowPause(child.events, name, id);
|
|
570
652
|
if (!walked)
|
|
571
653
|
return undefined;
|
|
654
|
+
if (walked.consent) {
|
|
655
|
+
// A node's own consent request, else one it raised again: followed down to the agent that asked (ADR 0128).
|
|
656
|
+
const below = madeCall(child.events, walked.asker, walked.consent.functionCallId)
|
|
657
|
+
? undefined
|
|
658
|
+
: await consentBelow({ ...key, appName: child.appName }, child.events, walked.asker, walked.consent.id, depth + 1);
|
|
659
|
+
const path = [name, ...(below?.path ?? [walked.asker])];
|
|
660
|
+
return { path, consent: { ...(below ?? walked.consent), path } };
|
|
661
|
+
}
|
|
572
662
|
const found = (walked.approval?.id ?? walked.question?.id);
|
|
573
663
|
const deeper = await nestedWorkflowPause({ ...key, appName: child.appName }, walked.asker, 'node', found, depth + 1);
|
|
574
664
|
const path = deeper ? [name, ...deeper.path] : [name, walked.asker];
|
|
@@ -64,7 +64,8 @@ export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.ts';
|
|
|
64
64
|
import type { PendingApproval } from './approvals.ts';
|
|
65
65
|
export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.ts';
|
|
66
66
|
export type { PendingApproval } from './approvals.ts';
|
|
67
|
-
import type { TurnStopReason } from './turnControl.ts';
|
|
67
|
+
import type { AgentBranch, TurnStopReason } from './turnControl.ts';
|
|
68
|
+
export type { AgentBranch } from './turnControl.ts';
|
|
68
69
|
/** One part of the user's message. Text is the common case; the genai part
|
|
69
70
|
* shapes (inlineData, fileData) pass through to the model unchanged. */
|
|
70
71
|
export type MessagePart = {
|
|
@@ -86,6 +87,18 @@ export interface TurnEvents {
|
|
|
86
87
|
onTextDelta?(delta: string): void;
|
|
87
88
|
/** The text streamed so far was narration before a tool call: discard it. */
|
|
88
89
|
onTextReset?(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Every event of a delegated run, live, with where it runs (ADR 0135): a
|
|
92
|
+
* subagent's thinking and text as partial events, then each event it
|
|
93
|
+
* stores (its tool calls, their results, its answer), and a nested
|
|
94
|
+
* workflow's events; at any depth, parallel calls interleaved and told
|
|
95
|
+
* apart by `branch.callId` and `branch.path`. The answering agent's
|
|
96
|
+
* events stay on onEvent and onTextDelta, which this does not change.
|
|
97
|
+
* Passing it makes each delegated run stream; what the sessions store is
|
|
98
|
+
* the same. Fires for a syndicate with guards too: a delegated run's text
|
|
99
|
+
* is never the answer a guard rewrites. An observer that throws is ignored.
|
|
100
|
+
*/
|
|
101
|
+
onAgentEvent?(event: TurnEvent, branch: AgentBranch): void;
|
|
89
102
|
/** Diagnostic lines (the server prefixes and prints them). */
|
|
90
103
|
log?(message: string): void;
|
|
91
104
|
warn?(message: string): void;
|
|
@@ -70,7 +70,7 @@ 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 { deepestPause, delegatedPauses, nestedWorkflowPause, routePause } from './native/interrupts.js';
|
|
73
|
+
import { deepestPause, delegatedPauses, nestedWorkflowPause, routePause, walkConsents } from './native/interrupts.js';
|
|
74
74
|
import { consentPinnedTo, entrySession } from './native/delegate.js';
|
|
75
75
|
import { workflowPauseEvent } from '../workflow/pause.js';
|
|
76
76
|
import { INPUT_REQUEST } from '../workflowConfig.js';
|
|
@@ -299,6 +299,10 @@ export async function runSyndicateTurn(opts) {
|
|
|
299
299
|
});
|
|
300
300
|
// A nested dispatch syndicate, wherever the turn reaches one, runs as a turn of its own under these controls (ADR 0120).
|
|
301
301
|
control.nestedDispatch = (run) => runNestedDispatch(opts, control, runtime, run);
|
|
302
|
+
// Delegated runs report to the caller as they run (ADR 0135).
|
|
303
|
+
const onAgentEvent = opts.events?.onAgentEvent;
|
|
304
|
+
if (onAgentEvent)
|
|
305
|
+
control.agentEvents = (event, branch) => onAgentEvent.call(opts.events, event, branch);
|
|
302
306
|
try {
|
|
303
307
|
const run = () => runWithTurnControl(control, () => runTurnInner(opts, control, runtime));
|
|
304
308
|
// A span a library opens through OpenTelemetry's own API (not the
|
|
@@ -485,6 +489,15 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
485
489
|
ev.log?.(`⏸ Authorization needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.provider}`);
|
|
486
490
|
return finish();
|
|
487
491
|
};
|
|
492
|
+
/** Whether the grant a consent request waits for is stored (an expired or unreadable one is not). */
|
|
493
|
+
const grantStored = async (open) => {
|
|
494
|
+
try {
|
|
495
|
+
return !!open.provider && !!(await credentialStore?.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
488
501
|
let granting;
|
|
489
502
|
// A call waiting for a grant inside a delegated subagent (ADR 0118): the grant's answer is stored here, and the open call carries it down.
|
|
490
503
|
let grantingBelow;
|
|
@@ -508,14 +521,7 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
508
521
|
result.error = { code: 'CONSENT_UNAVAILABLE', message: `A call waits for a ${open.provider} authorization, but this turn has no credential store.` };
|
|
509
522
|
return finish();
|
|
510
523
|
}
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
granted = !!open.provider && !!(await credentialStore.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
514
|
-
}
|
|
515
|
-
catch {
|
|
516
|
-
// Expired or unreadable: not granted. The request stands.
|
|
517
|
-
}
|
|
518
|
-
if (!granted)
|
|
524
|
+
if (!(await grantStored(open)))
|
|
519
525
|
return consentPause(open);
|
|
520
526
|
// The route's own turn carries the grant's answer down to its call; the message goes to it as it came.
|
|
521
527
|
if (routeWaits)
|
|
@@ -527,6 +533,28 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
527
533
|
ev.log?.(`✓ Authorization granted: ${(open.path ?? [open.agent]).join(' → ')} → ${open.provider}`);
|
|
528
534
|
}
|
|
529
535
|
}
|
|
536
|
+
// A walk waiting for grants (ADR 0128): a node's own consent request, or one a node raised again from below it. Once every
|
|
537
|
+
// grant is stored, the message becomes their answers and the walk resumes those nodes; until then it repeats the first
|
|
538
|
+
// request still missing and stores nothing, as at the top.
|
|
539
|
+
if (!decision && isWorkflowSyndicate(config)) {
|
|
540
|
+
const waiting = await walkConsents(pauseKey, existing?.events ?? []);
|
|
541
|
+
if (waiting.length > 0) {
|
|
542
|
+
if (!credentialStore) {
|
|
543
|
+
result.status = 'failed';
|
|
544
|
+
result.error = { code: 'CONSENT_UNAVAILABLE', message: `A call waits for a ${waiting[0].provider} authorization, but this turn has no credential store.` };
|
|
545
|
+
return finish();
|
|
546
|
+
}
|
|
547
|
+
for (const open of waiting)
|
|
548
|
+
if (!(await grantStored(open)))
|
|
549
|
+
return consentPause(open);
|
|
550
|
+
granting = waiting[0];
|
|
551
|
+
parts = waiting.map((open) => credentialResponsePart(open.id, open.provider));
|
|
552
|
+
for (const open of waiting)
|
|
553
|
+
ev.log?.(`✓ Authorization granted: ${(open.path ?? [open.agent]).join(' → ')} → ${open.provider}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/** The parts a paused workflow route's walk resumes on once its grant is stored (ADR 0128): the grant's answer to its credential request. */
|
|
557
|
+
const routeGrant = consentRoute?.consent ? [credentialResponsePart(consentRoute.consent.id, consentRoute.consent.provider)] : undefined;
|
|
530
558
|
// ── Questions (lib/runtime/questions.ts) ───────────────────────────────────
|
|
531
559
|
// While an agent's `ask_user` call is open, a plain-text message is its
|
|
532
560
|
// answer: it becomes that call's response, and the agent that asked
|
|
@@ -651,6 +679,8 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
651
679
|
memory: nativeMemory(opts.memoryService),
|
|
652
680
|
...(selfCorrection ? { selfCorrection } : {}),
|
|
653
681
|
...(credentialStore ? { credentials: credentialStore } : {}),
|
|
682
|
+
// The consent step, its flows bound to the app the grants are read under, wherever the walk's session is filed (ADR 0128).
|
|
683
|
+
...(consentStep ? { consent: consentPinnedTo(consentStep, credentialApp) } : {}),
|
|
654
684
|
...(compileOpts.log ? { log: compileOpts.log } : {}),
|
|
655
685
|
});
|
|
656
686
|
}
|
|
@@ -747,7 +777,7 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
747
777
|
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
748
778
|
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
749
779
|
// Filed under the agent path (ADR 0119), else continued under the route's name alone where ADR 0106 filed it.
|
|
750
|
-
const resumingWalk = resumingRoute !== undefined || routeAnswer !== undefined;
|
|
780
|
+
const resumingWalk = resumingRoute !== undefined || routeAnswer !== undefined || consentRoute !== undefined;
|
|
751
781
|
const child = await entrySession(store, { appName, userId, sessionId, events: shared.events }, route, 'route', kept(shared.state), resumingWalk);
|
|
752
782
|
const childKey = { appName: child.appName, userId, sessionId };
|
|
753
783
|
const before = new Map(Object.entries(kept(child.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
@@ -760,7 +790,7 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
760
790
|
};
|
|
761
791
|
let walked;
|
|
762
792
|
try {
|
|
763
|
-
walked = await runAgent({ agent, sid: sessionId, appName: child.appName, userParts: routeAnswer ?? parts, sessions: sessionService, stage: 'dispatch', route: resolution, publishToolStatus: true, errorPolicy: 'collect' });
|
|
793
|
+
walked = await runAgent({ agent, sid: sessionId, appName: child.appName, userParts: routeAnswer ?? routeGrant ?? parts, sessions: sessionService, stage: 'dispatch', route: resolution, publishToolStatus: true, errorPolicy: 'collect' });
|
|
764
794
|
}
|
|
765
795
|
catch (err) {
|
|
766
796
|
const last = control.stopReason ? undefined : err;
|
|
@@ -1087,6 +1117,10 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
1087
1117
|
const pending = pendingApproval(after?.events ?? []);
|
|
1088
1118
|
if (pending)
|
|
1089
1119
|
return pause(await deepen(pending));
|
|
1120
|
+
// A node waiting for a grant (ADR 0128): its own request, or one from below it, with the path from the node down.
|
|
1121
|
+
const [consent] = await walkConsents(pauseKey, after?.events ?? []);
|
|
1122
|
+
if (consent)
|
|
1123
|
+
return consentPause(consent);
|
|
1090
1124
|
if (answer.inputRequests.length)
|
|
1091
1125
|
return question(await deepen(answer.inputRequests[answer.inputRequests.length - 1]));
|
|
1092
1126
|
result.text = answer.text;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* to their provider SDK and a cancel or a deadline stops the HTTP call in
|
|
23
23
|
* flight, not just the loop around it.
|
|
24
24
|
*/
|
|
25
|
+
import type { TurnEvent } from './events.ts';
|
|
25
26
|
/** Why a turn was stopped before it finished on its own. */
|
|
26
27
|
export type TurnStopReason = 'canceled' | 'deadline' | 'step_limit';
|
|
27
28
|
export interface TurnControl {
|
|
@@ -53,6 +54,32 @@ export interface TurnControl {
|
|
|
53
54
|
* which sit below the turn runner and cannot import it.
|
|
54
55
|
*/
|
|
55
56
|
nestedDispatch?: (run: NestedDispatchRun) => Promise<NestedDispatchEnd>;
|
|
57
|
+
/**
|
|
58
|
+
* Receives every event a delegated run yields, as it yields it, with
|
|
59
|
+
* where that run sits (ADR 0135): set by the turn runner when the caller
|
|
60
|
+
* passed `events.onAgentEvent`, read where a delegated call runs
|
|
61
|
+
* (lib/runtime/native/delegate.ts), which sits below the turn runner.
|
|
62
|
+
* Its presence also makes each delegated run stream, so its thinking and
|
|
63
|
+
* text arrive as partial events.
|
|
64
|
+
*/
|
|
65
|
+
agentEvents?: (event: TurnEvent, branch: AgentBranch) => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Where a delegated run sits in the turn (ADR 0135): the agent, the
|
|
69
|
+
* delegation path from the agent that took the call down to it, and the id
|
|
70
|
+
* of the call that started it.
|
|
71
|
+
*/
|
|
72
|
+
export interface AgentBranch {
|
|
73
|
+
/** The delegated agent: a subagent, or a nested workflow's name for an event its walk yielded. */
|
|
74
|
+
agent: string;
|
|
75
|
+
/**
|
|
76
|
+
* The agents from the caller at the top of the delegation down to this
|
|
77
|
+
* one, itself last: `['Boss', 'Researcher']`, `['Boss', 'Researcher',
|
|
78
|
+
* 'Fetcher']`. Below a nested workflow it starts again at the node's agent.
|
|
79
|
+
*/
|
|
80
|
+
path: readonly string[];
|
|
81
|
+
/** The id of the function call, in the caller's event, that started this run: what pairs it with the caller's tool call. */
|
|
82
|
+
callId?: string;
|
|
56
83
|
}
|
|
57
84
|
/** One run of a nested dispatch syndicate (ADR 0120): what it is, where its conversation is filed, and the message. */
|
|
58
85
|
export interface NestedDispatchRun {
|
|
@@ -19,6 +19,7 @@ import { PostgresSessionService } from './sessionService.ts';
|
|
|
19
19
|
import { PostgresTaskStore } from './taskStore.ts';
|
|
20
20
|
import type { CredentialStoreOptions } from '../../tools/credentialStore.ts';
|
|
21
21
|
import type { CredentialStore } from '../../tools/auth.ts';
|
|
22
|
+
import type { PayloadPolicy } from '../../observability/supabaseSpanExporter.ts';
|
|
22
23
|
import type { AuditSink } from '../../observability/audit.ts';
|
|
23
24
|
import type { RlsHardeningStatus } from '../rlsStatus.ts';
|
|
24
25
|
import type { TaskBackend, TaskBackendOptions } from '../../tools/taskTools.ts';
|
|
@@ -28,6 +29,8 @@ export { PostgresSessionService } from './sessionService.ts';
|
|
|
28
29
|
export { PostgresTaskStore } from './taskStore.ts';
|
|
29
30
|
export { postgresMemoryStore } from './memoryStore.ts';
|
|
30
31
|
export { postgresCredentialRows, postgresCredentialStore } from './credentialStore.ts';
|
|
32
|
+
export { inProcessTurnLock } from '../../a2a/turnLock.ts';
|
|
33
|
+
export type { TurnLock } from '../../a2a/turnLock.ts';
|
|
31
34
|
export interface PostgresStorageOptions {
|
|
32
35
|
/** A Postgres URL. Ignored when `pool` is given. */
|
|
33
36
|
connectionString?: string;
|
|
@@ -54,6 +57,28 @@ export interface PostgresStorageOptions {
|
|
|
54
57
|
};
|
|
55
58
|
/** Owner of an A2A call, for the task store. Default: the SDK's user scope. */
|
|
56
59
|
taskOwner?: PostgresTaskStoreOptions['ownerResolver'];
|
|
60
|
+
/**
|
|
61
|
+
* The turn lock to use instead of the advisory lock below. The advisory
|
|
62
|
+
* lock is what makes several instances on one database take turns; it
|
|
63
|
+
* needs a database that gives each connection its own session. Embedded
|
|
64
|
+
* Postgres (PGlite behind pglite-socket) runs every connection in ONE
|
|
65
|
+
* session, where an advisory lock is re-entrant and so excludes nothing:
|
|
66
|
+
* there, one process owns the database, and `inProcessTurnLock()`
|
|
67
|
+
* (exported here) is the correct lock. When given, no turn-lock
|
|
68
|
+
* pool is opened.
|
|
69
|
+
*/
|
|
70
|
+
turnLock?: TurnLock;
|
|
71
|
+
/**
|
|
72
|
+
* Write the telemetry ledger (adk_turns, adk_telemetry, adk_payloads) to
|
|
73
|
+
* this database (ADR 0131): the rows the Supabase sink writes, with the
|
|
74
|
+
* same payload policy (TELEMETRY_PAYLOADS, or `policy`) and redaction.
|
|
75
|
+
* Needs db/telemetry.sql applied. `close()` flushes and detaches it.
|
|
76
|
+
* Default off.
|
|
77
|
+
*/
|
|
78
|
+
ledger?: boolean | {
|
|
79
|
+
policy?: PayloadPolicy;
|
|
80
|
+
deadLetterFile?: string;
|
|
81
|
+
};
|
|
57
82
|
/**
|
|
58
83
|
* Connections reserved for turn locks: each running turn holds one for its
|
|
59
84
|
* length, apart from the main pool so a long turn never starves queries.
|
|
@@ -22,12 +22,16 @@ import { postgresCredentialStore } from './credentialStore.js';
|
|
|
22
22
|
import { searchPathOption } from '../schema.js';
|
|
23
23
|
import { POSTGRES_RLS_QUERY, evaluatePostgresRls } from '../rlsStatus.js';
|
|
24
24
|
import { postgresAuditSink } from '../../observability/audit.js';
|
|
25
|
+
import { attachLedger } from '../../observability/tracer.js';
|
|
26
|
+
import { postgresLedgerClient } from '../../observability/supabaseSpanExporter.js';
|
|
25
27
|
import { createHash, randomUUID } from 'node:crypto';
|
|
26
28
|
import { hostname } from 'node:os';
|
|
27
29
|
export { PostgresSessionService } from './sessionService.js';
|
|
28
30
|
export { PostgresTaskStore } from './taskStore.js';
|
|
29
31
|
export { postgresMemoryStore } from './memoryStore.js';
|
|
30
32
|
export { postgresCredentialRows, postgresCredentialStore } from './credentialStore.js';
|
|
33
|
+
// The single-process turn lock, for an embedded database (`turnLock` below).
|
|
34
|
+
export { inProcessTurnLock } from '../../a2a/turnLock.js';
|
|
31
35
|
/**
|
|
32
36
|
* A pool emits 'error' when an idle connection dies (a database restart, a
|
|
33
37
|
* failover, an administrator's terminate); unhandled, that event crashes the
|
|
@@ -69,7 +73,7 @@ export function postgresStorage(options) {
|
|
|
69
73
|
...(options.pool ? options.pool.options : { connectionString: options.connectionString, ...options.poolConfig }),
|
|
70
74
|
max: options.lockPoolMax ?? 20,
|
|
71
75
|
}), 'turn-lock pool'));
|
|
72
|
-
const
|
|
76
|
+
const advisoryTurnLock = async (key, { waitMs, signal }) => {
|
|
73
77
|
const lockId = advisoryLockId(key);
|
|
74
78
|
const client = await locks().connect();
|
|
75
79
|
const deadline = Date.now() + Math.max(0, waitMs);
|
|
@@ -105,6 +109,10 @@ export function postgresStorage(options) {
|
|
|
105
109
|
};
|
|
106
110
|
return release;
|
|
107
111
|
};
|
|
112
|
+
const turnLock = options.turnLock ?? advisoryTurnLock;
|
|
113
|
+
const detachLedger = options.ledger
|
|
114
|
+
? attachLedger(postgresLedgerClient(pool), options.ledger === true ? {} : options.ledger)
|
|
115
|
+
: undefined;
|
|
108
116
|
const audit = postgresAuditSink(pool);
|
|
109
117
|
const credentials = options.credentials ? postgresCredentialStore(pool, { ...options.credentials, audit }) : undefined;
|
|
110
118
|
return {
|
|
@@ -160,6 +168,7 @@ export function postgresStorage(options) {
|
|
|
160
168
|
return counts;
|
|
161
169
|
},
|
|
162
170
|
async close() {
|
|
171
|
+
await detachLedger?.();
|
|
163
172
|
if (lockPool)
|
|
164
173
|
await lockPool.end();
|
|
165
174
|
if (owned)
|