Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bcb181b
Initial WIP on ai-sdk integration
tconley1428 Sep 16, 2025
a04854e
Tools and a basic hello workflow now work
tconley1428 Sep 17, 2025
925bd9d
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 Nov 10, 2025
d45be90
Leveraging plugin for AI SDK integration
tconley1428 Nov 3, 2025
c7af3f3
Some changes
tconley1428 Nov 10, 2025
d334684
Add experimental telemetry validation
tconley1428 Nov 12, 2025
a285bcc
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 Nov 12, 2025
90c745a
Remove AI markdown
tconley1428 Nov 12, 2025
fe533f2
Linting and project structure
tconley1428 Nov 12, 2025
34fc1ca
Update dependencies
tconley1428 Nov 12, 2025
c01f8bb
Linting and project structure
tconley1428 Nov 12, 2025
a330d99
Fix build
tconley1428 Nov 12, 2025
21bc8d5
Linting
tconley1428 Nov 12, 2025
181727c
Docstrings
tconley1428 Nov 13, 2025
e7faeed
Merge branch 'main' into ai/initial
tconley1428 Nov 13, 2025
844be62
Clean up
tconley1428 Nov 13, 2025
843b112
Fix core version
tconley1428 Nov 13, 2025
839dabb
Revert lock changes to minimum
tconley1428 Nov 13, 2025
89948bd
Adding MCP and activity config support
tconley1428 Nov 14, 2025
fd9fbbc
Linting
tconley1428 Nov 14, 2025
bd5d9be
Fix error suppression location after lint
tconley1428 Nov 14, 2025
613f32e
Test fix of the fetch-esm CI issue
mjameswh Nov 18, 2025
022d814
Bump GHA mac runners to macos15
mjameswh Nov 18, 2025
a83c35d
Try using latest mcp server
tconley1428 Nov 18, 2025
bae6997
Skip MCP test for now
tconley1428 Nov 18, 2025
5a89bd4
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 Nov 21, 2025
5396f36
:wqMerge remote-tracking branch 'origin/main' into ai/initial
tconley1428 Nov 21, 2025
fafb2ac
Fix pnpm rebase
tconley1428 Nov 21, 2025
4a66eb1
PR Feedback
tconley1428 Nov 24, 2025
e3f4d70
Update lockfile
tconley1428 Nov 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"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.

"@temporalio/client": "workspace:*",
"@temporalio/cloud": "workspace:*",
"@temporalio/common": "workspace:*",
Expand All @@ -56,9 +57,9 @@
"temporalio": "file:packages/meta"
},
"devDependencies": {
"@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?

"@opentelemetry/core": "1.25.1",
"@opentelemetry/sdk-node": "0.52.1",
"@tsconfig/node18": "^18.2.4",
"@types/fs-extra": "^11.0.4",
"@types/ms": "^0.7.34",
Expand Down
47 changes: 47 additions & 0 deletions packages/ai-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@temporalio/ai-sdk",
"version": "1.13.2",
"description": "Temporal AI SDK integration package",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"keywords": [
"temporal",
"workflow",
"ai",
"ai-sdk",
"llm"
],
"author": "Temporal Technologies Inc. <sdk@temporal.io>",
"license": "MIT",
"dependencies": {
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/mcp": "^0.0.8",
"@temporalio/plugin": "workspace:*",
"@temporalio/workflow": "workspace:*",
"@ungap/structured-clone": "^1.3.0",
"headers-polyfill": "^4.0.3",
"web-streams-polyfill": "^4.2.0"
},
"peerDependencies": {
"ai": "^5.0.91"
},
"engines": {
"node": ">= 18.0.0"
},
"bugs": {
"url": "https://github.com/temporalio/sdk-typescript/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/temporalio/sdk-typescript.git",
"directory": "packages/ai-sdk"
},
"homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/ai-sdk",
"publishConfig": {
"access": "public"
},
"files": [
"src",
"lib"
]
}
88 changes: 88 additions & 0 deletions packages/ai-sdk/src/activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
LanguageModelV2CallOptions,
LanguageModelV2CallWarning,
LanguageModelV2Content,
LanguageModelV2FinishReason,
LanguageModelV2ResponseMetadata,
LanguageModelV2Usage,
ProviderV2,
SharedV2Headers,
SharedV2ProviderMetadata,
} from '@ai-sdk/provider';
import type { experimental_MCPClient as MCPClient } from '@ai-sdk/mcp';
import type { ToolCallOptions } from 'ai';

export interface ListToolResult {
description?: string;
inputSchema: any;
}

export interface ListToolArgs {
clientArgs?: any;
}

export interface CallToolArgs {
clientArgs?: any;
name: string;
args: any;
options: ToolCallOptions;
}

/**
* Creates Temporal activities for AI model invocation using the provided AI SDK provider.
* These activities allow workflows to call AI models while maintaining Temporal's
* execution guarantees and replay safety.
*
* @param provider The AI SDK provider to use for model invocations
* @param mcpClientFactory
* @returns An object containing the activity functions
*
* @experimental The AI SDK integration is an experimental feature; APIs may change without notice.
*/
export const createActivities = (provider: ProviderV2, mcpClientFactory?: (_: any) => Promise<MCPClient>): object => {
const activities = {
async invokeModel(
modelId: string,
options: LanguageModelV2CallOptions
): Promise<{
content: Array<LanguageModelV2Content>;
finishReason: LanguageModelV2FinishReason;
usage: LanguageModelV2Usage;
providerMetadata?: SharedV2ProviderMetadata;
request?: { body?: unknown };
response?: LanguageModelV2ResponseMetadata & { headers?: SharedV2Headers; body?: unknown };
warnings: Array<LanguageModelV2CallWarning>;
}> {
const model = provider.languageModel(modelId);
return await model.doGenerate(options);
},
};
if (mcpClientFactory === undefined) {
return activities;
}
return {
...activities,
async listTools(args: ListToolArgs): Promise<Record<string, ListToolResult>> {
const mcpClient = await mcpClientFactory(args.clientArgs);
const tools = await mcpClient.tools();

return Object.fromEntries(
Object.entries(tools).map(([k, v]) => [
k,
{
description: v.description,
inputSchema: v.inputSchema,
},
])
);
},
async callTool(args: CallToolArgs): Promise<any> {
const mcpClient = await mcpClientFactory(args.clientArgs);
const tools = await mcpClient.tools();
if (!(args.name in tools)) {
throw new Error(`Tool ${args.name} not found.`);
}
return tools[args.name].execute(args.args, args.options);
},
};
};
7 changes: 7 additions & 0 deletions packages/ai-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// eslint-disable-next-line import/no-unassigned-import
import './load-polyfills';

export * from './mcp';
export * from './plugin';
export * from './provider';
export * from './testing';
18 changes: 18 additions & 0 deletions packages/ai-sdk/src/load-polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Headers } from 'headers-polyfill';
import { inWorkflowContext } from '@temporalio/workflow';

if (inWorkflowContext()) {
// Apply Headers polyfill
if (typeof globalThis.Headers === 'undefined') {
globalThis.Headers = Headers;
}

// eslint-disable-next-line @typescript-eslint/no-require-imports,import/no-unassigned-import
require('web-streams-polyfill/polyfill');
// Attach the polyfill as a Global function
if (!('structuredClone' in globalThis)) {
// eslint-disable-next-line @typescript-eslint/no-require-imports,import/no-unassigned-import
const structuredClone = require('@ungap/structured-clone');
globalThis.structuredClone = structuredClone.default;
}
}
40 changes: 40 additions & 0 deletions packages/ai-sdk/src/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ToolSet } from 'ai';
import * as workflow from '@temporalio/workflow';
import type { ActivityOptions } from '@temporalio/workflow';
import type { ListToolResult } from './activities';

export class TemporalMCPClient {
constructor(
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
readonly clientArgs?: any,
readonly options?: ActivityOptions
) {}

async tools(): Promise<ToolSet> {
const tools: Record<string, ListToolResult> = await workflow
.proxyActivities(this.options ?? { startToCloseTimeout: '10 minutes' })
.listTools({ clientArgs: this.clientArgs });
return Object.fromEntries(
Object.entries(tools).map(([toolName, toolResult]) => [
toolName,
{
execute: async (args: any, options) =>
await workflow
.proxyActivities({
summary: toolName,
...(this.options ?? { startToCloseTimeout: '10 minutes' }),
})
.callTool({ name: toolName, args, options, clientArgs: this.clientArgs }),
inputSchema: {
...toolResult.inputSchema,
_type: undefined,
validate: undefined,
[Symbol.for('vercel.ai.schema')]: true,
[Symbol.for('vercel.ai.validator')]: true,
},
type: 'dynamic',
},
])
);
}
}
24 changes: 24 additions & 0 deletions packages/ai-sdk/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ProviderV2 } from '@ai-sdk/provider';
import type { experimental_MCPClient as MCPClient } from '@ai-sdk/mcp';
import { SimplePlugin } from '@temporalio/plugin';
import { createActivities } from './activities';

export interface AiSDKPluginOptions {
modelProvider: ProviderV2;
mcpClientFactory?: (args?: any) => Promise<MCPClient>;
}

/**
* A Temporal plugin that integrates AI SDK providers for use in workflows.
* This plugin creates activities that allow workflows to invoke AI models.
*
* @experimental The AI SDK plugin is an experimental feature; APIs may change without notice.
*/
export class AiSDKPlugin extends SimplePlugin {
constructor(options: AiSDKPluginOptions) {
super({
name: 'AiSDKPlugin',
activities: createActivities(options.modelProvider, options.mcpClientFactory),
});
}
}
92 changes: 92 additions & 0 deletions packages/ai-sdk/src/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
EmbeddingModelV2,
ImageModelV2,
LanguageModelV2,
LanguageModelV2CallOptions,
LanguageModelV2CallWarning,
LanguageModelV2Content,
LanguageModelV2FinishReason,
LanguageModelV2ResponseMetadata,
LanguageModelV2Usage,
ProviderV2,
SharedV2Headers,
SharedV2ProviderMetadata,
} from '@ai-sdk/provider';
import * as workflow from '@temporalio/workflow';
import { ActivityOptions } from '@temporalio/workflow';

/**
* A language model implementation that delegates AI model calls to Temporal activities.
* This allows workflows to invoke AI models through the Temporal execution model.
*
* @experimental The AI SDK integration is an experimental feature; APIs may change without notice.
*/
export class TemporalLanguageModel implements LanguageModelV2 {
readonly specificationVersion = 'v2';
readonly provider = 'temporal';
readonly supportedUrls = {};

constructor(
readonly modelId: string,
readonly options?: ActivityOptions
) {}

async doGenerate(options: LanguageModelV2CallOptions): Promise<{
content: Array<LanguageModelV2Content>;
finishReason: LanguageModelV2FinishReason;
usage: LanguageModelV2Usage;
providerMetadata?: SharedV2ProviderMetadata;
request?: { body?: unknown };
response?: LanguageModelV2ResponseMetadata & { headers?: SharedV2Headers; body?: unknown };
warnings: Array<LanguageModelV2CallWarning>;
}> {
const result = await workflow
.proxyActivities(this.options ?? { startToCloseTimeout: '10 minutes' })
.invokeModel(this.modelId, options);
if (result === undefined) {
throw new Error('Received undefined response from model activity.');
}
if (result.response !== undefined) {
result.response.timestamp = new Date(result.response.timestamp);
}
return result;
}

doStream(_options: LanguageModelV2CallOptions): PromiseLike<{
stream: any;
request?: { body?: unknown };
response?: { headers?: SharedV2Headers };
}> {
throw new Error('Streaming not supported.');
}
}

/**
* A Temporal-specific provider implementation that creates AI models which execute
* through Temporal activities. This provider integrates AI SDK models with Temporal's
* execution model to ensure reliable, durable AI model invocations.
*
* @experimental The AI SDK integration is an experimental feature; APIs may change without notice.
*/
export class TemporalProvider implements ProviderV2 {
constructor(readonly options?: ActivityOptions) {}

imageModel(_modelId: string): ImageModelV2 {
throw new Error('Not implemented');
}

languageModel(modelId: string): LanguageModelV2 {
return new TemporalLanguageModel(modelId, this.options);
}

textEmbeddingModel(_modelId: string): EmbeddingModelV2<string> {
throw new Error('Not implemented');
}
}

/**
* A singleton instance of TemporalProvider for convenient use in applications.
*
* @experimental The AI SDK integration is an experimental feature; APIs may change without notice.
*/
export const temporalProvider: TemporalProvider = new TemporalProvider();
Loading
Loading