@@ -155,6 +155,13 @@ def find_resolved_parameters(fn: Callable[..., Any]) -> dict[str, tuple[Resolve,
155155 for name in inspect .signature (fn ).parameters :
156156 annotation = hints .get (name )
157157 if get_origin (annotation ) is not Annotated :
158+ # A `Resolve` marker is only honored at the top level; flag (rather than
159+ # silently drop) one buried in a union, e.g. `Annotated[T, Resolve(f)] | None`.
160+ if _contains_resolve (annotation ):
161+ raise InvalidSignature (
162+ f"Parameter { name !r} of { _resolver_name (fn )!r} wraps `Resolve(...)` in a "
163+ "union; annotate the parameter directly as `Annotated[T, Resolve(...)]`"
164+ )
158165 continue
159166 type_arg , * metadata = get_args (annotation )
160167 marker = next ((m for m in metadata if isinstance (m , Resolve )), None )
@@ -163,6 +170,13 @@ def find_resolved_parameters(fn: Callable[..., Any]) -> dict[str, tuple[Resolve,
163170 return resolved
164171
165172
173+ def _contains_resolve (annotation : Any ) -> bool :
174+ """True when a `Resolve` marker is nested inside `annotation` (e.g. a union member)."""
175+ if get_origin (annotation ) is Annotated :
176+ return any (isinstance (m , Resolve ) for m in get_args (annotation )[1 :])
177+ return any (_contains_resolve (arg ) for arg in get_args (annotation ))
178+
179+
166180def _elicit_return_schema (return_annotation : Any ) -> type [BaseModel ] | None :
167181 """Extract `T` from a resolver return type's `Elicit[T]` arm, if present.
168182
@@ -187,12 +201,13 @@ def _is_union(annotation: Any) -> bool:
187201def _wants_union (type_arg : Any ) -> bool :
188202 """True when `type_arg` is an `ElicitationResult` member (or a union of them).
189203
190- Handles the bare `ElicitationResult[T]` alias (a `TypeAliasType` carrying the
191- union on `__value__`), an explicit `AcceptedElicitation[T] | ... ` union, and a
192- single member.
204+ Handles the subscripted `ElicitationResult[T]` alias (a `TypeAliasType` whose
205+ union is on the origin's `__value__`), the bare `ElicitationResult` alias (the
206+ `__value__` is on `type_arg` itself), an explicit `AcceptedElicitation[T] | ...`
207+ union, and a single member.
193208 """
194- origin = get_origin ( type_arg )
195- value = getattr (origin , "__value__" , None )
209+ # Unwrap the `ElicitationResult` alias whether it is bare or subscripted.
210+ value = getattr (type_arg , "__value__" , None ) or getattr ( get_origin ( type_arg ) , "__value__" , None )
196211 if value is not None :
197212 type_arg = value
198213 members = get_args (type_arg ) if get_origin (type_arg ) is not None else (type_arg ,)
@@ -330,16 +345,17 @@ def __init__(
330345
331346
332347def _state_key (fn : Callable [..., Any ]) -> str :
333- """Process -stable wire key for a resolver's elicitation .
348+ """Worker -stable base wire key for a resolver, derived only from registration data .
334349
335- `id(fn)` isn't stable across `input_required` rounds, so key `input_requests` /
336- `request_state` by `module:qualname`. Bound methods add their `__self__` id so
337- two instances of the same method get distinct questions and stored outcomes
338- (the registered `Resolve(...)` holds the instance for the call's lifetime).
350+ `input_requests`/`request_state` must round-trip through the client and resume on
351+ any worker (stateless HTTP), so the key carries no `id(...)`: it is the resolver's
352+ `module:qualname` (a callable object uses its type's). Distinct resolvers that
353+ share this base - two instances of one method, two closures from one factory - are
354+ disambiguated deterministically by `build_resolver_plans` (`base`, `base#1`, ...).
339355 """
340- base = f" { getattr (fn , '__module__' , '' ) } : { getattr ( fn , '__qualname__' , fn )!s } "
341- bound_self = getattr (fn , "__self__ " , None )
342- return f"{ base } # { id ( bound_self ) } " if bound_self is not None else base
356+ qualname = getattr (fn , "__qualname__" , None ) or type ( fn ). __qualname__
357+ module = getattr (fn , "__module__ " , None ) or type ( fn ). __module__
358+ return f"{ module } : { qualname } "
343359
344360
345361async def resolve_arguments (
@@ -396,7 +412,11 @@ async def _resolve(fn: Callable[..., Any], res: _Resolution) -> ElicitationResul
396412 if wire_key in res .pending :
397413 # Already asked this round by another consumer; don't run the resolver again.
398414 raise _Pending
399- if wire_key in res .state :
415+ # Restore a prior round's outcome directly only when its model is known from the
416+ # `Elicit[T]` return arm. Without that (a resolver that elicits but isn't annotated
417+ # `-> ... Elicit[T]`), fall through and re-run the resolver so `_elicit` can
418+ # re-validate the stored answer against the live `Elicit.schema`.
419+ if wire_key in res .state and (plan .elicit_schema is not None or res .state [wire_key ].action != "accept" ):
400420 outcome = _outcome_from_state (res .state [wire_key ], plan .elicit_schema )
401421 res .cache [cache_key ] = outcome
402422 # Carry the restored answer forward: if a later resolver is still pending,
@@ -449,6 +469,13 @@ async def _elicit(elicit: Elicit[Any], key: str, res: _Resolution) -> Elicitatio
449469 if not res .input_required :
450470 return await res .context .elicit (elicit .message , elicit .schema )
451471
472+ # Answered in a prior round (restored without a known schema, e.g. an unannotated
473+ # resolver): re-validate the stored entry against the live `Elicit.schema`.
474+ if key in res .state and key not in res .answers :
475+ outcome = _outcome_from_state (res .state [key ], elicit .schema )
476+ res .elicited [key ] = outcome
477+ return outcome
478+
452479 answer = res .answers .get (key )
453480 if answer is None :
454481 res .pending [key ] = _elicit_request (elicit )
0 commit comments