melchizedek-agents 0.16.2 → 0.17.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 +17 -0
- package/AGENT_SETUP.md +2 -1
- package/CHANGELOG.md +142 -0
- package/DOCUMENTATION.md +237 -7
- package/QUICKSTART.md +2 -2
- package/README.md +125 -149
- package/config/agents/examples/harness.yaml +135 -0
- package/config/agents/examples/model_zoo.yaml +23 -9
- package/config/agents/examples/pipeline.yaml +120 -0
- package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
- package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
- package/config/agents/examples/weather.yaml +59 -0
- package/config/agents/syndicateSchema.yaml +112 -1
- package/db/migrations/0011_erase_expired.sql +198 -0
- package/dist/lib/a2a/app.d.ts +7 -0
- package/dist/lib/a2a/app.js +7 -1
- package/dist/lib/a2a/executor.js +41 -0
- package/dist/lib/compile.d.ts +40 -1
- package/dist/lib/compile.js +132 -14
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +17 -0
- package/dist/lib/doctor.js +17 -4
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/loadSyndicate.d.ts +46 -0
- package/dist/lib/loadSyndicate.js +19 -3
- package/dist/lib/memory/erase.d.ts +5 -0
- package/dist/lib/memory/erase.js +5 -0
- package/dist/lib/models/capabilities.js +10 -0
- package/dist/lib/models/endpoints.d.ts +1 -1
- package/dist/lib/models/endpoints.js +4 -1
- package/dist/lib/models/gateway.d.ts +2 -2
- package/dist/lib/models/gateway.js +4 -4
- package/dist/lib/models/kimiLlm.d.ts +123 -0
- package/dist/lib/models/kimiLlm.js +171 -0
- package/dist/lib/models/providerMap.d.ts +2 -2
- package/dist/lib/models/providerMap.js +4 -1
- package/dist/lib/models/registry.d.ts +1 -1
- package/dist/lib/models/registry.js +11 -3
- package/dist/lib/net/redirects.d.ts +14 -0
- package/dist/lib/net/redirects.js +94 -0
- package/dist/lib/observability/tracer.d.ts +1 -1
- package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
- package/dist/lib/persistence/supabaseProvider.js +2 -31
- package/dist/lib/runtime/questions.d.ts +55 -0
- package/dist/lib/runtime/questions.js +107 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +42 -4
- package/dist/lib/runtime/syndicateTurn.js +162 -16
- package/dist/lib/storage/postgres/index.d.ts +6 -0
- package/dist/lib/storage/postgres/index.js +11 -0
- package/dist/lib/storage/rlsStatus.d.ts +45 -0
- package/dist/lib/storage/rlsStatus.js +69 -0
- package/dist/lib/syndicateSchema.d.ts +169 -0
- package/dist/lib/syndicateSchema.js +327 -2
- package/dist/lib/toolRegistry.js +9 -0
- package/dist/lib/tools/openapiTools.d.ts +46 -0
- package/dist/lib/tools/openapiTools.js +195 -0
- package/dist/lib/tools/skillToolset.d.ts +135 -0
- package/dist/lib/tools/skillToolset.js +247 -0
- package/dist/lib/tools/urlContextTool.d.ts +29 -0
- package/dist/lib/tools/urlContextTool.js +39 -0
- package/dist/lib/workflow.d.ts +72 -0
- package/dist/lib/workflow.js +180 -0
- package/dist/lib/workflowConfig.d.ts +95 -0
- package/dist/lib/workflowConfig.js +93 -0
- package/dist/scripts/syndicate_chat.js +39 -5
- package/package.json +4 -1
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +8 -4
- package/skills/briefs/melchizedek-memory.md +1 -1
- package/skills/briefs/melchizedek-models.md +1 -1
- package/skills/briefs/melchizedek-scribe.md +4 -4
- package/skills/briefs/melchizedek-serve.md +1 -1
- package/skills/briefs/melchizedek.md +4 -1
- package/skills/melchizedek/SKILL.md +4 -1
- package/skills/melchizedek-author/SKILL.md +7 -3
- package/skills/melchizedek-memory/SKILL.md +1 -1
- package/skills/melchizedek-models/SKILL.md +1 -1
- package/skills/melchizedek-scribe/SKILL.md +3 -3
- package/skills/melchizedek-serve/SKILL.md +1 -1
- /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
- /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
|
@@ -105,7 +105,7 @@ export declare function serverToolEvents(ev: any): Array<{
|
|
|
105
105
|
}>;
|
|
106
106
|
export declare function traceAgentRun(stream: AsyncIterableIterator<Event>, metadata: TraceMetadata): AsyncGenerator<Event, void, void>;
|
|
107
107
|
export interface LlmCallMeta {
|
|
108
|
-
/** Provider id, e.g. 'anthropic', 'openai', 'xai', 'ollama', 'gemini'. */
|
|
108
|
+
/** Provider id, e.g. 'anthropic', 'openai', 'xai', 'moonshot', 'ollama', 'gemini'. */
|
|
109
109
|
provider: string;
|
|
110
110
|
/** The model id as declared in the agent YAML (e.g. 'ollama/qwen3:8b'). */
|
|
111
111
|
model: string;
|
|
@@ -9,12 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { BaseSessionService, BaseMemoryService } from '@google/adk';
|
|
11
11
|
import type { Embedder, MemoryExtractor } from '../memory/providers.ts';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
applied: boolean;
|
|
15
|
-
/** Human-readable explanation for logs. */
|
|
16
|
-
detail: string;
|
|
17
|
-
}
|
|
12
|
+
import type { RlsHardeningStatus } from '../storage/rlsStatus.ts';
|
|
13
|
+
export type { RlsHardeningStatus } from '../storage/rlsStatus.ts';
|
|
18
14
|
export interface PersistenceServices {
|
|
19
15
|
sessionService: BaseSessionService;
|
|
20
16
|
memoryService: BaseMemoryService | undefined;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Service construction (SupabaseSessionService, SupabaseVectorMemoryService)
|
|
9
9
|
*/
|
|
10
10
|
import { isPlaceholderValue } from '../loadEnv.js';
|
|
11
|
+
import { evaluateRlsRows } from '../storage/rlsStatus.js';
|
|
11
12
|
/**
|
|
12
13
|
* Returns true if all required Supabase credentials are present in the
|
|
13
14
|
* environment. Does NOT throw — callers use this to decide whether to
|
|
@@ -59,37 +60,7 @@ export async function createSupabaseServices(options) {
|
|
|
59
60
|
detail: 'db/hardening.sql has not been applied (status function missing)',
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
const unprotected = ['adk_memory_facts', 'adk_sessions'].filter((t) => !rows.some((r) => r.table_name === t && r.rls_enabled));
|
|
64
|
-
if (unprotected.length > 0) {
|
|
65
|
-
return {
|
|
66
|
-
applied: false,
|
|
67
|
-
detail: `RLS is disabled on: ${unprotected.join(', ')}`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
// adk_agent_registry is optional (only deployments that boot
|
|
71
|
-
// `registry:<id>` create it) but NOT advisory: an unprotected registry
|
|
72
|
-
// is agent takeover via the anon key, so a present-and-open table
|
|
73
|
-
// fails the check outright. Absent table => no row => nothing to say.
|
|
74
|
-
const registryRow = rows.find((r) => r.table_name === 'adk_agent_registry');
|
|
75
|
-
if (registryRow && !registryRow.rls_enabled) {
|
|
76
|
-
return {
|
|
77
|
-
applied: false,
|
|
78
|
-
detail: 'RLS is disabled on: adk_agent_registry (agent definitions are ' +
|
|
79
|
-
'writable with the anon key) — re-run db/hardening.sql',
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
// Advisory only — adk_telemetry is optional (db/telemetry.sql). If it
|
|
83
|
-
// exists but hardening.sql wasn't re-run afterwards, say so without
|
|
84
|
-
// failing the check (the mandatory tables are protected).
|
|
85
|
-
const telemetryRow = rows.find((r) => r.table_name === 'adk_telemetry');
|
|
86
|
-
if (telemetryRow && !telemetryRow.rls_enabled) {
|
|
87
|
-
return {
|
|
88
|
-
applied: true,
|
|
89
|
-
detail: 'RLS enabled on adk_memory_facts and adk_sessions — but NOT on adk_telemetry; re-run db/hardening.sql',
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return { applied: true, detail: 'RLS enabled on adk_memory_facts and adk_sessions' };
|
|
63
|
+
return evaluateRlsRows((data ?? []));
|
|
93
64
|
}
|
|
94
65
|
catch (err) {
|
|
95
66
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/runtime/questions.ts — an agent asks the person a question mid-turn,
|
|
3
|
+
* and the next message is the answer.
|
|
4
|
+
*
|
|
5
|
+
* WHY: A syndicate that is put in front of users needs to ask: "which of
|
|
6
|
+
* these three accounts?", "what is the order number?". Before this, an
|
|
7
|
+
* agent could only end its turn with a question in its text and hope the
|
|
8
|
+
* next message answered it, with nothing in the session saying a question
|
|
9
|
+
* was open. `ask_user` is ADK's long-running tool mechanism under one YAML
|
|
10
|
+
* name: the call is recorded, the run ends without a response to it, the
|
|
11
|
+
* turn ends `input-required` (`result.input`), and the next message on the
|
|
12
|
+
* conversation becomes the call's response, so the agent resumes its own
|
|
13
|
+
* tool loop where it asked, with the question and the answer side by side
|
|
14
|
+
* in its history.
|
|
15
|
+
*
|
|
16
|
+
* The same `input-required` path carries a workflow's `ask_user` node
|
|
17
|
+
* (lib/workflow.ts) and an approval (ADR 0028); a client handles all three
|
|
18
|
+
* by showing `result.input` (or the approval) and sending the next message.
|
|
19
|
+
*
|
|
20
|
+
* WHERE: only on an agent the turn runs directly (the orchestrator, or a
|
|
21
|
+
* plan-dispatch route), as with approval gates: inside a delegated
|
|
22
|
+
* subagent ADK swallows the pause. Not inside a workflow node yet. The
|
|
23
|
+
* schema refuses both.
|
|
24
|
+
*/
|
|
25
|
+
import { LongRunningFunctionTool } from '@google/adk';
|
|
26
|
+
import type { Event } from '@google/adk';
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
import type { PendingInput } from '../workflowConfig.ts';
|
|
29
|
+
/** The registry name, and the function-call name an open question carries. */
|
|
30
|
+
export declare const ASK_USER = "ask_user";
|
|
31
|
+
export declare const MAX_OPTIONS = 10;
|
|
32
|
+
/**
|
|
33
|
+
* The tool. Its execute returns nothing: a long-running call with no result
|
|
34
|
+
* ends the run, and ADK waits for a function response with the call's id.
|
|
35
|
+
*/
|
|
36
|
+
export declare const askUserTool: LongRunningFunctionTool<z.ZodObject<{
|
|
37
|
+
question: z.ZodString;
|
|
38
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
39
|
+
}, z.core.$strip>>;
|
|
40
|
+
/** The question an `ask_user` call carries, as the turn result reports it. */
|
|
41
|
+
export declare function questionFrom(author: string | undefined, call: {
|
|
42
|
+
name?: string;
|
|
43
|
+
id?: string;
|
|
44
|
+
args?: Record<string, unknown>;
|
|
45
|
+
}): PendingInput | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* The question still waiting for an answer in a session, or undefined. A
|
|
48
|
+
* question the person moved past (a text message after it was answered)
|
|
49
|
+
* is not pending; an answered one carries a function response with its id.
|
|
50
|
+
*/
|
|
51
|
+
export declare function pendingQuestion(events: readonly Event[]): PendingInput | undefined;
|
|
52
|
+
/** The message part that answers question `id` with the person's text. */
|
|
53
|
+
export declare function questionAnswerPart(id: string, answer: string): Record<string, unknown>;
|
|
54
|
+
/** Index of the user message that started the turn holding call `id` (replayed raw on a dispatch resume). */
|
|
55
|
+
export declare function turnStartOfCall(events: readonly Event[], id: string): number;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/runtime/questions.ts — an agent asks the person a question mid-turn,
|
|
3
|
+
* and the next message is the answer.
|
|
4
|
+
*
|
|
5
|
+
* WHY: A syndicate that is put in front of users needs to ask: "which of
|
|
6
|
+
* these three accounts?", "what is the order number?". Before this, an
|
|
7
|
+
* agent could only end its turn with a question in its text and hope the
|
|
8
|
+
* next message answered it, with nothing in the session saying a question
|
|
9
|
+
* was open. `ask_user` is ADK's long-running tool mechanism under one YAML
|
|
10
|
+
* name: the call is recorded, the run ends without a response to it, the
|
|
11
|
+
* turn ends `input-required` (`result.input`), and the next message on the
|
|
12
|
+
* conversation becomes the call's response, so the agent resumes its own
|
|
13
|
+
* tool loop where it asked, with the question and the answer side by side
|
|
14
|
+
* in its history.
|
|
15
|
+
*
|
|
16
|
+
* The same `input-required` path carries a workflow's `ask_user` node
|
|
17
|
+
* (lib/workflow.ts) and an approval (ADR 0028); a client handles all three
|
|
18
|
+
* by showing `result.input` (or the approval) and sending the next message.
|
|
19
|
+
*
|
|
20
|
+
* WHERE: only on an agent the turn runs directly (the orchestrator, or a
|
|
21
|
+
* plan-dispatch route), as with approval gates: inside a delegated
|
|
22
|
+
* subagent ADK swallows the pause. Not inside a workflow node yet. The
|
|
23
|
+
* schema refuses both.
|
|
24
|
+
*/
|
|
25
|
+
import { LongRunningFunctionTool } from '@google/adk';
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
/** The registry name, and the function-call name an open question carries. */
|
|
28
|
+
export const ASK_USER = 'ask_user';
|
|
29
|
+
export const MAX_OPTIONS = 10;
|
|
30
|
+
/**
|
|
31
|
+
* The tool. Its execute returns nothing: a long-running call with no result
|
|
32
|
+
* ends the run, and ADK waits for a function response with the call's id.
|
|
33
|
+
*/
|
|
34
|
+
export const askUserTool = new LongRunningFunctionTool({
|
|
35
|
+
name: ASK_USER,
|
|
36
|
+
description: 'Ask the person one question and wait for their answer before you continue: a missing detail you need, or a choice only they can make. ' +
|
|
37
|
+
'Pass `options` when the answer is one of a few choices. The turn ends here; the person\'s next message is the answer, returned to you as this call\'s result. ' +
|
|
38
|
+
'Ask only what you cannot find out or reasonably assume, one question per call.',
|
|
39
|
+
parameters: z.object({
|
|
40
|
+
question: z.string().min(1).max(500).describe('The question, as the person will read it.'),
|
|
41
|
+
options: z
|
|
42
|
+
.array(z.string().min(1).max(120))
|
|
43
|
+
.max(MAX_OPTIONS)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('The choices, when the answer is one of a few. Omit for a free answer.'),
|
|
46
|
+
}),
|
|
47
|
+
execute: async (_args, context) => {
|
|
48
|
+
if (context)
|
|
49
|
+
context.actions.skipSummarization = true;
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const partsOf = (e) => (e.content?.parts ?? []);
|
|
54
|
+
const hasUserText = (e) => e.author === 'user' && partsOf(e).some((p) => typeof p.text === 'string' && p.text.trim() && !p.thought);
|
|
55
|
+
/** The question an `ask_user` call carries, as the turn result reports it. */
|
|
56
|
+
export function questionFrom(author, call) {
|
|
57
|
+
if (call.name !== ASK_USER || !call.id)
|
|
58
|
+
return undefined;
|
|
59
|
+
const args = call.args ?? {};
|
|
60
|
+
const options = Array.isArray(args.options) ? args.options.filter((o) => typeof o === 'string') : [];
|
|
61
|
+
return {
|
|
62
|
+
id: call.id,
|
|
63
|
+
node: author ?? '',
|
|
64
|
+
message: typeof args.question === 'string' ? args.question : '',
|
|
65
|
+
...(options.length ? { payload: { options } } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The question still waiting for an answer in a session, or undefined. A
|
|
70
|
+
* question the person moved past (a text message after it was answered)
|
|
71
|
+
* is not pending; an answered one carries a function response with its id.
|
|
72
|
+
*/
|
|
73
|
+
export function pendingQuestion(events) {
|
|
74
|
+
const answered = new Set();
|
|
75
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
76
|
+
const e = events[i];
|
|
77
|
+
if (hasUserText(e))
|
|
78
|
+
return undefined;
|
|
79
|
+
for (const p of partsOf(e)) {
|
|
80
|
+
if (p.functionResponse?.name === ASK_USER && p.functionResponse.id)
|
|
81
|
+
answered.add(p.functionResponse.id);
|
|
82
|
+
}
|
|
83
|
+
for (const p of partsOf(e)) {
|
|
84
|
+
const call = p.functionCall;
|
|
85
|
+
if (call?.name !== ASK_USER || !call.id || answered.has(call.id))
|
|
86
|
+
continue;
|
|
87
|
+
return questionFrom(e.author, call);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/** The message part that answers question `id` with the person's text. */
|
|
93
|
+
export function questionAnswerPart(id, answer) {
|
|
94
|
+
return { functionResponse: { id, name: ASK_USER, response: { result: answer } } };
|
|
95
|
+
}
|
|
96
|
+
/** Index of the user message that started the turn holding call `id` (replayed raw on a dispatch resume). */
|
|
97
|
+
export function turnStartOfCall(events, id) {
|
|
98
|
+
let callAt = -1;
|
|
99
|
+
for (let i = events.length - 1; i >= 0 && callAt === -1; i--) {
|
|
100
|
+
if (partsOf(events[i]).some((p) => p.functionCall?.id === id))
|
|
101
|
+
callAt = i;
|
|
102
|
+
}
|
|
103
|
+
for (let i = callAt; i >= 0; i--)
|
|
104
|
+
if (hasUserText(events[i]))
|
|
105
|
+
return i;
|
|
106
|
+
return Math.max(0, callAt);
|
|
107
|
+
}
|
|
@@ -26,12 +26,17 @@
|
|
|
26
26
|
* time, and an outer AbortSignal cancels it. All three abort the provider
|
|
27
27
|
* request in flight, not just the loop around it.
|
|
28
28
|
*/
|
|
29
|
+
import type { BasePlugin } from '@google/adk';
|
|
29
30
|
import type { BaseMemoryService, BaseSessionService, Event, LlmAgent } from '@google/adk';
|
|
31
|
+
import type { PendingInput } from '../workflow.ts';
|
|
32
|
+
export { describeInput } from '../workflow.ts';
|
|
33
|
+
export type { PendingInput } from '../workflow.ts';
|
|
30
34
|
import type { CompileOptions } from '../compile.ts';
|
|
31
35
|
import type { RouteResolution } from '../dispatch.ts';
|
|
32
36
|
import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
|
|
37
|
+
export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.ts';
|
|
33
38
|
import type { PendingApproval } from './approvals.ts';
|
|
34
|
-
export { approvalResponsePart, pendingApproval } from './approvals.ts';
|
|
39
|
+
export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.ts';
|
|
35
40
|
export type { PendingApproval } from './approvals.ts';
|
|
36
41
|
import type { TurnStopReason } from './turnControl.ts';
|
|
37
42
|
/** One part of the user's message. Text is the common case; the genai part
|
|
@@ -140,11 +145,20 @@ export interface DrainedRun {
|
|
|
140
145
|
code: string;
|
|
141
146
|
message: string;
|
|
142
147
|
};
|
|
148
|
+
/** Workflow runs: questions `ask_user` nodes raised (`adk_request_input`). */
|
|
149
|
+
inputRequests: PendingInput[];
|
|
150
|
+
/** Workflow runs: errors nodes reported along the way (a retried attempt, a failed node). */
|
|
151
|
+
nodeErrors: Array<{
|
|
152
|
+
node: string;
|
|
153
|
+
code: string;
|
|
154
|
+
message: string;
|
|
155
|
+
}>;
|
|
143
156
|
}
|
|
144
|
-
export type TurnStage = 'classify' | 'dispatch' | 'delegate';
|
|
157
|
+
export type TurnStage = 'classify' | 'dispatch' | 'delegate' | 'workflow';
|
|
145
158
|
export interface RouteDecision extends RouteResolution {
|
|
146
|
-
/** `approval`: the turn resumed the route that asked for an approval (ADR 0028).
|
|
147
|
-
|
|
159
|
+
/** `approval`: the turn resumed the route that asked for an approval (ADR 0028).
|
|
160
|
+
* `answer`: the message answered the route's open `ask_user` question. */
|
|
161
|
+
decidedBy: 'override' | 'forced' | 'classifier' | 'approval' | 'answer';
|
|
148
162
|
}
|
|
149
163
|
export interface SyndicateTurnResult {
|
|
150
164
|
/** completed: an answer was produced. failed: an error, a deadline or the
|
|
@@ -154,6 +168,9 @@ export interface SyndicateTurnResult {
|
|
|
154
168
|
/** The call waiting for approval, when status is input-required. Answer it
|
|
155
169
|
* with `approvalResponsePart(approval.id, approved)` as the next turn's part. */
|
|
156
170
|
approval?: PendingApproval;
|
|
171
|
+
/** The question a workflow's `ask_user` node asked, when status is
|
|
172
|
+
* input-required. The next message on the conversation is the answer. */
|
|
173
|
+
input?: PendingInput;
|
|
157
174
|
/** The text the user receives (relay fallback and guards applied). */
|
|
158
175
|
text: string;
|
|
159
176
|
error?: {
|
|
@@ -188,6 +205,20 @@ export interface TurnUsage {
|
|
|
188
205
|
outputTokens: number;
|
|
189
206
|
thinkingTokens: number;
|
|
190
207
|
}
|
|
208
|
+
export declare const DEFAULT_MODEL_ERROR_RETRIES = 2;
|
|
209
|
+
export declare const DEFAULT_TOOL_ERROR_RETRIES = 3;
|
|
210
|
+
/**
|
|
211
|
+
* ADK's reflect-and-retry plugins, on by default. The model plugin turns a
|
|
212
|
+
* reply ADK marks malformed (MALFORMED_FUNCTION_CALL) into a retry with
|
|
213
|
+
* guidance instead of a failed turn; the tool plugin answers a tool that
|
|
214
|
+
* threw with structured guidance and caps its retries. Every retry is a
|
|
215
|
+
* model call under the turn's max_steps. Fresh instances per run: their
|
|
216
|
+
* counters are per invocation, but a run is the unit a surface reasons in.
|
|
217
|
+
*/
|
|
218
|
+
export declare function retryPlugins(retries: {
|
|
219
|
+
model_errors?: number;
|
|
220
|
+
tool_errors?: number;
|
|
221
|
+
} | undefined): BasePlugin[];
|
|
191
222
|
/**
|
|
192
223
|
* Drains one ADK event stream into a DrainedRun. The answer is the LAST
|
|
193
224
|
* event's text, not every event's: a tool-using agent narrates between
|
|
@@ -203,6 +234,13 @@ export declare function drainAgentStream(stream: AsyncIterable<Event>, opts?: {
|
|
|
203
234
|
subagentNames?: Set<string>;
|
|
204
235
|
/** Forward partial reply text to events.onTextDelta (answering stages only). */
|
|
205
236
|
streamText?: boolean;
|
|
237
|
+
/**
|
|
238
|
+
* What an event carrying an error means. `fail` (default): the run is
|
|
239
|
+
* over, the error is the result. `collect`: note it and read on — a
|
|
240
|
+
* workflow node that will retry emits its failed attempt, and a node
|
|
241
|
+
* that gives up ends the stream itself.
|
|
242
|
+
*/
|
|
243
|
+
errorPolicy?: 'fail' | 'collect';
|
|
206
244
|
}): Promise<DrainedRun>;
|
|
207
245
|
/** True when an orchestrator's text is just the name of a tool it called —
|
|
208
246
|
* the degenerate relay ("FinancialAnalyst", 3 tokens). Deliberately narrow:
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
* time, and an outer AbortSignal cancels it. All three abort the provider
|
|
27
27
|
* request in flight, not just the loop around it.
|
|
28
28
|
*/
|
|
29
|
-
import { InMemorySessionService, Runner, StreamingMode, getFunctionCalls, getFunctionResponses } from '@google/adk';
|
|
30
|
-
import { compileGraph, compileSubagent } from '../compile.js';
|
|
29
|
+
import { InMemorySessionService, ReflectAndRetryModelPlugin, ReflectAndRetryToolPlugin, Runner, StreamingMode, getFunctionCalls, getFunctionResponses } from '@google/adk';
|
|
30
|
+
import { agentGates, compileGraph, compileSubagent } from '../compile.js';
|
|
31
|
+
import { ROUTE_STEP_SUFFIX, compileWorkflow, describeInput, inputRequestFrom, isWorkflowSyndicate } from '../workflow.js';
|
|
32
|
+
export { describeInput } from '../workflow.js';
|
|
31
33
|
import { isDispatchSyndicate, matchRouteOverride, resolveRoute } from '../dispatch.js';
|
|
32
34
|
import { collectGrounding, describeGrounding, newGroundingState, webSourcesLine } from '../grounding.js';
|
|
33
35
|
import { resolveGuards } from '../guards/index.js';
|
|
@@ -36,13 +38,36 @@ import { registerAvailableProviders } from '../models/registry.js';
|
|
|
36
38
|
import { traceAgentRun } from '../observability/tracer.js';
|
|
37
39
|
import { ProjectedSessionService, renderTranscriptDigest } from '../session/transcript.js';
|
|
38
40
|
import { approvalDecisionIn, describeApproval, interruptedTurnStart, pendingApproval } from './approvals.js';
|
|
39
|
-
|
|
41
|
+
import { pendingQuestion, questionAnswerPart, questionFrom, turnStartOfCall } from './questions.js';
|
|
42
|
+
export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.js';
|
|
43
|
+
export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.js';
|
|
40
44
|
import { RemoteA2AAgent, remoteContextId, remoteToolOutput } from '../a2a/remoteAgent.js';
|
|
41
45
|
import { createTurnControl, runWithTurnControl, stopCode, stopMessage } from './turnControl.js';
|
|
42
46
|
const THOUGHTS_PREVIEW_CHARS = 600;
|
|
43
47
|
const CLASSIFIER_PREAMBLE = `RECENT CONVERSATION, oldest first — context for your classification only. `
|
|
44
48
|
+ `Never answer it, and never route on it alone; it is here so you can tell a `
|
|
45
49
|
+ `follow-up, a redo request, or a challenge to a previous answer apart from small talk.\n`;
|
|
50
|
+
// ── Self-correction (ADR 0034) ───────────────────────────────────────────────
|
|
51
|
+
export const DEFAULT_MODEL_ERROR_RETRIES = 2;
|
|
52
|
+
export const DEFAULT_TOOL_ERROR_RETRIES = 3;
|
|
53
|
+
/**
|
|
54
|
+
* ADK's reflect-and-retry plugins, on by default. The model plugin turns a
|
|
55
|
+
* reply ADK marks malformed (MALFORMED_FUNCTION_CALL) into a retry with
|
|
56
|
+
* guidance instead of a failed turn; the tool plugin answers a tool that
|
|
57
|
+
* threw with structured guidance and caps its retries. Every retry is a
|
|
58
|
+
* model call under the turn's max_steps. Fresh instances per run: their
|
|
59
|
+
* counters are per invocation, but a run is the unit a surface reasons in.
|
|
60
|
+
*/
|
|
61
|
+
export function retryPlugins(retries) {
|
|
62
|
+
const model = retries?.model_errors ?? DEFAULT_MODEL_ERROR_RETRIES;
|
|
63
|
+
const tool = retries?.tool_errors ?? DEFAULT_TOOL_ERROR_RETRIES;
|
|
64
|
+
const plugins = [];
|
|
65
|
+
if (model > 0)
|
|
66
|
+
plugins.push(new ReflectAndRetryModelPlugin({ maxRetries: model }));
|
|
67
|
+
if (tool > 0)
|
|
68
|
+
plugins.push(new ReflectAndRetryToolPlugin({ maxRetries: tool, throwExceptionIfRetryExceeded: false }));
|
|
69
|
+
return plugins;
|
|
70
|
+
}
|
|
46
71
|
// ── Draining one agent's stream ──────────────────────────────────────────────
|
|
47
72
|
function formatToolArgs(args) {
|
|
48
73
|
if (!args || Object.keys(args).length === 0)
|
|
@@ -75,10 +100,13 @@ export async function drainAgentStream(stream, opts = {}) {
|
|
|
75
100
|
thoughts: '',
|
|
76
101
|
tokens: { input: 0, output: 0, thinking: 0 },
|
|
77
102
|
eventCount: 0,
|
|
103
|
+
inputRequests: [],
|
|
104
|
+
nodeErrors: [],
|
|
78
105
|
};
|
|
79
106
|
const grounding = newGroundingState();
|
|
80
107
|
let groundingAnnounced = false;
|
|
81
108
|
let streamed = false;
|
|
109
|
+
let lastNode = '';
|
|
82
110
|
for await (const event of stream) {
|
|
83
111
|
const e = event;
|
|
84
112
|
d.eventCount++;
|
|
@@ -95,8 +123,24 @@ export async function drainAgentStream(stream, opts = {}) {
|
|
|
95
123
|
ev.onProgress?.('Invoking tool: web_search');
|
|
96
124
|
groundingAnnounced = true;
|
|
97
125
|
}
|
|
126
|
+
// A workflow event names its node; say when the graph moves on. The
|
|
127
|
+
// root (a path with no dot) and the hidden route steps are not nodes a
|
|
128
|
+
// person declared, so they stay out of the progress.
|
|
129
|
+
const nodePath = e.nodeInfo?.path;
|
|
130
|
+
if (nodePath && nodePath.includes('.') && e.author && !String(e.author).endsWith(ROUTE_STEP_SUFFIX) && e.author !== lastNode && e.partial !== true) {
|
|
131
|
+
lastNode = e.author;
|
|
132
|
+
ev.log?.(`⇢ Node: ${e.author}`);
|
|
133
|
+
if (opts.publishToolStatus)
|
|
134
|
+
ev.onProgress?.(`Running node: ${e.author}`);
|
|
135
|
+
}
|
|
98
136
|
if ((e.errorCode || e.errorMessage) && e.errorCode !== 'STOP') {
|
|
99
|
-
|
|
137
|
+
const error = { code: String(e.errorCode ?? 'ERROR'), message: String(e.errorMessage ?? '') };
|
|
138
|
+
if (opts.errorPolicy === 'collect') {
|
|
139
|
+
d.nodeErrors.push({ node: String(e.author ?? ''), ...error });
|
|
140
|
+
ev.warn?.(`Node ${e.author ?? '?'} reported [${error.code}]: ${error.message}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
d.error = error;
|
|
100
144
|
ev.warn?.(`Error [${d.error.code}]: ${d.error.message}`);
|
|
101
145
|
break;
|
|
102
146
|
}
|
|
@@ -123,6 +167,13 @@ export async function drainAgentStream(stream, opts = {}) {
|
|
|
123
167
|
ev.onProgress?.(`Delegating to subagent: ${target}`);
|
|
124
168
|
continue;
|
|
125
169
|
}
|
|
170
|
+
const inputRequest = inputRequestFrom(e.author, call) ?? questionFrom(e.author, call);
|
|
171
|
+
if (inputRequest) {
|
|
172
|
+
// An ask_user node: the workflow waits for the person (lib/workflow.ts).
|
|
173
|
+
d.inputRequests.push(inputRequest);
|
|
174
|
+
ev.log?.(`⏸ ${describeInput(inputRequest)}`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
126
177
|
if (!name)
|
|
127
178
|
continue;
|
|
128
179
|
d.invokedToolNames.add(name);
|
|
@@ -234,7 +285,7 @@ async function runTurnInner(opts, control) {
|
|
|
234
285
|
const transform = opts.transformAgent ?? ((a) => a);
|
|
235
286
|
const subagentNames = new Set((config.subagents ?? []).map((s) => s.name));
|
|
236
287
|
const trace = opts.trace === false ? undefined : opts.trace ?? {};
|
|
237
|
-
|
|
288
|
+
let parts = opts.parts.map((p) => (typeof p === 'string' ? { text: p } : p));
|
|
238
289
|
const messageText = parts.map((p) => (typeof p.text === 'string' ? p.text : '')).join('\n');
|
|
239
290
|
const result = {
|
|
240
291
|
status: 'completed',
|
|
@@ -286,12 +337,39 @@ async function runTurnInner(opts, control) {
|
|
|
286
337
|
// The log names the call, not its arguments: they are user content.
|
|
287
338
|
ev.log?.(`✓ Approval ${decision.approved ? 'granted' : 'refused'}: ${resuming.agent} → ${resuming.tool}`);
|
|
288
339
|
}
|
|
340
|
+
// ── Questions (lib/runtime/questions.ts) ───────────────────────────────────
|
|
341
|
+
// While an agent's `ask_user` call is open, a plain-text message is its
|
|
342
|
+
// answer: it becomes that call's response, and the agent that asked
|
|
343
|
+
// resumes its own tool loop. A workflow's pauses are ADK's own business.
|
|
344
|
+
let answering;
|
|
345
|
+
if (!decision && !isWorkflowSyndicate(config)) {
|
|
346
|
+
const question = pendingQuestion(existing?.events ?? []);
|
|
347
|
+
const plainText = parts.length > 0 && parts.every((p) => typeof p.text === 'string');
|
|
348
|
+
if (question && plainText) {
|
|
349
|
+
answering = { agent: question.node, id: question.id };
|
|
350
|
+
parts = [questionAnswerPart(question.id, messageText)];
|
|
351
|
+
ev.log?.(`✓ Answer to ${question.node}'s question`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** The agent a dispatch turn must resume, and the call its interrupted turn holds. */
|
|
355
|
+
const resumeTarget = resuming ? { agent: resuming.agent, id: resuming.id, why: 'resuming an approval' } : answering ? { ...answering, why: 'answering its question' } : undefined;
|
|
289
356
|
/** After the answering run: is a gated call now waiting? */
|
|
290
357
|
const awaitingApproval = async (agentName) => {
|
|
291
358
|
const after = await sessionService.getSession({ appName, userId, sessionId });
|
|
292
359
|
const pending = pendingApproval(after?.events ?? []);
|
|
293
360
|
return pending && pending.agent === agentName ? pending : undefined;
|
|
294
361
|
};
|
|
362
|
+
/** After the answering run: did the agent ask the person something? */
|
|
363
|
+
const asked = (run) => {
|
|
364
|
+
const input = run.inputRequests[run.inputRequests.length - 1];
|
|
365
|
+
if (!input)
|
|
366
|
+
return undefined;
|
|
367
|
+
result.status = 'input-required';
|
|
368
|
+
result.input = input;
|
|
369
|
+
result.text = input.message;
|
|
370
|
+
ev.log?.(`⏸ Input needed: ${describeInput(input)}`);
|
|
371
|
+
return finish();
|
|
372
|
+
};
|
|
295
373
|
const pause = (pending) => {
|
|
296
374
|
result.status = 'input-required';
|
|
297
375
|
result.approval = pending;
|
|
@@ -305,6 +383,7 @@ async function runTurnInner(opts, control) {
|
|
|
305
383
|
agent: params.agent,
|
|
306
384
|
appName,
|
|
307
385
|
sessionService: params.sessions,
|
|
386
|
+
plugins: retryPlugins(config.retries),
|
|
308
387
|
...(opts.memoryService ? { memoryService: opts.memoryService } : {}),
|
|
309
388
|
});
|
|
310
389
|
let stream = runner.runAsync({
|
|
@@ -342,6 +421,7 @@ async function runTurnInner(opts, control) {
|
|
|
342
421
|
publishToolStatus: params.publishToolStatus,
|
|
343
422
|
subagentNames,
|
|
344
423
|
streamText: params.stage !== 'classify' && !guarded,
|
|
424
|
+
errorPolicy: params.errorPolicy,
|
|
345
425
|
});
|
|
346
426
|
}
|
|
347
427
|
catch (err) {
|
|
@@ -359,16 +439,16 @@ async function runTurnInner(opts, control) {
|
|
|
359
439
|
// Deterministic overrides first: when the message itself decides the
|
|
360
440
|
// route there is nothing to classify, and the classifier call is skipped.
|
|
361
441
|
const warnings = [];
|
|
362
|
-
let resolution =
|
|
442
|
+
let resolution = resumeTarget ? null : matchRouteOverride(messageText, config, warnings);
|
|
363
443
|
let decidedBy = 'override';
|
|
364
444
|
for (const w of warnings)
|
|
365
445
|
ev.warn?.(w);
|
|
366
|
-
if (
|
|
367
|
-
if (!subagentNames.has(
|
|
368
|
-
throw new Error(`
|
|
446
|
+
if (resumeTarget) {
|
|
447
|
+
if (!subagentNames.has(resumeTarget.agent)) {
|
|
448
|
+
throw new Error(`Call ${resumeTarget.id} was raised by '${resumeTarget.agent}', which is not a route of this syndicate.`);
|
|
369
449
|
}
|
|
370
|
-
resolution = { route:
|
|
371
|
-
decidedBy = 'approval';
|
|
450
|
+
resolution = { route: resumeTarget.agent, reason: resumeTarget.why, fellBack: false, fallbackReason: '', viaOverride: false };
|
|
451
|
+
decidedBy = resuming ? 'approval' : 'answer';
|
|
372
452
|
}
|
|
373
453
|
else if (resolution) {
|
|
374
454
|
ev.log?.(`⇄ Route pinned by override: ${resolution.route}`);
|
|
@@ -437,9 +517,13 @@ async function runTurnInner(opts, control) {
|
|
|
437
517
|
agent: routeAgent,
|
|
438
518
|
sid: sessionId,
|
|
439
519
|
userParts: parts,
|
|
440
|
-
// Resuming an approval: the interrupted turn
|
|
441
|
-
// finds the call the
|
|
442
|
-
sessions: new ProjectedSessionService(sessionService, routeCfg.name, resuming
|
|
520
|
+
// Resuming an approval or answering a question: the interrupted turn
|
|
521
|
+
// is replayed raw, so ADK finds the call the message answers.
|
|
522
|
+
sessions: new ProjectedSessionService(sessionService, routeCfg.name, resuming
|
|
523
|
+
? { rawFrom: (events) => interruptedTurnStart(events, resuming.id) }
|
|
524
|
+
: answering
|
|
525
|
+
? { rawFrom: (events) => turnStartOfCall(events, answering.id) }
|
|
526
|
+
: {}),
|
|
443
527
|
stage: 'dispatch',
|
|
444
528
|
route: resolution,
|
|
445
529
|
publishToolStatus: true,
|
|
@@ -452,11 +536,14 @@ async function runTurnInner(opts, control) {
|
|
|
452
536
|
result.error = answer.error;
|
|
453
537
|
return finish();
|
|
454
538
|
}
|
|
455
|
-
if (!routeCfg.a2a_agent_url && routeCfg
|
|
539
|
+
if (!routeCfg.a2a_agent_url && agentGates(routeCfg)) {
|
|
456
540
|
const pending = await awaitingApproval(routeCfg.name);
|
|
457
541
|
if (pending)
|
|
458
542
|
return pause(pending);
|
|
459
543
|
}
|
|
544
|
+
const askedRoute = asked(answer);
|
|
545
|
+
if (askedRoute)
|
|
546
|
+
return askedRoute;
|
|
460
547
|
result.text = answer.text;
|
|
461
548
|
if (!result.text) {
|
|
462
549
|
// Naming the route turns a blank reply into a lead (the XScout outage
|
|
@@ -466,6 +553,60 @@ async function runTurnInner(opts, control) {
|
|
|
466
553
|
result.text = `${resolution.route} returned no output — the server logs carry the upstream error.`;
|
|
467
554
|
}
|
|
468
555
|
}
|
|
556
|
+
else if (isWorkflowSyndicate(config)) {
|
|
557
|
+
// ══ WORKFLOW ═════════════════════════════════════════════════════════
|
|
558
|
+
// The syndicate is a graph (lib/workflow.ts): every agent a node, run
|
|
559
|
+
// by ADK's Workflow in the shared session. A node agent sees only its
|
|
560
|
+
// input unless its YAML says otherwise, so no projection is needed. An
|
|
561
|
+
// `ask_user` node ends the turn input-required; the next message
|
|
562
|
+
// resumes the graph where it waited.
|
|
563
|
+
const compiled = await compileWorkflow(config, compileOpts, transform);
|
|
564
|
+
try {
|
|
565
|
+
answer = await runAgent({
|
|
566
|
+
agent: compiled.workflow,
|
|
567
|
+
sid: sessionId,
|
|
568
|
+
userParts: parts,
|
|
569
|
+
sessions: sessionService,
|
|
570
|
+
stage: 'workflow',
|
|
571
|
+
publishToolStatus: true,
|
|
572
|
+
errorPolicy: 'collect',
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
// A node that gave up (its retries spent, a timeout, a schema it could
|
|
577
|
+
// not satisfy) ends the run by throwing; the last reported error names it.
|
|
578
|
+
const last = control.stopReason ? undefined : err;
|
|
579
|
+
if (!last)
|
|
580
|
+
throw err;
|
|
581
|
+
result.status = 'failed';
|
|
582
|
+
result.failedStage = 'workflow';
|
|
583
|
+
result.error = { code: 'NODE_FAILED', message: last.message };
|
|
584
|
+
return finish();
|
|
585
|
+
}
|
|
586
|
+
result.answer = answer;
|
|
587
|
+
if (answer.error || control.stopReason) {
|
|
588
|
+
result.status = 'failed';
|
|
589
|
+
result.failedStage = 'workflow';
|
|
590
|
+
result.error = answer.error;
|
|
591
|
+
return finish();
|
|
592
|
+
}
|
|
593
|
+
if (answer.inputRequests.length) {
|
|
594
|
+
const input = answer.inputRequests[answer.inputRequests.length - 1];
|
|
595
|
+
result.status = 'input-required';
|
|
596
|
+
result.input = input;
|
|
597
|
+
result.text = input.message;
|
|
598
|
+
ev.log?.(`⏸ Input needed: ${describeInput(input)}`);
|
|
599
|
+
return finish();
|
|
600
|
+
}
|
|
601
|
+
result.text = answer.text;
|
|
602
|
+
if (!result.text) {
|
|
603
|
+
const failed = answer.nodeErrors[answer.nodeErrors.length - 1];
|
|
604
|
+
ev.warn?.(`The workflow produced no output${failed ? ` (last node error: ${failed.node} [${failed.code}] ${failed.message})` : ''}.`);
|
|
605
|
+
result.text = failed
|
|
606
|
+
? `The workflow ended without an answer — ${failed.node} failed [${failed.code}]: ${failed.message}`
|
|
607
|
+
: 'The workflow ended without an answer.';
|
|
608
|
+
}
|
|
609
|
+
}
|
|
469
610
|
else {
|
|
470
611
|
// ══ DELEGATE ═════════════════════════════════════════════════════════
|
|
471
612
|
// Subagents are AgentTools; the orchestrator relays the answer it got.
|
|
@@ -488,11 +629,14 @@ async function runTurnInner(opts, control) {
|
|
|
488
629
|
result.error = answer.error;
|
|
489
630
|
return finish();
|
|
490
631
|
}
|
|
491
|
-
if (config.orchestrator
|
|
632
|
+
if (agentGates(config.orchestrator)) {
|
|
492
633
|
const pending = await awaitingApproval(config.orchestrator.name);
|
|
493
634
|
if (pending)
|
|
494
635
|
return pause(pending);
|
|
495
636
|
}
|
|
637
|
+
const askedOrchestrator = asked(answer);
|
|
638
|
+
if (askedOrchestrator)
|
|
639
|
+
return askedOrchestrator;
|
|
496
640
|
result.text = answer.text;
|
|
497
641
|
// Failed-relay fallback: an orchestrator can botch the hop that relays a
|
|
498
642
|
// specialist's answer — STOP with no text, or the bare tool NAME as its
|
|
@@ -563,6 +707,8 @@ async function runRemoteRoute(name, url, messageText, sessionId, control, ev) {
|
|
|
563
707
|
}
|
|
564
708
|
function emptyRun(error) {
|
|
565
709
|
return {
|
|
710
|
+
inputRequests: [],
|
|
711
|
+
nodeErrors: [],
|
|
566
712
|
text: '',
|
|
567
713
|
lastToolResultText: '',
|
|
568
714
|
toolResultTexts: [],
|
|
@@ -17,6 +17,7 @@ import type { Embedder, MemoryExtractor } from '../../memory/providers.ts';
|
|
|
17
17
|
import type { EraseCounts, EraseOptions } from '../../memory/erase.ts';
|
|
18
18
|
import { PostgresSessionService } from './sessionService.ts';
|
|
19
19
|
import { PostgresTaskStore } from './taskStore.ts';
|
|
20
|
+
import type { RlsHardeningStatus } from '../rlsStatus.ts';
|
|
20
21
|
import type { TaskBackend } from '../../tools/taskTools.ts';
|
|
21
22
|
import type { TurnLock } from '../../a2a/turnLock.ts';
|
|
22
23
|
import type { PostgresTaskStoreOptions } from './taskStore.ts';
|
|
@@ -70,6 +71,11 @@ export interface PostgresStorage {
|
|
|
70
71
|
erase: (scopeKey: string, options?: EraseOptions) => Promise<EraseCounts>;
|
|
71
72
|
/** The highest migration recorded in melchizedek_schema_version; null if none. */
|
|
72
73
|
schemaVersion: () => Promise<number | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Whether db/hardening.sql is in force where an API could expose the
|
|
76
|
+
* tables (lib/storage/rlsStatus.ts). Never throws.
|
|
77
|
+
*/
|
|
78
|
+
rlsHardening: () => Promise<RlsHardeningStatus>;
|
|
73
79
|
/**
|
|
74
80
|
* One turn at a time per conversation, across every instance on this
|
|
75
81
|
* database: a session-level advisory lock on a dedicated connection, so a
|