melchizedek-agents 1.3.1 → 1.5.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 +134 -0
- package/DOCUMENTATION.md +61 -5
- package/ONBOARDING.md +3 -3
- package/README.md +1 -0
- 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 +38 -12
- 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 +2 -0
- 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 +9 -0
- package/dist/lib/observability/tracer.js +45 -0
- 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/delegate.d.ts +2 -0
- package/dist/lib/runtime/native/delegate.js +9 -2
- package/dist/lib/runtime/native/interrupts.d.ts +33 -3
- package/dist/lib/runtime/native/interrupts.js +99 -9
- package/dist/lib/runtime/syndicateTurn.js +41 -11
- 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
|
@@ -43,6 +43,7 @@ const { ExportResultCode } = core;
|
|
|
43
43
|
import { hasSupabaseCredentials } from '../persistence/supabaseProvider.js';
|
|
44
44
|
import { TELEMETRY_SCHEMA_VERSION, isModelCallSpan } from './lineage.js';
|
|
45
45
|
import { redactRow, telemetryRedactor } from './redact.js';
|
|
46
|
+
import { outputsDir } from '../paths.js';
|
|
46
47
|
/**
|
|
47
48
|
* TELEMETRY_PAYLOADS: off | errors | sample (default) | all
|
|
48
49
|
* TELEMETRY_PAYLOAD_SAMPLE: 0..1, default 0.10
|
|
@@ -300,7 +301,42 @@ export function telemetrySinkEnabled() {
|
|
|
300
301
|
return process.env.TELEMETRY_SUPABASE === 'true' && hasSupabaseCredentials();
|
|
301
302
|
}
|
|
302
303
|
export function deadLetterPath() {
|
|
303
|
-
return join(
|
|
304
|
+
return join(outputsDir(), 'telemetry-deadletter.ndjson');
|
|
305
|
+
}
|
|
306
|
+
/** The tables the exporter writes; nothing else is ever named in its SQL. */
|
|
307
|
+
export const LEDGER_TABLES = ['adk_telemetry', 'adk_turns', 'adk_payloads'];
|
|
308
|
+
const quoteIdent = (name) => `"${name.replace(/"/g, '""')}"`;
|
|
309
|
+
/**
|
|
310
|
+
* The ledger over a direct Postgres connection (ADR 0131): the same rows the
|
|
311
|
+
* Supabase sink writes, through one parameterised statement per batch. The
|
|
312
|
+
* rows go in as one JSON array and `jsonb_populate_recordset` gives each
|
|
313
|
+
* value its column's type, so JSON, arrays and timestamps need no mapping
|
|
314
|
+
* here. Only the columns the rows carry are named, so the rest keep their
|
|
315
|
+
* defaults. The table must be one of LEDGER_TABLES and every column name
|
|
316
|
+
* comes from the row builders above, never from span content.
|
|
317
|
+
*/
|
|
318
|
+
export function postgresLedgerClient(pool) {
|
|
319
|
+
return {
|
|
320
|
+
from(table) {
|
|
321
|
+
return {
|
|
322
|
+
async insert(rows) {
|
|
323
|
+
if (!LEDGER_TABLES.includes(table))
|
|
324
|
+
return { error: { message: `not a ledger table: ${table}` } };
|
|
325
|
+
const columns = [...new Set(rows.flatMap((r) => Object.entries(r).filter(([, v]) => v !== undefined).map(([k]) => k)))];
|
|
326
|
+
if (!columns.length)
|
|
327
|
+
return { error: null };
|
|
328
|
+
const list = columns.map(quoteIdent).join(', ');
|
|
329
|
+
try {
|
|
330
|
+
await pool.query(`INSERT INTO ${quoteIdent(table)} (${list}) SELECT ${list} FROM jsonb_populate_recordset(NULL::${quoteIdent(table)}, $1::jsonb)`, [JSON.stringify(rows)]);
|
|
331
|
+
return { error: null };
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return { error: { message: err instanceof Error ? err.message : String(err) } };
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
};
|
|
304
340
|
}
|
|
305
341
|
const PENDING_TTL_MS = 10 * 60 * 1000;
|
|
306
342
|
const DECISION_TTL_MS = 30 * 60 * 1000;
|
|
@@ -3,6 +3,7 @@ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
|
3
3
|
import type { LlmResponse } from '../models/genaiMapping.ts';
|
|
4
4
|
import type { TurnEvent as Event } from '../runtime/events.ts';
|
|
5
5
|
import type { ModelRequest } from '../models/contract.ts';
|
|
6
|
+
import type { ExporterOptions, LedgerClient } from './supabaseSpanExporter.ts';
|
|
6
7
|
/**
|
|
7
8
|
* Optional OTLP export for a live trace viewer (Phoenix, Langfuse, Grafana
|
|
8
9
|
* Tempo, Jaeger — anything that speaks OTLP/HTTP). Set
|
|
@@ -15,6 +16,14 @@ import type { ModelRequest } from '../models/contract.ts';
|
|
|
15
16
|
* Supabase stays the system of record; this is a viewer.
|
|
16
17
|
*/
|
|
17
18
|
export declare function otlpEndpoint(): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Writes the ledger (adk_turns, adk_telemetry, adk_payloads) through
|
|
21
|
+
* `client`, batched off the turn's path, with the same rows, payload policy
|
|
22
|
+
* and redaction as the Supabase sink. One ledger is attached at a time;
|
|
23
|
+
* attaching another replaces it. Returns a detach that flushes what is
|
|
24
|
+
* buffered first.
|
|
25
|
+
*/
|
|
26
|
+
export declare function attachLedger(client: LedgerClient, options?: Omit<ExporterOptions, 'client'>): () => Promise<void>;
|
|
18
27
|
/**
|
|
19
28
|
* Flushes buffered span processors (console is synchronous; the Supabase
|
|
20
29
|
* batch sink is not). Call before process exit in short-lived scripts so
|
|
@@ -51,6 +51,49 @@ function otlpProcessor() {
|
|
|
51
51
|
let isOtelInitialized = false;
|
|
52
52
|
let tracerProvider;
|
|
53
53
|
let telemetryExporter;
|
|
54
|
+
/**
|
|
55
|
+
* The ledger an embedding application attaches at run time (ADR 0131):
|
|
56
|
+
* `attachLedger` (or `postgresStorage({ ledger: true })`) sets it, before or
|
|
57
|
+
* after tracing starts. The processor below is always registered and does
|
|
58
|
+
* nothing until then.
|
|
59
|
+
*/
|
|
60
|
+
let attachedLedger;
|
|
61
|
+
class AttachedLedgerProcessor {
|
|
62
|
+
onStart() { }
|
|
63
|
+
onEnd(span) {
|
|
64
|
+
attachedLedger?.processor.onEnd(span);
|
|
65
|
+
}
|
|
66
|
+
forceFlush() {
|
|
67
|
+
return attachedLedger ? attachedLedger.processor.forceFlush().then(() => attachedLedger?.exporter.forceFlush()) : Promise.resolve();
|
|
68
|
+
}
|
|
69
|
+
shutdown() {
|
|
70
|
+
return this.forceFlush();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Writes the ledger (adk_turns, adk_telemetry, adk_payloads) through
|
|
75
|
+
* `client`, batched off the turn's path, with the same rows, payload policy
|
|
76
|
+
* and redaction as the Supabase sink. One ledger is attached at a time;
|
|
77
|
+
* attaching another replaces it. Returns a detach that flushes what is
|
|
78
|
+
* buffered first.
|
|
79
|
+
*/
|
|
80
|
+
export function attachLedger(client, options = {}) {
|
|
81
|
+
if (telemetrySinkEnabled()) {
|
|
82
|
+
console.warn('[TELEMETRY] ⚠ a ledger is attached while TELEMETRY_SUPABASE=true: every row is written twice. Use one sink.');
|
|
83
|
+
}
|
|
84
|
+
const exporter = new SupabaseSpanExporter({ ...options, client });
|
|
85
|
+
const entry = { exporter, processor: new sdkBase.BatchSpanProcessor(exporter) };
|
|
86
|
+
const previous = attachedLedger;
|
|
87
|
+
attachedLedger = entry;
|
|
88
|
+
void previous?.processor.shutdown();
|
|
89
|
+
initializeTracing();
|
|
90
|
+
return async () => {
|
|
91
|
+
if (attachedLedger === entry)
|
|
92
|
+
attachedLedger = undefined;
|
|
93
|
+
await entry.processor.forceFlush();
|
|
94
|
+
await entry.exporter.shutdown();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
54
97
|
/**
|
|
55
98
|
* Flushes buffered span processors (console is synchronous; the Supabase
|
|
56
99
|
* batch sink is not). Call before process exit in short-lived scripts so
|
|
@@ -59,6 +102,7 @@ let telemetryExporter;
|
|
|
59
102
|
export async function flushTracing() {
|
|
60
103
|
await tracerProvider?.forceFlush?.();
|
|
61
104
|
await telemetryExporter?.forceFlush?.();
|
|
105
|
+
await attachedLedger?.exporter.forceFlush();
|
|
62
106
|
}
|
|
63
107
|
// ── In-process span listeners ────────────────────────────────────────────────
|
|
64
108
|
// Lets a script (e.g. scripts/demo_model_optionality.ts) read finished spans
|
|
@@ -231,6 +275,7 @@ export function initializeTracing() {
|
|
|
231
275
|
const spanProcessors = [
|
|
232
276
|
new SpanLineageProcessor(),
|
|
233
277
|
new SimpleSpanProcessor(new JsonConsoleExporter()),
|
|
278
|
+
new AttachedLedgerProcessor(),
|
|
234
279
|
];
|
|
235
280
|
// Optional durable sink (TELEMETRY_SUPABASE=true + Supabase credentials):
|
|
236
281
|
// llm.request and Syndicate Execution spans also land in adk_telemetry.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/paths.ts — where the engine writes on disk (ADR 0134).
|
|
3
|
+
*
|
|
4
|
+
* Each location keeps its own variable, which wins. Without one, the
|
|
5
|
+
* default is under MELCHIZEDEK_DATA_DIR when that is set, so an embedding
|
|
6
|
+
* application points every location at its own data directory with one
|
|
7
|
+
* variable; with neither, the defaults are the historical ones (the working
|
|
8
|
+
* directory's outputs/, and ~/.melchizedek for the ChatGPT sign-in).
|
|
9
|
+
*
|
|
10
|
+
* location own variable with MELCHIZEDEK_DATA_DIR default
|
|
11
|
+
* generated files, spool OUTPUTS_DIR <data>/outputs <cwd>/outputs
|
|
12
|
+
* task lists and jobs MELCHIZEDEK_TASKS_FILE <data>/outputs/tasks.json <cwd>/outputs/tasks.json
|
|
13
|
+
* ChatGPT sign-in MELCHIZEDEK_CHATGPT_SIGNIN_FILE <data>/chatgpt-signin.json ~/.melchizedek/chatgpt-signin.json
|
|
14
|
+
*
|
|
15
|
+
* Read at each call, never cached. Imports only node built-ins.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DATA_DIR_ENV = "MELCHIZEDEK_DATA_DIR";
|
|
18
|
+
/** MELCHIZEDEK_DATA_DIR, resolved; undefined when unset. */
|
|
19
|
+
export declare function dataDir(env?: NodeJS.ProcessEnv): string | undefined;
|
|
20
|
+
/** Where generated files (images) and the telemetry spool go. */
|
|
21
|
+
export declare function outputsDir(env?: NodeJS.ProcessEnv): string;
|
|
22
|
+
/** The task tools' JSON file, when tasks are not in Postgres. */
|
|
23
|
+
export declare function tasksFile(env?: NodeJS.ProcessEnv): string;
|
|
24
|
+
/** The local Sign in with ChatGPT credential file. */
|
|
25
|
+
export declare function chatGptSignInFile(env?: NodeJS.ProcessEnv): string;
|
|
26
|
+
/**
|
|
27
|
+
* The engine's own package root (the directory of its package.json named
|
|
28
|
+
* melchizedek-agents), found from this module's location, whatever the
|
|
29
|
+
* working directory is; undefined if it cannot be found.
|
|
30
|
+
*/
|
|
31
|
+
export declare function enginePackageRoot(from?: string): string | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/paths.ts — where the engine writes on disk (ADR 0134).
|
|
3
|
+
*
|
|
4
|
+
* Each location keeps its own variable, which wins. Without one, the
|
|
5
|
+
* default is under MELCHIZEDEK_DATA_DIR when that is set, so an embedding
|
|
6
|
+
* application points every location at its own data directory with one
|
|
7
|
+
* variable; with neither, the defaults are the historical ones (the working
|
|
8
|
+
* directory's outputs/, and ~/.melchizedek for the ChatGPT sign-in).
|
|
9
|
+
*
|
|
10
|
+
* location own variable with MELCHIZEDEK_DATA_DIR default
|
|
11
|
+
* generated files, spool OUTPUTS_DIR <data>/outputs <cwd>/outputs
|
|
12
|
+
* task lists and jobs MELCHIZEDEK_TASKS_FILE <data>/outputs/tasks.json <cwd>/outputs/tasks.json
|
|
13
|
+
* ChatGPT sign-in MELCHIZEDEK_CHATGPT_SIGNIN_FILE <data>/chatgpt-signin.json ~/.melchizedek/chatgpt-signin.json
|
|
14
|
+
*
|
|
15
|
+
* Read at each call, never cached. Imports only node built-ins.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
export const DATA_DIR_ENV = 'MELCHIZEDEK_DATA_DIR';
|
|
22
|
+
const set = (env, name) => env[name]?.trim() || undefined;
|
|
23
|
+
/** MELCHIZEDEK_DATA_DIR, resolved; undefined when unset. */
|
|
24
|
+
export function dataDir(env = process.env) {
|
|
25
|
+
const d = set(env, DATA_DIR_ENV);
|
|
26
|
+
return d ? resolve(d) : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Where generated files (images) and the telemetry spool go. */
|
|
29
|
+
export function outputsDir(env = process.env) {
|
|
30
|
+
const own = set(env, 'OUTPUTS_DIR');
|
|
31
|
+
if (own)
|
|
32
|
+
return resolve(own);
|
|
33
|
+
const data = dataDir(env);
|
|
34
|
+
return data ? join(data, 'outputs') : join(process.cwd(), 'outputs');
|
|
35
|
+
}
|
|
36
|
+
/** The task tools' JSON file, when tasks are not in Postgres. */
|
|
37
|
+
export function tasksFile(env = process.env) {
|
|
38
|
+
const own = set(env, 'MELCHIZEDEK_TASKS_FILE');
|
|
39
|
+
if (own)
|
|
40
|
+
return resolve(own);
|
|
41
|
+
const data = dataDir(env);
|
|
42
|
+
return data ? join(data, 'outputs', 'tasks.json') : join(process.cwd(), 'outputs', 'tasks.json');
|
|
43
|
+
}
|
|
44
|
+
/** The local Sign in with ChatGPT credential file. */
|
|
45
|
+
export function chatGptSignInFile(env = process.env) {
|
|
46
|
+
const own = set(env, 'MELCHIZEDEK_CHATGPT_SIGNIN_FILE');
|
|
47
|
+
if (own)
|
|
48
|
+
return resolve(own);
|
|
49
|
+
const data = dataDir(env);
|
|
50
|
+
return data ? join(data, 'chatgpt-signin.json') : join(homedir(), '.melchizedek', 'chatgpt-signin.json');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The engine's own package root (the directory of its package.json named
|
|
54
|
+
* melchizedek-agents), found from this module's location, whatever the
|
|
55
|
+
* working directory is; undefined if it cannot be found.
|
|
56
|
+
*/
|
|
57
|
+
export function enginePackageRoot(from = fileURLToPath(import.meta.url)) {
|
|
58
|
+
let dir = dirname(from);
|
|
59
|
+
for (let i = 0; i < 6; i++) {
|
|
60
|
+
const pkg = join(dir, 'package.json');
|
|
61
|
+
if (existsSync(pkg)) {
|
|
62
|
+
try {
|
|
63
|
+
if (JSON.parse(readFileSync(pkg, 'utf-8')).name === 'melchizedek-agents')
|
|
64
|
+
return dir;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* not ours; keep climbing */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const parent = dirname(dir);
|
|
71
|
+
if (parent === dir)
|
|
72
|
+
break;
|
|
73
|
+
dir = parent;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
@@ -57,6 +57,27 @@ export declare function consentFrom(author: string | undefined, call: {
|
|
|
57
57
|
* message a grant (as pendingQuestion reads ask_user).
|
|
58
58
|
*/
|
|
59
59
|
export declare function pendingConsent(events: readonly Event[]): PendingConsent | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* The `adk_request_credential` call a pending request stands for, rebuilt
|
|
62
|
+
* from what the request carries (ADR 0128): the shape consentFrom reads,
|
|
63
|
+
* with the request's id, the paused call's id, the provider, the
|
|
64
|
+
* authorization URL, the state and the scopes. A workflow node raises it
|
|
65
|
+
* again on its caller's walk when the request itself waits below it (in a
|
|
66
|
+
* delegated call, or in a nested dispatch syndicate's conversation). It
|
|
67
|
+
* carries what the stored request carries and nothing more: no client
|
|
68
|
+
* secret, no code verifier.
|
|
69
|
+
*/
|
|
70
|
+
export declare function credentialRequestCall(consent: PendingConsent): {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
args: Record<string, unknown>;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Whether `value`, a walk's answer to a credential request, is a grant's
|
|
77
|
+
* answer (credentialResponsePart's response): `{ credentialKey, granted: true }`.
|
|
78
|
+
* A plain-text message the walk read as the answer is not one.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isGrantAnswer(value: unknown): boolean;
|
|
60
81
|
/**
|
|
61
82
|
* The message part that answers request `id` once the grant is stored. It
|
|
62
83
|
* names the provider and carries no credential: the grant is in the
|
|
@@ -53,6 +53,38 @@ export function pendingConsent(events) {
|
|
|
53
53
|
}
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The `adk_request_credential` call a pending request stands for, rebuilt
|
|
58
|
+
* from what the request carries (ADR 0128): the shape consentFrom reads,
|
|
59
|
+
* with the request's id, the paused call's id, the provider, the
|
|
60
|
+
* authorization URL, the state and the scopes. A workflow node raises it
|
|
61
|
+
* again on its caller's walk when the request itself waits below it (in a
|
|
62
|
+
* delegated call, or in a nested dispatch syndicate's conversation). It
|
|
63
|
+
* carries what the stored request carries and nothing more: no client
|
|
64
|
+
* secret, no code verifier.
|
|
65
|
+
*/
|
|
66
|
+
export function credentialRequestCall(consent) {
|
|
67
|
+
return {
|
|
68
|
+
id: consent.id,
|
|
69
|
+
name: CREDENTIAL_REQUEST,
|
|
70
|
+
args: {
|
|
71
|
+
function_call_id: consent.functionCallId,
|
|
72
|
+
auth_config: {
|
|
73
|
+
credentialKey: consent.provider,
|
|
74
|
+
authScheme: { type: 'oauth2', flows: { authorizationCode: { scopes: Object.fromEntries(consent.scopes.map((s) => [s, ''])) } } },
|
|
75
|
+
exchangedAuthCredential: { authType: 'oauth2', oauth2: { authUri: consent.authUri, state: consent.state } },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether `value`, a walk's answer to a credential request, is a grant's
|
|
82
|
+
* answer (credentialResponsePart's response): `{ credentialKey, granted: true }`.
|
|
83
|
+
* A plain-text message the walk read as the answer is not one.
|
|
84
|
+
*/
|
|
85
|
+
export function isGrantAnswer(value) {
|
|
86
|
+
return isRecord(value) && value.granted === true && typeof value.credentialKey === 'string' && value.credentialKey !== '';
|
|
87
|
+
}
|
|
56
88
|
/**
|
|
57
89
|
* The message part that answers request `id` once the grant is stored. It
|
|
58
90
|
* names the provider and carries no credential: the grant is in the
|
|
@@ -137,6 +137,8 @@ export interface WorkflowSubagentRun {
|
|
|
137
137
|
log?: (message: string) => void;
|
|
138
138
|
selfCorrection: SelfCorrection;
|
|
139
139
|
credentials?: AgentLoopContext['credentials'];
|
|
140
|
+
/** The caller's consent step, pinned to the caller's app (consentPinnedTo, ADR 0128): a node of the walk pauses for a missing grant. */
|
|
141
|
+
consent?: AgentLoopContext['consent'];
|
|
140
142
|
}
|
|
141
143
|
/** How a nested workflow's walk ended, as far as a delegated call reads it (lib/workflow/turn.ts NativeWorkflowEnd). */
|
|
142
144
|
export interface WorkflowSubagentEnd {
|
|
@@ -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';
|
|
@@ -484,6 +485,8 @@ async function walkChild(workflow, userParts, context, scope, resuming) {
|
|
|
484
485
|
...(ctx.memory ? { memory: ctx.memory } : {}),
|
|
485
486
|
...(ctx.log ? { log: ctx.log } : {}),
|
|
486
487
|
...(ctx.credentials ? { credentials: ctx.credentials } : {}),
|
|
488
|
+
// A node that needs a grant pauses the walk, and the pause reaches the turn through the open call (ADR 0128).
|
|
489
|
+
...(ctx.consent ? { consent: consentPinnedTo(ctx.consent, ctx.session.appName) } : {}),
|
|
487
490
|
});
|
|
488
491
|
let last;
|
|
489
492
|
for (;;) {
|
|
@@ -528,8 +531,9 @@ function dispatchParts(parts) {
|
|
|
528
531
|
/**
|
|
529
532
|
* The request call a paused nested dispatch syndicate's turn waits on, as a
|
|
530
533
|
* workflow node raises it again on its caller's walk: an approval request
|
|
531
|
-
* pinned to the gated call,
|
|
532
|
-
*
|
|
534
|
+
* pinned to the gated call, an input request carrying the question, or a
|
|
535
|
+
* credential request rebuilt from the consent request (credentialRequestCall,
|
|
536
|
+
* ADR 0128), under the nested request's id.
|
|
533
537
|
*/
|
|
534
538
|
function requestCallOf(end) {
|
|
535
539
|
const id = end.interruptId;
|
|
@@ -556,6 +560,9 @@ function requestCallOf(end) {
|
|
|
556
560
|
},
|
|
557
561
|
};
|
|
558
562
|
}
|
|
563
|
+
const consent = end.consent;
|
|
564
|
+
if (consent && consent.id === id)
|
|
565
|
+
return credentialRequestCall(consent);
|
|
559
566
|
return undefined;
|
|
560
567
|
}
|
|
561
568
|
/**
|
|
@@ -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];
|