Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/amplifier_agent_lib/bundle/hook_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,11 +427,11 @@ async def on_orchestrator_complete(self, event: str, data: dict[str, Any]) -> Ho
satisfied; the meaningful payload is the cost total).

Note: ``sessionCostTotal`` reflects what ``collect_contributions``
returns, which may differ from summing per-call ``cost`` fields.
Sub-agent sessions can report higher totals due to how the kernel
accumulates contributions across the coordinator hierarchy. This is
a kernel concern (`bridge_child_cost` semantics in foundation), not a
bug in this hook.
returns, which may differ from summing per-call ``cost`` fields. The
kernel does not accumulate across a coordinator hierarchy -- it reads
only the channels registered on this coordinator. A parent's total
includes delegated spend solely because ``spawn_sub_session`` calls
``bridge_child_cost`` to re-register the child's frozen total here.
"""
collect = getattr(self._coordinator, "collect_contributions", None)
if collect is None:
Expand Down
21 changes: 17 additions & 4 deletions src/amplifier_agent_lib/spawn.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
Explicitly OUT OF SCOPE for this MVP (noted with comments):
- Recursive session.spawn on child coordinator (grandchild delegation will
fail with the delegate tool's own clear error message; that is acceptable)
- Cost bridging (bridge_child_cost)
- Display nesting (push_nesting / pop_nesting)
- Provider preference plumbing beyond simple config inclusion
- session.resume capability (only spawn, not resume)
Expand Down Expand Up @@ -346,7 +345,6 @@ async def spawn_sub_session(**kwargs: Any) -> dict[str, Any]:
MVP out-of-scope items (marked with # MVP-SKIP comments):
- Recursive session.spawn on child coordinator (grandchild delegation fails
with delegate tool's own error message — acceptable for MVP)
- Cost bridging (bridge_child_cost)
- Display nesting (push_nesting / pop_nesting)
- Provider preference plumbing beyond config inclusion

Expand All @@ -362,7 +360,7 @@ async def spawn_sub_session(**kwargs: Any) -> dict[str, Any]:
``"self"``.
"""
from amplifier_core import AmplifierSession
from amplifier_foundation import generate_sub_session_id
from amplifier_foundation import bridge_child_cost, generate_sub_session_id

# -- Unpack kwargs (matches tool-delegate calling convention) --------
agent_name: str = kwargs["agent_name"]
Expand Down Expand Up @@ -524,9 +522,24 @@ async def _system_factory() -> str:
child_session.coordinator.register_capability("display.emit", parent_display_emit)
await mount_streaming_hook(child_session.coordinator, {})

# -- Execute the task and clean up ---------------------------------
# -- Execute the task, bridge its cost, and clean up ----------------
# bridge_child_cost reads the child's `session.cost` contributions and
# re-registers the frozen total on the PARENT coordinator, so a delegated
# turn's spend is visible to whoever asks the parent for its cost. It must
# run BEFORE cleanup(), which tears the child coordinator down and takes
# its contributions with it.
#
# Placement matches amplifier_foundation's own spawn(): bridge inside the
# try, `finally` stays teardown-only. A failed delegation's spend is not
# bridged, and that is deliberate -- the two spawn implementations report
# the same number for the same delegation.
try:
response = await child_session.execute(instruction)
await bridge_child_cost(
child_session.coordinator,
parent_session.coordinator,
sub_session_id,
)
finally:
await child_session.cleanup()

Expand Down