Skip to content

fix(subagent): freshen explicit condensers per spawn#3743

Merged
ak684 merged 2 commits into
mainfrom
alona/fix-subagent-condenser-metrics
Jun 16, 2026
Merged

fix(subagent): freshen explicit condensers per spawn#3743
ak684 merged 2 commits into
mainfrom
alona/fix-subagent-condenser-metrics

Conversation

@ak684

@ak684 ak684 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

HUMAN:
Fixes explicit-condenser metrics sharing (follow-up to #3696). Verified fresh per-spawn condenser/LLM/Metrics isolation via the subagent registry tests.

  • A human has tested these changes.

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.py passes, including new tests that two spawns get distinct condenser, LLM, and Metrics objects and that the condenser usage_id is distinct from the agent's. Broader tests/sdk/subagent tests/sdk/context = 544 pass. pre-commit (ruff + pyright) clean.


Why

In #3696, the explicit/custom condenser branch of agent_definition_to_factory returned agent_def.condenser by reference. The factory runs once per sub-agent spawn, so every spawn shared one condenser instance — and therefore one Metrics object. 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 distinct usage_id, so a condenser whose usage_id matched the agent's had its tokens deduped out of conversation stats.

Summary

  • Each spawn gets a fresh copy of an explicit condenser with its own LLM and reset metrics (no shared Metrics object).
  • The condenser LLM usage_id is set distinct from the agent's — normalized to condenser only on collision; an already-distinct id is preserved.
  • The default-condenser path is unchanged.

Issue Number

Follow-up to #3696.

How to Test

uv run pytest tests/sdk/subagent/test_subagent_registry.py

Covers: fresh instance/LLM/metrics per spawn, colliding usage_id normalized, already-distinct usage_id preserved.

Type

  • Bug fix

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

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:1183560-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-1183560-python \
  ghcr.io/openhands/agent-server:1183560-python

All tags pushed for this build

ghcr.io/openhands/agent-server:1183560-golang-amd64
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-golang-amd64
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-golang-amd64
ghcr.io/openhands/agent-server:1183560-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:1183560-golang-arm64
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-golang-arm64
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-golang-arm64
ghcr.io/openhands/agent-server:1183560-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:1183560-java-amd64
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-java-amd64
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-java-amd64
ghcr.io/openhands/agent-server:1183560-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:1183560-java-arm64
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-java-arm64
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-java-arm64
ghcr.io/openhands/agent-server:1183560-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:1183560-python-amd64
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-python-amd64
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-python-amd64
ghcr.io/openhands/agent-server:1183560-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:1183560-python-arm64
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-python-arm64
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-python-arm64
ghcr.io/openhands/agent-server:1183560-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:1183560-golang
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-golang
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-golang
ghcr.io/openhands/agent-server:1183560-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:1183560-java
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-java
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-java
ghcr.io/openhands/agent-server:1183560-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:1183560-python
ghcr.io/openhands/agent-server:1183560bc58f6c66e03a367070cffd6c525feed9-python
ghcr.io/openhands/agent-server:alona-fix-subagent-condenser-metrics-python
ghcr.io/openhands/agent-server:1183560-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 1183560-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 1183560-python-amd64) are also available if needed

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.
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/subagent
   registry.py140695%117, 246–247, 251, 322–323
TOTAL31165855072% 

@ak684 ak684 marked this pull request as ready for review June 16, 2026 00:34

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ 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.json

Relevant 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.json

Relevant 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.
@ak684 ak684 merged commit 0d77efc into main Jun 16, 2026
36 checks passed
@ak684 ak684 deleted the alona/fix-subagent-condenser-metrics branch June 16, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants