Skip to content

Commit d977769

Browse files
committed
feat(plugins): add ReflectAndRetryToolPlugin and ReflectAndRetryModelPlugin for self-healing error recovery
1 parent 495c85c commit d977769

8 files changed

Lines changed: 1995 additions & 0 deletions

core/src/common.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,29 @@ export {LLMRegistry} from './models/registry.js';
195195
export type {BaseLlmType} from './models/registry.js';
196196
export {RoutedLlm} from './models/routed_llm.js';
197197
export type {LlmRouter} from './models/routed_llm.js';
198+
export {
199+
GLOBAL_SCOPE_KEY,
200+
REFLECT_AND_RETRY_RESPONSE_TYPE,
201+
ScopedFailureTracker,
202+
TrackingScope,
203+
resolveScopeKey,
204+
type PerItemFailuresCounter,
205+
type ToolFailureResponse,
206+
} from './plugins/_reflect_retry_utils.js';
198207
export {BasePlugin, ContextCompactionTrigger} from './plugins/base_plugin.js';
199208
export {GlobalInstructionPlugin} from './plugins/global_instruction_plugin.js';
200209
export {LoggingPlugin} from './plugins/logging_plugin.js';
201210
export {PluginManager} from './plugins/plugin_manager.js';
211+
export {
212+
ADK_HANDLE_MODEL_ERROR_TOOL_NAME,
213+
RESERVED_TOOL_CALL_ERROR_TYPE,
214+
ReflectAndRetryModelPlugin,
215+
type ReflectAndRetryModelPluginOptions,
216+
} from './plugins/reflect_retry_model_plugin.js';
217+
export {
218+
ReflectAndRetryToolPlugin,
219+
type ReflectAndRetryToolPluginOptions,
220+
} from './plugins/reflect_retry_tool_plugin.js';
202221
export {
203222
InMemoryPolicyEngine,
204223
PolicyOutcome,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export const REFLECT_AND_RETRY_RESPONSE_TYPE =
8+
'ERROR_HANDLED_BY_REFLECT_AND_RETRY_PLUGIN';
9+
export const GLOBAL_SCOPE_KEY = '__global_reflect_and_retry_scope__';
10+
11+
/**
12+
* Defines the lifecycle scope for tracking failure counts.
13+
*/
14+
export enum TrackingScope {
15+
/** Track failures within the current agent invocation. */
16+
INVOCATION = 'invocation',
17+
/** Track failures globally across all invocations. */
18+
GLOBAL = 'global',
19+
}
20+
21+
/**
22+
* A mapping from an item's (tool or model) name to its consecutive failure count.
23+
*/
24+
export type PerItemFailuresCounter = Map<string, number>;
25+
26+
/**
27+
* Response containing tool failure details and retry guidance.
28+
*/
29+
export interface ToolFailureResponse {
30+
response_type: string;
31+
error_type: string;
32+
error_details: string;
33+
retry_count: number;
34+
reflection_guidance: string;
35+
}
36+
37+
/**
38+
* Resolves the scope key based on tracking scope and invocation ID.
39+
*
40+
* @param scope - The tracking scope (INVOCATION or GLOBAL).
41+
* @param invocationId - The invocation ID (required for INVOCATION scope).
42+
* @returns The resolved scope key string.
43+
*/
44+
export function resolveScopeKey(
45+
scope: TrackingScope,
46+
invocationId?: string,
47+
): string {
48+
if (scope === TrackingScope.INVOCATION) {
49+
if (!invocationId) {
50+
throw new Error('invocation_id must be provided for INVOCATION scope');
51+
}
52+
return invocationId;
53+
} else if (scope === TrackingScope.GLOBAL) {
54+
return GLOBAL_SCOPE_KEY;
55+
}
56+
throw new Error(`Unknown scope: ${scope}`);
57+
}
58+
59+
/**
60+
* Thread-safe failure counter scoped by invocation or global key.
61+
*/
62+
export class ScopedFailureTracker {
63+
private readonly scopedFailureCounters = new Map<
64+
string,
65+
Map<string, number>
66+
>();
67+
private lockPromise: Promise<void> = Promise.resolve();
68+
69+
private async acquireLock(): Promise<() => void> {
70+
let release: () => void;
71+
const nextLock = new Promise<void>((resolve) => {
72+
release = resolve;
73+
});
74+
const currentLock = this.lockPromise;
75+
this.lockPromise = this.lockPromise.then(() => nextLock);
76+
await currentLock;
77+
return release!;
78+
}
79+
80+
/**
81+
* Atomically increments and returns the failure count for an item.
82+
*
83+
* @param scopeKey - The scope identifier (e.g. invocation ID or global key).
84+
* @param itemName - The name of the tool or model.
85+
* @returns The updated failure count.
86+
*/
87+
async increment(scopeKey: string, itemName: string): Promise<number> {
88+
const release = await this.acquireLock();
89+
try {
90+
let counter = this.scopedFailureCounters.get(scopeKey);
91+
if (!counter) {
92+
counter = new Map<string, number>();
93+
this.scopedFailureCounters.set(scopeKey, counter);
94+
}
95+
const current = (counter.get(itemName) ?? 0) + 1;
96+
counter.set(itemName, current);
97+
return current;
98+
} finally {
99+
release();
100+
}
101+
}
102+
103+
/**
104+
* Atomically resets the failure count for an item and cleans up state.
105+
*
106+
* @param scopeKey - The scope identifier.
107+
* @param itemName - The name of the tool or model.
108+
*/
109+
async reset(scopeKey: string, itemName: string): Promise<void> {
110+
const release = await this.acquireLock();
111+
try {
112+
const counter = this.scopedFailureCounters.get(scopeKey);
113+
if (counter) {
114+
counter.delete(itemName);
115+
if (counter.size === 0) {
116+
this.scopedFailureCounters.delete(scopeKey);
117+
}
118+
}
119+
} finally {
120+
release();
121+
}
122+
}
123+
124+
/**
125+
* Gets the current failure count for an item.
126+
*
127+
* @param scopeKey - The scope identifier.
128+
* @param itemName - The name of the tool or model.
129+
*/
130+
async getCount(scopeKey: string, itemName: string): Promise<number> {
131+
const release = await this.acquireLock();
132+
try {
133+
return this.scopedFailureCounters.get(scopeKey)?.get(itemName) ?? 0;
134+
} finally {
135+
release();
136+
}
137+
}
138+
}

core/src/plugins/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export * from './_reflect_retry_utils.js';
8+
export * from './base_plugin.js';
9+
export * from './global_instruction_plugin.js';
10+
export * from './logging_plugin.js';
11+
export * from './plugin_manager.js';
12+
export * from './reflect_retry_model_plugin.js';
13+
export * from './reflect_retry_tool_plugin.js';
14+
export * from './security_plugin.js';

0 commit comments

Comments
 (0)