Skip to content

fix: memoise ToolResolver in resolve_tools to avoid per-agent rebuild#3123

Merged
Leebana merged 2 commits into
mainfrom
claude/issue-3121-20260716-1616
Jul 17, 2026
Merged

fix: memoise ToolResolver in resolve_tools to avoid per-agent rebuild#3123
Leebana merged 2 commits into
mainfrom
claude/issue-3121-20260716-1616

Conversation

@praisonai-triage-agent

@praisonai-triage-agent praisonai-triage-agent Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #3121

Summary

resolve_tools() rebuilt a fresh ToolRegistry and ToolResolver on every call. Callers loop over agents/steps passing the same registry object, so for a workflow with N agents the resolver was built N times — discarding its per-instance caches (_resolve_cache, _local_tools_cache) and re-executing the user's local tools.py once per agent.

Change

  • Extracted resolver construction into a new _get_resolver() helper, memoised on (id(registry), template_dir, autoload_enabled).
  • A guard verifies the stored registry is the same object before returning the cache (protects against id() reuse after GC).
  • resolve_tools() now reuses one resolver across the whole workflow build.

Wrapper-only change (praisonai/templates/tool_override.py); no caller signatures changed, canonical praisonai_code.ToolResolver untouched.

Capability preserved

  • Resolution order/results unchanged (local → registry overrides → praisonaiagents → praisonai-tools → core registry → plugins).
  • --tools/--tools-dir, TEMPLATE.yaml tools_sources, template-local tools.py, and the PRAISONAI_ALLOW_TEMPLATE_TOOLS security gate all intact (autoload state is part of the cache key).

Validation

  • New manual check: resolver built once for 5 repeated calls with the same registry; rebuilt when a different registry object is passed.
  • tests/unit/test_templates.py + test_tool_override_autoload_gate.py: 84 passed.

Generated with Claude Code

Summary by CodeRabbit

  • Performance Improvements

    • Improved repeated tool resolution by reusing resolver setup when the same configuration is used.
    • Reduced redundant loading and execution of template-local tools.
  • Compatibility

    • Continued support for registries containing callable tools and tools exposing a run() method.
    • Preserved optional automatic loading of template-local tools.

…fixes #3121)

Callers invoke resolve_tools() once per agent/step over the same registry
object, and each call rebuilt a fresh ToolRegistry + ToolResolver, discarding
per-instance caches and re-executing local tools.py once per agent. Extract
resolver construction into _get_resolver(), memoised on
(id(registry), template_dir, autoload_enabled), so one resolver is reused
across a workflow build. Resolution order, results, and the security gate are
unchanged.

Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MervinPraison MervinPraison added pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet pipeline/final-claude-pending Reviews done; waiting for FINAL @claude labels Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e0950d4f-384c-4a92-9556-2cf22dc286e3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ToolResolver instances are now memoized by registry identity, template directory, and autoload state. Resolver construction is centralized, override tools retain priority handling, and resolve_tools() reuses one resolver across requested tools.

Changes

Resolver reuse

Layer / File(s) Summary
Memoized resolver construction
src/praisonai/praisonai/templates/tool_override.py
Adds resolver caching and centralizes registry construction, override registration, .run() wrapping, and template autoload handling.
Resolution path integration
src/praisonai/praisonai/templates/tool_override.py
Updates resolve_tools() to reuse a resolver across repeated tool resolutions for the same configuration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Suggested labels: pipeline/awaiting-merge-gate

Suggested reviewers: mervinpraison

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: memoizing ToolResolver in resolve_tools to prevent repeated rebuilds.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-3121-20260716-1616

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reuses tool resolvers across repeated workflow calls. The main changes are:

  • Adds resolver memoization by registry identity, template directory, and autoload state.
  • Preserves the template-tool security gate in the cache key.
  • Bounds retained resolver entries with an LRU cache.
  • Bypasses caching for freshly generated default registries.

Confidence Score: 5/5

This looks safe to merge.

  • The cache is bounded to prevent growth across workflow builds.
  • Registry identity is checked before a cached resolver is reused.
  • No blocking issues were found in the updated code.

Important Files Changed

Filename Overview
src/praisonai/praisonai/templates/tool_override.py Adds bounded resolver reuse while preserving registry overrides and the template-tool autoload gate.

Reviews (2): Last reviewed commit: "fix: bound resolver memo cache and skip ..." | Re-trigger Greptile

Comment on lines +491 to +496
cache_key = (id(registry), template_dir, autoload)

cached = _resolver_cache.get(cache_key)
# Guard against id() reuse after GC: verify the stored registry is the same
# object we were passed before returning the cached resolver.
if cached is not None and cached[0] is registry:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Registry Mutations Leave Stale Tools

When the same registry dictionary is changed between calls, this identity-only key returns a resolver containing the original snapshot. Added tools remain unavailable, while removed or replaced tools can still resolve to old callables; callers that update a shared registry therefore receive stale results.

Comment thread src/praisonai/praisonai/templates/tool_override.py Outdated
Comment thread src/praisonai/praisonai/templates/tool_override.py Outdated
@MervinPraison

Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
  6. Repo routing: agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; optional sandbox backends → PraisonAI-Plugins (praisonai.sandbox entry point) — request changes if wrongly added to praisonaiagents/

Phase 2: FIX Valid Issues
7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
8. Also independently identify and fix any gaps or issues you find in the changed code — do not rely only on prior reviewer feedback
9. Push all code fixes directly to THIS branch (do NOT create a new PR)
10. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
11. If all issues are resolved, approve the PR / close the Issue
12. If blocking issues remain, request changes / leave clear action items

@MervinPraison MervinPraison added pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:cooldown Blocked: post-push or @claude cooldown and removed pipeline/final-claude-pending Reviews done; waiting for FINAL @claude pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet labels Jul 16, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task —— View job


Final Architecture Review — PR #3123 ✅ Approved (fixes pushed)

  • Read AGENTS.md + changed file
  • Phase 1: Architecture review
  • Phase 2: Evaluate reviewer feedback (Greptile P2 issues)
  • Phase 2: Fix valid issues
  • Phase 3: Test (84 passed + new memo checks)
  • Phase 3: Final verdict

Phase 1 — Architecture (per AGENTS.md)

  • Protocol-driven: ✅ Wrapper-only change in praisonai/templates/tool_override.py. Canonical praisonai_code.ToolResolver untouched. No new params on Agent, no new exports.
  • Backward compatible: ✅ No caller signatures changed. Resolution order preserved (local → registry overrides → praisonaiagents → praisonai-tools → core → plugins). Security gate PRAISONAI_ALLOW_TEMPLATE_TOOLS kept in the cache key.
  • Performance: ✅ Genuine hot-path win — recipe/core.py:1045 and cli/features/templates.py:605/657 loop resolve_tools() over N agents/steps with one shared tool_registry. Before, the resolver + registry were rebuilt N times and local tools.py re-executed per agent.
  • SDK value: ✅ Removes real redundant work without adding surface area. Not scope creep.
  • Routing: ✅ Correct package (wrapper), no rust/ts touched.

Phase 2 — Valid reviewer issues fixed

Evaluated Greptile's three P2 findings:

Finding Verdict Action
Default registry=None always misses cache + leaks VALID Now skip caching when registry is None (fresh dict per call ⇒ unstable id(); caching only leaked).
Workflow resolvers retained forever (memory growth) VALID Cache converted to a bounded LRU (OrderedDict, cap 8) so memory no longer grows with build count.
Registry mutations return stale tools Accepted, low-risk Callers build-once and never mutate the registry mid-loop; identity guard already present. Not changed (would break the intended build-once fast path).

File modified: src/praisonai/praisonai/templates/tool_override.py (+30/−8 this commit)

Phase 3 — Validation

  • test_tool_override_autoload_gate.py + test_templates.py: 84 passed (security gate intact).
  • New behavioural checks: resolver reused for same registry across 5 calls; rebuilt on a different registry; registry=None not cached; LRU capped at 8 after 12 distinct registries.

Verdict: ✅ Approve. The optimization is sound and the two valid memory/cache-lifecycle issues raised by Greptile are now fixed. Fixes pushed to claude/issue-3121-20260716-1616 (commit 43027c0b7).

· Branch

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/praisonai/praisonai/templates/tool_override.py`:
- Around line 490-491: Update the cache flow around _autoload_tools_enabled and
ToolResolver construction to resolve the effective tools.py path, including
current-working-directory resolution for relative template_dir values, before
creating cache_key or constructing the resolver. Key the cache by that resolved
path so cwd changes and equivalent relative paths cannot reuse a registry for
the wrong project.
- Around line 493-529: Protect the entire cache lookup, resolver construction,
and `_resolver_cache` store sequence with a module-level lock, including the
`ToolResolver` creation and optional `tools.py` loading. Keep the existing
registry identity check and cache key behavior, and add a concurrent-call test
confirming callers receive the same resolver instance and opted-in template
tools are loaded only once.
- Around line 18-24: The _resolver_cache and resolve_tools flow need bounded
lifecycle management and stable default-registry memoization. Use a stable
sentinel/key for registry=None so the default registry is created only on a
cache miss, and bound or explicitly clean up cache entries; apply the same
changes to the related cache accesses and registry creation at the referenced
resolve_tools paths.
- Around line 493-497: Update the resolver cache logic surrounding
_resolver_cache and its lookup so cached entries also reflect the current
registry contents, not only registry identity. Add a registry-content version or
fingerprint to cache validation/key generation, ensuring additions,
replacements, and removals trigger resolver rebuilding while unchanged
registries still reuse cached resolvers; preserve existing APIs and add coverage
for mutations between calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42545730-719a-4202-b9cf-f7e5a375d455

📥 Commits

Reviewing files that changed from the base of the PR and between 254f1ef and 0a22983.

📒 Files selected for processing (1)
  • src/praisonai/praisonai/templates/tool_override.py

Comment on lines +18 to +24
# Memoisation cache for ToolResolver instances built by resolve_tools().
# Keyed on (id(registry), template_dir, autoload_enabled) so that repeated
# calls within a single workflow build (once per agent/step, over the same
# registry object) reuse one resolver and its per-instance caches instead of
# rebuilding the registry+resolver and re-executing local tools.py per agent.
# The registry object is also stored to guard against id() reuse after GC.
_resolver_cache: Dict[Any, Any] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the cache and reuse the default-registry entry.

The cache permanently retains every registry and resolver. Worse, registry=None creates a new dictionary before keying, so every such call misses and leaks another entry rather than being memoized.

Use a bounded cache or explicit lifecycle cleanup, and use a stable sentinel/key so the default registry is only created on a cache miss.

Also applies to: 486-491, 529-530

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai/praisonai/templates/tool_override.py` around lines 18 - 24, The
_resolver_cache and resolve_tools flow need bounded lifecycle management and
stable default-registry memoization. Use a stable sentinel/key for registry=None
so the default registry is created only on a cache miss, and bound or explicitly
clean up cache entries; apply the same changes to the related cache accesses and
registry creation at the referenced resolve_tools paths.

Comment on lines +490 to +491
autoload = _autoload_tools_enabled()
cache_key = (id(registry), template_dir, autoload)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Key the cache by the resolver’s effective resolved tools path.

ToolResolver resolves its path against the current working directory during construction, but this key stores raw template_dir. Reusing the registry after a cwd change can therefore resolve tools from the previous project; relative template paths have the same problem.

Resolve the effective tools.py path before both keying and construction.

Proposed direction
-    cache_key = (id(registry), template_dir, autoload)
+    tools_path = None
+    if autoload:
+        base = Path(template_dir) if template_dir else Path.cwd()
+        tools_path = str((base / "tools.py").resolve())
+    cache_key = (id(registry), tools_path, autoload)

-    template_tools_path = None
-    if template_dir and autoload:
-        template_tools_path = str(Path(template_dir) / "tools.py")
+    template_tools_path = tools_path

Also applies to: 517-526

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai/praisonai/templates/tool_override.py` around lines 490 - 491,
Update the cache flow around _autoload_tools_enabled and ToolResolver
construction to resolve the effective tools.py path, including
current-working-directory resolution for relative template_dir values, before
creating cache_key or constructing the resolver. Key the cache by that resolved
path so cwd changes and equivalent relative paths cannot reuse a registry for
the wrong project.

Comment on lines +493 to +497
cached = _resolver_cache.get(cache_key)
# Guard against id() reuse after GC: verify the stored registry is the same
# object we were passed before returning the cached resolver.
if cached is not None and cached[0] is registry:
return cached[1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Invalidate cached resolvers when the registry dictionary changes.

Identity-only lookup returns a resolver containing the original copied entries after callers add, replace, or remove tools from the same registry dictionary. Previously, rebuilding observed those mutations.

Include a registry-content version/fingerprint in the key or provide mutation-aware invalidation, with tests covering updates between calls.

As per coding guidelines, “Ensure backward compatibility with existing APIs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai/praisonai/templates/tool_override.py` around lines 493 - 497,
Update the resolver cache logic surrounding _resolver_cache and its lookup so
cached entries also reflect the current registry contents, not only registry
identity. Add a registry-content version or fingerprint to cache validation/key
generation, ensuring additions, replacements, and removals trigger resolver
rebuilding while unchanged registries still reuse cached resolvers; preserve
existing APIs and add coverage for mutations between calls.

Source: Coding guidelines

Comment on lines +493 to +529
cached = _resolver_cache.get(cache_key)
# Guard against id() reuse after GC: verify the stored registry is the same
# object we were passed before returning the cached resolver.
if cached is not None and cached[0] is registry:
return cached[1]

# Create a ToolRegistry instance for high-priority overrides
tool_registry = ToolRegistry()

# Don't manually load template tools here; let ToolResolver handle it to
# avoid double-execution. Populate tool_registry ONLY with registry
# overrides, which have HIGHER priority than template tools in ToolResolver.
if registry:
# Filter out lazy-loaded tuples from registry and register callables
for name, tool in registry.items():
if not isinstance(tool, tuple):
if callable(tool):
tool_registry.register_function(name, tool)
elif hasattr(tool, "run") and callable(getattr(tool, "run", None)):
# Support non-callable tools with a run method
def make_callable(t):
return lambda *args, **kwargs: t.run(*args, **kwargs)
tool_registry.register_function(name, make_callable(tool))

# Only pass template tools path if autoload is enabled (security gate)
template_tools_path = None
if template_dir and autoload:
template_tools_path = str(Path(template_dir) / "tools.py")

# Create ToolResolver with the registry having highest priority.
# Template tools will only be loaded if autoload is enabled.
resolver = ToolResolver(
tools_py_path=template_tools_path,
registry=tool_registry
)

_resolver_cache[cache_key] = (registry, resolver)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Synchronize cache lookup and resolver construction.

Concurrent calls can both miss, construct separate resolvers, and return different instances. This defeats memoization and may execute an opted-in tools.py more than once.

Guard the complete lookup/build/store sequence with a module-level lock and add a concurrent-call test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai/praisonai/templates/tool_override.py` around lines 493 - 529,
Protect the entire cache lookup, resolver construction, and `_resolver_cache`
store sequence with a module-level lock, including the `ToolResolver` creation
and optional `tools.py` loading. Keep the existing registry identity check and
cache key behavior, and add a concurrent-call test confirming callers receive
the same resolver instance and opted-in template tools are loaded only once.

Addresses reviewer feedback on the ToolResolver memoisation:
- Cap the resolver cache as a small LRU (OrderedDict) so a long-running
  process that builds many workflows no longer retains every registry,
  its tool callables/modules and resolver caches for the process life.
- Skip caching entirely when registry is None: a fresh registry is built
  per call so its id() is never stable, meaning caching it only leaked
  memory and never produced a hit.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison added pipeline/blocked:stale-final Blocked: FINAL stale after new commits and removed pipeline/blocked:stale-final Blocked: FINAL stale after new commits labels Jul 16, 2026
@praisonai-triage-agent praisonai-triage-agent Bot removed the pipeline/blocked:cooldown Blocked: post-push or @claude cooldown label Jul 16, 2026
@Leebana
Leebana merged commit 8b11d60 into main Jul 17, 2026
33 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:ci Blocked: CI not green on HEAD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ToolResolver rebuilt on every resolve_tools() call inside per-agent/per-step loops

2 participants