Skip to content

Conversation

@tconley1428
Copy link
Contributor

@tconley1428 tconley1428 commented Oct 2, 2025

What was changed

Added a new AiSDKPlugin to support running AI SDK in workflows. The plugin registers new activities for model invocation and optionally MCP tools. Workflows can then use temporalProvider to create models in workflows.

Why?

Checklist

  1. Closes

  2. How was this tested:
    Added a new test suite with local model coverage. AI_SDK_REMOTE_TESTS env variable is available to run against a real endpoint.

  3. Any docs updates needed?

@tconley1428 tconley1428 changed the title Ai/initial Integration with AI SDK Nov 13, 2025
@tconley1428 tconley1428 marked this pull request as ready for review November 17, 2025 18:34
@tconley1428 tconley1428 requested a review from a team as a code owner November 17, 2025 18:34
Copy link
Member

@chris-olszewski chris-olszewski left a comment

Choose a reason for hiding this comment

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

Apologies for slow review. My biggest concern is usage of any for some of the user facing types where ideally we would want to rely on the type system to prevent mismatching args.

"@opentelemetry/api": "^1.7.0",
"@opentelemetry/core": "^1.19.0",
"@opentelemetry/sdk-node": "^0.46.0",
"@opentelemetry/api": "1.9.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason we need these to have pinned versions instead of version ranges?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chris said the same, I thought I updated it. Will check again

};

const compiler = webpack(this.webpackConfigHook(options));
const compiler = webpack(this.webpackConfigHook(options))!;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any reason for the non-null assertion operator on this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have local issues with this for some reason, meant to remove it for PR.

@@ -0,0 +1,5648 @@
lockfileVersion: '9.0'
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be no lockfile in this directory. It's probably the result of a mistake when you rebased your branch over Chris' works on switching the repo to PNPM.

Just delete the file, and the sibling node_modules directory if you have one on your local machine, then rerun pnpm i from the root of the project.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good

"docs": "cd packages/docs && pnpm run maybe-install-deps-and-build-docs"
},
"dependencies": {
"@temporalio/ai-sdk": "workspace:*",
Copy link
Contributor

Choose a reason for hiding this comment

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

You also need to add to the meta package's pacakge.json, tsconfig.json and index.ts files. These control what's included in generated API docs.

"@opentelemetry/sdk-trace-base": "^1.19.0",
"@opentelemetry/semantic-conventions": "^1.19.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@opentelemetry/api": "1.9.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we avoid pinned dependencies here?

import { Worker } from './helpers';
import EventType = temporal.api.enums.v1.EventType;

const remoteTests = !!process.env.AI_SDK_REMOTE_TESTS;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know that !!"false" is true?

Please use the following idiom instead (or look at isSet in worker-options.ts).
That will ensure that this variable follows the same thruthness conventions as our other env variables.

Suggested change
const remoteTests = !!process.env.AI_SDK_REMOTE_TESTS;
const remoteTests = ['1', 't', 'true'].includes((process.env.AI_SDK_REMOTE_TESTS ?? 'false').toLowerCase()),

});

test('Hello world agent responds in haikus', async (t) => {
t.timeout(120 * 1000);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit worried about setting individual test timeout to 2 minutes. That means that eventual bugs that would prevent a worker from shutting down will cause the CI job to time out while running this test (which runs very early because ai starts with an a), with almost no context on what failed.

I suppose that's only pertinent when remoteTests is true, right?
And remoteTests would be false in CI, right?
If so, can we only bump up the test timeout to 2 minutes when remote tests are enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, we could do that. I don't currently have it set to run the remoteTests at all in CI.

async invokeModel(
modelId: string,
options: LanguageModelV2CallOptions
): Promise<{
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this inline type is duplicated in doGenerate, and possibly other places. Can we extract that to a single name ddefinition?

*
* @experimental The AI SDK integration is an experimental feature; APIs may change without notice.
*/
export const createActivities = (provider: ProviderV2, mcpClientFactory?: (_: any) => Promise<MCPClient>): object => {
Copy link
Contributor

Choose a reason for hiding this comment

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

A few things:

  • Prefer the function syntax rather than the const lambda form unless there's a reason for the later;
  • Make a named type definition for McpClientFactory.
  • Make the arg's type unknown rather than any.
Suggested change
export const createActivities = (provider: ProviderV2, mcpClientFactory?: (_: any) => Promise<MCPClient>): object => {
// In mcp.ts
type McpClientFactory = (args: unknown) => Promise<MCPClient>;
export function createActivities(provider: ProviderV2, mcpClientFactory?: McpClientFactory): object {

*/
export const createActivities = (provider: ProviderV2, mcpClientFactory?: (_: any) => Promise<MCPClient>): object => {
const activities = {
async invokeModel(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we not namespace those activities somehow, to avoid eventual conflicts with user defined activities, or those from other plugins? Could be something like e.g. __ai_temporal_invokeModel?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We didn't in Python, and it does come with the downside of being UI clutter since the name is shown there.

return await model.doGenerate(options);
},
};
if (mcpClientFactory === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should still register the listTools and callTool activities even if there is no actual tool provided. The workflow side has no way to figure this out. listTools could return an empty array, and callTool could would throw the Tool xxcxc not found error, or even No tool registered.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That brings some reminders up for me. I need to reevaluate this to allow multiple MCP servers, that will change some things.

) {}

async tools(): Promise<ToolSet> {
const tools: Record<string, ListToolResult> = await workflow
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it the way you did this in Python? I mean, listTools being an activity call, which is call everytime tools() is called?

A few things that can be considered to make this more efficient:

  • Cache tools after the first call
  • Make listTools a local activity call instead of a regular activity;
  • Extract the list of tools at bundle time and inject it as a static value into the workflow bundle (so no activity call needed to list tools).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is one of the implementations we provided. Caching is a potential addition, but tools() is generally called once.

Extract the list of tools at bundle time and inject it as a static value into the workflow bundle (so no activity call needed to list tools).

I don't believe this is valid as the tool list is not known to be static.

import { SimplePlugin } from '@temporalio/plugin';
import { createActivities } from './activities';

export interface AiSDKPluginOptions {
Copy link
Contributor

Choose a reason for hiding this comment

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

Need an experimental annotation here. And also some docs.

*
* @experimental The AI SDK plugin is an experimental feature; APIs may change without notice.
*/
export class AiSDKPlugin extends SimplePlugin {
Copy link
Contributor

Choose a reason for hiding this comment

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

nil: This is inconsistent. AI and SDK are both acronyms, and so both shoujld either be written with only the first letter in capital, or both entirely in uppercase.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same for options.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point

warnings: Array<LanguageModelV2CallWarning>;
}> {
const result = await workflow
.proxyActivities(this.options ?? { startToCloseTimeout: '10 minutes' })
Copy link
Contributor

Choose a reason for hiding this comment

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

The ?? here will not guarantee that startToCloseTimeout is set, as this.options could be an empty object or similar.

Might be better to do { startToCloseTimeout: '10 minutes' , ...this.options } instead.

I'd also suggest doing that in the class constructor instead of here, but no strong strong opinion on that one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think that's what I was thinking but not what I did, good call.

@@ -0,0 +1,88 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import {
import type {

@@ -0,0 +1,92 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import {
import type {

@@ -0,0 +1,99 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import {
import type {

"author": "Temporal Technologies Inc. <sdk@temporal.io>",
"license": "MIT",
"dependencies": {
"@ai-sdk/provider": "^2.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

I was able to build the project with only type imports to the @ai/* packages, so it should be possible to lower these two dependencies to dev + peer dependencies instead of regular ones.

*
* @experimental The AI SDK integration is an experimental feature; APIs may change without notice.
*/
export class TestProvider implements ProviderV2 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not clear why it is pertinent for this mock to be part of our public API. That would be used in a user's tests on the worker side, not in workflow, so there's no concern regarding eventual import of non-workflow-safe code.

Doesn't the ai package itself provide this kind of test provider? Or some other external package that we could recommend instead of taking responsibility for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They have something kind of similar, although it doesn't do exactly what I needed. In Python we did expose it because OpenAI had basically no unit test support, but I could see an argument here for keeping it in test at least until there's a strong need.

Copy link
Contributor

@mjameswh mjameswh left a comment

Choose a reason for hiding this comment

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

See my previous comments.

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.

4 participants