fix(kennel): read the agent name from RunContext.agent, not four dead probes - #56
Open
breedx wants to merge 1 commit into
Open
fix(kennel): read the agent name from RunContext.agent, not four dead probes#56breedx wants to merge 1 commit into
breedx wants to merge 1 commit into
Conversation
… probes
`_agent_name_from_context` tried `agent_name` and `name` on the RunContext,
then the same two on `deps`, then returned "unknown". None of those fields
exist on `RunContext` — pydantic-ai carries the agent on `.agent` and the
name on `Agent.name`:
RunContext.__dataclass_fields__ has "agent_name" -> False
RunContext.__dataclass_fields__ has "name" -> False
RunContext.__dataclass_fields__ has "agent" -> True
So no probe could match and the fallback was not a fallback: it was the only
reachable branch, on every call. `agent_wing()` faithfully turned that into
`agent:unknown`, which means the wing documented as one agent's own
cross-project reflections was really a single shared bucket for every agent
in the process. Observed in a deployment: `kennel_list_wings` returned only
`agent:unknown` and `repo:<path>`; no per-agent wing had ever existed.
Repo-wing bylines were unaffected, which is what made it hard to spot — they
come from the `agent_run_end` callback parameter, not from this probe.
Now reads `ctx.agent.name` and nothing else.
`RunContext.agent` is typed `Agent | None` and an unnamed `Agent` is legal,
so "no name" is a real answer. It returns `""` rather than a stand-in, and
`_resolve_wing` raises `KennelScopeError` at the one point something asks to
be scoped by the agent. Refusing at the lookup instead would break callers
that never needed the name — `kennel_remember` defaults to the repo wing —
so the refusal sits where the mis-scoped write would actually happen.
Tests use `SimpleNamespace`, not `Mock`: a Mock answers to every attribute,
so it satisfies the old probe and hides which field is read. Three of them
fail against the previous implementation, including "two agents do not share
one wing".
2122 passed, 2 skipped.
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.
The bug
_agent_name_from_contextprobes four attributes, none of which exist:Against pydantic-ai 2.35:
The agent is on
.agent, the name onAgent.name. So no probe can match, and the "best-effort fallback" is the only reachable branch — every call, not an edge case.agent_wing()then faithfully rendersagent:unknown.Why it matters
The agent wing is documented as one agent's own cross-project reflections. In practice it is one shared bucket for every agent in the process — not private, not per-agent. Anything written there trusting the docstring is mis-scoped.
Observed in a deployment:
kennel_list_wingsreturned onlyagent:unknown(2 drawers) andrepo:<path>(218). No per-agent wing had ever existed.What made it hard to spot: repo-wing bylines are correct. They come from the
agent_run_endcallback parameter (register_callbacks.py:42→record_run_end→metadata["agent"]), not from this probe. So the name is plainly available in the process; only this lookup misses it.The fix
Read
ctx.agent.name. One field, no probe.RunContext.agentis typedAgent | Noneand an unnamedAgentis legal, so "no name" is a real answer — returned as""rather than a stand-in._resolve_wingraisesKennelScopeErrorat the one point something actually asks to be scoped by the agent.Refusing at the lookup instead would break callers that never needed the name —
kennel_rememberdefaults to the repo wing — so the refusal sits where the mis-scoped write would happen. (I tried it the other way first; it broke 8 existing tests, all of them repo-wing writes.)Tests
tests/test_puppy_kennel_agent_wing.py, 7 cases. Three fail against the current implementation, including "two agents do not share one wing" — both collapse toagent:unknowntoday.They use
SimpleNamespace, notMock, deliberately: aMockauto-vivifies every attribute, so it satisfies the old probe and hides which field is actually read. That property is plausibly why this shipped.ruff check+ruff format --checkcleanNote
This does not migrate existing
agent:unknowndrawers. After the fix they simply stop being written to; the old bucket remains until someone decides what to do with it. Happy to follow up if you'd like a migration.