Skip to content

ACP agent cannot use LLM for auxiliary tasks (title generation, vision check, sub-agents, etc.) #2471

@simonrosenberg

Description

@simonrosenberg

Problem

The ACPAgent uses a sentinel LLM(model="acp-managed") (acp_agent.py:117-119) that cannot make any direct LLM calls. This is by design — the ACP server manages the main conversational LLM — but several auxiliary features in the SDK and tools rely on agent.llm being a real, callable LLM. These features silently degrade or break when using ACP.

All places where agent.llm is used and ACP can't fulfill it

1. Title generation — degraded (fallback to truncation)

  • File: openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:918-924
  • generate_title() checks if llm_to_use.model == "acp-managed" and sets llm_to_use = None, falling back to generate_fallback_title() which just truncates the first user message
  • Impact: Conversations get low-quality titles like "Hey can you hel..." instead of LLM-generated ones with emoji categorization
  • Also: title_utils.py:119llm.completion(messages) is the actual call that never happens

2. Vision capability check — incorrect result

  • File: openhands-tools/openhands/tools/file_editor/definition.py:219
  • conv_state.agent.llm.vision_is_active() returns False for the sentinel LLM (since "acp-managed" is not a known model in litellm)
  • Impact: The file editor tool never advertises image viewing support, even when the ACP server's underlying model (e.g., Claude Sonnet) fully supports vision

3. Delegate tool sub-agent creation — broken

  • File: openhands-tools/openhands/tools/delegate/impl.py:155-170
  • parent_llm = parent_conversation.agent.llm then parent_llm.model_copy() to create sub-agent LLMs
  • The factory function receives the sentinel LLM copy, and the sub-agent will fail if it tries to call completion() or responses() on it
  • Impact: Delegation to non-ACP sub-agents is broken

4. Task manager sub-agent creation — broken

  • File: openhands-tools/openhands/tools/task/manager.py:309-317
  • Same pattern as delegate: parent_llm = parent.agent.llmparent_llm.model_copy(update={"stream": False})
  • Sub-agents receive the sentinel LLM and can't make calls
  • Impact: Task sub-agents that need their own LLM are broken

5. TOM consult message formatting — degraded

  • File: openhands-tools/openhands/tools/tom_consult/executor.py:143
  • conversation.state.agent.llm.format_messages_for_llm(messages) uses the sentinel LLM's default formatting
  • Impact: Messages are formatted without model-specific optimizations (no caching, no vision formatting, etc.)

6. Context condensation — explicitly unsupported

  • File: openhands-sdk/openhands/sdk/context/condenser/llm_summarizing_condenser.py:180
  • ACPAgent.init_state() raises NotImplementedError if a condenser is configured
  • Impact: No context condensation available for ACP agents (expected, but worth noting)

7. Model name in system prompts — incorrect

  • File: openhands-sdk/openhands/sdk/agent/base.py:222-228
  • self.llm.model returns "acp-managed" instead of the actual model name
  • Used in static_system_message property for template rendering and get_model_prompt_spec()
  • Impact: Model-specific prompt optimizations are skipped (though for ACP this is handled by the ACP server, so less critical)

Can we instantiate a real OpenHands LLM alongside ACP?

Yes, technically. The LLM class only requires model to be non-empty. A minimal instantiation:

from openhands.sdk import LLM
from pydantic import SecretStr

auxiliary_llm = LLM(
    model="claude-haiku-4-5-20251001",  # cheap model for auxiliary tasks
    api_key=SecretStr(os.environ["ANTHROPIC_API_KEY"]),
)

Key considerations:

  1. API keys are often already available — The ACP agent's environment typically has ANTHROPIC_API_KEY or OPENAI_API_KEY set (they're needed for ACP server authentication). These same keys could be used for an auxiliary LLM.

  2. Cost control — Auxiliary tasks (title generation, vision check) are lightweight. Using a small/cheap model (e.g., Haiku) would add negligible cost.

  3. The sentinel LLM already has metrics trackingACPAgent._record_usage() uses self.llm.metrics for ACP usage stats. An auxiliary LLM would need separate metrics to avoid conflating ACP costs with auxiliary LLM costs.

  4. Not all auxiliary uses need a full LLM — Some just need model metadata (vision check, model name). These could potentially be solved by passing the ACP server's model info without instantiating a real LLM.

Proposed solutions

Option A: Auxiliary LLM field on ACPAgent

Add an optional auxiliary_llm (or utility_llm) field to ACPAgent. When set, auxiliary features use it instead of the sentinel. When not set, current fallback behavior is preserved.

class ACPAgent(AgentBase):
    auxiliary_llm: LLM | None = Field(
        default=None,
        description="Optional real LLM for auxiliary tasks (title generation, etc.). "
        "If None, auxiliary tasks use fallback behavior.",
    )

Pros: Clean separation, opt-in, doesn't change ACP semantics
Cons: Every callsite needs to check auxiliary_llm or self.llm, adds complexity

Option B: Make the sentinel LLM real but cheap

Instead of LLM(model="acp-managed"), create a real LLM from env vars with a cheap model:

def _make_auxiliary_llm() -> LLM:
    api_key = os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENAI_API_KEY")
    if api_key:
        return LLM(model="claude-haiku-4-5-20251001", api_key=SecretStr(api_key))
    return LLM(model="acp-managed")  # fallback sentinel

Pros: Transparent — all existing agent.llm callsites just work
Cons: Mixes ACP and direct LLM costs/metrics, may surprise users with unexpected API calls

Option C: Expose model metadata from ACP server

For features that only need model info (vision support, model name), expose metadata from the ACP InitializeResponse or session info. Only use a real LLM for features that actually need completion().

Pros: Minimal API calls, solves vision/model-name issues cleanly
Cons: Doesn't solve title generation or sub-agent creation

Option D: Use ACP fork_session() for auxiliary tasks

Similar to how ask_agent() already uses fork_session(), title generation could fork the session and ask the ACP server to generate a title.

Pros: Uses the same billing/model as the main conversation
Cons: Heavy for simple tasks, depends on ACP server supporting arbitrary prompts in forks

Recommendation

A combination of Option A + Option C seems best:

  • Expose ACP server model metadata to solve vision/model-name checks without any LLM calls
  • Add an optional auxiliary_llm for tasks that genuinely need completion() (title generation)
  • For sub-agent creation (delegate/task), consider using fork_session() or requiring the factory to support ACP sub-agents

🤖 Generated with Claude Code

Metadata

Metadata

Labels

acpAbout ACPllmAbout LLMs.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions