Skip to content

feat(plugins): add ReflectAndRetryToolPlugin and ReflectAndRetryModelPlugin for self-healing error recovery - #631

Open
Varun-S10 wants to merge 2 commits into
google:mainfrom
Varun-S10:feat/reflect-retry-tool-plugin
Open

feat(plugins): add ReflectAndRetryToolPlugin and ReflectAndRetryModelPlugin for self-healing error recovery#631
Varun-S10 wants to merge 2 commits into
google:mainfrom
Varun-S10:feat/reflect-retry-tool-plugin

Conversation

@Varun-S10

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Related: Feature parity for tool and model self-reflection error recovery

2. Description of Change:

Problem:
When an AI agent calls a tool with invalid or hallucinated parameters, or when a tool encounters a temporary network or runtime error, the agent throws an error and terminates the turn immediately. There was no built-in reflection mechanism to give the model a chance to analyze the error, adjust its arguments, and retry the operation safely.

Solution:
Implemented ReflectAndRetryToolPlugin and companion ReflectAndRetryModelPlugin in @google/adk:

  1. Intercepts tool exceptions and error payloads automatically before the agent crashes.
  2. Feeds structured reflection guidance back to the AI (describing the error, arguments used, retry count, and schema hints) so the model can correct its mistake and retry.
  3. Implements thread-safe failure counting using async locks to safely handle concurrent tool calls.
  4. Provides customizable tracking scopes (per-invocation or global across all turns).
  5. Automatically resets failure counters back to 0 as soon as a tool succeeds.
  6. Catches model errors such as malformed function calls and enables automated self-correction.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Summary of test results (npm run test:unit):

  • 220 test suites passed (100% pass rate)
  • 3,006 total tests passed
  • Added 19 unit tests in core/test/plugins/reflect_retry_tool_plugin_test.ts
  • Added 9 unit tests in core/test/plugins/reflect_retry_model_plugin_test.ts
  • Added 3 integration tests in core/test/plugins/reflect_retry_integration_test.ts
  • npm run lint passed with 0 errors and 0 warnings
  • npm run format:check passed with zero issues
  • npm run docs:check passed with 0 TypeDoc warnings

Manual End-to-End (E2E) Tests:

  • Created a standalone verification test outside the repository to test the compiled build.
  • Tested tool failure interception, structured guidance generation, parameter self-correction, retry count auto-reset on success, and max retry threshold limits.
  • Tested compatibility with existing plugins (LoggingPlugin, GlobalInstructionPlugin, SecurityPlugin) with zero regressions.
  • All 29 standalone verification tests passed.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

This feature provides full parity with the Python ADK implementation and improves agent reliability against parameter hallucinations and transient tool errors without requiring external dependencies or databases.

@Varun-S10 Varun-S10 self-assigned this Aug 7, 2026
@Varun-S10 Varun-S10 added the needs review [Status] The PR/issue is awaiting review from the maintainer label Aug 7, 2026

@AmaadMartin AmaadMartin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is green on ubuntu, macOS, and Windows, and the plugin logic and tests look sound. I am holding for two reasons. First, the PR bundles an unrelated agent_loader behavior change and integration-test edits; please split them out. Second, it uses avoidable type-escape casts. The rest are optional nits.

Comment thread dev/src/utils/agent_loader.ts Outdated
Comment on lines +496 to +497
fileOrDir.name === 'node_modules' ||
fileOrDir.name.startsWith('.')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a nit. Split the unrelated changes into a separate PR.

This PR adds the reflect-and-retry plugins. These changes are not part of that feature:

  • agent_loader.ts skips node_modules and dot directories.
  • agent_dirname_test.ts and app_loader_test.ts change timeouts and add preloadAgents().

The loader change alters directory discovery for every user. A reviewer must judge it on its own. Please move it to its own PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I have split out the agent_loader changes and integration test edits into a separate PR: #674

reflection_guidance: reflectionMessage,
};

return response as unknown as Record<string, unknown>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a nit. Remove the as unknown as double cast.

return response as unknown as Record<string, unknown>;

The double cast turns off type checking. It appears here and at line 420. Add an index signature to ToolFailureResponse so it is a Record<string, unknown>:

export interface ToolFailureResponse {
  // ...existing fields
  [key: string]: unknown;
}

Then return response with no cast. I did not run tsc on this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added [key: string]: unknown; to ToolFailureResponse and removed the as unknown as double casts. Returning response directly now.

Comment on lines +194 to +202
async extractErrorFromResult({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
tool,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
toolArgs,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
toolContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
result,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. Match the base_plugin stub pattern.

base_plugin.ts marks an unused-param stub with one eslint-disable on a single params object. This override destructures four params and repeats the disable four times. Take one params object and use one disable, like the base class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Updated extractErrorFromResult to take a single params object with one disable comment matching base_plugin.ts.

Comment on lines +30 to +34
response_type: string;
error_type: string;
error_details: string;
retry_count: number;
reflection_guidance: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, optional. These public fields use snake_case:

response_type: string;
// ...
reflection_guidance: string;

The codebase uses camelCase for TypeScript interfaces. If this is the model-facing payload shape, keep it and add a comment. Otherwise rename the fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept snake_case for model reflection payload parity with Python ADK, and added JSDoc documentation explaining the schema.

Comment thread core/src/plugins/index.ts Outdated
* SPDX-License-Identifier: Apache-2.0
*/

export * from './_reflect_retry_utils.js';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. This barrel file is unused. Delete it.

core/package.json exports only ., and index.ts re-exports common.ts, which imports each plugin file directly. No ./plugins subpath exists. The agents, tools, and models directories have no index.ts. This file adds an inconsistent, unreferenced entry point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Deleted core/src/plugins/index.ts.

* @param trackingScope - Optional tracking scope if positional arguments are used.
*/
constructor(
optionsOrName?: ReflectAndRetryToolPluginOptions | string,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. Drop the positional-args constructor.

optionsOrName?: ReflectAndRetryToolPluginOptions | string,
maxRetries?: number,
// ...

The options object already covers every field. The positional form adds a second API and extra branching with no caller that needs it. The same applies in reflect_retry_model_plugin.ts:74. Keep the options object only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Removed the positional constructor arguments and kept only the options object form for both plugins.

@Varun-S10

Copy link
Copy Markdown
Contributor Author

Thank you for the review @AmaadMartin. I have addressed all the feedback. Could you please take another look?

@AmaadMartin AmaadMartin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 5ee24d2. All prior findings are resolved.

  • Both "Not a nit" items are fixed: the unrelated agent_loader changes are gone, and the as unknown as double casts in the tool plugin are replaced with an index signature on ToolFailureResponse and a plain return.
  • The nits are addressed: the barrel index.ts is removed, both constructors take an options object only, the stub uses one params and one disable, and the snake_case payload fields carry a parity comment.

New public types are exported from common.ts. The remaining as unknown as casts are in test mocks, and instanceof is only instanceof Error. CI run-tests passes on ubuntu, macOS, and Windows at this head. The PR text contains no injection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review [Status] The PR/issue is awaiting review from the maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants