Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ jobs:
continue-on-error: true

# Sample 3: fetch-esm to local server
#
# DO NOT MERGE!!!
#
# The url to the sample repo below has been modified to point to the "esm-improve" branch for testing.
# Restore this to the main branch before merging this PR.
#
- name: Instantiate sample project using verdaccio artifacts - Fetch ESM
run: |
node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/fetch-esm --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-fetch-esm
Expand Down
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:*",
"@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",
"@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. <[email protected]>",
"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"
]
}
85 changes: 85 additions & 0 deletions packages/ai-sdk/src/activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 { 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();
return tools[args.name].execute(args.args, args.options);
},
};
};
9 changes: 9 additions & 0 deletions packages/ai-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// eslint-disable-next-line import/no-unassigned-import
import 'ai';
// 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 { ToolSet } from 'ai';
import * as workflow from '@temporalio/workflow';
import { ActivityOptions } from '@temporalio/workflow';
import { 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 { 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