feat: expose per-agent execution context - #774
Conversation
|
[Wes's CodePuppy Agent Review] Reviewed at head SummaryAdds Entered in two places:
The implementation is idiomatic and faithfully mirrors the established Really Should Fix These
Nits
Scope & description parityScope is tight — no YAGNI (32 lines, no speculative parameters) and no DRY violation (checked every One parity gap: the "per-agent tool config" motivation in the description is not yet served by the code that landed (finding 1). Everything else in the description is backed by real code. Nice work — this is a clean, well-tested change. Landing the scope widening would make the seam do what the description promises. |
|
Thanks @WSxDemise — all three P1s addressed in |
WSxDemise
left a comment
There was a problem hiding this comment.
[Wes's CodePuppyAgent Review]
Re-reviewed at c55d9fe9. Verified every change against the code rather than the summary. CI green (quality / macOS 3.13 / windows-encoding).
Verified
| Item | Status | Evidence |
|---|---|---|
| P1 — scope owned by task body | Fixed | _runtime.py:829-836 — run_agent_task() wraps _run_agent_task_body(); create_task at :942 is now bare, so scheduling changes can't break it |
P1 — agent_run_start / agent_run_end |
Fixed | _runtime.py:917-922 and :1098-1110, wrapped where they fire |
| P1 — regression test | Present | test_agent_run_lifecycle_hooks_resolve_executing_agent asserts both hooks resolve the agent and it's None after; unregister_callback teardown confirmed at callbacks.py:240 |
P2 — BaseAgent | None |
Done | agent_execution_context.py:38 under TYPE_CHECKING |
| P2 — executor caveat | Done | Module docstring, lines 9-10 |
| P2 — top-level placement rationale | Done | Docstring lines 6-8 |
| P2 — test rename | Done | tests/test_agent_execution_context.py |
| Description parity | Fixed | New Coverage/Not-covered paragraph + follow-up section now match the diff |
Correction to my own review
My first P1 was over-called, and I want to be explicit about it. I claimed the seam was "dark at the exact hook the PR was written for" and that the motivating use case wasn't served. The factual half was right — registration-time hooks do fire before any scope exists. The impact half was wrong.
I inferred from the phrase "per-agent tool config" that the consumer read at registration time. I never checked. Core-plugins spill registers on post_tool_call (code_puppy_core_plugins/spill/register_callbacks.py:247), which fires at tool-result time — inside the task body, and therefore covered by the original 5d63220d scope, let alone this one. The package was installed in the venv the whole time; I could have confirmed it in one grep.
So your resolution is the correct one, and stronger than my proposed fix: widening scope to cover registration would have added surface area for a consumer that doesn't exist, when on_register_agent_tools already receives agent_name explicitly (callbacks.py:807). Documenting it as a non-goal is right. My apologies for the noise — I should have verified the consumer before asserting the use case was broken.
The other two P1s I'd still call fair: the create_task fragility was real regardless of consumer, and the lifecycle-hook gap was genuine.
Remaining
Nothing blocking. One optional follow-up, unchanged in priority from last round: no end-to-end test for a nested main-run → sub-agent invocation asserting the innermost agent wins and the outer is restored. The unit test covers nesting with sentinels and each path is covered in isolation, so this is a nice-to-have, not a gap.
on_agent_run_cancel in the sub-agent path (subagent_invocation.py:510) sits inside the executing_agent_context(agent_config) block — checked, correctly covered.
Approving. Clean fix, and the non-goal documentation is the better call.
What
Adds a
ContextVar-backed seam exposing the agent instance that owns the current model run:code_puppy/agent_execution_context.py—executing_agent_context(agent)+get_executing_agent()agents/_runtime.py(the task body owns its own scope, andagent_run_start/agent_run_endare wrapped where they fire) and around sub-agent invocation intools/subagent_invocation.py. The innermost agent wins and outer state is restored on exit.Coverage: run-scoped hooks — the agent task body (
on_agent_run_context, tool-call hooks),agent_run_start/agent_run_end, and sub-agent runs. Not covered: build/registration-time hooks (on_register_agent_toolsetc.) — those fire before a run exists and already receiveagent_nameexplicitly, so they don't need this seam.Why
Plugins currently have to consult the process-global agent manager to know "which agent is running", which is wrong under concurrent sessions and sub-agents. This gives them a run-scoped, async-safe answer. Needed by core-plugins for per-agent tool config (e.g. an agent that disables spill); the plugin side uses a guarded import, so it degrades gracefully on older runtimes and this lands independently.
Tests
tests/agents/test_execution_context.py)Noneafter63 passedacross the four touched test files; ruff clean.Review follow-up (WSxDemise's review)
run_agent_task()(the body owns its scope, robust to scheduling changes) instead of wrapping thecreate_taskcall;agent_run_start/agent_run_endare wrapped where they fire — with a regression test asserting both hooks resolve the agent.get_executing_agent()is annotatedBaseAgent | NoneunderTYPE_CHECKING.run_in_executorpropagation caveat and why the module sits at top level.tests/test_agent_execution_context.pyper repo convention.agent_nameexplicitly, and the motivating consumer (core-plugins spill) reads per-agent config at tool-result time, not registration time.ruff --select I) artifacts, left as-is.