diff --git a/docs/best-practices/worker.mdx b/docs/best-practices/worker.mdx index 1115ae69ac..15c237b22f 100644 --- a/docs/best-practices/worker.mdx +++ b/docs/best-practices/worker.mdx @@ -61,7 +61,7 @@ production-ready Worker image: [Dockerfile](https://github.com/temporalio/reference-app-orders-go/blob/main/Dockerfile) ```Dockerfile -FROM golang:1.23.8 AS oms-builder +FROM golang:1.24.1 AS oms-builder WORKDIR /usr/src/oms diff --git a/docs/develop/go/cancellation.mdx b/docs/develop/go/cancellation.mdx index 314a2092d0..da9fe4674d 100644 --- a/docs/develop/go/cancellation.mdx +++ b/docs/develop/go/cancellation.mdx @@ -38,9 +38,7 @@ process and your use case which determines whether you want to return the Cancel or Complete status regardless of whether a Cancellation has propagated to and/or skipped Activities. - [sample-apps/go/features/cancellation/workflow.go](https://github.com/temporalio/documentation/blob/main/sample-apps/go/features/cancellation/workflow.go) - ```go // ... // YourWorkflow is a Workflow Definition that shows how it can be canceled. @@ -79,7 +77,6 @@ func YourWorkflow(ctx workflow.Context) error { return err } ``` - ## Handle Cancellation in an Activity {#handle-cancellation-in-an-activity} diff --git a/docs/develop/go/temporal-nexus.mdx b/docs/develop/go/temporal-nexus.mdx index 882f310f6c..287efbb876 100644 --- a/docs/develop/go/temporal-nexus.mdx +++ b/docs/develop/go/temporal-nexus.mdx @@ -108,7 +108,7 @@ const HelloServiceName = "my-hello-service" const EchoOperationName = "echo" type EchoInput struct { - Message string + Message string } type EchoOutput EchoInput @@ -142,30 +142,24 @@ However, implementations are free to make arbitrary calls to other services or d // ... import ( - "context" - "fmt" + "context" + "fmt" - "github.com/nexus-rpc/sdk-go/nexus" + "github.com/nexus-rpc/sdk-go/nexus" - "go.temporal.io/sdk/client" - "go.temporal.io/sdk/temporalnexus" - "go.temporal.io/sdk/workflow" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporalnexus" + "go.temporal.io/sdk/workflow" - "github.com/temporalio/samples-go/nexus/service" + "github.com/temporalio/samples-go/nexus/service" ) // NewSyncOperation is a meant for exposing simple RPC handlers. -var EchoOperation = nexus.NewSyncOperation( - service.EchoOperationName, - func( - ctx context.Context, - input service.EchoInput, - options nexus.StartOperationOptions - ) (service.EchoOutput, error) { - // Use temporalnexus.GetClient to get the client that the worker was initialized with to perform client calls - // such as signaling, querying, and listing workflows. Implementations are free to make arbitrary calls to other - // services or databases, or perform simple computations such as this one. - return service.EchoOutput(input), nil +var EchoOperation = nexus.NewSyncOperation(service.EchoOperationName, func(ctx context.Context, input service.EchoInput, options nexus.StartOperationOptions) (service.EchoOutput, error) { + // Use temporalnexus.GetClient to get the client that the worker was initialized with to perform client calls + // such as signaling, querying, and listing workflows. Implementations are free to make arbitrary calls to other + // services or databases, or perform simple computations such as this one. + return service.EchoOutput(input), nil }) ``` @@ -180,22 +174,15 @@ See alternatives [here](https://pkg.go.dev/go.temporal.io/sdk/temporalnexus). [nexus/handler/app.go](https://github.com/temporalio/samples-go/blob/main/nexus/handler/app.go) ```go // ... -var HelloOperation = temporalnexus.NewWorkflowRunOperation( - service.HelloOperationName, - HelloHandlerWorkflow, - func( - ctx context.Context, - input service.HelloInput, - options nexus.StartOperationOptions - ) (client.StartWorkflowOptions, error) { - return client.StartWorkflowOptions{ - // Workflow IDs should typically be business meaningful IDs and are used to dedupe workflow starts. - // For this example, we're using the request ID allocated by Temporal when the caller workflow schedules - // the operation, this ID is guaranteed to be stable across retries of this operation. - ID: options.RequestID, - // Task queue defaults to the task queue this operation is handled on. - }, nil - }) +var HelloOperation = temporalnexus.NewWorkflowRunOperation(service.HelloOperationName, HelloHandlerWorkflow, func(ctx context.Context, input service.HelloInput, options nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ + // Workflow IDs should typically be business meaningful IDs and are used to dedupe workflow starts. + // For this example, we're using the request ID allocated by Temporal when the caller workflow schedules + // the operation, this ID is guaranteed to be stable across retries of this operation. + ID: options.RequestID, + // Task queue defaults to the task queue this operation is handled on. + }, nil +}) ``` @@ -218,27 +205,23 @@ A Nexus Operation can only take one input parameter. If you want a Nexus Operati [nexus-multiple-arguments/handler/app.go](https://github.com/temporalio/samples-go/blob/main/nexus-multiple-arguments/handler/app.go) ```go var HelloOperation = temporalnexus.MustNewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[service.HelloInput, service.HelloOutput]{ - Name: service.HelloOperationName, - Handler: func( - ctx context.Context, - input service.HelloInput, - options nexus.StartOperationOptions - ) (temporalnexus.WorkflowHandle[service.HelloOutput], error) { - return temporalnexus.ExecuteUntypedWorkflow[service.HelloOutput]( - ctx, - options, - client.StartWorkflowOptions{ - // Workflow IDs should typically be business meaningful IDs and are used to dedupe workflow starts. - // For this example, we're using the request ID allocated by Temporal when the caller workflow schedules - // the operation, this ID is guaranteed to be stable across retries of this operation. - ID: options.RequestID, - }, - HelloHandlerWorkflow, - input.Name, - input.Language, - ) - }, - }) + Name: service.HelloOperationName, + Handler: func(ctx context.Context, input service.HelloInput, options nexus.StartOperationOptions) (temporalnexus.WorkflowHandle[service.HelloOutput], error) { + return temporalnexus.ExecuteUntypedWorkflow[service.HelloOutput]( + ctx, + options, + client.StartWorkflowOptions{ + // Workflow IDs should typically be business meaningful IDs and are used to dedupe workflow starts. + // For this example, we're using the request ID allocated by Temporal when the caller workflow schedules + // the operation, this ID is guaranteed to be stable across retries of this operation. + ID: options.RequestID, + }, + HelloHandlerWorkflow, + input.Name, + input.Language, + ) + }, +}) ``` @@ -253,47 +236,47 @@ After developing an asynchronous Nexus Operation handler to start a Workflow, th package main import ( - "log" - "os" + "log" + "os" - "go.temporal.io/sdk/client" - "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" - "github.com/nexus-rpc/sdk-go/nexus" - "github.com/temporalio/samples-go/nexus/handler" - "github.com/temporalio/samples-go/nexus/options" - "github.com/temporalio/samples-go/nexus/service" + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/temporalio/samples-go/nexus/handler" + "github.com/temporalio/samples-go/nexus/options" + "github.com/temporalio/samples-go/nexus/service" ) const ( - taskQueue = "my-handler-task-queue" + taskQueue = "my-handler-task-queue" ) func main() { - // The client and worker are heavyweight objects that should be created once per process. - clientOptions, err := options.ParseClientOptionFlags(os.Args[1:]) - if err != nil { - log.Fatalf("Invalid arguments: %v", err) - } - c, err := client.Dial(clientOptions) - if err != nil { - log.Fatalln("Unable to create client", err) - } - defer c.Close() - - w := worker.New(c, taskQueue, worker.Options{}) - service := nexus.NewService(service.HelloServiceName) - err = service.Register(handler.EchoOperation, handler.HelloOperation) - if err != nil { - log.Fatalln("Unable to register operations", err) - } - w.RegisterNexusService(service) - w.RegisterWorkflow(handler.HelloHandlerWorkflow) - - err = w.Run(worker.InterruptCh()) - if err != nil { - log.Fatalln("Unable to start worker", err) - } + // The client and worker are heavyweight objects that should be created once per process. + clientOptions, err := options.ParseClientOptionFlags(os.Args[1:]) + if err != nil { + log.Fatalf("Invalid arguments: %v", err) + } + c, err := client.Dial(clientOptions) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + w := worker.New(c, taskQueue, worker.Options{}) + service := nexus.NewService(service.HelloServiceName) + err = service.Register(handler.EchoOperation, handler.HelloOperation) + if err != nil { + log.Fatalln("Unable to register operations", err) + } + w.RegisterNexusService(service) + w.RegisterWorkflow(handler.HelloHandlerWorkflow) + + err = w.Run(worker.InterruptCh()) + if err != nil { + log.Fatalln("Unable to start worker", err) + } } ``` @@ -308,46 +291,46 @@ Import the Service API package that has the necessary service and operation name package caller import ( - "github.com/temporalio/samples-go/nexus/service" - "go.temporal.io/sdk/workflow" + "github.com/temporalio/samples-go/nexus/service" + "go.temporal.io/sdk/workflow" ) const ( - TaskQueue = "my-caller-workflow-task-queue" - endpointName = "my-nexus-endpoint-name" + TaskQueue = "my-caller-workflow-task-queue" + endpointName = "my-nexus-endpoint-name" ) func EchoCallerWorkflow(ctx workflow.Context, message string) (string, error) { - c := workflow.NewNexusClient(endpointName, service.HelloServiceName) + c := workflow.NewNexusClient(endpointName, service.HelloServiceName) - fut := c.ExecuteOperation(ctx, service.EchoOperationName, service.EchoInput{Message: message}, workflow.NexusOperationOptions{}) + fut := c.ExecuteOperation(ctx, service.EchoOperationName, service.EchoInput{Message: message}, workflow.NexusOperationOptions{}) - var res service.EchoOutput - if err := fut.Get(ctx, &res); err != nil { - return "", err - } + var res service.EchoOutput + if err := fut.Get(ctx, &res); err != nil { + return "", err + } - return res.Message, nil + return res.Message, nil } func HelloCallerWorkflow(ctx workflow.Context, name string, language service.Language) (string, error) { - c := workflow.NewNexusClient(endpointName, service.HelloServiceName) - - fut := c.ExecuteOperation(ctx, service.HelloOperationName, service.HelloInput{Name: name, Language: language}, workflow.NexusOperationOptions{}) - var res service.HelloOutput - - // Optionally wait for the operation to be started. NexusOperationExecution will contain the operation token in - // case this operation is asynchronous, which is a handle that can be used to perform additional actions like - // cancelling an operation. - var exec workflow.NexusOperationExecution - if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { - return "", err - } - if err := fut.Get(ctx, &res); err != nil { - return "", err - } - - return res.Message, nil + c := workflow.NewNexusClient(endpointName, service.HelloServiceName) + + fut := c.ExecuteOperation(ctx, service.HelloOperationName, service.HelloInput{Name: name, Language: language}, workflow.NexusOperationOptions{}) + var res service.HelloOutput + + // Optionally wait for the operation to be started. NexusOperationExecution will contain the operation token in + // case this operation is asynchronous, which is a handle that can be used to perform additional actions like + // cancelling an operation. + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { + return "", err + } + if err := fut.Get(ctx, &res); err != nil { + return "", err + } + + return res.Message, nil } ``` @@ -363,37 +346,37 @@ After developing the caller Workflow, the next step is to register it with a Wor package main import ( - "log" - "os" + "log" + "os" - "github.com/temporalio/samples-go/nexus/caller" - "github.com/temporalio/samples-go/nexus/options" + "github.com/temporalio/samples-go/nexus/caller" + "github.com/temporalio/samples-go/nexus/options" - "go.temporal.io/sdk/client" - "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" ) func main() { - // The client and worker are heavyweight objects that should be created once per process. - clientOptions, err := options.ParseClientOptionFlags(os.Args[1:]) - if err != nil { - log.Fatalf("Invalid arguments: %v", err) - } - c, err := client.Dial(clientOptions) - if err != nil { - log.Fatalln("Unable to create client", err) - } - defer c.Close() - - w := worker.New(c, caller.TaskQueue, worker.Options{}) - - w.RegisterWorkflow(caller.EchoCallerWorkflow) - w.RegisterWorkflow(caller.HelloCallerWorkflow) - - err = w.Run(worker.InterruptCh()) - if err != nil { - log.Fatalln("Unable to start worker", err) - } + // The client and worker are heavyweight objects that should be created once per process. + clientOptions, err := options.ParseClientOptionFlags(os.Args[1:]) + if err != nil { + log.Fatalf("Invalid arguments: %v", err) + } + c, err := client.Dial(clientOptions) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + w := worker.New(c, caller.TaskQueue, worker.Options{}) + + w.RegisterWorkflow(caller.EchoCallerWorkflow) + w.RegisterWorkflow(caller.HelloCallerWorkflow) + + err = w.Run(worker.InterruptCh()) + if err != nil { + log.Fatalln("Unable to start worker", err) + } } ``` @@ -408,52 +391,52 @@ To initiate the caller Workflow, a starter program is required. package main import ( - "context" - "log" - "os" - "time" + "context" + "log" + "os" + "time" - "go.temporal.io/sdk/client" + "go.temporal.io/sdk/client" - "github.com/temporalio/samples-go/nexus/caller" - "github.com/temporalio/samples-go/nexus/options" - "github.com/temporalio/samples-go/nexus/service" + "github.com/temporalio/samples-go/nexus/caller" + "github.com/temporalio/samples-go/nexus/options" + "github.com/temporalio/samples-go/nexus/service" ) func main() { - clientOptions, err := options.ParseClientOptionFlags(os.Args[1:]) - if err != nil { - log.Fatalf("Invalid arguments: %v", err) - } - c, err := client.Dial(clientOptions) - if err != nil { - log.Fatalln("Unable to create client", err) - } - defer c.Close() - runWorkflow(c, caller.EchoCallerWorkflow, "Nexus Echo 👋") - runWorkflow(c, caller.HelloCallerWorkflow, "Nexus", service.ES) + clientOptions, err := options.ParseClientOptionFlags(os.Args[1:]) + if err != nil { + log.Fatalf("Invalid arguments: %v", err) + } + c, err := client.Dial(clientOptions) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + runWorkflow(c, caller.EchoCallerWorkflow, "Nexus Echo 👋") + runWorkflow(c, caller.HelloCallerWorkflow, "Nexus", service.ES) } func runWorkflow(c client.Client, workflow interface{}, args ...interface{}) { - ctx := context.Background() - workflowOptions := client.StartWorkflowOptions{ - ID: "nexus_hello_caller_workflow_" + time.Now().Format("20060102150405"), - TaskQueue: caller.TaskQueue, - } - - wr, err := c.ExecuteWorkflow(ctx, workflowOptions, workflow, args...) - if err != nil { - log.Fatalln("Unable to execute workflow", err) - } - log.Println("Started workflow", "WorkflowID", wr.GetID(), "RunID", wr.GetRunID()) - - // Synchronously wait for the workflow completion. - var result string - err = wr.Get(context.Background(), &result) - if err != nil { - log.Fatalln("Unable get workflow result", err) - } - log.Println("Workflow result:", result) + ctx := context.Background() + workflowOptions := client.StartWorkflowOptions{ + ID: "nexus_hello_caller_workflow_" + time.Now().Format("20060102150405"), + TaskQueue: caller.TaskQueue, + } + + wr, err := c.ExecuteWorkflow(ctx, workflowOptions, workflow, args...) + if err != nil { + log.Fatalln("Unable to execute workflow", err) + } + log.Println("Started workflow", "WorkflowID", wr.GetID(), "RunID", wr.GetRunID()) + + // Synchronously wait for the workflow completion. + var result string + err = wr.Get(context.Background(), &result) + if err != nil { + log.Fatalln("Unable get workflow result", err) + } + log.Println("Workflow result:", result) } ``` diff --git a/docs/develop/java/temporal-client.mdx b/docs/develop/java/temporal-client.mdx index 1c25c87eb5..40040f79fc 100644 --- a/docs/develop/java/temporal-client.mdx +++ b/docs/develop/java/temporal-client.mdx @@ -55,7 +55,7 @@ Use the `newLocalServiceStubs` method to create a stub that points to the Tempor [sample-apps/java/client/devserver-client-sample/src/main/java/clientsample/YourCallerApp.java](https://github.com/temporalio/documentation/blob/main/sample-apps/java/client/devserver-client-sample/src/main/java/clientsample/YourCallerApp.java) -```java {13,17} +```java {3,7} // ... // Create an instance that connects to a Temporal Service running on the local // machine, using the default port (7233) diff --git a/docs/develop/typescript/ai-sdk.mdx b/docs/develop/typescript/ai-sdk.mdx new file mode 100644 index 0000000000..6a4aab0a52 --- /dev/null +++ b/docs/develop/typescript/ai-sdk.mdx @@ -0,0 +1,275 @@ +--- +id: ai-sdk +title: Vercel AI SDK Integration +sidebar_label: Vercel AI SDK Integration +toc_max_heading_level: 2 +keywords: + - ai + - agents + - vercel +tags: + - AI SDK + - TypeScript SDK + - Temporal SDKs +description: Implement AI applications in TypeScript using the Temporal TypeScript SDK and the AI SDK. +--- + +Temporal's integration with [Vercel's AI SDK](https://ai-sdk.dev/) lets you use the AI SDK's API directly in Workflow +code while Temporal handles Durable Execution. + +Like all API calls, LLM API calls are non-deterministic. In a [Temporal Application](/glossary#temporal-application), +that means you cannot make LLM calls directly from a [Workflow](/glossary#workflow); they must run as +[Activities](/glossary#activity). The AI SDK plugin handles this automatically: when you call methods in the AI SDK such +as `generateText()`, the plugin wraps those calls in Activities behind the scenes. This preserves the Vercel AI SDK's +developer experience that you are already familiar with while Temporal handles Durable Execution for you. + +All code snippets in this guide are taken from the TypeScript SDK +[ai-sdk samples](https://github.com/temporalio/samples-typescript/tree/main/ai-sdk). Refer to the samples for the +complete code and run them locally. + +:::info + +The Vercel AI SDK Integration is in Pre-release. Refer to the +[Temporal product release stages guide](/evaluate/development-production-features/release-stages) for more information. + +::: + +## Prerequisites + +- This guide assumes you are already familiar with the Vercel AI SDK. If you aren't, refer to the + [Vercel AI SDK documentation](https://ai-sdk.dev/) for more details. +- If you are new to Temporal, we also recommend you read the [Understanding Temporal](/evaluate/understanding-temporal) + document or take the [Temporal 101](https://learn.temporal.io/courses/temporal_101/) course to understand the basics + of Temporal. +- Ensure you have set up your local development environment by following the + [Set up your local with the TypeScript SDK](/develop/typescript/set-up-your-local-typescript) guide. When you are + done, leave the Temporal Development Server running if you want to test your code locally. + +## Configure Workers to use the AI SDK + +Workers are the compute layer of a Temporal Application. They are responsible for executing the code that defines your +[Workflows](/glossary#workflow) and [Activities](/glossary#activity). Before you can execute a Workflow or Activity with +the Vercel AI SDK, you need to create a Worker and configure it to use the AI SDK plugin. + +Follow the steps below to configure your Worker. + +1. Install the `@temporalio/ai-sdk` package. + + ```bash + npm install @temporalio/ai-sdk + ``` + +2. Create a `worker.ts` file and configure the Worker to use the AI SDK plugin. + + ```ts {9-11} + import { openai } from '@ai-sdk/openai'; + import { AiSDKPlugin } from '@temporalio/ai-sdk'; + + //... other import statements, initializing a connection + // to the Temporal Service to be used by the Worker + + const worker = await Worker.create({ + plugins: [ + new AiSDKPlugin({ + modelProvider: openai, + }), + ], + connection, + namespace: 'default', + taskQueue: 'ai-sdk', + workflowsPath: require.resolve('./workflows'), + activities, + }); + + // ... code that runs the worker + ``` + + The `modelProvider` specifies which AI provider to use when creating models. Choose the provider that best suits your + needs. In the Worker options, you are also specifying that the Worker polls the `ai-sdk` Task Queue for work in the + `default` Namespace. Make sure that you configure your Client application to use the same Task Queue and Namespace. + +3. Run the Worker. This Worker will now poll the Temporal Service for work on the `ai-sdk` Task Queue in the `default` + Namespace until you stop it. + + ```bash + nodemon worker.ts + ``` + + You must ensure the Worker process has access to your API credentials. Most provider SDKs read credentials from + environment variables. Refer to the [Vercel AI SDK documentation](https://ai-sdk.dev/providers/ai-sdk-providers) for + instructions on how to set up your environment variables for the provider you chose. + + :::tip + + You only need to give provider credentials to the Worker process. The client application, meaning the application + that sends requests to the Temporal Service to start Workflow Executions, doesn't need to know about the credentials. + + ::: + +See the full example at [ai-sdk samples](https://github.com/temporalio/samples-typescript/tree/main/ai-sdk). + +## Develop a Simple Haiku Agent + +To help you get started, you can develop a simple Haiku Agent that generates haikus based on a prompt. + +If you weren't using Temporal, you would write code like this to generate a haiku: + +```ts +import { generateText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +async function haikuAgent(prompt: string): Promise { + const result = await generateText({ + model: openai('gpt-4o-mini'), + prompt, + system: 'You only respond in haikus.', + }); + return result.text; +} +``` + +To add Durable Execution to your agent, implement the agent as a Temporal Workflow. Use the AI SDK as you normally +would, but pass `temporalProvider.languageModel()` as the model. The string you provide (like `'gpt-4o-mini'`) is passed +to your configured `modelProvider` to create the model. + +```ts {2,6} +import { generateText } from 'ai'; +import { temporalProvider } from '@temporalio/ai-sdk'; + +export async function haikuAgent(prompt: string): Promise { + const result = await generateText({ + model: temporalProvider.languageModel('gpt-4o-mini'), + prompt, + system: 'You only respond in haikus.', + }); + return result.text; +} +``` + +With only two line changes, you have added Durable Execution to your agent. Your agent now gets automatic retries, +timeouts, and the ability to run for extended periods without losing state if the process crashes. + +## Provide your durable agent with tools + +The Vercel AI SDK lets you provide tools to your agents, and when the model calls them, they execute in the Workflow. +Since tool functions run in Workflow context, they must follow Workflow rules. That means they must call Activities or +Child Workflows to perform non-deterministic operations like API calls. + +For example, if you want to call an external API to get the weather, you would implement it as an Activity and call it +from the tool function. The following is an example of an Activity that gets the weather for a given location: + + +[ai-sdk/src/activities.ts](https://github.com/temporalio/samples-typescript/blob/main/ai-sdk/src/activities.ts) +```ts +export async function getWeather(input: { + location: string; +}): Promise<{ city: string; temperatureRange: string; conditions: string }> { + console.log('Activity execution'); + return { + city: input.location, + temperatureRange: '14-20C', + conditions: 'Sunny with wind.', + }; +} +``` + + +Then in your agent implementation, provide the tool to the model using the `tools` option and instruct the model to use +the tool when needed. + +```ts {15-23} +import { proxyActivities } from '@temporalio/workflow'; +import { generateText, tool } from 'ai'; +import { temporalProvider } from '@temporalio/ai-sdk'; +import { z } from 'zod'; + +const { getWeather } = proxyActivities({ + startToCloseTimeout: '1 minute', +}); + +export async function toolsAgent(question: string): Promise { + const result = await generateText({ + model: temporalProvider.languageModel('gpt-4o-mini'), + prompt: question, + system: 'You are a helpful agent.', + tools: { + getWeather: tool({ + description: 'Get the weather for a given city', + inputSchema: z.object({ + location: z.string().describe('The location to get the weather for'), + }), + execute: getWeather, + }), + }, + stopWhen: stepCountIs(5), + }); + return result.text; +} +``` + +## Integrate with Model Context Protocol (MCP) servers + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that lets AI applications connect +to external tools and data sources. Calls to MCP servers, being calls to external APIs, are non-deterministic and would +usually need to be implemented as Activities. The Temporal AI SDK integration handles this for you and provides a +built-in implementation of a stateless MCP client that you can use inside Workflows. + +Follow the steps below to integrate your agent with an MCP server. + +1. Create a connection to the MCP servers using the `experimental_createMCPClient` function from the `@ai-sdk/mcp` + package. You can register multiple MCP servers by providing multiple factory functions in `mcpClientFactories`. + + ```ts + import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'; + import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + + const mcpClientFactories = { + testServer: () => + createMCPClient({ + transport: new StdioClientTransport({ + command: 'node', + args: ['lib/mcp-server.js'], + }), + }), + }; + ``` + + The example uses `StdioClientTransport` as the transport mechanisms for client-server communication. Each time the + Worker processes a Task that requires communication with the MCP server, it will start the server process and connect + to it as required by the Task. + +2. Configure the Worker to use the MCP client factories. + + ```ts {5} + const worker = await Worker.create({ + plugins: [ + new AiSDKPlugin({ + modelProvider: openai, + mcpClientFactories + }), + ]}, + ... + ); + ``` + +3. In your agent Workflow, use `TemporalMCPClient` to get tools from the MCP server by referencing it by name: + + ```ts {4-5,9} + import { TemporalMCPClient, temporalProvider } from '@temporalio/ai-sdk'; + + export async function mcpAgent(prompt: string): Promise { + const mcpClient = new TemporalMCPClient({ name: 'testServer' }); + const tools = await mcpClient.tools(); + const result = await generateText({ + model: temporalProvider.languageModel('gpt-4o-mini'), + prompt, + tools, + system: 'You are a helpful agent, You always use your tools when needed.', + stopWhen: stepCountIs(5), + }); + return result.text; + } + ``` + + Both listing tools and calling them run as Activities behind the scenes, giving you automatic retries, timeouts, and + full observability. diff --git a/docs/develop/typescript/cancellation.mdx b/docs/develop/typescript/cancellation.mdx index 09a6117974..b02fb7b441 100644 --- a/docs/develop/typescript/cancellation.mdx +++ b/docs/develop/typescript/cancellation.mdx @@ -67,9 +67,7 @@ To simplify checking for cancellation, use the ### Internal cancellation example - [packages/test/src/workflows/cancel-timer-immediately.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/cancel-timer-immediately.ts) - ```ts import { CancelledFailure, CancellationScope, sleep } from '@temporalio/workflow'; @@ -90,15 +88,12 @@ export async function cancelTimer(): Promise { } } ``` - Alternatively, the preceding can be written as the following. - [packages/test/src/workflows/cancel-timer-immediately-alternative-impl.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/cancel-timer-immediately-alternative-impl.ts) - ```ts import { CancelledFailure, CancellationScope, sleep } from '@temporalio/workflow'; @@ -117,7 +112,6 @@ export async function cancelTimerAltImpl(): Promise { } } ``` - ### External cancellation example @@ -127,9 +121,7 @@ The following code shows how to handle Workflow cancellation by an external clie {/* TODO: add a sample here of how this Workflow could be cancelled using a WorkflowHandle */} - [packages/test/src/workflows/handle-external-workflow-cancellation-while-activity-running.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/handle-external-workflow-cancellation-while-activity-running.ts) - ```ts import { CancellationScope, proxyActivities, isCancellation } from '@temporalio/workflow'; import type * as activities from '../activities'; @@ -153,7 +145,6 @@ export async function handleExternalWorkflowCancellationWhileActivityRunning(url } } ``` - ### nonCancellable example @@ -161,9 +152,7 @@ export async function handleExternalWorkflowCancellationWhileActivityRunning(url `CancellationScope.nonCancellable` prevents cancellation from propagating to children. - [activities-cancellation-heartbeating/src/cancellation-scopes.ts](https://github.com/temporalio/samples-typescript/blob/main/activities-cancellation-heartbeating/src/cancellation-scopes.ts) - ```ts export async function nonCancellable(url: string): Promise { // Prevent Activity from being cancelled and await completion. @@ -171,7 +160,6 @@ export async function nonCancellable(url: string): Promise { return CancellationScope.nonCancellable(() => httpGetJSON(url)); } ``` - ### withTimeout example @@ -180,9 +168,7 @@ A common operation is to cancel one or more Activities if a deadline elapses. `w `CancellationScope` that is automatically cancelled after a timeout. - [packages/test/src/workflows/multiple-activities-single-timeout.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/multiple-activities-single-timeout.ts) - ```ts import { CancellationScope, proxyActivities } from '@temporalio/workflow'; import type * as activities from '../activities'; @@ -197,7 +183,6 @@ export function multipleActivitiesSingleTimeout(urls: string[], timeoutMs: numbe return CancellationScope.withTimeout(timeoutMs, () => Promise.all(urls.map((url) => httpGetJSON(url)))); } ``` - ### scope.cancelRequested @@ -205,9 +190,7 @@ export function multipleActivitiesSingleTimeout(urls: string[], timeoutMs: numbe You can await `cancelRequested` to make a Workflow aware of cancellation while waiting on `nonCancellable` scopes. - [packages/test/src/workflows/cancel-requested-with-non-cancellable.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/cancel-requested-with-non-cancellable.ts) - ```ts import { CancellationScope, CancelledFailure, proxyActivities } from '@temporalio/workflow'; import type * as activities from '../activities'; @@ -232,7 +215,6 @@ export async function resumeAfterCancellation(url: string): Promise { return result; } ``` - ### Cancellation scopes and callbacks @@ -242,9 +224,7 @@ the rare case that code uses callbacks and needs to handle cancellation, a callb `CancellationScope.cancelRequested` promise. - [packages/test/src/workflows/cancellation-scopes-with-callbacks.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/cancellation-scopes-with-callbacks.ts) - ```ts import { CancellationScope } from '@temporalio/workflow'; @@ -259,7 +239,6 @@ export async function cancellationScopesWithCallbacks(): Promise { }); } ``` - ### Nesting cancellation scopes @@ -267,9 +246,7 @@ export async function cancellationScopesWithCallbacks(): Promise { You can achieve complex flows by nesting cancellation scopes. - [packages/test/src/workflows/nested-cancellation.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/nested-cancellation.ts) - ```ts import { CancellationScope, proxyActivities, isCancellation } from '@temporalio/workflow'; @@ -293,7 +270,6 @@ export async function nestedCancellation(url: string): Promise { }); } ``` - ### Sharing promises between scopes @@ -302,9 +278,7 @@ Operations like Timers and Activities are cancelled by the cancellation scope th these operations can be awaited in different scopes. - [activities-cancellation-heartbeating/src/cancellation-scopes.ts](https://github.com/temporalio/samples-typescript/blob/main/activities-cancellation-heartbeating/src/cancellation-scopes.ts) - ```ts export async function sharedScopes(): Promise { // Start activities in the root scope @@ -323,13 +297,10 @@ export async function sharedScopes(): Promise { // await Promise.all([p1, p2]); } ``` - - [activities-cancellation-heartbeating/src/cancellation-scopes.ts](https://github.com/temporalio/samples-typescript/blob/main/activities-cancellation-heartbeating/src/cancellation-scopes.ts) - ```ts export async function shieldAwaitedInRootScope(): Promise { let p: Promise | undefined = undefined; @@ -341,7 +312,6 @@ export async function shieldAwaitedInRootScope(): Promise { return p; } ``` - ## Cancel an Activity from a Workflow {#cancel-an-activity} diff --git a/docs/develop/typescript/index.mdx b/docs/develop/typescript/index.mdx index e86f02269a..1e8ea31767 100644 --- a/docs/develop/typescript/index.mdx +++ b/docs/develop/typescript/index.mdx @@ -2,7 +2,9 @@ id: index title: TypeScript SDK developer guide sidebar_label: TypeScript SDK -description: This guide offers a thorough overview of structures, features, and best practices for developing with Temporal's TypeScript SDK, including Workflows, Client, Testing, Failure Detection, and more. +description: + This guide offers a thorough overview of structures, features, and best practices for developing with Temporal's + TypeScript SDK, including Workflows, Client, Testing, Failure Detection, and more. toc_max_heading_level: 4 keywords: - typescript @@ -15,8 +17,7 @@ import * as Components from '@site/src/components'; ![TypeScript SDK Banner](/img/assets/banner-typescript-temporal.png) -:::info TYPESCRIPT SPECIFIC RESOURCES -Build Temporal Applications with the TypeScript SDK. +:::info TYPESCRIPT SPECIFIC RESOURCES Build Temporal Applications with the TypeScript SDK. **Temporal TypeScript Technical Resources:** @@ -30,11 +31,13 @@ Build Temporal Applications with the TypeScript SDK. - [Temporal TypeScript Community Slack](https://temporalio.slack.com/archives/C01DKSMU94L) - [TypeScript SDK Forum](https://community.temporal.io/tag/typescript-sdk) - ::: + +::: ## [Core application](/develop/typescript/core-application) -Use the essential components of a Temporal Application (Workflows, Activities, and Workers) to build and run a Temporal application. +Use the essential components of a Temporal Application (Workflows, Activities, and Workers) to build and run a Temporal +application. - [Develop a Basic Workflow](/develop/typescript/core-application#develop-workflows) - [Develop a Basic Activity](/develop/typescript/core-application#develop-activities) @@ -79,7 +82,8 @@ Send messages to and read the state of Workflow Executions. Interrupt a Workflow Execution with a Cancel or Terminate action. - [Cancellation scopes in Typescript](/develop/typescript/cancellation#cancellation-scopes) -- [Reset a Workflow](/develop/typescript/cancellation#reset): Resume a Workflow Execution from an earlier point in its Event History. +- [Reset a Workflow](/develop/typescript/cancellation#reset): Resume a Workflow Execution from an earlier point in its + Event History. ## [Asynchronous Activity Completion](/develop/typescript/asynchronous-activity-completion) @@ -123,9 +127,10 @@ Use compression, encryption, and other data handling by implementing custom conv - [Custom Payload Codec](/develop/typescript/converters-and-encryption#custom-payload-conversion) -## Temporal Nexus +## [Temporal Nexus](/develop/typescript/nexus) -The [Temporal Nexus](/develop/typescript/nexus) feature guide shows how to use Temporal Nexus to connect durable executions within and across Namespaces using a Nexus Endpoint, a Nexus Service contract, and Nexus Operations. +The Temporal Nexus feature guide shows how to use Temporal Nexus to connect durable +executions within and across Namespaces using a Nexus Endpoint, a Nexus Service contract, and Nexus Operations. - [Create a Nexus Endpoint to route requests from caller to handler](/develop/typescript/nexus#create-nexus-endpoint) - [Define the Nexus Service contract](/develop/typescript/nexus#define-nexus-service-contract) @@ -164,3 +169,9 @@ Manage inbound and outbound SDK calls, enhance tracing, and add authorization to - [How to implement interceptors](/develop/typescript/interceptors#interceptors) - [Register an interceptor](/develop/typescript/interceptors#register-interceptor) + +## [Vercel AI SDK Integration](/develop/typescript/ai-sdk) + +Integrate the Vercel AI SDK with Temporal to build durable AI agents and AI-powered applications. + +- [Vercel AI SDK Integration](/develop/typescript/ai-sdk) diff --git a/sidebars.js b/sidebars.js index e9ae7cc425..8e3fe05780 100644 --- a/sidebars.js +++ b/sidebars.js @@ -245,6 +245,7 @@ module.exports = { 'develop/typescript/child-workflows', 'develop/typescript/continue-as-new', 'develop/typescript/interceptors', + 'develop/typescript/ai-sdk', ], }, { @@ -627,11 +628,11 @@ module.exports = { id: 'best-practices/index', }, items: [ - "best-practices/managing-namespace", - "best-practices/managing-aps-limits", - "best-practices/cloud-access-control", - "best-practices/security-controls", - "best-practices/worker", + 'best-practices/managing-namespace', + 'best-practices/managing-aps-limits', + 'best-practices/cloud-access-control', + 'best-practices/security-controls', + 'best-practices/worker', ], }, {