Skip to content

Refine task subagent to run as full tool agent - #1218

Closed
aantn wants to merge 1 commit into
masterfrom
codex/linear-mention-rob-37-large-tool-calls-make-llm-hit-contex
Closed

aantn wants to merge 1 commit into
masterfrom
codex/linear-mention-rob-37-large-tool-calls-make-llm-hit-contex

Conversation

@aantn

@aantn aantn commented Dec 22, 2025 •

Copy link
Copy Markdown
Collaborator

Summary

  • Rework the Task sub-agent to spin up a fresh ToolCallingLLM with full tool access (no nested sub-agents) for processing large tool outputs
  • Update prompts and metadata handling so summaries explicitly note tool usage and truncation while keeping the main thread’s context lean
  • Add targeted tests and lightweight local stubs for missing test-only dependencies to keep unit runs working in minimal environments

Testing

  • PYTEST_ADDOPTS="--cov-fail-under=0 --cov=holmes" pytest tests/core/test_task_subagent.py

Codex Task

Summary by CodeRabbit

  • New Features

    • Introduced a task sub-agent system that automatically summarizes large tool outputs, preventing conversation clutter.
    • Added configurable settings to control sub-agent behavior, including output size thresholds and maximum input/summary lengths.
  • Tests

    • Added test coverage for task sub-agent functionality.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: Codex <codex@openai.com>
@coderabbitai

coderabbitai Bot commented Dec 22, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request introduces a TaskSubAgent system that conditionally summarizes large tool outputs to prevent prompt pollution. It adds stub implementations for external dependencies (autoevals, braintrust), integrates TaskSubAgent into ToolCallingLLM with metadata propagation, introduces environment configuration variables, and provides supporting prompts and tests.

Changes

Cohort / File(s) Change Summary
External package stubs
autoevals.py, braintrust/__init__.py, braintrust/oai.py
Minimal stub implementations for external package dependencies; adds no-op classes and functions to satisfy test imports (LLMClassifier, Span, Dataset, Experiment, etc.).
TaskSubAgent core implementation
holmes/core/task_subagent.py
Introduces TaskSubAgentConfig dataclass and TaskSubAgent class with conditional summarization logic; includes methods for content truncation, threshold checking, and subagent instantiation; integrates with Jinja2 templating and error handling with fallbacks.
ToolCallingLLM integration
holmes/core/tool_calling_llm.py
Extends ToolCallingLLM.init to create and manage TaskSubAgent instance; adds task_subagent_summaries metadata collection and propagation throughout both streaming and non-streaming tool call paths.
Configuration and prompts
holmes/common/env_vars.py, holmes/plugins/prompts/task_subagent_summarize_tool.jinja2
Adds three environment variables (ENABLE_TASK_SUBAGENT, TASK_SUBAGENT_SUMMARY_MAX_CHARS, TASK_SUBAGENT_MAX_INPUT_CHARS) and a Jinja2 template defining TaskSubAgent prompting behavior and guidelines.
Test infrastructure
pytest_shared_session_scope.py, tests/core/test_task_subagent.py
Provides pytest shim for shared session scope with no-op decorator and sentinel tokens; adds comprehensive tests for TaskSubAgent summarization behavior under threshold and over-threshold conditions.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ToolCallingLLM
    participant TaskSubAgent
    participant SubAgentLLM as SubAgent LLM
    participant ToolExecutor

    Client->>ToolCallingLLM: call() with tool executor
    ToolCallingLLM->>ToolExecutor: execute tool call
    ToolExecutor-->>ToolCallingLLM: return ToolCallResult (large output)
    
    alt Content exceeds threshold
        ToolCallingLLM->>TaskSubAgent: summarize_tool_message(tool_result)
        TaskSubAgent->>TaskSubAgent: _should_summarize() check
        TaskSubAgent->>TaskSubAgent: _truncate_input() if needed
        TaskSubAgent->>TaskSubAgent: _create_subagent()
        TaskSubAgent->>SubAgentLLM: invoke with Jinja2 prompt template
        SubAgentLLM-->>TaskSubAgent: return summary + metadata
        TaskSubAgent-->>ToolCallingLLM: summarized_message + metadata (tool_call_id, char counts)
        ToolCallingLLM->>ToolCallingLLM: append metadata to task_subagent_summaries
    else Content under threshold
        ToolCallingLLM->>ToolCallingLLM: keep original message
    end
    
    ToolCallingLLM-->>Client: final response with propagated task_subagent_summaries metadata
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • TaskSubAgent conditional logic: Verify _should_summarize and _truncate_input logic handles edge cases (empty content, exact threshold boundaries, Unicode handling)
  • Metadata propagation in ToolCallingLLM: Ensure task_subagent_summaries is correctly accumulated and propagated across both streaming and non-streaming code paths, with no metadata loss
  • Template content accuracy: Confirm Jinja2 template placeholders match actual variable names passed from task_subagent.py
  • Error handling and fallback behavior: Verify graceful fallback to truncated input when subagent invocation fails

Possibly related PRs

Suggested reviewers

  • moshemorad
  • arikalon1

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.13% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refine task subagent to run as full tool agent' directly reflects the main change: reworking the task sub-agent to instantiate a fresh ToolCallingLLM with full tool access.

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

@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: 0

🧹 Nitpick comments (4)
braintrust/oai.py (1)

1-2: Add type hints for consistency with project standards.

Per coding guidelines, type hints are required. Consider adding type annotations to maintain consistency.

🔎 Suggested improvement
-def wrap_openai(client):
-    return client
+from typing import TypeVar
+
+T = TypeVar("T")
+
+def wrap_openai(client: T) -> T:
+    """Passthrough stub for braintrust.oai.wrap_openai."""
+    return client
braintrust/__init__.py (1)

1-35: Add a module docstring to clarify this is a stub.

Unlike autoevals.py, this file lacks a docstring explaining its purpose as a test stub. Adding one improves maintainability and aligns with the pattern in other stub files.

🔎 Suggested improvement
+"""
+Minimal stub for the `braintrust` package used in tests.
+Provides no-op implementations so imports succeed without the real dependency.
+"""
+
+
 class Span:
     def __init__(self, *args, **kwargs):

The unused argument warnings from Ruff are expected for stub implementations maintaining API compatibility.

holmes/common/env_vars.py (1)

116-124: Consider adding inline comments explaining the threshold values.

Other environment variables in this file include explanatory comments (e.g., lines 89-91 for TOOL_MAX_ALLOCATED_CONTEXT_WINDOW_PCT). Adding similar context for the new thresholds would improve maintainability.

🔎 Suggested improvement
 # Task sub-agent settings
 ENABLE_TASK_SUBAGENT = load_bool("ENABLE_TASK_SUBAGENT", True)
+# Maximum characters for the sub-agent's summary output
 TASK_SUBAGENT_SUMMARY_MAX_CHARS = int(
     os.environ.get("TASK_SUBAGENT_SUMMARY_MAX_CHARS", 4000)
 )
+# Tool outputs exceeding this threshold trigger sub-agent summarization
 TASK_SUBAGENT_MAX_INPUT_CHARS = int(
     os.environ.get("TASK_SUBAGENT_MAX_INPUT_CHARS", 120000)
 )
holmes/core/tool_calling_llm.py (1)

335-335: Redundant initialization of task_subagent_metadata.

This variable is re-declared inside the while loop at line 467, making this outer initialization dead code. The per-iteration reset is intentional (metadata is accumulated into metadata dict at lines 533-536 after each batch), but this line serves no purpose.

🔎 Proposed fix
         metadata: Dict[Any, Any] = {}
-        task_subagent_metadata: list[dict] = []
         while i < max_steps:
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af8b2a5 and b67aa6a.

📒 Files selected for processing (9)
  • autoevals.py
  • braintrust/__init__.py
  • braintrust/oai.py
  • holmes/common/env_vars.py
  • holmes/core/task_subagent.py
  • holmes/core/tool_calling_llm.py
  • holmes/plugins/prompts/task_subagent_summarize_tool.jinja2
  • pytest_shared_session_scope.py
  • tests/core/test_task_subagent.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • braintrust/oai.py
  • holmes/common/env_vars.py
  • autoevals.py
  • braintrust/__init__.py
  • holmes/core/tool_calling_llm.py
  • tests/core/test_task_subagent.py
  • pytest_shared_session_scope.py
  • holmes/core/task_subagent.py
holmes/plugins/prompts/**/*.jinja2

📄 CodeRabbit inference engine (CLAUDE.md)

holmes/plugins/prompts/**/*.jinja2: Prompts: holmes/plugins/prompts/{name}.jinja2 file structure
Use Jinja2 templates for investigation prompts

Files:

  • holmes/plugins/prompts/task_subagent_summarize_tool.jinja2
holmes/core/tool_calling_llm.py

📄 CodeRabbit inference engine (CLAUDE.md)

holmes/core/tool_calling_llm.py: Use LiteLLM for multi-provider support (OpenAI, Anthropic, Azure, etc.) in LLM integration
Implement structured tool calling with automatic retry and error handling

Files:

  • holmes/core/tool_calling_llm.py
tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Tests: Match source structure under tests/

Files:

  • tests/core/test_task_subagent.py
🧠 Learnings (4)
📚 Learning: 2025-12-21T13:17:48.366Z
Learnt from: CR
Repo: HolmesGPT/holmesgpt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T13:17:48.366Z
Learning: Applies to holmes/plugins/prompts/**/*.jinja2 : Use Jinja2 templates for investigation prompts

Applied to files:

  • holmes/plugins/prompts/task_subagent_summarize_tool.jinja2
📚 Learning: 2025-12-21T13:17:48.366Z
Learnt from: CR
Repo: HolmesGPT/holmesgpt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T13:17:48.366Z
Learning: Applies to holmes/plugins/prompts/**/*.jinja2 : Prompts: holmes/plugins/prompts/{name}.jinja2 file structure

Applied to files:

  • holmes/plugins/prompts/task_subagent_summarize_tool.jinja2
📚 Learning: 2025-12-21T13:17:48.366Z
Learnt from: CR
Repo: HolmesGPT/holmesgpt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T13:17:48.366Z
Learning: Applies to holmes/core/tool_calling_llm.py : Use LiteLLM for multi-provider support (OpenAI, Anthropic, Azure, etc.) in LLM integration

Applied to files:

  • holmes/core/tool_calling_llm.py
  • tests/core/test_task_subagent.py
📚 Learning: 2025-12-21T13:17:48.366Z
Learnt from: CR
Repo: HolmesGPT/holmesgpt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T13:17:48.366Z
Learning: Applies to holmes/core/tool_calling_llm.py : Implement structured tool calling with automatic retry and error handling

Applied to files:

  • holmes/core/tool_calling_llm.py
  • tests/core/test_task_subagent.py
🧬 Code graph analysis (3)
holmes/plugins/prompts/task_subagent_summarize_tool.jinja2 (1)
holmes/core/tools.py (1)
  • _load_llm_instructions (767-772)
tests/core/test_task_subagent.py (6)
holmes/core/llm.py (2)
  • LLM (91-134)
  • TokenCountMetadata (57-64)
holmes/core/models.py (2)
  • ToolCallResult (23-64)
  • as_tool_call_message (30-40)
holmes/core/task_subagent.py (3)
  • TaskSubAgent (24-149)
  • TaskSubAgentConfig (17-21)
  • summarize_tool_message (82-149)
holmes/core/tool_calling_llm.py (2)
  • LLMResult (143-156)
  • prompt_call (280-300)
holmes/core/tools.py (2)
  • StructuredToolResult (79-105)
  • StructuredToolResultStatus (52-76)
holmes/core/tools_utils/tool_executor.py (1)
  • ToolExecutor (14-58)
holmes/core/task_subagent.py (3)
holmes/core/models.py (1)
  • ToolCallResult (23-64)
holmes/core/tools_utils/tool_executor.py (1)
  • ToolExecutor (14-58)
holmes/plugins/prompts/__init__.py (1)
  • load_and_render_prompt (27-54)
🪛 Ruff (0.14.8)
autoevals.py

18-18: Unused method argument: args

(ARG002)


18-18: Unused method argument: kwargs

(ARG002)


22-22: Unused function argument: args

(ARG001)


22-22: Unused function argument: kwargs

(ARG001)

braintrust/__init__.py

6-6: Unused method argument: args

(ARG002)


6-6: Unused method argument: kwargs

(ARG002)


15-15: Unused method argument: args

(ARG002)


15-15: Unused method argument: kwargs

(ARG002)

tests/core/test_task_subagent.py

27-27: Unused method argument: messages

(ARG002)


27-27: Unused method argument: tools

(ARG002)


39-39: Unused method argument: args

(ARG002)


39-39: Unused method argument: kwargs

(ARG002)


40-40: Avoid specifying long messages outside the exception class

(TRY003)


76-76: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: llm_evals
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.10)
  • GitHub Check: build
🔇 Additional comments (12)
autoevals.py (1)

1-23: LGTM - stub implementation is appropriate for test scaffolding.

The unused arguments flagged by Ruff are intentional for API compatibility with the real autoevals package. The stub provides sufficient structure for imports to succeed in tests.

Consider adding a type hint for the metadata parameter for completeness:

def __init__(self, score: float = 0.0, metadata: dict | None = None):
pytest_shared_session_scope.py (1)

1-23: LGTM!

The shim is well-documented and provides appropriate no-op behavior for local test runs. The sentinel token pattern using type() is a reasonable approach for creating lightweight namespace-like objects.

holmes/plugins/prompts/task_subagent_summarize_tool.jinja2 (1)

1-23: LGTM!

The prompt template is well-structured with clear role definition, constraints, and guidelines. The Jinja2 variable placeholders ({{ parent_user_message }}, {{ tool_name }}, etc.) follow the project's prompts pattern. Based on learnings, this correctly uses Jinja2 templates for investigation prompts.

holmes/common/env_vars.py (1)

118-118: Verify that enabling the task subagent by default is intentional.

ENABLE_TASK_SUBAGENT defaults to True, meaning this feature will be active for all users upon deployment. Confirm this is the desired rollout strategy versus an opt-in approach.

tests/core/test_task_subagent.py (1)

1-131: Good test coverage for core TaskSubAgent scenarios.

The tests effectively validate the conditional summarization behavior:

  • Under-threshold: content unchanged, no subagent invocation
  • Over-threshold: subagent runs, summary prefix added, metadata populated

Consider adding edge case tests in a follow-up:

  • Non-string content in the message
  • Empty string content
  • Subagent exception handling (to verify fallback behavior)
holmes/core/tool_calling_llm.py (3)

168-186: Clean integration of TaskSubAgent into ToolCallingLLM.

The constructor properly:

  • Adds the enable_task_subagent parameter with sensible default
  • Passes the flag through to TaskSubAgent initialization
  • Shares the same llm, tool_executor, and max_steps with the subagent

514-523: Correct integration of TaskSubAgent for tool message summarization.

The flow properly:

  • Obtains the original tool message
  • Delegates to summarize_tool_message with full context
  • Collects metadata when summarization occurs
  • Uses the (possibly summarized) message going forward

1036-1045: Consistent TaskSubAgent integration in streaming path.

The streaming path correctly mirrors the non-streaming implementation:

  • Processes tool messages through summarize_tool_message
  • Accumulates metadata across iterations
  • Includes metadata in all exit paths (ANSWER_END, APPROVAL_REQUIRED, max_steps exception)
holmes/core/task_subagent.py (4)

16-21: Well-structured configuration dataclass.

The config cleanly separates environment-driven defaults while allowing programmatic overrides. The enabled field's fallback to True ensures the feature is on by default unless explicitly disabled.


30-47: Good constructor design with dependency injection.

The subagent_factory parameter enables clean unit testing without invoking the real ToolCallingLLM, as demonstrated in the test file. The config-based max_steps override provides flexibility for subagent-specific limits.


68-80: Correct handling of circular dependency and nested subagent prevention.

The lazy import avoids the circular dependency between task_subagent.py and tool_calling_llm.py. Setting enable_task_subagent=False when creating the nested ToolCallingLLM correctly prevents infinite subagent spawning.


82-149: Well-structured summarization with proper fallbacks.

The method correctly:

  • Guards against non-string content and small outputs
  • Extracts context from parent messages for better summarization
  • Handles subagent failures gracefully with fallback to truncated content
  • Returns both the modified message and metadata for transparency

Minor note: Line 106 str(last_user_message) is redundant since last_user_message is already a string from the .get("content", "") call, but it's harmless.

@github-actions

Copy link
Copy Markdown
Contributor

Results of HolmesGPT evals

  • ask_holmes: 0/37 test cases were successful, 36 regressions
Test suite Test case Status
ask 01_how_many_pods ❌
ask 02_what_is_wrong_with_pod ❌
ask 04_related_k8s_events ❌
ask 05_image_version ❌
ask 09_crashpod ❌
ask 10_image_pull_backoff ❌
ask 110_k8s_events_image_pull ❌
ask 11_init_containers ❌
ask 13a_pending_node_selector_basic ❌
ask 14_pending_resources ❌
ask 15_failed_readiness_probe ❌
ask 163_compaction_follow_up ❌
ask 17_oom_kill ❌
ask 18_oom_kill_from_issues_history ❌
ask 19_detect_missing_app_details ❌
ask 20_long_log_file_search ❌
ask 24_misconfigured_pvc ❌
ask 24a_misconfigured_pvc_basic ❌
ask 28_permissions_error ❌
ask 39_failed_toolset ❌
ask 41_setup_argo ❌
ask 42_dns_issues_steps_new_tools ⚠️
ask 43_current_datetime_from_prompt ❌
ask 45_fetch_deployment_logs_simple ❌
ask 51_logs_summarize_errors ❌
ask 53_logs_find_term ❌
ask 54_not_truncated_when_getting_pods ❌
ask 59_label_based_counting ❌
ask 60_count_less_than ❌
ask 61_exact_match_counting ❌
ask 63_fetch_error_logs_no_errors ❌
ask 79_configmap_mount_issue ❌
ask 83_secret_not_found ❌
ask 86_configmap_like_but_secret ❌
ask 93_calling_datadog[0] ❌
ask 93_calling_datadog[1] ❌
ask 93_calling_datadog[2] ❌

Legend

  • ✅ the test was successful
  • :minus: the test was skipped
  • ⚠️ the test failed but is known to be flaky or known to fail
  • 🚧 the test had a setup failure (not a code regression)
  • 🔧 the test failed due to mock data issues (not a code regression)
  • 🚫 the test was throttled by API rate limits/overload
  • ❌ the test failed and should be fixed before merging the PR

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

Copy link
Copy Markdown
Contributor

@aantn aantn closed this Dec 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant