Skip to content

Let Agent be run in a Temporal workflow by moving model requests, tool calls, and MCP to Temporal activities #2225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

DouweM
Copy link
Collaborator

@DouweM DouweM commented Jul 17, 2025

@DouweM DouweM self-assigned this Jul 17, 2025
Copy link

github-actions bot commented Jul 17, 2025

Docs Preview

commit: 46c1f79
Preview URL: https://bd1142c9-pydantic-ai-previews.pydantic.workers.dev

@mattbrandman
Copy link
Contributor

I'm so excited for this to be natively supported

@nir-litt
Copy link

Thank you for taking the time to look into native temporal support - this is a topic I’m really interested in seeing progress on.

One key feature I’d love to see is support for HITL (Human-in-the-Loop) during tool calls when using temporal and agents. My suggestion on how to achieve that (and not being to specific for HITL) is to pause the workflow before a tool call waiting on a condition for a signal. This will allow, among other things, to wait for human input.

Currently, tool calls are handled like this:

async def call_tool(
    self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT]
) -> Any:
    serialized_run_context = self.serialize_run_context(ctx)
    return await workflow.execute_activity(
        activity=self.call_tool_activity,
        arg=FunctionCallToolParams(name=name, tool_args=tool_args, serialized_run_context=serialized_run_context),
        **self.temporal_settings.__dict__,
    )

To add the wait support, we can do something like:
(This example is a bit abstract - we will have to understand (at least):

  • how to pass the self.allow_tool_call from the workflow to this class or effect it with some callback instead of just setting it
  • pass all the data of the tool call for the handler to be able to decide which action to take)
async def call_tool(
    self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT]
) -> Any:
    # --- ADDED: tool call pause using signal and wait_condition ---
    await workflow.wait_condition(lambda: self.allow_tool_call)
    # --- END ADDED CODE ---
    serialized_run_context = self.serialize_run_context(ctx)
    activity_result =  await workflow.execute_activity(
        activity=self.call_tool_activity,
        arg=FunctionCallToolParams(name=name, tool_args=tool_args, serialized_run_context=serialized_run_context),
        **self.temporal_settings.__dict__,
    )
    # --- ADDED: tool call reset state ---
    self.allow_tool_call = False
    return activity_result
    # --- END ADDED CODE ---

This will allow users to define their workflow something like:

@workflow.signal
def approve_tool_call(self):
    ... # some code to determine the tool is OK to run
    self.allow_tool_call = True

This way, the workflow will pause at the tool call and only continue once the signal is approved.

of course this would be optional and the default ,that can be overwritten by the user, will be to approve the request

Appreciate the ongoing work!

Copy link
Contributor

hyperlint-ai bot commented Jul 21, 2025

PR Change Summary

Introduced the Temporal Agent feature by enhancing toolset definitions and adding optional IDs for better identification in error messages.

  • Added optional ID parameters to FunctionToolset for better identification in error messages.
  • Updated LangChainToolset and ACIToolset to include IDs for Slack and Open Weather Map respectively.
  • Enhanced documentation to clarify the use of IDs in durable execution environments.

Modified Files

  • docs/tools.md
  • docs/toolsets.md

How can I customize these reviews?

Check out the Hyperlint AI Reviewer docs for more information on how to customize the review.

If you just want to ignore it on this PR, you can add the hyperlint-ignore label to the PR. Future changes won't trigger a Hyperlint review.

Note specifically for link checks, we only check the first 30 links in a file and we cache the results for several hours (for instance, if you just added a page, you might experience this). Our recommendation is to add hyperlint-ignore to the PR to ignore the link check for this PR.

@DouweM DouweM force-pushed the temporal-agent branch 2 times, most recently from 6e8c76a to 5f6cfa7 Compare July 22, 2025 23:37
@DouweM DouweM changed the title WIP: Temporal Agent Let Agent be run in a Temporal workflow by moving model requests, tool calls, and MCP to Temporal activities Jul 24, 2025

@activity.defn(name='model_request')
async def request_activity(params: _RequestParams) -> ModelResponse:
return await original_request(params.messages, params.model_settings, params.model_request_parameters)
Copy link
Collaborator Author

@DouweM DouweM Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider disabling retries within an activity, and having it all be handled by Temporal, which will have to know whether an error is retryable and how long to back off for: https://github.com/temporalio/sdk-python/blob/3244f8bffebee05e0e7efefb1240a75039903dda/temporalio/contrib/openai_agents/_invoke_model_activity.py#L231

Copy link
Collaborator Author

@DouweM DouweM Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way we should document the 3 levels of retries: Temporal > https://ai.pydantic.dev/retries/ > provider client

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be pydantic_ai.ext.temporal like ext.langchain and ext.aci, or pydantic_ai.temporal like pydantic_ai.ag_ui? We haven't been super consistent, but I'd rather not clutter the top namespace with integrations. But maybe it doesn't matter and we should just go for the shorter one, at least for significant integrations that aren't just convenience wrappers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get confirmation on this from @Kludex and @samuelcolvin before merging

Base automatically changed from everything-but-temporal to main August 8, 2025 01:28
@DouweM DouweM marked this pull request as ready for review August 8, 2025 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Native temporal support
6 participants