Skip to content

Commit 1a15cb6

Browse files
committed
Scope the resolver run-once guarantee to questions, not resolver bodies
Each question is asked exactly once per call, but on the multi-round-trip form an eliciting resolver's body runs again to consume its answer, and a resolver that answered without asking may run again whenever the call resumes. The tutorial info box, its recap bullet, and the refund_desk caveats now say exactly that, so authors don't hang side effects on a runs-at-most-once reading that only holds for the synchronous form.
1 parent ef53c4d commit 1a15cb6

2 files changed

Lines changed: 11 additions & 7 deletions

File tree

docs/tutorial/dependencies.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,20 @@ That's the right default for a precondition: no answer, no order. When declining
122122
multi-round-trip `tools/call` - the server returns it, the client's `elicitation_callback`
123123
answers it, and the `Client` retries the call for you (**[Multi-round-trip requests](../advanced/multi-round-trip.md)**). On
124124
**2025-11-25** and earlier it is a synchronous elicitation request mid-call. Each question is
125-
asked exactly once per call; a resolver that answered *without* asking, like `check_stock`,
126-
may run again when the call resumes after a question. When it resumes, each answer is matched
127-
back to its question, so an eliciting resolver must derive its question deterministically from
128-
the tool's arguments and earlier answers - a per-call generated value (a `default_factory` id,
129-
a timestamp) is re-derived on each round and must not appear in a question the answer is meant
130-
to bind to.
125+
asked exactly once per call - a guarantee about the question, not the resolver. In the
126+
multi-round-trip form an eliciting resolver runs again to consume its answer, so code before
127+
its `return Elicit(...)` runs on the asking round and again on the answering one; a resolver
128+
that answered *without* asking, like `check_stock`, may run again whenever the call resumes
129+
after a question. When it resumes, each answer is matched back to its question, so an
130+
eliciting resolver must derive its question deterministically from the tool's arguments and
131+
earlier answers - a per-call generated value (a `default_factory` id, a timestamp) is
132+
re-derived on each round and must not appear in a question the answer is meant to bind to.
131133

132134
## Recap
133135

134136
* `Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value.
135137
* A resolved parameter is invisible to the model and cannot be supplied by a client. Values the model must not invent - prices, identities, permissions - belong here.
136-
* A resolver's parameters are resolved the same way: the `Context`, another `Resolve(...)`, or a tool argument by name. The graph runs each resolver at most once, however many consumers it has; a resolver that never asked may run again when a call resumes after a question.
138+
* A resolver's parameters are resolved the same way: the `Context`, another `Resolve(...)`, or a tool argument by name. The graph runs each resolver at most once per round, however many consumers it has; each question is asked exactly once, an eliciting resolver runs again to consume its answer, and a resolver that never asked may run again when a call resumes.
137139
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
138140
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
139141

examples/stories/refund_desk/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ uv run python -m stories.refund_desk.client --http
6262
within a round each resolver runs at most once, keyed by function identity.
6363
Across 2026 rounds only *elicited* outcomes persist (in `requestState`); a
6464
resolver that resolves without eliciting is pure and may re-run each round.
65+
An eliciting resolver's body runs again too — once to ask, once more to
66+
consume its answer.
6567
An answer is matched back to its question when the call resumes, so an
6668
eliciting resolver must derive its question deterministically from the
6769
tool's arguments and earlier answers; a per-call generated value (a

0 commit comments

Comments
 (0)