-
Notifications
You must be signed in to change notification settings - Fork 190
Description
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()checksif llm_to_use.model == "acp-managed"and setsllm_to_use = None, falling back togenerate_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:119—llm.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()returnsFalsefor 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.llmthenparent_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()orresponses()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.llm→parent_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()raisesNotImplementedErrorif 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.modelreturns"acp-managed"instead of the actual model name- Used in
static_system_messageproperty for template rendering andget_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:
-
API keys are often already available — The ACP agent's environment typically has
ANTHROPIC_API_KEYorOPENAI_API_KEYset (they're needed for ACP server authentication). These same keys could be used for an auxiliary LLM. -
Cost control — Auxiliary tasks (title generation, vision check) are lightweight. Using a small/cheap model (e.g., Haiku) would add negligible cost.
-
The sentinel LLM already has metrics tracking —
ACPAgent._record_usage()usesself.llm.metricsfor ACP usage stats. An auxiliary LLM would need separate metrics to avoid conflating ACP costs with auxiliary LLM costs. -
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 sentinelPros: 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_llmfor tasks that genuinely needcompletion()(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