|
24 | 24 | from mcp.shared.message import ServerMessageMetadata |
25 | 25 | from rbt.mcp.v1.session_rbt import Session |
26 | 26 | from reboot.aio.applications import Application |
27 | | -from reboot.aio.contexts import WorkflowContext |
| 27 | +from reboot.aio.contexts import EffectValidation, WorkflowContext |
28 | 28 | from reboot.aio.external import ExternalContext |
29 | 29 | from reboot.aio.types import StateRef |
| 30 | +from reboot.aio.workflows import at_least_once |
30 | 31 | from reboot.mcp.event_store import DurableEventStore, replay |
31 | 32 | from reboot.mcp.servicers.session import ( |
32 | 33 | SessionServicer, |
33 | 34 | _servers, |
34 | 35 | _context, |
35 | 36 | ) |
36 | | -from reboot.aio.workflows import at_least_once |
37 | 37 | from reboot.mcp.servicers.stream import StreamServicer |
38 | 38 | from reboot.std.collections.v1 import sorted_map |
39 | 39 | from rebootdev.aio.headers import CONSENSUS_ID_HEADER, STATE_REF_HEADER |
40 | 40 | from rebootdev.memoize.v1.memoize_rbt import Memoize |
| 41 | +from rebootdev.settings import DOCS_BASE_URL |
41 | 42 | from starlette.applications import Starlette |
42 | 43 | from starlette.requests import Request |
43 | 44 | from starlette.responses import Response, StreamingResponse |
@@ -663,12 +664,12 @@ def _wrap_tool(fn: mcp.types.AnyFunction) -> mcp.types.AnyFunction: |
663 | 664 |
|
664 | 665 | wrapper_signature = signature.replace(parameters=wrapper_parameters) |
665 | 666 |
|
666 | | - async def wrapper(ctx: fastmcp.Context, *args, **kwargs): |
667 | | - |
668 | | - context: WorkflowContext | None = _context.get() |
669 | | - |
670 | | - assert context is not None |
671 | | - |
| 667 | + async def wrapper( |
| 668 | + ctx: fastmcp.Context, |
| 669 | + context: WorkflowContext, |
| 670 | + *args, |
| 671 | + **kwargs, |
| 672 | + ): |
672 | 673 | # To account for the lack of "intersection" types in |
673 | 674 | # Python (which is actively being worked on), we instead |
674 | 675 | # create a new dynamic `DurableContext` instance that |
@@ -938,19 +939,49 @@ async def send_request_and_wait_for_result(): |
938 | 939 |
|
939 | 940 | return fn(**dict(bound.arguments)) |
940 | 941 | except: |
941 | | - # TODO: print stack trace after we've fixed `memoize` |
942 | | - # effect validation bug. |
943 | | - # |
944 | | - # import traceback |
945 | | - # traceback.print_exc() |
| 942 | + import traceback |
| 943 | + traceback.print_exc() |
946 | 944 | raise |
947 | 945 |
|
948 | | - setattr(wrapper, "__signature__", wrapper_signature) |
949 | | - wrapper.__name__ = fn.__name__ |
950 | | - wrapper.__doc__ = fn.__doc__ |
| 946 | + async def wrapper_validating_effects( |
| 947 | + ctx: fastmcp.Context, |
| 948 | + *args, |
| 949 | + **kwargs, |
| 950 | + ): |
| 951 | + context: WorkflowContext | None = _context.get() |
| 952 | + |
| 953 | + assert context is not None |
| 954 | + |
| 955 | + # Checkpoint the context since it is the `IdempotencyManager`. |
| 956 | + checkpoint = context.checkpoint() |
| 957 | + |
| 958 | + result = await wrapper(ctx, context, *args, **kwargs) |
| 959 | + |
| 960 | + if context._effect_validation == EffectValidation.DISABLED: |
| 961 | + return result |
| 962 | + |
| 963 | + # Effect validation is enabled. |
| 964 | + logger.info( |
| 965 | + f"Re-running tool '{fn.__name__}' " |
| 966 | + f"to validate effects. See {DOCS_BASE_URL}/develop/side_effects " |
| 967 | + "for more information." |
| 968 | + ) |
| 969 | + |
| 970 | + # Restore the context to the checkpoint we took above so we |
| 971 | + # can re-execute `callable` as though it is being retried from |
| 972 | + # scratch. |
| 973 | + context.restore(checkpoint) |
| 974 | + |
| 975 | + # TODO: check if `result` is different (we don't do this for |
| 976 | + # other effect validation so we're also not doing it now). |
| 977 | + |
| 978 | + return await wrapper(ctx, context, *args, **kwargs) |
951 | 979 |
|
952 | | - return wrapper |
| 980 | + setattr(wrapper_validating_effects, "__signature__", wrapper_signature) |
| 981 | + wrapper_validating_effects.__name__ = fn.__name__ |
| 982 | + wrapper_validating_effects.__doc__ = fn.__doc__ |
953 | 983 |
|
| 984 | + return wrapper_validating_effects |
954 | 985 |
|
955 | 986 |
|
956 | 987 | class StreamableHTTPASGIApp: |
|
0 commit comments