From 1c86c5bbd43518024bc550337663558bb61a9304 Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 15 May 2026 11:28:44 +0200 Subject: [PATCH 1/6] add langchain docs --- docs/ai-quickstart.mdx | 142 ++++++++++++++++++++- docs/ai/index.mdx | 1 + docs/ai/patterns/durable-agents.mdx | 76 ++++++++++- docs/ai/patterns/error-handling.mdx | 80 ++++++++++-- docs/ai/patterns/human-in-the-loop.mdx | 109 +++++++++++++++- docs/ai/patterns/multi-agent.mdx | 105 ++++++++++++++- docs/ai/patterns/parallelization.mdx | 53 +++++++- docs/ai/patterns/remote-agents.mdx | 71 ++++++++++- docs/ai/patterns/rollback.mdx | 2 +- docs/ai/patterns/sessions.mdx | 108 +++++++++++++++- docs/ai/patterns/tools.mdx | 59 +++++++++ docs/ai/patterns/workflow-evaluator.mdx | 84 +++++++++++- docs/ai/patterns/workflow-orchestrator.mdx | 88 ++++++++++++- docs/ai/patterns/workflow-parallel.mdx | 62 ++++++++- docs/ai/patterns/workflow-sequential.mdx | 114 +++++++++++++++-- docs/ai/sdk-integrations/langchain.mdx | 17 +++ docs/docs.json | 1 + docs/img/ai/sdk-integrations/langchain.svg | 7 + docs/snippets/tour/ai/setup-langchain.mdx | 14 ++ 19 files changed, 1138 insertions(+), 55 deletions(-) create mode 100644 docs/ai/sdk-integrations/langchain.mdx create mode 100644 docs/img/ai/sdk-integrations/langchain.svg create mode 100644 docs/snippets/tour/ai/setup-langchain.mdx diff --git a/docs/ai-quickstart.mdx b/docs/ai-quickstart.mdx index 85fffe47..7106983f 100644 --- a/docs/ai-quickstart.mdx +++ b/docs/ai-quickstart.mdx @@ -104,7 +104,7 @@ async function getWeather(ctx: restate.Context, city: string) { // AGENT const run = async (ctx: restate.Context, { prompt }: { prompt: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), // Persist LLM responses middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -589,7 +589,7 @@ class WeatherPrompt(BaseModel): # AGENT weather_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a helpful agent that provides weather updates.", ) @@ -649,6 +649,142 @@ Once you restart the service, the agent resumes at the weather tool call and suc **Next step:** Learn more about [Durable Agents](/ai/patterns/durable-agents) and how Restate makes your AI agents resilient to failures. + + + + + + +**Prerequisites**: +- Python >= v3.12 +- [uv](https://docs.astral.sh/uv/getting-started/installation/) +- OpenAI API key (get one at [OpenAI](https://platform.openai.com/)) + + + + + + + + +Get the weather agent template for [LangChain](https://www.langchain.com/) and Restate: +```shell +restate example python-langchain-template && cd python-langchain-template +``` + + + + +Export your OpenAI key and run the agent: +```shell +export OPENAI_API_KEY=your_openai_api_key_here +uv run . +``` + +The weather agent is now listening on port 9080. + + + +Tell Restate where the service is running (`http://localhost:9080`), so Restate can discover and register the services and handlers behind this endpoint. +You can do this via the UI (`http://localhost:9070`): + + + Restate UI Playground + + +If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`. + + +When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it. +If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature. + + + + +Invoke the agent via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground. + +Or invoke via `curl`: +```shell +curl localhost:8080/agent/run --json '{"message": "What is the weather in San Francisco?"}' +``` +Output: `The weather in San Francisco is sunny and 23°C.` + + + + + +The agent you just invoked uses Durable Execution to make agents resilient to failures. Restate persisted all LLM calls and tool execution steps, so if anything fails, the agent can resume exactly where it left off. + +We did this by attaching Restate's `RestateMiddleware` to the LangChain agent so every LLM call is journaled, and by using [Restate Context actions](/foundations/actions) (e.g. `restate_context().run_typed`) inside tools to make side effects durable: + +```python expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/template/agent.py"} +import restate +from langchain.agents import create_agent +from langchain_core.messages import AnyMessage +from langchain_core.tools import tool +from langchain.chat_models import init_chat_model +from pydantic import BaseModel +from restate.ext.langchain import RestateMiddleware, restate_context + + +class WeatherPrompt(BaseModel): + message: str = "What is the weather in San Francisco?" + + +# TOOL +@tool +async def get_weather(city: str) -> dict: + """Get the current weather for a given city.""" + + async def call_weather_api() -> dict: + return {"temperature": 23, "description": "Sunny and warm."} + + # Durable step: results are journaled, so on retry we replay the value + # rather than re-hitting the API. + return await restate_context().run_typed(f"Get weather {city}", call_weather_api) + + +# AGENT +weather_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + tools=[get_weather], + system_prompt="You are a helpful agent that provides weather updates.", + middleware=[RestateMiddleware()], +) + + +# AGENT SERVICE +agent_service = restate.Service("agent") + + +@agent_service.handler() +async def run(_ctx: restate.Context, req: WeatherPrompt) -> str: + result = await weather_agent.ainvoke( + {"messages": [{"role": "user", "content": req.message}]} + ) + return result["messages"][-1].content +``` + + +The Invocations tab of the Restate UI shows us how Restate captured each LLM call and tool step in a journal. + + +Let the weather tool raise an exception by adding the following line to the `get_weather` function: +```python +raise Exception("[👻 SIMULATED] Fetching weather failed: Weather API down...") +``` + +You can see in the Restate UI how each LLM call and tool step gets durably executed. +We can see how the weather tool is currently stuck, because the weather API is down. + +Fix the problem, by removing the exception again. + +Once you restart the service, the agent resumes at the weather tool call and successfully completes the request. + + +**Next step:** +Learn more about [Durable Agents](/ai/patterns/durable-agents) and how Restate makes your AI agents resilient to failures. + @@ -952,7 +1088,7 @@ async def run(ctx: restate.Context, message: WeatherPrompt) -> str | None: # Call the LLM async def call_llm() -> Message: resp = await acompletion( - model="gpt-4o-mini", messages=messages, tools=TOOLS + model="gpt-5.4", messages=messages, tools=TOOLS ) return resp.choices[0].message diff --git a/docs/ai/index.mdx b/docs/ai/index.mdx index 3285b394..c1693441 100644 --- a/docs/ai/index.mdx +++ b/docs/ai/index.mdx @@ -39,6 +39,7 @@ Whether you're building chatbots, autonomous agents, or AI-powered workflows, Re + diff --git a/docs/ai/patterns/durable-agents.mdx b/docs/ai/patterns/durable-agents.mdx index e70f08ff..6f9d2f82 100644 --- a/docs/ai/patterns/durable-agents.mdx +++ b/docs/ai/patterns/durable-agents.mdx @@ -25,6 +25,7 @@ Select your SDK: + @@ -65,7 +66,7 @@ async function getWeather(ctx: restate.Context, city: string) { // AGENT const run = async (ctx: restate.Context, { prompt }: { prompt: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), // Persist LLM responses middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -277,7 +278,7 @@ class WeatherPrompt(BaseModel): # AGENT weather_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a helpful agent that provides weather updates.", ) @@ -313,6 +314,70 @@ The main difference is the use of the **Restate Context** at key points: 2. **Persisting LLM responses**: Wrap your agent with `RestateAgent` so every LLM response is saved in the Restate Server and replayed during recovery. The `RestateAgent` is provided via the Pydantic AI extensions in the Restate SDK. 3. **Resilient tool execution**: Use `restate_context().run_typed()` inside tools to make steps durable. The result is persisted and retried until it succeeds. + + + +To implement a durable agent, you use the Restate SDK in combination with [LangChain](https://www.langchain.com/). + +Here's a weather agent that looks up the weather for a city: + +```python agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/template/agent.py"} +import restate +from langchain.agents import create_agent +from langchain_core.messages import AnyMessage +from langchain_core.tools import tool +from langchain.chat_models import init_chat_model +from pydantic import BaseModel +from restate.ext.langchain import RestateMiddleware, restate_context + + +class WeatherPrompt(BaseModel): + message: str = "What is the weather in San Francisco?" + + +# TOOL +@tool +async def get_weather(city: str) -> dict: + """Get the current weather for a given city.""" + + async def call_weather_api() -> dict: + return {"temperature": 23, "description": "Sunny and warm."} + + # Durable step: results are journaled, so on retry we replay the value + # rather than re-hitting the API. + return await restate_context().run_typed(f"Get weather {city}", call_weather_api) + + +# AGENT +weather_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + tools=[get_weather], + system_prompt="You are a helpful agent that provides weather updates.", + middleware=[RestateMiddleware()], +) + + +# AGENT SERVICE +agent_service = restate.Service("agent") + + +@agent_service.handler() +async def run(_ctx: restate.Context, req: WeatherPrompt) -> str: + result = await weather_agent.ainvoke( + {"messages": [{"role": "user", "content": req.message}]} + ) + return result["messages"][-1].content +``` + + + +You define your agent and tools as you normally would with LangChain. + +The main difference is the use of the **Restate Context** at key points: +1. **Restate service handler**: The agent runs inside a Restate service handler, giving it a durable execution context. Restate exposes the handler as an HTTP endpoint you can call via `curl`, the Restate UI, or any HTTP client. +2. **Persisting LLM responses**: Attach `RestateMiddleware()` to your agent so every LLM call is journaled and replayed during recovery. The middleware is provided via the LangChain extensions in the Restate SDK. +3. **Resilient tool execution**: Wrap durable side effects in tools with `restate_context().run_typed()`. The middleware does not auto-journal tool calls — you choose which steps are durable. + @@ -463,7 +528,7 @@ async def run(ctx: restate.Context, message: WeatherPrompt) -> str | None: # Call the LLM async def call_llm() -> Message: resp = await acompletion( - model="gpt-4o-mini", messages=messages, tools=TOOLS + model="gpt-5.4", messages=messages, tools=TOOLS ) return resp.choices[0].message @@ -521,6 +586,11 @@ The Restate UI (`http://localhost:9070`) shows the step-by-step execution trace Agent execution trace in Restate UI + + + Agent execution trace in Restate UI + + Agent execution trace in Restate UI diff --git a/docs/ai/patterns/error-handling.mdx b/docs/ai/patterns/error-handling.mdx index 98ef63f9..b1ac3ec8 100644 --- a/docs/ai/patterns/error-handling.mdx +++ b/docs/ai/patterns/error-handling.mdx @@ -20,6 +20,7 @@ Restate distinguishes between two types of errors: + @@ -39,7 +40,7 @@ You can limit Restate's retries with the `maxRetryAttempts` option in `durableCa ```typescript errorhandling/fail-on-terminal-tool-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/errorhandling/fail-on-terminal-tool-agent.ts#max_attempts_example"} const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); ``` @@ -55,17 +56,18 @@ Once Restate's retries are exhausted, the invocation fails with a `TerminalError Restate's `DurableRunner` lets you specify the retry behavior for LLM calls: ```python error_handling.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/error_handling.py#handle"} -try: - result = await DurableRunner.run( - agent, - req.message, - llm_retry_opts=LlmRetryOpts( +@agent_service.handler() +async def run(_ctx: restate.Context, req: WeatherPrompt) -> str: + try: + run_opts = RunOptions( max_attempts=3, initial_retry_interval=timedelta(seconds=2) - ), - ) -except restate.TerminalError as e: - # Handle terminal errors gracefully - return f"The agent couldn't complete the request: {e.message}" + ) + result = await DurableRunner.run(agent, req.message, run_options=run_opts) + except restate.TerminalError as e: + # Handle terminal errors gracefully + return f"The agent couldn't complete the request: {e.message}" + + return result.final_output ``` @@ -77,8 +79,11 @@ By default, the runner retries ten times with an initial interval of one second. Configure the number of retries for LLM calls when activating the Restate plugin for your ADK App: ```python error_handling.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/google-adk/tour-of-agents/app/error_handling.py#retries"} +run_options = RunOptions(max_attempts=3, initial_retry_interval=timedelta(seconds=1)) app = App( - name=APP_NAME, root_agent=agent, plugins=[RestatePlugin(max_model_call_retries=3)] + name=APP_NAME, + root_agent=agent, + plugins=[RestatePlugin(run_options=run_options)], ) ``` @@ -100,6 +105,22 @@ restate_agent = RestateAgent( By default, the runner retries ten times with an initial interval of one second. Once Restate's retries are exhausted, the invocation fails with a `TerminalError` and won't be retried further. + + + +Restate's `RestateMiddleware` lets you specify the retry behavior for LLM calls via `RunOptions`: + +```python error_handling.py +agent = create_agent( + model=init_chat_model("openai:gpt-4o-mini"), + tools=[get_weather], + middleware=[RestateMiddleware(run_options=RunOptions(max_attempts=3))], +) +``` + + +By default, the middleware retries indefinitely with exponential backoff. Once Restate's retries are exhausted, the invocation fails with a `TerminalError` and won't be retried further. + @@ -317,6 +338,41 @@ async def run(_ctx: restate.Context, req: WeatherPrompt) -> str: ``` + + + +When agent tools use Restate Context actions like `ctx.run`, Restate automatically retries transient errors in these operations. This makes your tools resilient to network failures, database hiccups, and other temporary issues. For all operations that might suffer from transient errors, use Context actions. + +For example, wrapping a tool call in `restate_context().run_typed()` makes it durable with automatic retries: + +```python error_handling.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/error_handling.py#here"} +@tool +async def get_weather(city: WeatherRequest) -> WeatherResponse: + """Get the current weather for a given city.""" + return await restate_context().run_typed( + "get weather", fetch_weather, RunOptions(max_attempts=3), req=city + ) +``` + + +For errors that should not be retried, raise a terminal error: + +```python +from restate import TerminalError + +raise TerminalError("This tool is not allowed to run for this input.") +``` + +Restate retries tool executions until they succeed. Terminal errors propagate past LangChain's tool-error handling back to the service handler, where you can catch them: + +```python error_handling.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/error_handling.py#handle"} +try: + result = await agent.ainvoke({"messages": req.message}) +except restate.TerminalError as e: + return f"The agent couldn't complete the request: {e.message}" +``` + + diff --git a/docs/ai/patterns/human-in-the-loop.mdx b/docs/ai/patterns/human-in-the-loop.mdx index b9685a78..d62b0efd 100644 --- a/docs/ai/patterns/human-in-the-loop.mdx +++ b/docs/ai/patterns/human-in-the-loop.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -21,6 +22,7 @@ Some agent actions need human approvals for dangerous actions: deploying code, s + @@ -43,7 +45,7 @@ Restate provides **durable promises** (awakeables) that survive crashes and rest ```typescript human-approval-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/human-approval-agent.ts#here"} const run = async (ctx: restate.Context, { prompt }: ClaimPrompt) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -283,6 +285,63 @@ See in the UI how the workflow resumes and finishes after the approval. + + + +```python human_approval_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/human_approval_agent.py#here"} +@tool +async def human_approval(claim: InsuranceClaim) -> str: + """Ask for human approval for high-value claims.""" + + # Create an awakeable that a human can resolve via the Restate API. + approval_id, approval_promise = restate_context().awakeable(type_hint=str) + + # Notify the reviewer (durable step). + await restate_context().run_typed( + "Request review", request_human_review, claim=claim, awakeable_id=approval_id + ) + + # Suspend until resolved. + return await approval_promise +``` + + + + +```bash +uv run app/human_approval_agent.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Use `curl` with `/send` to start the claim asynchronously, without waiting for the result: +```bash +curl localhost:8080/HumanClaimApprovalAgent/run/send \ +--json '{"message": "Process my hospital bill of 3000USD for a broken leg."}' +``` + +You can restart the service to see how Restate continues waiting for the approval. + +If you wait for more than a minute, the invocation will get suspended. + +Invocation overview + + +Simulate approving the claim by executing the **curl request that was printed in the service logs**, similar to: + +```bash +curl localhost:8080/restate/awakeables/sign_1M28aqY6ZfuwBmRnmyP/resolve --json 'true' +``` + +See in the UI how the workflow resumes and finishes after the approval. + +Invocation overview + + + @@ -642,6 +701,54 @@ Restart the service and check in the UI how the process will block for the remai You can also lower the timeout to a few seconds to see how the timeout path is taken. + + + +Use `restate.select` to race the approval promise against a sleep timer: + +```python human_approval_agent_with_timeout.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/human_approval_agent_with_timeout.py#here"} +@tool +async def human_approval(claim: InsuranceClaim) -> str: + """Ask for human approval for high-value claims.""" + approval_id, approval_promise = restate_context().awakeable(type_hint=bool) + + await restate_context().run_typed( + "Request review", request_human_review, claim=claim, awakeable_id=approval_id + ) + + # Wait at most 3 hours for a human reply. + match await restate.select( + approval=approval_promise, + timeout=restate_context().sleep(timedelta(hours=3)), + ): + case ["approval", approved]: + return "Approved" if approved else "Rejected" + case _: + return "Approval timed out - Evaluate with AI" +``` + + + +Start the timeout agent (stop any previously running agent first): +```bash +uv run app/human_approval_agent_with_timeout.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Send a request to the service: +```bash +curl localhost:8080/HumanClaimApprovalWithTimeoutsAgent/run/send \ +--json '{"message": "Process my hospital bill of 3000USD for a broken leg."}' +``` +Restart the service and check in the UI how the process will block for the remaining time without starting over. + +You can also lower the timeout to a few seconds to see how the timeout path is taken. + + diff --git a/docs/ai/patterns/multi-agent.mdx b/docs/ai/patterns/multi-agent.mdx index 108a3250..33a0cbda 100644 --- a/docs/ai/patterns/multi-agent.mdx +++ b/docs/ai/patterns/multi-agent.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -21,6 +22,7 @@ Many agent systems need a **router** that decides which specialist agent should + @@ -68,7 +70,7 @@ async function runFraudAgent(model: LanguageModel, claim: InsuranceClaim){ const run = async (ctx: restate.Context, claim: ClaimInput) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -300,19 +302,19 @@ With Pydantic AI, specialist agents are wrapped in `RestateAgent` and exposed as ```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/multi_agent.py#here"} medical_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="Review medical claims for coverage and necessity. Approve/deny up to $50,000.", ) restate_medical_agent = RestateAgent(medical_agent) car_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="Assess car claims for liability and damage. Approve/deny up to $25,000.", ) restate_car_agent = RestateAgent(car_agent) intake_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="Route insurance claims to the appropriate specialist using the available tools.", ) @@ -373,6 +375,101 @@ In the UI, you can see that the agent called the sub-agents and is waiting for t Once all sub-agents return, the main agent continues and makes a decision. + + + +LangChain's `create_agent` doesn't ship a first-class handoff primitive, but the pattern is expressed cleanly by exposing each specialist as a tool on the intake agent. The intake agent picks which specialist to invoke; each specialist call is a normal LangChain agent run, fully durable through Restate's middleware. + +Conversation history (and which specialist most recently handled a claim) is stored in a Virtual Object so subsequent calls with the same key remember prior context. + +```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/multi_agent.py#here"} +medical_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt=( + "You are a medical insurance specialist. Review medical claims for " + "coverage and necessity. Approve/deny up to $50,000." + ), + middleware=[RestateMiddleware()], +) + +car_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt=( + "You are a car insurance specialist. Assess car claims for liability " + "and damage. Approve/deny up to $25,000." + ), + middleware=[RestateMiddleware()], +) + + +@tool +async def to_medical_specialist(claim_json: str) -> str: + """Hand the claim to the medical specialist for evaluation.""" + result = await medical_agent.ainvoke({"messages": claim_json}) + return result["messages"][-1].content + + +@tool +async def to_car_specialist(claim_json: str) -> str: + """Hand the claim to the car specialist for evaluation.""" + result = await car_agent.ainvoke({"messages": claim_json}) + return result["messages"][-1].content + + +intake_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + tools=[to_medical_specialist, to_car_specialist], + system_prompt=( + "You are an intake agent. Route insurance claims to the appropriate " + "specialist. Always call exactly one specialist tool, then summarize " + "their decision." + ), + middleware=[RestateMiddleware()], +) + + +agent_service = restate.VirtualObject("MultiAgentClaimApproval") + + +@agent_service.handler() +async def run(ctx: restate.ObjectContext, claim: InsuranceClaim) -> str: + history = await ctx.get("messages", type_hint=ChatHistory) or ChatHistory() + history.messages.append(HumanMessage(content=f"Claim: {claim.model_dump_json()}")) + + result = await intake_agent.ainvoke({"messages": history.messages}) + + ctx.set("messages", ChatHistory(messages=result["messages"])) + return result["messages"][-1].content +``` + + + + +```bash +uv run app/multi_agent.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Start a request for a claim that needs to be analyzed by multiple agents: +```bash +curl localhost:8080/MultiAgentClaimApproval/session123/run --json '{ + "date":"2024-10-01", + "category":"orthopedic", + "reason":"hospital bill for a broken leg", + "amount":3000, + "placeOfService":"General Hospital" +}' +``` + +In the UI, you can see that the intake agent called a specialist tool and is waiting for the response. + +Once the specialist returns, the intake agent continues and summarizes the decision. + + diff --git a/docs/ai/patterns/parallelization.mdx b/docs/ai/patterns/parallelization.mdx index be29bf90..12488fbf 100644 --- a/docs/ai/patterns/parallelization.mdx +++ b/docs/ai/patterns/parallelization.mdx @@ -12,6 +12,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -36,6 +37,7 @@ Instead, you use Restate's durable execution primitives (`RestatePromise.all()` + @@ -49,7 +51,7 @@ Restate makes sure that all parallel tasks are retried and recovered until they ```typescript parallel-tools-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/parallel-tools-agent.ts#here"} const run = async (ctx: restate.Context, claim: ClaimInput) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -272,6 +274,55 @@ In the UI, you can see the tool steps running in parallel: + + +**⚠️ The Restate middleware serializes parallel tool calls within the same LLM turn via a turnstile, so tool bodies replay deterministically. Inside a single tool body you can fan out freely with `restate.gather()`.** + +To use parallel tool calls with LangChain, create a tool that runs multiple analyses in parallel. The LLM calls one tool, and that tool fans out work internally using `restate.gather()` to run durable execution steps concurrently. + +Restate makes sure that all parallel tasks are retried and recovered until they succeed. If one step fails, only that step is retried while the successful results are preserved. + +```python parallel_tools_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/parallel_tools_agent.py#here"} +@tool +async def calculate_metrics(claim: InsuranceClaim) -> list[str]: + """Calculate claim metrics: eligibility, cost, and fraud risk.""" + ctx = restate_context() + + # Run the sub-steps in parallel with durable execution. + eligibility, cost, fraud = await restate.gather( + ctx.run_typed("eligibility", check_eligibility, claim=claim), + ctx.run_typed("cost", compare_to_standard_rates, claim=claim), + ctx.run_typed("fraud", check_fraud, claim=claim), + ) + return [await eligibility, await cost, await fraud] +``` + + + + +```bash +uv run app/parallel_tools_agent.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Start a request: +```bash +curl localhost:8080/ParallelToolClaimAgent/run --json '{ + "date":"2024-10-01", + "category":"orthopedic", + "reason":"hospital bill for a broken leg", + "amount":3000, + "placeOfService":"General Hospital" +}' +``` + +In the UI, you can see the tool steps running in parallel. + + diff --git a/docs/ai/patterns/remote-agents.mdx b/docs/ai/patterns/remote-agents.mdx index 135bb8cc..932f9bbd 100644 --- a/docs/ai/patterns/remote-agents.mdx +++ b/docs/ai/patterns/remote-agents.mdx @@ -10,6 +10,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -20,6 +21,7 @@ When agents need to scale independently, run on different infrastructure, or be + @@ -50,7 +52,7 @@ With the Vercel AI, specialist agents are exposed as tools. The LLM decides whic ```typescript remote-agents.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/remote-agents.ts#here"} const run = async (ctx: restate.Context, claim: ClaimInput) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -92,7 +94,7 @@ export const eligibilityAgent = restate.service({ handlers: { run: async (ctx: restate.Context, claim: InsuranceClaim) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); const { text } = await generateText({ @@ -353,7 +355,7 @@ Each specialist agent runs as its own Restate service: ```python eligibility_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/utils/utils.py#eligibility"} eligibility_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="Decide whether the following claim is eligible for reimbursement." "Respond with eligible if it's a medical claim, and not eligible otherwise.", ) @@ -402,6 +404,69 @@ Once all sub-agents return, the main agent continues and makes a decision. + + + +With LangChain, you expose each specialist as a separate Restate service and call it via `restate_context().service_call()`. Restate's middleware journals the LLM response that picks the specialist, and the inter-service hop is durable: caller and callee are journaled independently. + +```python remote_agents.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/remote_agents.py#here"} +# Durable service call to the fraud agent; persisted and retried by Restate. +@tool +async def check_fraud(claim: InsuranceClaim) -> str: + """Analyze the probability of fraud.""" + return await restate_context().service_call(run_fraud_agent, claim) + + +agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + tools=[check_eligibility, check_fraud], + system_prompt=( + "You are a claim approval engine. Analyze the claim and use your " + "tools to decide whether to approve it." + ), + middleware=[RestateMiddleware()], +) + + +agent_service = restate.Service("MultiAgentClaimApproval") + + +@agent_service.handler() +async def run(_ctx: restate.Context, claim: InsuranceClaim) -> str: + result = await agent.ainvoke({"messages": f"Claim: {claim.model_dump_json()}"}) + return result["messages"][-1].content +``` + + +Each specialist agent runs as its own Restate service. The eligibility and fraud agents are defined as standalone LangChain agents in their own Restate services, called via `restate_context().service_call()`. + + + +```bash +uv run app/remote_agents.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Start a request for a claim that needs to be analyzed by multiple agents: +```bash +curl localhost:8080/MultiAgentClaimApproval/run --json '{ + "date":"2024-10-01", + "category":"orthopedic", + "reason":"hospital bill for a broken leg", + "amount":3000, + "placeOfService":"General Hospital" +}' +``` + +In the UI, you can see that the agent called the sub-agents and is waiting for their responses. + +Once all sub-agents return, the main agent continues and makes a decision. + + diff --git a/docs/ai/patterns/rollback.mdx b/docs/ai/patterns/rollback.mdx index 00e60cae..f79ae5c2 100644 --- a/docs/ai/patterns/rollback.mdx +++ b/docs/ai/patterns/rollback.mdx @@ -44,7 +44,7 @@ const book = async (ctx: Context, { id, prompt }: BookingRequest) => { const undo_list: { (): restate.RestatePromise }[] = []; const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); diff --git a/docs/ai/patterns/sessions.mdx b/docs/ai/patterns/sessions.mdx index 38f14ab4..93c1fb62 100644 --- a/docs/ai/patterns/sessions.mdx +++ b/docs/ai/patterns/sessions.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -21,6 +22,7 @@ Restate **Virtual Objects** give your agents persistent, isolated sessions. Each + @@ -51,7 +53,7 @@ const chatAgent = restate.object({ { input: schema(ChatMessageSchema) }, async (ctx: restate.ObjectContext, { message }: { message: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -247,7 +249,7 @@ To turn your agent into a stateful session, you need two changes compared to a r ```python chat_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/chat_agent.py#here"} agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a helpful assistant.", ) restate_agent = RestateAgent(agent) @@ -305,6 +307,72 @@ curl localhost:8080/Chat/session456/message \ ``` + + + +To turn your agent into a stateful session, you need two changes compared to a regular [durable agent](/ai/patterns/durable-agents): +1. **Define a Virtual Object**: use `restate.VirtualObject()` instead of `restate.Service()`. This gives each session key its own isolated state. +2. **Manage conversation history** using the object's K/V store: use `ctx.get()` and `ctx.set()` to read and write the message history. + +```python chat_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/chat_agent.py#here"} +chat = restate.VirtualObject("Chat") + +agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="You are a helpful assistant.", + middleware=[RestateMiddleware()], +) + + +@chat.handler() +async def message(ctx: restate.ObjectContext, req: ChatMessage) -> str: + history = await ctx.get("messages", type_hint=ChatHistory) or ChatHistory() + history.messages.append(HumanMessage(content=req.message)) + + result = await agent.ainvoke({"messages": history.messages}) + + ctx.set("messages", ChatHistory(messages=result["messages"])) + return result["messages"][-1].content + + +@chat.handler(kind="shared") +async def get_history(ctx: restate.ObjectSharedContext) -> ChatHistory: + return await ctx.get("messages", type_hint=ChatHistory) or ChatHistory() +``` + + + + +```bash +uv run app/chat_agent.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Ask the agent to do some task. Specify the Virtual Object ID in the URL, for example for `session123`: +```bash +curl localhost:8080/Chat/session123/message \ +--json '{"message": "Make a poem about durable execution."}' +``` + +Continue the conversation with the same session ID. The agent remembers previous context: +```bash +curl localhost:8080/Chat/session123/message \ +--json '{"message": "Shorten it to 2 lines."}' +``` + +Go to the state tab of the UI to view the conversation history. + +Send a message to a different session. It starts a completely separate conversation: +```bash +curl localhost:8080/Chat/session456/message \ +--json '{"message": "What are the benefits of durable execution?"}' +``` + + @@ -481,6 +549,11 @@ The state tab of the Restate UI lets you query the state of each session: Conversation State Management + + +Conversation State Management + + Conversation State Management @@ -585,6 +658,26 @@ curl localhost:8080/Chat/session101/message/send --json '{"message": "explain ev curl localhost:8080/Chat/session202/message/send --json '{"message": "what is the difference between async and sync processing?"}' ``` +The UI shows how Restate queues the requests per session to ensure consistency: + +Concurrency control in action + + + + + +Send several messages concurrently to different chat sessions: +```bash +curl localhost:8080/Chat/session123/message/send --json '{"message": "make a poem about durable execution"}' & +curl localhost:8080/Chat/session456/message/send --json '{"message": "what are the benefits of durable execution?"}' & +curl localhost:8080/Chat/session789/message/send --json '{"message": "how does workflow orchestration work?"}' & +curl localhost:8080/Chat/session123/message/send --json '{"message": "can you make it rhyme better?"}' & +curl localhost:8080/Chat/session456/message/send --json '{"message": "what about fault tolerance in distributed systems?"}' & +curl localhost:8080/Chat/session789/message/send --json '{"message": "give me a practical example"}' & +curl localhost:8080/Chat/session101/message/send --json '{"message": "explain event sourcing in simple terms"}' & +curl localhost:8080/Chat/session202/message/send --json '{"message": "what is the difference between async and sync processing?"}' +``` + The UI shows how Restate queues the requests per session to ensure consistency: Concurrency control in action @@ -680,6 +773,17 @@ Call the handler to get the conversation history: curl localhost:8080/Chat/session123/get_history ``` + + + +To retrieve state, view the UI's state tab or add a handler that reads it. Have a look at the `get_history` handler in the example above. + +Call the handler to get the conversation history: + +```bash +curl localhost:8080/Chat/session123/get_history +``` + diff --git a/docs/ai/patterns/tools.mdx b/docs/ai/patterns/tools.mdx index 758ecc4a..e799f35f 100644 --- a/docs/ai/patterns/tools.mdx +++ b/docs/ai/patterns/tools.mdx @@ -12,6 +12,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; Agent tools come in two flavors: **local tools** that run inside the agent handler, and **workflows exposed as tools** that run as separate services. This page focuses on deploying workflows as agent tools. @@ -22,6 +23,7 @@ For local tools (wrapping tool logic in `ctx.run()` within the agent handler), s + ## Why expose workflows as tools? @@ -133,6 +135,27 @@ async def review(ctx: restate.Context, claim: InsuranceClaim) -> str: ``` + + + +```python sub_workflow_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/sub_workflow_agent.py#wf"} +# Sub-workflow service for human approval. +human_approval_workflow = restate.Service("HumanApprovalWorkflow") + + +@human_approval_workflow.handler() +async def review(ctx: restate.Context, claim: InsuranceClaim) -> str: + """Request human approval for a claim and wait for response.""" + approval_id, approval_promise = ctx.awakeable(type_hint=str) + + await ctx.run_typed( + "Request review", request_human_review, claim=claim, awakeable_id=approval_id + ) + + return await approval_promise +``` + + @@ -291,6 +314,42 @@ curl localhost:8080/restate/awakeables/sign_1M28aqY6ZfuwBmRnmyP/resolve --json ' ``` + + + +```python sub_workflow_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/sub_workflow_agent.py#here"} +@tool +async def human_approval(claim: InsuranceClaim) -> str: + """Ask for human approval for high-value claims.""" + return await restate_context().service_call(review, claim) +``` + + + + +```bash +uv run app/sub_workflow_agent.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Start a request that triggers the human approval sub-workflow: +```bash +curl localhost:8080/SubWorkflowClaimAgent/run/send \ + --json '{"message": "Process my hospital bill of 3000USD for a broken leg."}' +``` + +In the UI, you can see the agent calling the sub-workflow and suspending while waiting for approval. + +Resolve the approval by executing the **curl request printed in the service logs**, similar to: +```bash +curl localhost:8080/restate/awakeables/sign_1M28aqY6ZfuwBmRnmyP/resolve --json 'true' +``` + + diff --git a/docs/ai/patterns/workflow-evaluator.mdx b/docs/ai/patterns/workflow-evaluator.mdx index 4a263a6e..ac2afe41 100644 --- a/docs/ai/patterns/workflow-evaluator.mdx +++ b/docs/ai/patterns/workflow-evaluator.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -32,6 +33,7 @@ Select your SDK: + @@ -44,7 +46,7 @@ A generator agent writes code, then an evaluator agent checks it. If the evaluat ```typescript workflow-evaluator-optimizer.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/workflow-evaluator-optimizer.ts#here"} const generate = async (ctx: restate.Context, {task}: { task: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -229,7 +231,10 @@ async def generate(ctx: restate.ObjectContext, req: CodeRequest) -> dict: events = eval_runner.run_async( user_id=ctx.key(), session_id=str(ctx.uuid()), - new_message=Content(role="user", parts=[Part.from_text(text=f"Task: {req.task}\n\nCode:\n{code}")]), + new_message=Content( + role="user", + parts=[Part.from_text(text=f"Task: {req.task}\n\nCode:\n{code}")], + ), ) evaluation = await parse_agent_response(events) if evaluation.startswith("PASS"): @@ -263,13 +268,13 @@ curl localhost:8080/CodeGenerator/user123/generate \ ```python workflow_evaluator_optimizer.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/workflow_evaluator_optimizer.py#here"} generator = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a code generator. Write clean, correct code.", ) restate_generator = RestateAgent(generator) evaluator = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="""You are a code reviewer. Evaluate the code for correctness, readability, and edge cases. Respond with PASS if acceptable, or FAIL: with specific issues to fix.""", @@ -325,6 +330,77 @@ curl localhost:8080/CodeGenerator/generate \ ``` + + + +```python workflow_evaluator_optimizer.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/workflow_evaluator_optimizer.py#here"} +generator = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="You are a code generator. Write clean, correct code.", + middleware=[RestateMiddleware()], +) + +evaluator = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt=( + "You are a code reviewer. Evaluate the code for correctness, " + "readability, and edge cases. Respond with PASS if acceptable, or " + "FAIL: with specific issues to fix." + ), + middleware=[RestateMiddleware()], +) + +code_service = restate.Service("CodeGenerator") + + +@code_service.handler() +async def generate(_ctx: restate.Context, req: CodeRequest) -> dict: + feedback = "" + max_iterations = 3 + + for i in range(max_iterations): + # Step 1: Generate code + prompt = ( + f"Task: {req.task}\n\nPrevious attempt was rejected:\n{feedback}\n\nPlease fix the issues." + if feedback + else f"Task: {req.task}" + ) + gen_result = await generator.ainvoke({"messages": prompt}) + code = gen_result["messages"][-1].content + + # Step 2: Evaluate the code + eval_result = await evaluator.ainvoke( + {"messages": f"Task: {req.task}\n\nCode:\n{code}"} + ) + evaluation = eval_result["messages"][-1].content + + if evaluation.startswith("PASS"): + return {"code": code, "iterations": i + 1} + + feedback = evaluation + + return {"code": "Max iterations reached", "iterations": max_iterations} +``` + + + + +```bash +uv run app/workflow_evaluator_optimizer.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Send a request: +```bash +curl localhost:8080/CodeGenerator/generate \ + --json '{"task": "Write a function that checks if a string is a palindrome"}' +``` + + diff --git a/docs/ai/patterns/workflow-orchestrator.mdx b/docs/ai/patterns/workflow-orchestrator.mdx index 678dbb9f..058a2b3c 100644 --- a/docs/ai/patterns/workflow-orchestrator.mdx +++ b/docs/ai/patterns/workflow-orchestrator.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -34,6 +35,7 @@ Select your SDK: + @@ -49,7 +51,7 @@ export const researchWorker = restate.service({ handlers: { research: async (ctx: restate.Context, {question}: { question: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); const { text: answer } = await generateText({ @@ -70,7 +72,7 @@ const orchestrator = restate.service({ { input: schema(ResearchRequestSchema) }, async (ctx: restate.Context, {topic}: { topic: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -134,12 +136,12 @@ curl localhost:8080/ResearchReport/generate \ planner = Agent( name="ResearchPlanner", instructions="You are a research planner. Break the topic into 2-4 research sub-tasks.", - output_type=TaskList + output_type=TaskList, ) researcher = Agent( name="Researcher", - instructions="You are a research assistant. Provide a concise, factual answer." + instructions="You are a research assistant. Provide a concise, factual answer.", ) writer = Agent( @@ -282,20 +284,20 @@ curl localhost:8080/ResearchReport/user123/generate \ ```python workflow_orchestrator.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/workflow_orchestrator.py#here"} planner = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a research planner. Break the topic into 2-4 research sub-tasks.", output_type=TaskList, ) restate_planner = RestateAgent(planner) researcher = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a research assistant. Provide a concise, factual answer.", ) restate_researcher = RestateAgent(researcher) writer = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a report writer. Combine the research findings into a cohesive report.", ) restate_writer = RestateAgent(writer) @@ -354,6 +356,78 @@ curl localhost:8080/ResearchReport/generate \ ``` + + + +```python workflow_orchestrator.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/workflow_orchestrator.py#here"} +planner = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="You are a research planner. Break the topic into 2-4 research sub-tasks.", + response_format=TaskList, + middleware=[RestateMiddleware()], +) + +researcher = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="You are a research assistant. Provide a concise, factual answer.", + middleware=[RestateMiddleware()], +) + +writer = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="You are a report writer. Combine the research findings into a cohesive report.", + middleware=[RestateMiddleware()], +) + +report_service = restate.Service("ResearchReport") + + +@report_service.handler() +async def generate(ctx: restate.Context, req: ReportRequest) -> dict: + # Step 1: Orchestrator creates a research plan. + plan_result = await planner.ainvoke({"messages": req.topic}) + tasks: list[ResearchTask] = plan_result["structured_response"].tasks + + # Step 2: Dispatch researchers in parallel. + worker_promises = [ctx.service_call(run_researcher, task) for task in tasks] + await restate.gather(*worker_promises) + findings = [await p for p in worker_promises] + + # Step 3: Combine into a report. + message = f"Topic: {req.topic}\n\nResearch findings:\n{json.dumps(findings)}" + report_result = await writer.ainvoke({"messages": message}) + + return {"report": report_result["messages"][-1].content, "task_count": len(tasks)} + + +researcher_service = restate.Service("Researcher") + + +@researcher_service.handler() +async def run_researcher(_ctx: restate.Context, task: ResearchTask) -> str: + result = await researcher.ainvoke({"messages": task.question}) + return result["messages"][-1].content +``` + + + + +```bash +uv run app/workflow_orchestrator.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Send a request: +```bash +curl localhost:8080/ResearchReport/generate \ + --json '{"topic": "The impact of renewable energy on global economies"}' +``` + + diff --git a/docs/ai/patterns/workflow-parallel.mdx b/docs/ai/patterns/workflow-parallel.mdx index 0a99537e..d52dfdc1 100644 --- a/docs/ai/patterns/workflow-parallel.mdx +++ b/docs/ai/patterns/workflow-parallel.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -35,6 +36,7 @@ Select your SDK: + @@ -54,7 +56,7 @@ const run = async (ctx: restate.Context, claim: ClaimInput) => { ]); const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -275,6 +277,64 @@ Once all sub-agents return, the main agent makes a decision. + + + +```python workflow_parallel.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/workflow_parallel.py#here"} +decision = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="You are a claim decision engine.", + middleware=[RestateMiddleware()], +) + + +agent_service = restate.Service("ParallelAgentClaimApproval") + + +@agent_service.handler() +async def run(ctx: restate.Context, claim: InsuranceClaim) -> str: + # Start multiple sub-agents in parallel with auto-retries and recovery. + eligibility = ctx.service_call(run_eligibility_agent, claim) + cost = ctx.service_call(run_rate_comparison_agent, claim) + fraud = ctx.service_call(run_fraud_agent, claim) + + await restate.gather(eligibility, cost, fraud) + + result = await decision.ainvoke( + {"messages": f"""Decide about claim: {claim.model_dump_json()}. + Base your decision on the following analyses: + Eligibility: {await eligibility} Cost {await cost} Fraud: {await fraud}"""} + ) + return result["messages"][-1].content +``` + + + + +```bash +uv run app/workflow_parallel.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Start a request for a claim that needs to be analyzed by multiple agents in parallel: +```bash +curl localhost:8080/ParallelAgentClaimApproval/run --json '{ + "date":"2024-10-01", + "category":"orthopedic", + "reason":"hospital bill for a broken leg", + "amount":3000, + "placeOfService":"General Hospital" +}' +``` + +In the UI, you can see that the handler called the sub-agents in parallel. +Once all sub-agents return, the main agent makes a decision. + + diff --git a/docs/ai/patterns/workflow-sequential.mdx b/docs/ai/patterns/workflow-sequential.mdx index 253ed23d..521411b6 100644 --- a/docs/ai/patterns/workflow-sequential.mdx +++ b/docs/ai/patterns/workflow-sequential.mdx @@ -11,6 +11,7 @@ import SetupVercel from '/snippets/tour/ai/setup-vercel.mdx'; import SetupOpenAI from '/snippets/tour/ai/setup-openai.mdx'; import SetupGoogleADK from '/snippets/tour/ai/setup-google-adk.mdx'; import SetupPydanticAI from '/snippets/tour/ai/setup-pydantic-ai.mdx'; +import SetupLangChain from '/snippets/tour/ai/setup-langchain.mdx'; import SetupRestateTS from '/snippets/common/setup-restate-ts.mdx'; import SetupRestatePy from '/snippets/common/setup-restate-py.mdx'; @@ -40,6 +41,7 @@ Select your SDK: + @@ -52,7 +54,7 @@ This workflow processes an insurance claim through four steps: two agentic steps ```typescript workflow-sequential.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/workflow-sequential.ts#here"} const process = async (ctx: Context, {prompt}: {prompt: string}) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -125,7 +127,7 @@ async def process(ctx: restate.Context, req: ClaimPrompt) -> dict: parse_agent = Agent( name="DocumentParser", instructions="Extract the claim amount, currency, category, and description.", - output_type=ClaimData + output_type=ClaimData, ) parsed = await DurableRunner.run(parse_agent, req.message) claim = parsed.final_output @@ -135,7 +137,9 @@ async def process(ctx: restate.Context, req: ClaimPrompt) -> dict: name="ClaimsAnalyst", instructions="Assess whether this claim is valid and determine the approved amount.", ) - analysis = await DurableRunner.run(analysis_agent, f"Claim: {parsed.final_output.model_dump_json()}") + analysis = await DurableRunner.run( + analysis_agent, f"Claim: {parsed.final_output.model_dump_json()}" + ) # Step 3: Convert currency (regular step) amount_usd = await ctx.run_typed( @@ -189,7 +193,7 @@ parse_agent = Agent( model="gemini-2.5-flash", name="document_parser", instruction="Extract the claim amount, currency, category, and description.", - output_schema=ClaimData + output_schema=ClaimData, ) parse_app = App(name="claims", root_agent=parse_agent, plugins=[RestatePlugin()]) parse_runner = Runner(app=parse_app, session_service=RestateSessionService()) @@ -226,17 +230,26 @@ async def process(ctx: restate.ObjectContext, req: ClaimPrompt) -> dict: # Step 3: Convert currency (regular step) amount_usd = await ctx.run_typed( - "Convert currency", convert_currency, - amount=claim.amount, source=claim.currency, target="USD", + "Convert currency", + convert_currency, + amount=claim.amount, + source=claim.currency, + target="USD", ) # Step 4: Process reimbursement (regular step) confirmation = await ctx.run_typed( - "Process payment", process_payment, - claim_id=str(ctx.uuid()), amount=amount_usd, + "Process payment", + process_payment, + claim_id=str(ctx.uuid()), + amount=amount_usd, ) - return {"analysis": analysis, "amount_usd": amount_usd, "confirmation": confirmation} + return { + "analysis": analysis, + "amount_usd": amount_usd, + "confirmation": confirmation, + } ``` @@ -266,14 +279,14 @@ curl localhost:8080/ClaimReimbursement/user123/process \ ```python workflow_sequential.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/pydantic-ai/tour-of-agents/app/workflow_sequential.py#here"} parse_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="Extract the claim amount, currency, category, and description.", output_type=ClaimData, ) restate_parse_agent = RestateAgent(parse_agent) analysis_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="Analyze the claim and approve/deny it.", output_type=bool, ) @@ -337,6 +350,81 @@ curl localhost:8080/ClaimReimbursement/process --json '{ ``` + + + +```python workflow_sequential.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/workflow_sequential.py#here"} +parse_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="Extract the claim amount, currency, category, and description.", + response_format=ClaimData, + middleware=[RestateMiddleware()], +) + + +analysis_agent = create_agent( + model=init_chat_model("openai:gpt-5.4"), + system_prompt="Assess whether this claim is valid and determine the approved amount.", + middleware=[RestateMiddleware()], +) + + +claim_service = restate.Service("ClaimReimbursement") + + +@claim_service.handler() +async def process(ctx: restate.Context, req: ClaimPrompt) -> dict: + # Step 1: Parse the claim document (structured-output LLM step). + parsed = await parse_agent.ainvoke({"messages": req.message}) + claim = parsed["structured_response"] + + # Step 2: Analyze the claim (LLM step). + analysis = await analysis_agent.ainvoke({"messages": claim.model_dump_json()}) + + # Step 3: Convert currency (regular durable step). + amount_usd = await ctx.run_typed( + "Convert currency", + convert_currency, + amount=claim.amount, + source=claim.currency, + target="USD", + ) + + # Step 4: Process reimbursement (regular durable step). + confirmation = await ctx.run_typed( + "Process payment", + process_payment, + claim_id=str(ctx.uuid()), + amount=amount_usd, + ) + + return { + "analysis": analysis["messages"][-1].content, + "amount_usd": amount_usd, + "confirmation": confirmation, + } +``` + + + + +```bash +uv run app/workflow_sequential.py +``` + +Register the agents with Restate: +```bash +restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations +``` + +Send a request: +```bash +curl localhost:8080/ClaimReimbursement/process --json '{ + "message": "Process my hospital bill of 2024-10-01 for 3000USD for a broken leg at General Hospital." +}' +``` + + @@ -347,7 +435,7 @@ async function process(ctx: Context, { message }: { message: string }) { "Parse claim", async () => { const { output } = await generateText({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), prompt: `Extract the claim amount, currency, category, and description. Input: ${message}`, output: Output.object({ schema: ClaimData }), }); @@ -361,7 +449,7 @@ async function process(ctx: Context, { message }: { message: string }) { "Evaluate claim", async () => { const { output: valid } = await generateText({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), system: "You are a claims analyst. Assess whether this claim is valid and determine the approved amount.", prompt: `Claim: ${JSON.stringify(output)}`, diff --git a/docs/ai/sdk-integrations/langchain.mdx b/docs/ai/sdk-integrations/langchain.mdx new file mode 100644 index 00000000..0f00ae2e --- /dev/null +++ b/docs/ai/sdk-integrations/langchain.mdx @@ -0,0 +1,17 @@ +--- +title: "Restate & LangChain" +sidebarTitle: "LangChain" +icon: "/img/languages/python.svg" +description: "Learn how to use LangChain with Restate." +--- + +Integrate LangChain with Restate for fault-tolerant agent execution with automatic retries and durable state. + + + +## Quick Start +- [Quickstart guide](/ai-quickstart) +- [Tour of Core Concepts](/ai/patterns/durable-agents) + +## Learn More +- [Examples](https://github.com/restatedev/ai-examples/tree/main/langchain-python): Includes a minimal weather agent template and a tour of twelve patterns covering durable sessions, human-in-the-loop, multi-agent systems, parallel tools, sub-workflows, and remote agents. diff --git a/docs/docs.json b/docs/docs.json index 1c024472..cbd133fa 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -391,6 +391,7 @@ "ai/sdk-integrations/openai-agents-sdk", "ai/sdk-integrations/google-adk", "ai/sdk-integrations/pydantic-ai", + "ai/sdk-integrations/langchain", "ai/sdk-integrations/litellm", "ai/sdk-integrations/integration-guide" ] diff --git a/docs/img/ai/sdk-integrations/langchain.svg b/docs/img/ai/sdk-integrations/langchain.svg new file mode 100644 index 00000000..dc672ff5 --- /dev/null +++ b/docs/img/ai/sdk-integrations/langchain.svg @@ -0,0 +1,7 @@ + + + Langchain Streamline Icon: https://streamlinehq.com + + LangChain + + \ No newline at end of file diff --git a/docs/snippets/tour/ai/setup-langchain.mdx b/docs/snippets/tour/ai/setup-langchain.mdx new file mode 100644 index 00000000..7711f595 --- /dev/null +++ b/docs/snippets/tour/ai/setup-langchain.mdx @@ -0,0 +1,14 @@ +[Install Restate](/installation) and launch it: +```bash +restate-server +``` + +Get the example: +```bash +restate example python-langchain-tour-of-agents && cd python-langchain-tour-of-agents +``` + +Export your [OpenAI API key](https://platform.openai.com/api-keys) and run the agent: +```bash +export OPENAI_API_KEY=sk-... +``` From 93deb7a715e556e3d6ceb0a84ca79caf92a7efb3 Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 15 May 2026 11:29:49 +0200 Subject: [PATCH 2/6] code load --- docs/cloud/connecting-services.mdx | 6 +++--- .../references/python/restate-pydantic-ai-agents.md | 4 ++-- .../references/ts/restate-vercel-ai-agents.md | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/cloud/connecting-services.mdx b/docs/cloud/connecting-services.mdx index 1459b9f3..92c9fd98 100644 --- a/docs/cloud/connecting-services.mdx +++ b/docs/cloud/connecting-services.mdx @@ -252,16 +252,16 @@ export class LambdaTsCdkStack extends cdk.Stack { }, }); - if (!process.env.RESTATE_ENV_ID || !process.env.RESTATE_AUTH_TOKEN) { + if (!process.env.RESTATE_ENV_ID || !process.env.RESTATE_API_KEY) { throw new Error( - "Required environment variables RESTATE_ENV_ID and RESTATE_AUTH_TOKEN are not set, please see README.", + "Required environment variables RESTATE_ENV_ID and RESTATE_API_KEY are not set, please see README.", ); } const restateEnvironment = new restate.RestateCloudEnvironment(this, "RestateCloud", { environmentId: process.env.RESTATE_ENV_ID! as restate.EnvironmentId, apiKey: new secrets.Secret(this, "RestateCloudApiKey", { - secretStringValue: cdk.SecretValue.unsafePlainText(process.env.RESTATE_AUTH_TOKEN!), + secretStringValue: cdk.SecretValue.unsafePlainText(process.env.RESTATE_API_KEY!), }), }); const deployer = new restate.ServiceDeployer(this, "ServiceDeployer"); diff --git a/restate-plugin/skills/building-restate-services/references/python/restate-pydantic-ai-agents.md b/restate-plugin/skills/building-restate-services/references/python/restate-pydantic-ai-agents.md index 01519654..c2ff2b40 100644 --- a/restate-plugin/skills/building-restate-services/references/python/restate-pydantic-ai-agents.md +++ b/restate-plugin/skills/building-restate-services/references/python/restate-pydantic-ai-agents.md @@ -22,7 +22,7 @@ class WeatherPrompt(BaseModel): # AGENT weather_agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a helpful agent that provides weather updates.", ) @@ -67,7 +67,7 @@ To add session management to the agent: ```python agent = Agent( - "openai:gpt-4o-mini", + "openai:gpt-5.4", system_prompt="You are a helpful assistant.", ) restate_agent = RestateAgent(agent) diff --git a/restate-plugin/skills/building-restate-services/references/ts/restate-vercel-ai-agents.md b/restate-plugin/skills/building-restate-services/references/ts/restate-vercel-ai-agents.md index 4c4ab720..46f6fc03 100644 --- a/restate-plugin/skills/building-restate-services/references/ts/restate-vercel-ai-agents.md +++ b/restate-plugin/skills/building-restate-services/references/ts/restate-vercel-ai-agents.md @@ -29,7 +29,7 @@ async function getWeather(ctx: restate.Context, city: string) { // AGENT const run = async (ctx: restate.Context, { prompt }: { prompt: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), // Persist LLM responses middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); @@ -92,7 +92,7 @@ const chatAgent = restate.object({ { input: schema(ChatMessageSchema) }, async (ctx: restate.ObjectContext, { message }: { message: string }) => { const model = wrapLanguageModel({ - model: openai("gpt-4o"), + model: openai("gpt-5.4"), middleware: durableCalls(ctx, { maxRetryAttempts: 3 }), }); From 17dc092069d38f1442a0f7edfca084ee27d58165 Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 15 May 2026 11:34:17 +0200 Subject: [PATCH 3/6] code load --- snippets/python/pyproject.toml | 2 +- snippets/python/uv.lock | 74 +++++++++++++++++----------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/snippets/python/pyproject.toml b/snippets/python/pyproject.toml index 40d10c3f..c3a0af55 100644 --- a/snippets/python/pyproject.toml +++ b/snippets/python/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Python code snippets for documentation" requires-python = ">=3.11" dependencies = [ - "restate-sdk[harness,serde]==0.17.1", + "restate-sdk[harness,serde]==0.18.0", "hypercorn", "pydantic", "requests", diff --git a/snippets/python/uv.lock b/snippets/python/uv.lock index 16592046..302c291c 100644 --- a/snippets/python/uv.lock +++ b/snippets/python/uv.lock @@ -651,7 +651,7 @@ requires-dist = [ { name = "openai-agents", specifier = ">=0.3.2" }, { name = "pydantic" }, { name = "requests" }, - { name = "restate-sdk", extras = ["serde", "harness"], specifier = "==0.17.1" }, + { name = "restate-sdk", extras = ["harness", "serde"], specifier = "==0.18.0" }, { name = "types-requests" }, ] @@ -726,44 +726,44 @@ wheels = [ [[package]] name = "restate-sdk" -version = "0.17.1" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/fc/3338a05358e664d232bab3afa8d80deaa9d7d45235a9ac2698dce1a08ad2/restate_sdk-0.17.1.tar.gz", hash = "sha256:1791d324ff8c4df3987389cddb66a2c53f0b69ecac01f0410af1672cca003a06", size = 255321, upload-time = "2026-04-08T12:16:50.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/7d/5c6fe1e4db881e2304b659ddf81fd642a4e48039e6c8fa68c66cabfb200a/restate_sdk-0.18.0.tar.gz", hash = "sha256:b80edb7afd5fd419533408fb591613f937a69e1c3db73d977c595b819cba8121", size = 294972, upload-time = "2026-05-13T08:47:26.952Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/1e/6a49631e6f0eb6e2fac124f504d6c8aa859f100a36048113979359d05086/restate_sdk-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3e94d4fa17e4c4d45f3e314918ea6c514fee74c18f3d2bde307e6141e149820d", size = 1952109, upload-time = "2026-04-08T12:16:19.279Z" }, - { url = "https://files.pythonhosted.org/packages/81/56/24b7dcbe9e7ff1eb58865e2b9f992628ad560bb755eb45b4f2be7e99b54f/restate_sdk-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc0ac0f45b1010316ea1bdb3765b20421554b4cb988d7d10aae2d7a7df950d76", size = 1879282, upload-time = "2026-04-08T12:16:13.062Z" }, - { url = "https://files.pythonhosted.org/packages/96/2f/985e20e25ea24cf4028c074183743ea61f899efdbd513262e9d006e0844d/restate_sdk-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56ef12dd7c5011dc87dacb16896e7b71d1ba3717c1e55676f0589221fa1bb02e", size = 2087574, upload-time = "2026-04-08T12:16:05.805Z" }, - { url = "https://files.pythonhosted.org/packages/69/c2/6153ee6ae4bec4602eb3e8b1f5788e6b272640269ab69c7cbff92ac19f19/restate_sdk-0.17.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:763248c415876eaea443ceb5d6efab498adfad103e10781c1b82c9d96316e316", size = 2117153, upload-time = "2026-04-08T12:15:52.456Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e6/74f80ca4c0fe2cb1a9ca00deba43a46ec8a1eae69b873c8c86dadac3fba3/restate_sdk-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27c30b5dee65921ad8dd38eeb5b40e898dbce1bcab447c26699b858483e0d776", size = 2290980, upload-time = "2026-04-08T12:16:26.045Z" }, - { url = "https://files.pythonhosted.org/packages/52/02/58c24ce9ea5cb83cf6adbc39baf991e7f69e6f0c7c45f7067104395cd4b4/restate_sdk-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c3568ba463328ed63b331b6e094adff0915d975b6d48826f39dbe6ed650d89bf", size = 2372439, upload-time = "2026-04-08T12:16:38.372Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2c/ed81a5a627c48e1cfec0fb9cda5fa074c21740e17f3255dc903de77e4c7d/restate_sdk-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8fe38825a5449f27ec1925d1b5ee5b441f7c74b85f899e4441b0fb80b00ff985", size = 1947436, upload-time = "2026-04-08T12:16:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/d53c95d880c4b93acba4724fd333741de6e00a5a28cd0a684c27d82e6dff/restate_sdk-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02eb17d6c049827e95a1d4aabf3df9bc45922f12f2beef598b9a6f1dae3b0b71", size = 1876542, upload-time = "2026-04-08T12:16:14.758Z" }, - { url = "https://files.pythonhosted.org/packages/79/12/6fec6bcd96dd9108a335b4b0845069cc08019a186ee591c346150a9b6783/restate_sdk-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8a227829e3c580cfdf5f8f71a23dfb88b93f511af2d5272fad80aeb5d35db9b", size = 2088764, upload-time = "2026-04-08T12:16:07.441Z" }, - { url = "https://files.pythonhosted.org/packages/e1/06/205063f91f47bacf2084781c87d762dd20c237c1748291417faf0c2361f7/restate_sdk-0.17.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bfc3dafb4ae97fbb856443215bebd103de2ad388b3711d4ddc29bb4df6bdfc02", size = 2118859, upload-time = "2026-04-08T12:15:53.681Z" }, - { url = "https://files.pythonhosted.org/packages/c5/99/263e8eca29ecc516b94a0bd6204f06ae65e26ebdc77fc0b32432266de6d7/restate_sdk-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be3a3d674ea4f549eb8afea449279430cb878a6becc31feb29ed3b62681788f7", size = 2294503, upload-time = "2026-04-08T12:16:27.289Z" }, - { url = "https://files.pythonhosted.org/packages/79/be/12fafe4ee1eeed45ef159c16cd36e99ed7d041414c0dd465512d2b156906/restate_sdk-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:79a73d4d366b461222ce72f85f874c8c3190aebf4ae0218a1eb3139c0553029d", size = 2374414, upload-time = "2026-04-08T12:16:39.889Z" }, - { url = "https://files.pythonhosted.org/packages/1f/25/abfe7862172a3c9a30a792ca3909bc633722d4f124df5e3bad3d58b64e16/restate_sdk-0.17.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:637439f1f17ba640214c78de2f266f859220d4c2c4cef27098d59cec37bf9b80", size = 1947838, upload-time = "2026-04-08T12:16:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/e4/54/ac8ae06eb0c72bbae7e17cae9210867dbdc2f54f108c3b3af8a985c801b4/restate_sdk-0.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4241da2a5f4224fca741ca6978933d0c897dc739c178caf04964da85101aaa05", size = 1876724, upload-time = "2026-04-08T12:16:16.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1c/3cbaa89242c99682a31b51e3d6af0c4e03a5531c56d1542f46e5c1c1e3e5/restate_sdk-0.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d3e763d1e8d68f417f0c2a9ad8157090009f2c469c3411a8764d6edbcb4545b", size = 2088656, upload-time = "2026-04-08T12:16:08.742Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a0/bda9ccc0ac6dc4b414ee0968f4abceb052e1fcb062c15ff7a6dafa50eff4/restate_sdk-0.17.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9fcb8154fdc9ee11baa890fb9f3c4d51f3efb1b87665578cf15168f3221dc1e8", size = 2119283, upload-time = "2026-04-08T12:15:55.27Z" }, - { url = "https://files.pythonhosted.org/packages/30/d4/d334844e33786392e82d8e08e7efb399f2daac8f29e11237125cc8bc5703/restate_sdk-0.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6f0e354d3540b4105e041fc47933420f02e5e23b71223ab2a19c9cd3dde597e9", size = 2294672, upload-time = "2026-04-08T12:16:29.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/ed/4b17812aee3a2760efe1db290f40e34a4db0460ea4c94d744a91b08e9190/restate_sdk-0.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f789e9a5d03a36cd9299b8b142ea6c08469ff5fb5e8c39e2c60ee75b198a9a0", size = 2374546, upload-time = "2026-04-08T12:16:41.555Z" }, - { url = "https://files.pythonhosted.org/packages/6e/39/6442e63553514b2a6631da0a9b0ed7794ab4c05ff39ea0af13379ad4f5e7/restate_sdk-0.17.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:6ad6e1c1380b57cfa8d0bf98700718590b5aebc6b3e132e2bd1158ff99edaeed", size = 2119018, upload-time = "2026-04-08T12:15:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/db/1a/198288f80427ac7e77e640f8eb8091cc2ed79a892280fe342bbbc664840e/restate_sdk-0.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d1d9245378a865f008b2bb0f1a52f4774069142e8b57b824fa609d35f0b1b683", size = 2292685, upload-time = "2026-04-08T12:16:30.312Z" }, - { url = "https://files.pythonhosted.org/packages/09/0b/d36781d19ffeee90f069a09c2784ad504d2a6c94590373ce85d87fefc06a/restate_sdk-0.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ca34b2d97ce1840900caf3f3e94806675ee947796c8c69ac21dacd7302c92551", size = 2374749, upload-time = "2026-04-08T12:16:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0e/ab4ae7e0170050f7b0b3c7a72a3e9ce1030102c70de1647bcf90ef4da4a7/restate_sdk-0.17.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:400bd205a50861908f2b0a6488a1b16d1309bf6c6cbbd1eb85e50cb9c24ae957", size = 1947905, upload-time = "2026-04-08T12:16:23.515Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b0/aa290ee4e650573144b6da95d4a09c06d8c0b6bb2f4e3dca5ea02418ceac/restate_sdk-0.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d6cf1a98d12e0cd76196e88e4511c1090a7155daf81658403433df046efbe380", size = 1877607, upload-time = "2026-04-08T12:16:17.623Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5b/4e8ffd14451ecd92a334df6f909ab4baa7af988d712bf5fe8198366c754a/restate_sdk-0.17.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf0a7ffb4c4cb1e8d2520d64ad114643573536834bb3f0c627680de20a977e9", size = 2088668, upload-time = "2026-04-08T12:16:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/ef/99/c450d808b00bb55d1603991f61169ba49eb261195dd5a5d7fbacec556b53/restate_sdk-0.17.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:565eac3da15db9a46b1235adccd120869ec8384ddd22de56dc7480a4b54d01d3", size = 2119970, upload-time = "2026-04-08T12:15:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0e/46eb0014bec627195b6cce17af72e8e74b631ca099021e7f5b8dcbe4a585/restate_sdk-0.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0d872556a76a42c2337d5799a3d20deadf04857866a64952b95717c18f725803", size = 2293384, upload-time = "2026-04-08T12:16:31.952Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9c/ac8492b82a280144cea4bd3f336666be5cabe0f6dc5635a4f8e7616753c2/restate_sdk-0.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7e363d32f696d8d65be1ff916b22d350feaa4a7d982adc1e579bbc03ddb31852", size = 2376876, upload-time = "2026-04-08T12:16:44.542Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f9/0d8eee64c2e1dc55b442a5c644a45c2cc9996d8b1d3fe7e17187931d6070/restate_sdk-0.17.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b08787f8de2823e4c04301a39f78a641e80a6e2765dc86a574496ed91b89dc17", size = 2117993, upload-time = "2026-04-08T12:15:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/ee/73/383218009235eae8674de9f6d3167ee39169f5ba078799c737bdeb412940/restate_sdk-0.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:57f912b4ab29eadad380380c1f3b6002b5e9c74fbdc02bd8f728f89bf30b25ac", size = 2292146, upload-time = "2026-04-08T12:16:33.222Z" }, - { url = "https://files.pythonhosted.org/packages/57/93/04d1fb1dd36d6b03b531445921573a495df3742e7c2ed377077fe1f42bdd/restate_sdk-0.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:34b2c5514f9b6198fd015d576f31d56f2086a2e65c60814cb2d3a9f92c3cd245", size = 2373849, upload-time = "2026-04-08T12:16:45.924Z" }, - { url = "https://files.pythonhosted.org/packages/44/b7/992de9c32457f8f44ea77e476e6db98f3b441d8e6bcec59d419e494c5fae/restate_sdk-0.17.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5568fb63bf94a51db3ee0fd34f61ae2a12d99c3ebb1f3e3f9cfc14e8e79f3953", size = 2086328, upload-time = "2026-04-08T12:16:11.329Z" }, - { url = "https://files.pythonhosted.org/packages/75/8a/0a2c803c606f1663ede7b362a8c3f98a14f026806464ff8e4f41a29a4c76/restate_sdk-0.17.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4292eaf5677f93ca9f5f5cef04a2d24333cae187a0a93ee405b0022216ea68ce", size = 2116372, upload-time = "2026-04-08T12:16:02.923Z" }, - { url = "https://files.pythonhosted.org/packages/f3/bb/eae5a52f044ae1a6d0b5b51463085a3770868cbf3b58929e12994d1a2da6/restate_sdk-0.17.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8ae29de2980ccfcea59efa0219a865dd156b162b0423b6152a9e9d002200fef6", size = 2290786, upload-time = "2026-04-08T12:16:35.793Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2e/e4c2e9b0a2c6c873bbdf286891e227f5947049f4f242b83066bc680876f0/restate_sdk-0.17.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5710d24d62a93e8a1a2d7ca3097ee73b6b09d3d20e863bd5e626d86ac6686dff", size = 2372337, upload-time = "2026-04-08T12:16:48.938Z" }, + { url = "https://files.pythonhosted.org/packages/40/7c/eb57e677bf2dd24ef1958e69055b6a3e38936cf3272ff904418a98ac130d/restate_sdk-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2b71a9f90fb50ddd2bda4d29d2c3151243cf68ce1c81b40336a1a1b747a6cedc", size = 1939354, upload-time = "2026-05-13T08:46:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/95/a7/9dba78fc4eeac37c3e4e18bf28004a9bc4f8897d44261a278599f590eb36/restate_sdk-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcb47f4e0385c89aaa6acf023a96a00c2e32dd987a141d3ee733a0182d2abf8d", size = 1868728, upload-time = "2026-05-13T08:46:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/ab627e1f94807e9ca8a0522ca351b34d582a36a565e2f6ab8a1af82ac924/restate_sdk-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec28529bfb1751d204f86f51c965f63fdb197c7669fa120e6cf3751f23c86d7", size = 2084884, upload-time = "2026-05-13T08:46:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/3b/21/a0fd159404c38428b46497c17a0c27aea92628894cb23e9d8dfe8342523b/restate_sdk-0.18.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ede66105b3d17b6b853b09703ed28b999e23b56d1a7de7dd383119dda892b2e2", size = 2116149, upload-time = "2026-05-13T08:46:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/2f/91/010f7e7e52e7bc54572a2511f61a84daf3f2ced445be2c7f1eb4bc5c573a/restate_sdk-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a79d0bb0bc2dfb1fd0c3a5638d7a2d4072e277e68031da4056d991e341abf34", size = 2292709, upload-time = "2026-05-13T08:46:57.631Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/0af06dce5fe4ec31b7d3507eb248cbdec2e4095ca78f04753e75e3d7e292/restate_sdk-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20a0434333e0fdf7edc2225c3d40b5e06b30972735f2c827245090a58585a078", size = 2375180, upload-time = "2026-05-13T08:47:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e9/29a223a47bfbf3b0323e2dc47c705689264f8f8224480c72b1394ed5de65/restate_sdk-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:31c6894e74f9b6a686fb29437c2c06e2e4239ff22f0feec9413e0b7c0553b5e7", size = 1937115, upload-time = "2026-05-13T08:46:50.758Z" }, + { url = "https://files.pythonhosted.org/packages/f3/4a/a62f7fc413b133c62c9d26142250a59a58cd0a8590037ad656bfef3a2f1b/restate_sdk-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed32fe5c9caedf7bf814b856815866c661c234cdbd23159be2d22db7c095267e", size = 1868487, upload-time = "2026-05-13T08:46:44.36Z" }, + { url = "https://files.pythonhosted.org/packages/50/48/9c17ea31679683f7e55ff873a5a6ec7d4ae72f6a7dfcc94b605366d4ced2/restate_sdk-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff649eb100eac5ef867d0ddf348ff25ee2f648e8d9dc7442f8ec045021b6b09a", size = 2087953, upload-time = "2026-05-13T08:46:36.416Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/3b09a6a67dfe0f8703cd0d1ae0c3d6b75bfa06359ec3570b825703bd5a74/restate_sdk-0.18.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7472a17673111315325015440d10fdac28b3210142a554c5edc16e8fdf70cc23", size = 2120507, upload-time = "2026-05-13T08:46:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/391fce3c020df7b58a3c61ec0e759741806e1c8d1c5b73188affba1fec52/restate_sdk-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8683b8ed5047f0c3b1247e7a68d0815f52259d5169142fe249d972a4c0eff758", size = 2295857, upload-time = "2026-05-13T08:46:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4e/0d8966ea02649b2e0916240a7a6acf34aa3fb79825eaa75e735e22003138/restate_sdk-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183f1c000de868e5e91a02eb4e4399c107eaf877dcc54dd5f27f57a33ee031f0", size = 2377519, upload-time = "2026-05-13T08:47:14.177Z" }, + { url = "https://files.pythonhosted.org/packages/e4/42/82df0cd12701090c321a576b2a9701f54cf2f8289f9cf811b9b28f2aca22/restate_sdk-0.18.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ac9e0809e2f8910612405894a71463d4cad16dbb0e5f8a4665110e20415870c4", size = 1936870, upload-time = "2026-05-13T08:46:52.493Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/9dab034b065f577ab32fb3a6f3304a7dddea47f84c9ccd80727a9627f609/restate_sdk-0.18.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c5a0ef6435d8b1341956c8fcebee039f328146959bc2fc67e33e2a0a6eaad6c", size = 1868536, upload-time = "2026-05-13T08:46:45.85Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b3/05f9f148516f69885ef314f0223033a2015e5a5e93e27ec2c59367c372d0/restate_sdk-0.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48fefa044418e1e62496a02e364db08dab9f6150092e856f3217bc6ef4a0f10a", size = 2087993, upload-time = "2026-05-13T08:46:38.169Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/74d6edd8b5c5803dc33849775f822d879fc4d7c4902aabfc70db45869d2a/restate_sdk-0.18.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:35392f0eaf9fa2c3dfe08146ac437fc98649e399fa0c819e3c8f764dbddda6c0", size = 2120262, upload-time = "2026-05-13T08:46:22.123Z" }, + { url = "https://files.pythonhosted.org/packages/f8/80/251b5f5dac2bb5ebe17856be45d04f38dbf59072fc3a5bd4d0217a7147bc/restate_sdk-0.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6f17284aa79e6f742e42f2cc114615d6b9a0af62ba31f51d33077d3d40cedd7", size = 2295657, upload-time = "2026-05-13T08:47:00.472Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8d/1d7241b993c30c809827f5c800da5dd30099b232ddc9a69e3207bad2e5e2/restate_sdk-0.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3032635c1f7005d49575efb002c16f1857c050495f7b0a6845526e59703f84d2", size = 2377920, upload-time = "2026-05-13T08:47:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/80/e3/4630364032955b2037f8daa1f4b961852ca0445a9d40381064f39454811e/restate_sdk-0.18.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:adcb116dd01117493a94979eae0d7cbe589cfcecbfc42f78750716678925312d", size = 2115301, upload-time = "2026-05-13T08:46:23.678Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/ffef4638a97760ce1e86646e4d0023ee179870024a701602e061c45319fb/restate_sdk-0.18.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:292f6452d112e92e7197337e1f82f20acda3a6bd8cf6992cb649dc211e1229c9", size = 2291985, upload-time = "2026-05-13T08:47:01.861Z" }, + { url = "https://files.pythonhosted.org/packages/a4/22/d086a8063838a8df0d809b51c832708c3f215af71dbf342a21827e67a0c5/restate_sdk-0.18.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f7c1aa3a09183105d36e97ccb8b02fbaa6698534c830b29c2b6a47982845c7b", size = 2375075, upload-time = "2026-05-13T08:47:17.986Z" }, + { url = "https://files.pythonhosted.org/packages/56/55/f6598d6e2cad5aa3e1c5e4446c7db522d4ea6fcf66a21781eac68c118dd9/restate_sdk-0.18.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c3d962095b1376b304ad4048095962ff0aca5ab7c7aa9fedc46cccfd7039c230", size = 1937400, upload-time = "2026-05-13T08:46:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c0/dda3035f6b8cab723d2b0520a4fd372988c26a1e505313a1b719ceb62f6d/restate_sdk-0.18.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ae5de9b06301efe3009aef8a4256e078dab670587ff3083eaf89661ade7a5d7", size = 1868792, upload-time = "2026-05-13T08:46:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/f773fde0b68a65320871032d171c9865c04f6d71729c5ebd0fb5736ce32b/restate_sdk-0.18.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d9950eb03a387ba3b5924719ded80a703afdb355866e6aca64f06c71c86645", size = 2089455, upload-time = "2026-05-13T08:46:39.683Z" }, + { url = "https://files.pythonhosted.org/packages/93/d0/93af71f5a0543bf695acfd45ee3ac255635270e219dff9771d2ed3edbcf6/restate_sdk-0.18.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b48c01cc83fc636a70875470663dee6c6f738f1bc4906ddb1e085568ffef0528", size = 2120869, upload-time = "2026-05-13T08:46:25.345Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6f/c69778bd14529bd9ecf1a01d5162000b11615dc745543a00667a7363f512/restate_sdk-0.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:531e863f20e74a6c0777c8e493a5f7ce12f8aace6de1e48e7dc0835f751a3ad3", size = 2296369, upload-time = "2026-05-13T08:47:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/88/99/bd748e3ab3af11d76b946854049cbb0510bcbcd422ea2133a6cab0343dd4/restate_sdk-0.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6d3f331589b4003ba1d59647daaf50bdedf05ab55cab384a7333b76ed0fed711", size = 2379620, upload-time = "2026-05-13T08:47:19.753Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/c5dc2b57d050f688e7366ac0f480a7b4883aa601d9cf07bee9ffbc2d3a1d/restate_sdk-0.18.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b1ef33a4a920860edb5fb1021eee3f772022a749ae69c6617419345a6e95ed6d", size = 2117118, upload-time = "2026-05-13T08:46:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/55dfd5bfbf0ce6ba14ec70d52381804029ff1faea075def18fcb66571239/restate_sdk-0.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:727525dd5a8a4848281a7077fc996b21c9d5ae21301b5771effd92b65583aca8", size = 2293691, upload-time = "2026-05-13T08:47:05.243Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/ba8a62ab06bb95c3e87a6a9ee5c315a7166d49dd74238149d6da834b9723/restate_sdk-0.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf97653f25b6b012f8009e387755b179de7d2431f656062869c8fb91d18bdc60", size = 2375004, upload-time = "2026-05-13T08:47:21.785Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/a99564cbe0b644a4707b797e00ca4fb816eb961036435a1c09838b14a4dd/restate_sdk-0.18.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9dcd39cc78db365f012876bd3a46011f2f95c8d6a1ed60e92e6960b936edd36", size = 2083579, upload-time = "2026-05-13T08:46:41.131Z" }, + { url = "https://files.pythonhosted.org/packages/85/ce/d53890baba84e16b9b4eb7c52458f00a7b5b98e03a87b80d3b6f6896b749/restate_sdk-0.18.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4853637bcdb5af69703ea0adfd9aa957ca1562889443dc8f1fbd5e8edced5f4a", size = 2115636, upload-time = "2026-05-13T08:46:30.694Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/34e748d0856c1a9e50ffcb4b74f47ada9e298c03532f9f3c2a5738895fa5/restate_sdk-0.18.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd16d423aca6a15cd08bc876d6cd6f94566fcd3794fa02482cca4a20ac0bdd46", size = 2291750, upload-time = "2026-05-13T08:47:09.084Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/af7f4d3c249bc8ff4292bbeba7d90746c1519cc8827066e1cf8bfa244e48/restate_sdk-0.18.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2f801a19f99261e72629b250ce59427e3d9f7c4d6e7efa83611e16537b51becd", size = 2374086, upload-time = "2026-05-13T08:47:25.36Z" }, ] [package.optional-dependencies] From aa775782eacc4725af71ee52f7745ba88d0a6bfa Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 15 May 2026 11:34:47 +0200 Subject: [PATCH 4/6] code load --- docs/ai/sdk-integrations/langchain.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/sdk-integrations/langchain.mdx b/docs/ai/sdk-integrations/langchain.mdx index 0f00ae2e..bcd2f7e1 100644 --- a/docs/ai/sdk-integrations/langchain.mdx +++ b/docs/ai/sdk-integrations/langchain.mdx @@ -14,4 +14,4 @@ Integrate LangChain with Restate for fault-tolerant agent execution with automat - [Tour of Core Concepts](/ai/patterns/durable-agents) ## Learn More -- [Examples](https://github.com/restatedev/ai-examples/tree/main/langchain-python): Includes a minimal weather agent template and a tour of twelve patterns covering durable sessions, human-in-the-loop, multi-agent systems, parallel tools, sub-workflows, and remote agents. +- [Examples](https://github.com/restatedev/ai-examples/tree/main/langchain-python): Includes a minimal weather agent template and a tour of patterns covering durable sessions, human-in-the-loop, multi-agent systems, parallel tools, sub-workflows, and remote agents. From 0727f35b80cc5f89d11e2d7479476b23287c62c8 Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 15 May 2026 11:55:18 +0200 Subject: [PATCH 5/6] small fix --- docs/ai/patterns/parallelization.mdx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/ai/patterns/parallelization.mdx b/docs/ai/patterns/parallelization.mdx index 12488fbf..8d2577d8 100644 --- a/docs/ai/patterns/parallelization.mdx +++ b/docs/ai/patterns/parallelization.mdx @@ -112,8 +112,7 @@ In the UI, you can see the tool steps running in parallel: -**⚠️ To ensure deterministic replay when using the OpenAI Agent SDK with Restate, tool calls are forced to be executed sequentially. -This is forced by the integration itself.** +**⚠️ Executing tool calls in parallel can lead to non-deterministic replays of journaled events on retries. To prevent this, when the LLM requests multiple tools in a single turn, the integration runs them one after the other instead of in parallel. The LLM still issues a single batch of tool calls (no extra LLM round-trips), but the tool executions themselves are serialized.** To use parallel tool calls with the OpenAI Agent SDK, create a tool that runs multiple analyses in parallel. The LLM calls one tool, and that tool fans out work internally using durable execution primitives. @@ -166,8 +165,7 @@ In the UI, you can see the tool steps running in parallel: -**⚠️ To ensure deterministic replay when using the Google ADK with Restate, tool calls are forced to be executed sequentially. -This is forced by the integration itself.** +**⚠️ Executing tool calls in parallel can lead to non-deterministic replays of journaled events on retries. To prevent this, when the LLM requests multiple tools in a single turn, the integration runs them one after the other instead of in parallel. The LLM still issues a single batch of tool calls (no extra LLM round-trips), but the tool executions themselves are serialized.** To use parallel tool calls with the Google ADK, create a tool that runs multiple analyses in parallel. The LLM calls one tool, and that tool fans out work internally using durable execution primitives. @@ -220,8 +218,7 @@ In the UI, you can see the tool steps running in parallel: -**⚠️ To ensure deterministic replay when using Pydantic AI with Restate, tool calls are forced to be executed sequentially. -This is forced by the integration itself.** +**⚠️ Executing tool calls in parallel can lead to non-deterministic replays of journaled events on retries. To prevent this, when the LLM requests multiple tools in a single turn, the integration runs them one after the other instead of in parallel. The LLM still issues a single batch of tool calls (no extra LLM round-trips), but the tool executions themselves are serialized.** To use parallel tool calls with Pydantic AI, create a tool that runs multiple analyses in parallel. The LLM calls one tool, and that tool fans out work internally using `restate.gather()` to run durable execution steps concurrently. @@ -276,7 +273,7 @@ In the UI, you can see the tool steps running in parallel: -**⚠️ The Restate middleware serializes parallel tool calls within the same LLM turn via a turnstile, so tool bodies replay deterministically. Inside a single tool body you can fan out freely with `restate.gather()`.** +**⚠️ Executing tool calls in parallel can lead to non-deterministic replays of journaled events on retries. To prevent this, when the LLM requests multiple tools in a single turn, the integration runs them one after the other instead of in parallel. The LLM still issues a single batch of tool calls (no extra LLM round-trips), but the tool executions themselves are serialized.** To use parallel tool calls with LangChain, create a tool that runs multiple analyses in parallel. The LLM calls one tool, and that tool fans out work internally using `restate.gather()` to run durable execution steps concurrently. From c7ae9fb451da62ae132abc289633fd3331f45f5c Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 15 May 2026 12:02:42 +0200 Subject: [PATCH 6/6] small fix --- docs/ai/patterns/remote-agents.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/patterns/remote-agents.mdx b/docs/ai/patterns/remote-agents.mdx index 932f9bbd..7028ef7a 100644 --- a/docs/ai/patterns/remote-agents.mdx +++ b/docs/ai/patterns/remote-agents.mdx @@ -407,7 +407,7 @@ Once all sub-agents return, the main agent continues and makes a decision. -With LangChain, you expose each specialist as a separate Restate service and call it via `restate_context().service_call()`. Restate's middleware journals the LLM response that picks the specialist, and the inter-service hop is durable: caller and callee are journaled independently. +With LangChain, you expose each specialist as a separate Restate service and call it via `restate_context().service_call()`. The LLM response that picks the specialist is durably persisted, so on recovery the routing decision is replayed without re-calling the LLM. ```python remote_agents.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/langchain-python/tour-of-agents/app/remote_agents.py#here"} # Durable service call to the fraud agent; persisted and retried by Restate.