Skip to content

fix(kennel): read the agent name from RunContext.agent, not four dead probes - #56

Open
breedx wants to merge 1 commit into
mpfaffenberger:mainfrom
breedx:fix/kennel-agent-wing-is-always-unknown
Open

fix(kennel): read the agent name from RunContext.agent, not four dead probes#56
breedx wants to merge 1 commit into
mpfaffenberger:mainfrom
breedx:fix/kennel-agent-wing-is-always-unknown

Conversation

@breedx

@breedx breedx commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The bug

_agent_name_from_context probes four attributes, none of which exist:

for attr in ("agent_name", "name"):      # not on RunContext
    ...
for attr in ("agent_name", "name"):      # nor on deps
    ...
return "unknown"                          # so: always this

Against pydantic-ai 2.35:

RunContext.__dataclass_fields__ has "agent_name"  -> False
RunContext.__dataclass_fields__ has "name"        -> False
RunContext.__dataclass_fields__ has "agent"       -> True

The agent is on .agent, the name on Agent.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 renders agent: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_wings returned only agent:unknown (2 drawers) and repo:<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_end callback parameter (register_callbacks.py:42record_run_endmetadata["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.agent is typed Agent | None and an unnamed Agent is legal, so "no name" is a real answer — returned as "" rather than a stand-in. _resolve_wing raises KennelScopeError at 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_remember defaults 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 to agent:unknown today.

They use SimpleNamespace, not Mock, deliberately: a Mock auto-vivifies every attribute, so it satisfies the old probe and hides which field is actually read. That property is plausibly why this shipped.

  • kennel suite: 58 passed
  • full suite: 2122 passed, 2 skipped
  • ruff check + ruff format --check clean

Note

This does not migrate existing agent:unknown drawers. 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.

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant