Resolve session-store method checks against the instance - #1215
Open
shashvat-singham wants to merge 2 commits into
Open
Resolve session-store method checks against the instance#1215shashvat-singham wants to merge 2 commits into
shashvat-singham wants to merge 2 commits into
Conversation
parse_message wraps malformed input in MessageParseError -- non-dict
data, a missing type, missing required fields all get the parser's own
error type. But a "message" field that is not a dict escaped as a bare
TypeError from indexing into it:
parse_message({"type": "user", "message": "hi"})
# TypeError: string indices must be integers, not 'str'
Same for the assistant branch. The existing handlers only catch
KeyError, so TypeError/AttributeError from indexing a non-dict fell
through, and a single malformed line from the CLI stream would surface
as an unrelated-looking TypeError instead of the documented parse error.
Catch TypeError/AttributeError alongside KeyError in both branches and
raise MessageParseError with the offending data attached, like every
other malformation.
_store_implements looked the method up on type(store), so it only saw
class-level definitions. SessionStore is a structural Protocol, though,
so an implementation assigned on the instance satisfies it just as well
-- and those stores were rejected before the subprocess even spawned:
class DelegatingStore(SessionStore):
def __init__(self, inner):
self.list_sessions = inner.list_sessions
validate_session_store_options(
ClaudeAgentOptions(session_store=DelegatingStore(inner),
continue_conversation=True)
)
# ValueError: continue_conversation with session_store requires the
# store to implement list_sessions()
even though calling list_sessions() on that store works fine. The same
applies to a store whose method is a functools.partial, and to a test
double patched with AsyncMock -- arguably the most common way to hit
this, since it fails only under continue_conversation.
Look the attribute up on the instance and compare the underlying
function against the Protocol default, so a bound method is still
matched against the default while a plain callable assigned on the
instance counts as an implementation.
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.
Problem
_store_implementsresolves the method ontype(store), so it only recognises class-level definitions. ButSessionStoreis a structuralProtocol— an implementation assigned on the instance satisfies it just as well, and calling it works fine at runtime. Those stores are nonetheless rejected during pre-flight validation:Verified against
main, three shapes all wrongly reported as not implementing it:_store_implementsasync def list_sessionsTrueTrue__init__(delegation)FalseTrueAsyncMockFalseTrueFalseFalseThe
AsyncMockrow is probably the most likely way to meet this in practice — someone stubbing a store in their own tests gets aValueErrorthat only appears whencontinue_conversation=True, pointing at a method their double clearly has.Change
Look the attribute up on the instance and compare the underlying function against the Protocol default:
A bound method is still matched against the Protocol default via
__func__, so an unimplemented store is still detected; anything that isn't a bound method (a plain callable assigned on the instance) is compared directly and is never the default. Thegetattr(impl, ...)also makes the existingimpl is Noneguard meaningful — previouslyimplwas computed and then not used for the decision.Confirmed the negative case still works: a store without
list_sessionsis still rejected, andtest_continue_conversation_requires_list_sessionsstill passes.Tests
Added
test_continue_conversation_ok_when_list_sessions_set_on_instance, which fails onmainand passes with the change.The 8 failures are all the
[trio]parametrisations and reproduce identically on an unmodified tree here (no trio backend in my env) — unrelated to this change; the[asyncio]side is green.