fix: align FakeAgentTools list_memories envelope to real SDK (data/meta) [INT-1008]#471
Open
AlexanderZ-Band wants to merge 5 commits into
Open
Conversation
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>
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>
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.
Closes INT-1008. Follow-up from the PR #422 review thread.
Problem
FakeAgentTools.list_memoriesreturned{memories, metadata}while the real SDK returns the FernListAgentMemoriesResponse—{data, meta}aftermodel_dump()at the adapter boundary.BaselineToolshand-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.
FakeAgentToolsnow owns the memories store (memories=seed param,store_memoryappends) andlist_memoriesconstructs an actualListAgentMemoriesResponse— memory dicts are coerced intoAgentMemorymodels,page_sizetruncates like a real first page,metacarries the platform'spage_size/total_countsemantics. The envelope has exactly one definition: the generated model.The type checker enforces the pairing.
list_memoriesis annotated-> ListAgentMemoriesResponseonAgentTools,FakeAgentTools, andAgentToolsProtocol(models re-exported viaband.client.rest). Mutation-verified: reverting the fake to return a dict makespyrefly checkfail at that line.Supporting changes:
serialize_tool_result()extracted fromexecute_tool_call_structuredas the single adapter-boundary serializer;BaselineTools.execute_tool_callnow delegatesband_list_memories/band_store_memoryto the fake's methods and flows every dispatch result through it — the duplicate store and hand-rolled envelope are goneadapters/pydantic_ai.py: the tightened protocol exposed thatband_list_memoriesdeclared-> dict[str, Any] | strbut returned the model; it nowmodel_dump()s, making the declared contract true{data, meta}memory_contentsprojection on the fake for readable, intent-oriented assertionsToolkit: intent over plumbing (commit 2)
Scenario tests poked raw internals in six places —
schema_requests[0]["include_memory"]indexing, privateadapter._message_historycomprehensions, and hand-rolled error-event counts (two of which re-implemented the existingassert_event_senthelper). The baseline toolkit now speaks intent:Observation.assert_tool_exposure(memory=..., contacts=...)— which optional tool groups the adapter offered the modelBaselineScenario.run_expecting_failure(...) -> Observation— failure turns keep the success-path assertion vocabulary instead of falling back to raw event pokesBaselineScenario.history_contents(room_id)— readable projection of a room's adapter historyAll 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 seedingtests/baseline/test_tools.py—BaselineToolsdispatch hands adapters the serialized real envelopeVerification
uv run ruff check ./uv run ruff format .— cleanuv run pyrefly check— 0 errors (and fails on the mutated dict return)🤖 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:
store_memoryno longer drops suppliedsubject_id/metadata, and builds its return via the FernAgentMemorymodel — 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_memorybecame stateful against the store (no more canned stubs incoherent withlist_memories), returning copies rather than mutable aliases; seeds validate at seed time.serialize_success_result, claude_sdk_format_success_payload, parlant (6 sites), and pydantic_aiband_list_memoriesall route throughserialize_tool_resultinstead of hand-rolledhasattr/model_dumpcopies.execute_tool_call_structuredis now the primary entry point (both entry points agree, mirroring the realAgentTools); memory get/supersede/archive dispatch delegates to the fake; listing filter args are forwarded instead of silently dropped.assert_event_sent(count=...)scopes to the givenmessage_type(the retry test now asserts "exactly one error event" as intended);run_expecting_failurerequires an expliciterrortype (noExceptiondefault that could swallow harness AssertionErrors);assert_tool_exposuregrew acountpin restoring the old exact-equality coverage; duplicate envelope assertions removed from the baseline dispatch test; conventions fixes intest_fake_tools.py.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, andlist_contact_requestsgot the same treatment aslist_memories— the fake constructs the real Fern response models ({data, metadata}, withreceived/sentnested underdata), return types are tightened onAgentTools/FakeAgentTools/AgentToolsProtocol, peer/contact seeds validate at seed time, and pydantic_ai's peer/contact wrappers serialize throughserialize_tool_result. Every fake list envelope now matches the platform.Bonus: the tightened
lookup_peersannotation immediately caught a live bug inAgentTools._lookup_peer—metadata.total_pagesis Optional, so pagination could comparepage >= Noneand crash on a platform response omitting it. Fixed (missingtotal_pagesnow 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:
lookup_peers/list_contactsechoed the requestedpageand a realtotal_pagesbut always served the first slice — a pager walking pages saw page-1 data repeated forever. Both now serve the requested page via a sharedpage_slicehelper (also adopted byfetch_room_context); regression test added. (list_memoriesis exempt — the real API takes nopage.)get_memoryreturnedNoneand supersede/archive raisedValueError, where the real tools raiseRuntimeError("Failed to ... memory - no response data")— one infidelity had been swapped for another. The fake now raises exactly like the real tools.store/get/supersede/archivereturned raw Fern models (onlylisthad been wrapped); all now serialize throughserialize_tool_result.metadataaliasing at memory boundaries (deep copies now), stale LLM-facing'peers'key inLookupPeersInput's description, parlant's deadpeers-key fallback + legacy test mocks, vacuous bareassert_event_sent(), andrest.pyimport/__all__ordering.Known and accepted (documented decisions, not defects):
BaselineTools' structured entry point raises instead of returningok=False(stated in its docstring — baseline tests fail loudly);total_pages=0for empty collections (platform convention unverified); the fake can't knowsource_agent_id. Remaining same-class work for a follow-up ticket:get_participantsand the mutation tools (add_contact/remove_contact/respond_contact_request,send_message/send_event) still return hand-rolled dicts, andparticipants/room_contextseeds are not validated.