Skip to content

Attribute async subagent cost to per_cron_job budgets (v17 delegate_task) #49

Description

@nujovich

Summary

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:

  • post_api_request (agent/conversation_loop.py): task_id, turn_id, api_request_id, session_id (executing agent = child), api_call_count, usage. No parent_session_id.
  • pre_tool_call / post_tool_call (model_tools.py): task_id, session_id (child), tool_call_id, turn_id, api_request_id. No parent_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 therefore subagent_start) runs synchronously, before dispatch_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:

  1. subagent_start → store { child_session_id: parent_session_id } (keep subagent_id and parent_turn_id for richer reporting).
  2. 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.
  3. 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.
  • Upstream 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_start handler builds the child_session_id → parent_session_id map
  • post_api_request resolves the parent and attributes child usage to the parent's per_cron_job budget
  • Verified with delegate_task(background=true) (async path)
  • Nested delegation resolves to the root cron job
  • Map entries cleaned up on subagent_stop and on parent session end (no leaks)
  • No double counting against global
  • Tests: pytest covering sync, async (background), and nested delegation; ruff check . clean
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions