fix(subagent): freshen explicit condensers per spawn#3743
Conversation
Addresses post-merge review of #3696. The explicit/custom condenser path in agent_definition_to_factory returned agent_def.condenser by reference, so every sub-agent spawn shared one condenser instance -> one shared Metrics object (double-counted/cross-contaminated condenser cost, plus a mutable-state race under concurrent same-type sub-agents). It also never normalized the condenser LLM usage_id, so a condenser sharing the agent's usage_id had its tokens deduped out of conversation stats. Now each spawn gets a fresh copy of the condenser with its own LLM + metrics, and the condenser usage_id is set distinct from the agent's (kept as-is when it already differs). The default-condenser path is unchanged. Tests: fresh instance/LLM/metrics per spawn, colliding usage_id normalized, distinct usage_id preserved.
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
Verified the explicit subagent condenser lifecycle through the SDK factory: the PR fixes shared condenser/LLM/Metrics objects and normalizes colliding condenser usage_ids while preserving already-distinct IDs.
Does this PR achieve its stated goal?
Yes. The PR set out to freshen explicit/custom condensers per subagent spawn so metrics do not collide, and to avoid condenser token dedupe when the condenser LLM shares the agent's usage_id. Running the same SDK usage script on origin/main reproduced the old behavior (same_metrics_object: true, condenser_usage_collides_with_agent: true); running it on aedfa5a2 showed distinct condenser, LLM, and Metrics objects per spawn, no usage_id collision, and preservation of an already-distinct my-condenser ID.
| Phase | Result |
|---|---|
| Environment Setup | ✅ make build completed and installed the uv environment successfully |
| CI Status | ✅ All reported checks are green except this in-progress QA Changes workflow; PR artifacts cleanup is skipped |
| Functional Verification | ✅ Standalone SDK script reproduced the bug on base and confirmed the fix on the PR commit |
Functional Verification
Test 1: Explicit custom condenser spawns get isolated lifecycle objects and safe usage IDs
Step 1 — Reproduce / establish baseline (without the fix):
Checked out origin/main and ran:
git checkout --detach origin/main && OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_subagent_condenser.py | tee /tmp/qa_base_output.jsonRelevant output:
{
"condenser_usage_collides_with_agent": true,
"same_condenser_llm_object": true,
"same_condenser_object": true,
"same_metrics_object": true,
"spawn_one": {
"agent_llm_usage_id": "default",
"condenser_llm_usage_id": "default",
"condenser_max_size": 40,
"condenser_keep_first": 2
},
"spawn_two": {
"agent_llm_usage_id": "default",
"condenser_llm_usage_id": "default"
}
}This confirms the reported bug exists on base: two factory spawns share the same explicit condenser, LLM, and Metrics objects, and the condenser LLM usage_id collides with the agent's default usage ID.
Step 2 — Apply the PR's changes:
Checked out the PR commit aedfa5a2ad0acfa72925b3d7536617b5a99e811e.
Step 3 — Re-run with the fix in place:
Ran the same SDK script:
git checkout --detach aedfa5a2ad0acfa72925b3d7536617b5a99e811e && OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_subagent_condenser.py | tee /tmp/qa_pr_output.jsonRelevant output:
{
"condenser_usage_collides_with_agent": false,
"same_condenser_llm_object": false,
"same_condenser_object": false,
"same_metrics_object": false,
"spawn_one": {
"agent_llm_usage_id": "default",
"condenser_llm_usage_id": "condenser",
"condenser_max_size": 40,
"condenser_keep_first": 2
},
"spawn_two": {
"agent_llm_usage_id": "default",
"condenser_llm_usage_id": "condenser"
},
"distinct_usage_id_case": {
"condenser_llm_usage_id": "my-condenser"
}
}This confirms the fix works for the stated SDK behavior: each spawn now receives separate condenser, LLM, and Metrics objects; a colliding usage_id is normalized to condenser; existing condenser config is preserved; and an already-distinct usage ID remains unchanged.
Issues Found
None.
This QA review was created by an AI agent (OpenHands) on behalf of the user.
Match the existing idiom in local_conversation._condenser_for_switched_llm instead of getattr duck-typing. LLMSummarizingCondenser is the only condenser type with an `llm` field, so behavior is unchanged.
HUMAN:
Fixes explicit-condenser metrics sharing (follow-up to #3696). Verified fresh per-spawn condenser/LLM/Metrics isolation via the subagent registry tests.
AGENT:
Follow-up to #3696 (default sub-agent condenser). Fixes the explicit/custom-condenser path so per-spawn metrics don't collide.
This is an object-lifecycle / metrics-isolation fix, so the proof is object identity, asserted directly:
uv run pytest tests/sdk/subagent/test_subagent_registry.pypasses, including new tests that two spawns get distinct condenser, LLM, andMetricsobjects and that the condenserusage_idis distinct from the agent's. Broadertests/sdk/subagent tests/sdk/context= 544 pass. pre-commit (ruff + pyright) clean.Why
In #3696, the explicit/custom condenser branch of
agent_definition_to_factoryreturnedagent_def.condenserby reference. The factory runs once per sub-agent spawn, so every spawn shared one condenser instance — and therefore oneMetricsobject. That double-counts / cross-contaminates condenser cost in the parent rollup, and is a mutable-state race under concurrent same-type sub-agents. The explicit path also never gave the condenser LLM a distinctusage_id, so a condenser whoseusage_idmatched the agent's had its tokens deduped out of conversation stats.Summary
Metricsobject).usage_idis set distinct from the agent's — normalized tocondenseronly on collision; an already-distinct id is preserved.Issue Number
Follow-up to #3696.
How to Test
Covers: fresh instance/LLM/metrics per spawn, colliding
usage_idnormalized, already-distinctusage_idpreserved.Type
Notes
Scoped to the explicit-condenser path; the default-condenser behavior from #3696 is untouched.
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:1183560-pythonRun
All tags pushed for this build
About Multi-Architecture Support
1183560-python) is a multi-arch manifest supporting both amd64 and arm641183560-python-amd64) are also available if needed