| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .. | ||
| __init__.py | ||
| client.py | ||
| README.md | ||
| server.py | ||
refund-desk
Resolver dependency injection: a tool parameter annotated Annotated[T, Resolve(fn)] is filled by running the resolver fn before the tool body,
instead of from the LLM-supplied arguments. Here refund_order(order_id, reason) refunds what the order record says — cents is resolver-computed and
does not appear in the input schema at all, so the model cannot supply or
inflate the amount. Resolvers form a DAG (load_order → refund_scope →
refund_amount / ask_restock), may return Elicit[...] to ask the human,
and run at most once per call. A resolver's own plain parameters are filled
from the tool's arguments by name — load_order(order_id) receives the
order_id the model passed to refund_order.
Run it
# stdio (default — the client spawns the server as a subprocess)
uv run python -m stories.refund_desk.client
# HTTP — the client self-hosts the server on a free port, runs, then tears it
# down (--legacy: resolver elicitation rides the push request today; the
# manifest pins this era, so bare --http runs the same leg)
uv run python -m stories.refund_desk.client --http --legacy
What to look at
server.pyrefund_order— the signature is the whole story:order_idandreasonare model-facing;centsandrestockcarryResolve(...)markers and never reach the input schema.client.pyassertspropertiesandrequiredare exactly{order_id, reason}.server.pyrefund_scope— the no-round-trip fast path: a one-line order returnsScope(full=True)directly; only a multi-line order returnsElicit(...). The ORD-7001 call completes with zero elicitations.server.py_scoped— the elicited SKU is human-typed free text; it is validated against the order (ToolErroron a miss) before any amount is computed.- The decline contrast:
refund_amounttakesscopeunwrapped, so declining the scope question aborts the wholecentschain with an error containing the framework'sResolver for parameter 'scope' could not resolve: elicitation was decline(the client sees it behind the usualError executing tool refund_order:prefix);restockkeeps theElicitationResultunion, so declining restock still refunds — just withrestocked: false. client.py— the scope counter proves memoization from outside: one call consumesrefund_scopefrom two resolvers but the question fires once.
Caveats
- Decline order. A declined unwrapped dependency aborts resolution in
tool-signature order —
centsresolves beforerestock, soask_restocknever runs. Don't rely on a later resolver's side effects after an earlier consumer can abort. - Memoization scope. Each resolver runs at most once per
tools/call, keyed by function identity; nothing is cached across calls or connections. - Validate elicited values. Elicited answers are human-typed; check them
against your records (as
_scopeddoes) before acting on them.
Spec
See also
legacy_elicitation/ (the push mechanism resolver elicitation rides on today),
mrtr/ (the 2026 input_required carrier; resolver DI will ride it once the
SDK wires them together).