Skip to content

fix: align FakeAgentTools list_memories envelope to real SDK (data/meta) [INT-1008]#471

Open
AlexanderZ-Band wants to merge 5 commits into
mainfrom
feat/fix-listmemories-envelope-in-fakeagenttools-to-mat-INT-1008
Open

fix: align FakeAgentTools list_memories envelope to real SDK (data/meta) [INT-1008]#471
AlexanderZ-Band wants to merge 5 commits into
mainfrom
feat/fix-listmemories-envelope-in-fakeagenttools-to-mat-INT-1008

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes INT-1008. Follow-up from the PR #422 review thread.

Problem

FakeAgentTools.list_memories returned {memories, metadata} while the real SDK returns the Fern ListAgentMemoriesResponse{data, meta} after model_dump() at the adapter boundary. BaselineTools hand-rolled the same wrong envelope a second time. Any adapter/test consuming the listing by .data/.meta (as production code must) was silently uncovered.

Fix (commit 1)

Two complementary mechanisms so the shape cannot drift again, rather than a one-off key rename:

The fake returns the real Fern type. FakeAgentTools now owns the memories store (memories= seed param, store_memory appends) and list_memories constructs an actual ListAgentMemoriesResponse — memory dicts are coerced into AgentMemory models, page_size truncates like a real first page, meta carries the platform's page_size/total_count semantics. The envelope has exactly one definition: the generated model.

The type checker enforces the pairing. list_memories is annotated -> ListAgentMemoriesResponse on AgentTools, FakeAgentTools, and AgentToolsProtocol (models re-exported via band.client.rest). Mutation-verified: reverting the fake to return a dict makes pyrefly check fail at that line.

Supporting changes:

  • serialize_tool_result() extracted from execute_tool_call_structured as the single adapter-boundary serializer; BaselineTools.execute_tool_call now delegates band_list_memories/band_store_memory to the fake's methods and flows every dispatch result through it — the duplicate store and hand-rolled envelope are gone
  • adapters/pydantic_ai.py: the tightened protocol exposed that band_list_memories declared -> dict[str, Any] | str but returned the model; it now model_dump()s, making the declared contract true
  • crewai test fixture's private mock aligned to {data, meta}
  • memory_contents projection on the fake for readable, intent-oriented assertions

Toolkit: intent over plumbing (commit 2)

Scenario tests poked raw internals in six places — schema_requests[0]["include_memory"] indexing, private adapter._message_history comprehensions, and hand-rolled error-event counts (two of which re-implemented the existing assert_event_sent helper). The baseline toolkit now speaks intent:

  • Observation.assert_tool_exposure(memory=..., contacts=...) — which optional tool groups the adapter offered the model
  • BaselineScenario.run_expecting_failure(...) -> Observation — failure turns keep the success-path assertion vocabulary instead of falling back to raw event pokes
  • BaselineScenario.history_contents(room_id) — readable projection of a room's adapter history

All six sites retrofitted; new tests assert readable projections (listed_contents, memory_contents) with what/why failure messages.

Tests

  • tests/testing/test_fake_tools.py::TestMemoryListing — store→list round-trip lands in exactly {data, meta}, first-page truncation semantics, constructor seeding
  • tests/baseline/test_tools.pyBaselineTools dispatch hands adapters the serialized real envelope

Verification

  • uv run ruff check . / uv run ruff format . — clean
  • uv run pyrefly check — 0 errors (and fails on the mutated dict return)
  • Unit suite: 3982 passed, 81 skipped

🤖 Generated with Claude Code

Review fixes (commit 3)

A 10-angle automated review of this branch surfaced 14 findings; all confirmed/plausible ones are fixed:

  • Fake memory fidelity: store_memory no longer drops supplied subject_id/metadata, and builds its return via the Fern AgentMemory model — store/get/list now serve one identical serialized shape (previously 9-key string-date store return vs 13-key datetime list items). get_memory/supersede_memory/archive_memory became stateful against the store (no more canned stubs incoherent with list_memories), returning copies rather than mutable aliases; seeds validate at seed time.
  • Serializer single source of truth, for real: crewai serialize_success_result, claude_sdk _format_success_payload, parlant (6 sites), and pydantic_ai band_list_memories all route through serialize_tool_result instead of hand-rolled hasattr/model_dump copies.
  • BaselineTools: execute_tool_call_structured is now the primary entry point (both entry points agree, mirroring the real AgentTools); memory get/supersede/archive dispatch delegates to the fake; listing filter args are forwarded instead of silently dropped.
  • Harness/test hardening: assert_event_sent(count=...) scopes to the given message_type (the retry test now asserts "exactly one error event" as intended); run_expecting_failure requires an explicit error type (no Exception default that could swallow harness AssertionErrors); assert_tool_exposure grew a count pin restoring the old exact-equality coverage; duplicate envelope assertions removed from the baseline dispatch test; conventions fixes in test_fake_tools.py.
  • The remaining sibling fakes (lookup_peers/list_contacts/list_contact_requests) still have legacy envelopes — now explicitly marked in their docstrings as diverging from the real SDK, pending their own alignment pass.

Sibling envelope alignment (commit 4)

The "legacy envelope" trap flagged in the review is now fixed rather than just marked: lookup_peers, list_contacts, and list_contact_requests got the same treatment as list_memories — the fake constructs the real Fern response models ({data, metadata}, with received/sent nested under data), return types are tightened on AgentTools/FakeAgentTools/AgentToolsProtocol, peer/contact seeds validate at seed time, and pydantic_ai's peer/contact wrappers serialize through serialize_tool_result. Every fake list envelope now matches the platform.

Bonus: the tightened lookup_peers annotation immediately caught a live bug in AgentTools._lookup_peermetadata.total_pages is Optional, so pagination could compare page >= None and crash on a platform response omitting it. Fixed (missing total_pages now means one page).

Second review round (commit 5)

Commits 3–4 were re-reviewed with the same multi-angle process as the original. Confirmed findings, all fixed:

  • Fake pagination was incoherent: lookup_peers/list_contacts echoed the requested page and a real total_pages but always served the first slice — a pager walking pages saw page-1 data repeated forever. Both now serve the requested page via a shared page_slice helper (also adopted by fetch_room_context); regression test added. (list_memories is exempt — the real API takes no page.)
  • Memory not-found fidelity: the fake's get_memory returned None and supersede/archive raised ValueError, where the real tools raise RuntimeError("Failed to ... memory - no response data") — one infidelity had been swapped for another. The fake now raises exactly like the real tools.
  • pydantic_ai memory tools: store/get/supersede/archive returned raw Fern models (only list had been wrapped); all now serialize through serialize_tool_result.
  • Nested-metadata aliasing at memory boundaries (deep copies now), stale LLM-facing 'peers' key in LookupPeersInput's description, parlant's dead peers-key fallback + legacy test mocks, vacuous bare assert_event_sent(), and rest.py import/__all__ ordering.

Known and accepted (documented decisions, not defects): BaselineTools' structured entry point raises instead of returning ok=False (stated in its docstring — baseline tests fail loudly); total_pages=0 for empty collections (platform convention unverified); the fake can't know source_agent_id. Remaining same-class work for a follow-up ticket: get_participants and the mutation tools (add_contact/remove_contact/respond_contact_request, send_message/send_event) still return hand-rolled dicts, and participants/room_context seeds are not validated.

The fake returned {memories, metadata} while the real SDK returns the
Fern ListAgentMemoriesResponse ({data, meta}), so tests consuming the
listing the way production code does were never covered.

- FakeAgentTools now owns the memories store (seedable, store_memory
  appends) and list_memories returns the real Fern model
- serialize_tool_result() extracted as the single adapter-boundary
  serializer; BaselineTools dispatch delegates to the fake and flows
  every result through it
- list_memories return type tightened to ListAgentMemoriesResponse on
  AgentTools, FakeAgentTools, and AgentToolsProtocol so pyrefly makes
  envelope drift a type error
- pydantic_ai band_list_memories now model_dump()s, making its declared
  dict return type true (caught by the tightened protocol)
- memory_contents projection on the fake for readable assertions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 21, 2026

Copy link
Copy Markdown

INT-1008

AlexanderZ-Band and others added 3 commits July 21, 2026 22:05
Scenario tests poked raw internals where the toolkit should speak
intent: schema_requests dict indexing, private _message_history
comprehensions, and hand-rolled error-event counts (two of which
re-implemented the existing assert_event_sent helper).

- Observation.assert_tool_exposure(memory=..., contacts=...) covers
  which optional tool groups the adapter offered the model
- BaselineScenario.run_expecting_failure() returns an Observation so
  failure paths share the success-path assertion vocabulary
- BaselineScenario.history_contents() projects a room's adapter
  history, replacing private-attribute pokes
- all six plumbing sites in test_scenarios.py retrofitted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FakeAgentTools memory tools are now coherent and shape-faithful:
- store_memory persists supplied subject_id/metadata and returns the
  real serialized AgentMemory shape (built via the Fern model, one
  definition), so store/get/list all serve identical dicts
- get_memory/supersede_memory/archive_memory operate on the store
  instead of returning canned stubs; unknown ids return None (get) or
  raise (supersede/archive); boundaries return copies, not aliases
- constructor seeds validate at seed time, not first list
- lookup_peers/list_contacts/list_contact_requests carry an explicit
  legacy-envelope warning until they get the list_memories treatment

serialize_tool_result is now actually the single definition: crewai,
claude_sdk, and parlant (6 sites) reuse it instead of hand-rolling
hasattr/model_dump; pydantic_ai's band_list_memories serializes through
it too, so dict-returning impls degrade gracefully.

BaselineTools defines execute_tool_call_structured as the primary entry
point (mirroring the real AgentTools relationship), delegates the
memory get/supersede/archive cases to the fake, and forwards listing
filter args instead of dropping them.

Harness/test hardening: assert_event_sent count now scopes to the given
message_type; run_expecting_failure requires an explicit error type;
assert_tool_exposure can pin the request count; duplicate envelope
assertions removed from the baseline dispatch test; conventions fixes
(__future__ import, -> None annotations) in test_fake_tools.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies the list_memories mechanism to the remaining divergent fakes:
lookup_peers, list_contacts, and list_contact_requests now construct the
real Fern response models (ListAgentPeersResponse, ListAgentContactsResponse,
ListAgentContactRequestsResponse), so their serialized envelopes are
{data, metadata} — with received/sent nested under data — exactly as the
platform returns them. The legacy-envelope warnings are gone because the
divergence is gone.

- return types tightened on AgentTools, FakeAgentTools, and
  AgentToolsProtocol so pyrefly keeps the pairing honest
- peer/contact seeds validate and canonicalize at seed time, like memories
- shared total_pages helper replaces three copies of the page arithmetic
- pydantic_ai peer/contact wrappers serialize through serialize_tool_result,
  making their declared dict returns true
- tests updated: platform-valid seeds, data/metadata assertions, crewai
  fixture mocks aligned to the real shapes

The tightened lookup_peers annotation exposed a live bug in _lookup_peer:
metadata.total_pages is Optional, so pagination could compare page >= None
and crash; a missing total_pages now means one page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- fake lookup_peers/list_contacts now serve the requested page via a
  shared page_slice helper (previously every page returned the first
  slice while echoing the requested page number and a real total_pages);
  fetch_room_context reuses the same helper
- fake memory not-found behavior matches the real tool: get_memory and
  supersede/archive raise RuntimeError with the real messages instead of
  returning None / raising ValueError
- memory boundaries return deep copies, so nested metadata no longer
  aliases the store
- pydantic_ai store/get/supersede/archive memory tools serialize through
  serialize_tool_result like their list sibling, making the declared
  dict returns true against the real Fern models
- LookupPeersInput's LLM-facing description no longer names the retired
  'peers' key; parlant's dead peers-key fallback removed and its test
  mocks aligned to the real envelope
- bare assert_event_sent() now asserts at least one event instead of
  passing vacuously; rest.py import/__all__ ordering aligned

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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