You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A synchronous prompt callback returning Awaitable is rendered as coroutine text #3709
Searched the issue tracker for an existing report.
Release line
2.x (current stable)
Description
A synchronous prompt callback annotated Awaitable[list[str]] returned a coroutine that resolves to an empty message list. Rendering produced one user message containing the quoted coroutine representation instead of awaiting the result. An equivalent async def callback correctly returns no messages.
PromptResult explicitly includes Awaitable[SyncPromptResult], and Prompt.from_function() accepts the callback. I expected the returned awaitable to be resolved before message conversion. The current path checks whether the callback is async, runs a synchronous callback in a worker thread, then serializes its coroutine result as fallback text. This loses the callback's intended content even though registration succeeds.
The test closes the coroutine in finally to avoid an unawaited-coroutine warning and checks an equivalent async callback. This concerns awaiting a result, separately from #3643's handling of synchronous Sequence results. Related typing discussion: #2202.
Python 3.14.7 / Pydantic 2.12.5. Fails on latest stable mcp/mcp-types 2.3.0 (2118f14) and current main 754254c. Each targeted run: 1 intentional failure, 2 passing existing controls. The same test runs against each version’s source.
AI-assisted investigation and test reproduction, with a human in the loop.
Thanks for the detailed reproduction. I reproduced this on current main (754254c08fe1c3eda18b69da3d336106fe8d72d6) using the locked project environment with CPython 3.12.14.
A synchronous prompt callback returning an awaitable that resolves to [] produced one user message containing the coroutine representation instead of no messages. An awaitable resolving to ["hi"] is rendered the same way. The synchronous callback runs in the worker thread, and its result then goes straight to the InputRequiredResult and message-conversion checks without the awaitable being resolved, even though PromptResult permits Awaitable[SyncPromptResult].
A bounded fix seems to be resolving an awaitable result once, after either callback path returns and before those checks. For tests, I’d cover awaitables resolving to [] and to ["hi"], and keep the existing synchronous worker-thread and async-callback tests.
@inanna-malick, if you’d like to fix this yourself, please go ahead. Otherwise, if maintainers agree with this behavior and test scope and want an outside PR, I’d be glad to take it if assigned.
The branch is on the wrong predicate. A synchronous callback can return an awaitable, so testing whether the callable is async cannot decide whether the result needs awaiting. The decision has to be made on the value that comes back.
The shape that covers all three cases in one branch is: call the callback, then check the result with inspect.isawaitable(result) and await it if true. Plain sync returns, sync returning an awaitable, and async callbacks then all converge, and the async-callback special case disappears rather than being handled twice.
Two consequences worth handling at the same time.
PromptResult advertises Awaitable[SyncPromptResult] and the runtime does not honor it. Either the runtime honors it, which the branch above does, or the type should be narrowed. Leaving the annotation wider than the runtime is how a caller ends up with a registration that succeeds and a payload that does not.
The fallback stringification. When the result is not awaited, the code path that serializes it as fallback text is what turns the coroutine into its own quoted representation, which is the user-visible symptom. That same path also leaves the coroutine unawaited, so in production it is a RuntimeWarning nobody reads rather than an error. Closing the coroutine when it is dropped, the way the test does, converts the silent case into a visible one.
A three-row table test is cheap and pins it: sync returning a value, sync returning an awaitable, and async. Assert the message list for each, and assert no warning is raised, since the warning is the only signal that the fallback ran.
Thanks for the clear report and for the independent reproduction and approach above. I have a narrowly scoped local implementation that resolves the callback’s returned value with inspect.isawaitable() immediately after either invocation path and before InputRequiredResult/message conversion, plus a regression test for a synchronous callback returning an awaitable list.
I used AI assistance while investigating and preparing the change, but I reviewed the proposed diff and local results and can explain and own it. Local validation is green for the focused prompt suite, type/lint checks, and the previously run full suite.
@inanna-malick should of course have first call as the reporter, and I don’t want to displace @leninworld’s earlier offer. If neither plans to take it and maintainers would welcome this outside contribution, I’d be glad to submit the reviewed patch if assigned (or if the issue is marked help wanted). No pressure either way.
Thanks, @AryanRG, for sharing your progress. I am still interested in implementing this fix, but I will wait for a maintainer to assign the issue or add the help wanted label before opening a pull request, in accordance with the contribution policy. I do not want to duplicate or displace anyone else’s work, so I will follow whichever contribution route the maintainers prefer.
Thanks @leninworld for independently reproducing this. I'm happy for you to take the fix forward if the maintainers want an outside PR; I'm not planning a competing patch.
Resolving the returned awaitable once before InputRequiredResult/message conversion matches the reported contract. The empty and nonempty results you suggested, together with the existing synchronous worker-thread and async-callback tests, sound like a focused scope. Assignment remains with the maintainers.
Activity
leninworld commentedon Oct 15, 2026
Thanks for the detailed reproduction. I reproduced this on current
main(754254c08fe1c3eda18b69da3d336106fe8d72d6) using the locked project environment with CPython 3.12.14.A synchronous prompt callback returning an awaitable that resolves to
[]produced one user message containing the coroutine representation instead of no messages. An awaitable resolving to["hi"]is rendered the same way. The synchronous callback runs in the worker thread, and its result then goes straight to theInputRequiredResultand message-conversion checks without the awaitable being resolved, even thoughPromptResultpermitsAwaitable[SyncPromptResult].A bounded fix seems to be resolving an awaitable result once, after either callback path returns and before those checks. For tests, I’d cover awaitables resolving to
[]and to["hi"], and keep the existing synchronous worker-thread and async-callback tests.@inanna-malick, if you’d like to fix this yourself, please go ahead. Otherwise, if maintainers agree with this behavior and test scope and want an outside PR, I’d be glad to take it if assigned.
Ben-Home commentedon Oct 16, 2026
The branch is on the wrong predicate. A synchronous callback can return an awaitable, so testing whether the callable is async cannot decide whether the result needs awaiting. The decision has to be made on the value that comes back.
The shape that covers all three cases in one branch is: call the callback, then check the result with
inspect.isawaitable(result)and await it if true. Plain sync returns, sync returning an awaitable, and async callbacks then all converge, and the async-callback special case disappears rather than being handled twice.Two consequences worth handling at the same time.
PromptResultadvertisesAwaitable[SyncPromptResult]and the runtime does not honor it. Either the runtime honors it, which the branch above does, or the type should be narrowed. Leaving the annotation wider than the runtime is how a caller ends up with a registration that succeeds and a payload that does not.The fallback stringification. When the result is not awaited, the code path that serializes it as fallback text is what turns the coroutine into its own quoted representation, which is the user-visible symptom. That same path also leaves the coroutine unawaited, so in production it is a RuntimeWarning nobody reads rather than an error. Closing the coroutine when it is dropped, the way the test does, converts the silent case into a visible one.
A three-row table test is cheap and pins it: sync returning a value, sync returning an awaitable, and async. Assert the message list for each, and assert no warning is raised, since the warning is the only signal that the fallback ran.
AryanRG commentedon Oct 16, 2026
Thanks for the clear report and for the independent reproduction and approach above. I have a narrowly scoped local implementation that resolves the callback’s returned value with
inspect.isawaitable()immediately after either invocation path and beforeInputRequiredResult/message conversion, plus a regression test for a synchronous callback returning an awaitable list.I used AI assistance while investigating and preparing the change, but I reviewed the proposed diff and local results and can explain and own it. Local validation is green for the focused prompt suite, type/lint checks, and the previously run full suite.
@inanna-malick should of course have first call as the reporter, and I don’t want to displace @leninworld’s earlier offer. If neither plans to take it and maintainers would welcome this outside contribution, I’d be glad to submit the reviewed patch if assigned (or if the issue is marked
help wanted). No pressure either way.leninworld commentedon Oct 16, 2026
Thanks, @AryanRG, for sharing your progress. I am still interested in implementing this fix, but I will wait for a maintainer to assign the issue or add the
help wantedlabel before opening a pull request, in accordance with the contribution policy. I do not want to duplicate or displace anyone else’s work, so I will follow whichever contribution route the maintainers prefer.inanna-malick commentedon Oct 16, 2026
Thanks @leninworld for independently reproducing this. I'm happy for you to take the fix forward if the maintainers want an outside PR; I'm not planning a competing patch.
Resolving the returned awaitable once before InputRequiredResult/message conversion matches the reported contract. The empty and nonempty results you suggested, together with the existing synchronous worker-thread and async-callback tests, sound like a focused scope. Assignment remains with the maintainers.
AI-assisted follow-up.