You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Since Hermes v17 made subagents async (delegate_task(background=true)), delegated work fans out in parallel — but per_cron_job budgets exclude subagent cost. Child tokens land in global totals (so a global cap still stops a runaway), but they are never attributed to the parent cron job, and therefore can't be enforced in-path per job.
This issue tracks closing that gap entirely from the plugin — no Hermes fork required.
Background (verified against NousResearch/hermes-agent@main)
The correlation primitive exists; it just isn't in the path where we count cost.
In-path budget hooks — child session_id only, no parent:
Async safety: with background=true, _build_child_agent (and therefore subagent_start) runs synchronously, beforedispatch_async_delegation. So the parent↔child mapping is established before any async post_api_request event fires — those later events can always resolve.
Proposed approach
A small in-memory map maintained across three hooks:
subagent_start → store { child_session_id: parent_session_id } (keep subagent_id and parent_turn_id for richer reporting).
post_api_request / post_tool_call → resolve the parent from the incoming child session_id; add the child's usage to the parent's per_cron_job tally.
subagent_stop → finalize and remove the entry.
Open design questions (input welcome — comment below)
Nested delegation (orchestrator role, max_spawn_depth > 1): should attribution resolve to the immediate parent or transitively to the root cron job? (Leaning root, for per_cron_job.)
Map storage & concurrency: module-level dict vs. a bounded/TTL cache; thread-safety given children run under a ThreadPoolExecutor.
Double counting: children already roll into global; the per-job attribution must not inflate the global tally.
Leaks: subagent_stop may not fire if a child is cancelled/interrupted. Need cleanup on parent session end as a backstop.
Docs: ONBOARDING.md notes that session_id in budget hooks is always the executing agent's, and parent_session_id lives only in subagent_start / subagent_stop
Good first contributions
These can be picked up independently — comment to claim one:
The subagent_start / subagent_stop map handlers + unit test.
The post_api_request resolver + attribution test.
The docs update in ONBOARDING.md.
A repro fixture that spawns background + nested subagents for the test suite.
Summary
Since Hermes v17 made subagents async (
delegate_task(background=true)), delegated work fans out in parallel — butper_cron_jobbudgets exclude subagent cost. Child tokens land inglobaltotals (so a global cap still stops a runaway), but they are never attributed to the parent cron job, and therefore can't be enforced in-path per job.This issue tracks closing that gap entirely from the plugin — no Hermes fork required.
Background (verified against
NousResearch/hermes-agent@main)The correlation primitive exists; it just isn't in the path where we count cost.
In-path budget hooks — child
session_idonly, no parent:post_api_request(agent/conversation_loop.py):task_id,turn_id,api_request_id,session_id(executing agent = child),api_call_count,usage. Noparent_session_id.pre_tool_call/post_tool_call(model_tools.py):task_id,session_id(child),tool_call_id,turn_id,api_request_id. Noparent_session_id.Delegation hooks — the only place the parent link lives:
subagent_start(tools/delegate_tool.py):parent_session_id,parent_turn_id,child_session_id,child_subagent_id(sa-{i}-{uuid8}).subagent_stop: same correlation, on completion.Async safety: with
background=true,_build_child_agent(and thereforesubagent_start) runs synchronously, beforedispatch_async_delegation. So the parent↔child mapping is established before any asyncpost_api_requestevent fires — those later events can always resolve.Proposed approach
A small in-memory map maintained across three hooks:
subagent_start→ store{ child_session_id: parent_session_id }(keepsubagent_idandparent_turn_idfor richer reporting).post_api_request/post_tool_call→ resolve the parent from the incoming childsession_id; add the child'susageto the parent'sper_cron_jobtally.subagent_stop→ finalize and remove the entry.Open design questions (input welcome — comment below)
orchestratorrole,max_spawn_depth > 1): should attribution resolve to the immediate parent or transitively to the root cron job? (Leaning root, forper_cron_job.)ThreadPoolExecutor.global; the per-job attribution must not inflate the global tally.subagent_stopmay not fire if a child is cancelled/interrupted. Need cleanup on parent session end as a backstop.delegated_role(Feature Request: Add delegated_role field to delegated sessions NousResearch/hermes-agent#40189, #40816): if/when it lands, enrich attribution with the role/profile name.Acceptance criteria
subagent_starthandler builds thechild_session_id → parent_session_idmappost_api_requestresolves the parent and attributes childusageto the parent'sper_cron_jobbudgetdelegate_task(background=true)(async path)subagent_stopand on parent session end (no leaks)globalpytestcovering sync, async (background), and nested delegation;ruff check .cleanONBOARDING.mdnotes thatsession_idin budget hooks is always the executing agent's, andparent_session_idlives only insubagent_start/subagent_stopGood first contributions
These can be picked up independently — comment to claim one:
subagent_start/subagent_stopmap handlers + unit test.post_api_requestresolver + attribution test.ONBOARDING.md.