feat(spawn): bridge delegated sub-agent cost to the parent session - #155
Merged
Merged
Conversation
The PR wires bridge_child_cost into spawn_sub_session so delegated sub-agent
spend reaches the parent's session.cost channel. Without it,
collect_contributions("session.cost") — and the sessionCostTotal the streaming
hook emits from it — reported only the parent's own LLM calls, so a turn that
delegated most of its work looked nearly free.
A self-review found two problems with the original commit, both now fixed:
1. The original put the bridge_child_cost call in the finally block.
amplifier_foundation's own spawn() (amplifier_foundation/bundle/_prepared.py:926-938)
bridges inside the try after execute, keeping finally teardown-only.
spawn_sub_session is the second implementation of that same capability, so the
divergence meant the same delegating turn would report a different session
total depending on which engine ran it. The call was moved inside the try,
matching foundation. Capturing spend from failed delegations is deliberately
dropped — no present need, and it was the only thing the finally placement
bought.
2. hook_streaming.py's on_orchestrator_complete docstring claimed sub-agent
totals differ "due to how the kernel accumulates contributions across the
coordinator hierarchy." That is false — amplifier-core's collect_contributions
(crates/amplifier-core/src/coordinator.rs:386) reads only the channels
registered on that one coordinator; there is no hierarchy walk. The docstring
now states the accurate mechanism: a parent's total includes delegated spend
solely because spawn_sub_session calls bridge_child_cost.
The commit also drops two stale "OUT OF SCOPE for this MVP — Cost bridging"
notes from the module and function docstrings in spawn.py.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Salil Das (sadlilas)
force-pushed
the
feat/bridge-delegated-subagent-cost
branch
from
August 26, 2026 20:32
48e52d9 to
e28475b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
spawn_sub_sessionlisted cost bridging as MVP out-of-scope and never calledamplifier_foundation.bridge_child_cost. Delegated sub-agent spend therefore never reached the parent'ssession.costchannel.Anything that asks the parent coordinator what a session cost —
collect_contributions("session.cost"), and thesessionCostTotalthe streaming hook emits from it — reported the parent's own LLM calls only. A turn that delegated most of its work looked nearly free.Why this is small
bridge_child_costis a top-levelamplifier_foundationexport. This package already depends onamplifier-foundation(pyproject.toml) and imports it in eleven places acrosssrc/— including this very module, two lines above the change (generate_sub_session_id).Nothing new is built. The call was simply never wired.
One wiring point covers both faces:
_runtime.pyandamplifier_agent_http/_session_runner.pyeach registersession.spawnagainst this same function.Placement
Before
cleanup().bridge_child_costreads the child's contributions and re-registers the frozen total on the parent.cleanup()tears the child coordinator down and takes those contributions with it — bridging after it would silently bridge nothing and look like it worked.Inside the
try, not infinally. This matchesamplifier_foundation's ownspawn()(amplifier_foundation/bundle/_prepared.py:926-938), which bridges afterexecuteand keepsfinallyteardown-only.spawn_sub_sessionis the second implementation of that capability; if the two disagree on when cost is bridged, the same delegating turn reports a different session total depending on which engine ran it. Spend from a failed delegation is not bridged. That is deliberate — it was the only thing afinallyplacement bought, and nothing needs it today.Scope
This changes exactly one number:
usage.sessionCostTotalon the NDJSON display stream, which the streaming hook computes fromcollect_contributions("session.cost")on the parent coordinator.Per-call
usage.costis untouched. Child sessions already inherit the parent'sdisplay.emitcapability and mount the same streaming hook, so per-call sub-agent costs — tagged withagentName— were already on the wire. The--output jsonenvelope is untouched as well; it carries no cost field (docs/spec/envelope-and-errors.md).Also in this change
Drops the two stale "OUT OF SCOPE for this MVP — Cost bridging" notes in the module and function docstrings.
Corrects the
on_orchestrator_completedocstring inbundle/hook_streaming.py, which attributed sub-agent total discrepancies to "how the kernel accumulates contributions across the coordinator hierarchy." The kernel does no such thing —collect_contributions(amplifier-core/crates/amplifier-core/src/coordinator.rs:386) reads only the channels registered on that one coordinator. A parent's total includes delegated spend solely because of the bridge call this PR adds, and the docstring now says so.Verification
No unit test accompanies this.
tests/in this repo carries e2e suites and nothing else, deliberately (pyproject.toml).🤖 Generated with Amplifier