Skip to content

Commit 0f932eb

Browse files
Add @trigger.dev/ai package with chat transport and tests
Co-authored-by: Eric Allam <eric@trigger.dev>
1 parent b4e08bd commit 0f932eb

File tree

9 files changed

+1210
-16
lines changed

9 files changed

+1210
-16
lines changed

packages/ai/package.json

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"name": "@trigger.dev/ai",
3+
"version": "4.3.3",
4+
"description": "Trigger.dev AI SDK integrations and chat transport",
5+
"license": "MIT",
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/triggerdotdev/trigger.dev",
12+
"directory": "packages/ai"
13+
},
14+
"type": "module",
15+
"files": [
16+
"dist"
17+
],
18+
"tshy": {
19+
"selfLink": false,
20+
"main": true,
21+
"module": true,
22+
"project": "./tsconfig.json",
23+
"exports": {
24+
"./package.json": "./package.json",
25+
".": "./src/index.ts"
26+
},
27+
"sourceDialects": [
28+
"@triggerdotdev/source"
29+
]
30+
},
31+
"scripts": {
32+
"clean": "rimraf dist .tshy .tshy-build .turbo",
33+
"build": "tshy && pnpm run update-version",
34+
"dev": "tshy --watch",
35+
"typecheck": "tsc --noEmit",
36+
"test": "vitest",
37+
"update-version": "tsx ../../scripts/updateVersion.ts",
38+
"check-exports": "attw --pack ."
39+
},
40+
"dependencies": {
41+
"@trigger.dev/core": "workspace:^4.3.3"
42+
},
43+
"devDependencies": {
44+
"@arethetypeswrong/cli": "^0.15.4",
45+
"ai": "^6.0.0",
46+
"rimraf": "^3.0.2",
47+
"tshy": "^3.0.2",
48+
"tsx": "4.17.0",
49+
"zod": "3.25.76"
50+
},
51+
"peerDependencies": {
52+
"ai": "^4.2.0 || ^5.0.0 || ^6.0.0",
53+
"zod": "^3.0.0 || ^4.0.0"
54+
},
55+
"engines": {
56+
"node": ">=18.20.0"
57+
},
58+
"exports": {
59+
"./package.json": "./package.json",
60+
".": {
61+
"import": {
62+
"@triggerdotdev/source": "./src/index.ts",
63+
"types": "./dist/esm/index.d.ts",
64+
"default": "./dist/esm/index.js"
65+
},
66+
"require": {
67+
"types": "./dist/commonjs/index.d.ts",
68+
"default": "./dist/commonjs/index.js"
69+
}
70+
}
71+
},
72+
"main": "./dist/commonjs/index.js",
73+
"types": "./dist/commonjs/index.d.ts",
74+
"module": "./dist/esm/index.js"
75+
}

packages/ai/src/ai.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from "vitest";
2+
import { z } from "zod";
3+
import { ai } from "./ai.js";
4+
import type { TaskWithSchema } from "@trigger.dev/core/v3";
5+
6+
describe("ai helper", function () {
7+
it("creates a tool from a schema task and executes through triggerAndWait", async function () {
8+
let receivedInput: unknown = undefined;
9+
10+
const fakeTask = {
11+
id: "fake-task",
12+
description: "A fake task",
13+
schema: z.object({
14+
name: z.string(),
15+
}),
16+
triggerAndWait: function (payload: { name: string }) {
17+
receivedInput = payload;
18+
const resultPromise = Promise.resolve({
19+
ok: true,
20+
id: "run_123",
21+
taskIdentifier: "fake-task",
22+
output: {
23+
greeting: `Hello ${payload.name}`,
24+
},
25+
});
26+
27+
return Object.assign(resultPromise, {
28+
unwrap: async function () {
29+
return {
30+
greeting: `Hello ${payload.name}`,
31+
};
32+
},
33+
});
34+
},
35+
} as unknown as TaskWithSchema<
36+
"fake-task",
37+
z.ZodObject<{ name: z.ZodString }>,
38+
{ greeting: string }
39+
>;
40+
41+
const tool = ai.tool(fakeTask);
42+
const result = await tool.execute?.(
43+
{
44+
name: "Ada",
45+
},
46+
undefined as never
47+
);
48+
49+
expect(receivedInput).toEqual({
50+
name: "Ada",
51+
});
52+
expect(result).toEqual({
53+
greeting: "Hello Ada",
54+
});
55+
});
56+
57+
it("throws when creating a tool from a task without schema", function () {
58+
const fakeTask = {
59+
id: "no-schema",
60+
description: "No schema task",
61+
schema: undefined,
62+
triggerAndWait: async function () {
63+
return {
64+
unwrap: async function () {
65+
return {};
66+
},
67+
};
68+
},
69+
} as unknown as TaskWithSchema<"no-schema", undefined, unknown>;
70+
71+
expect(function () {
72+
ai.tool(fakeTask);
73+
}).toThrowError("task has no schema");
74+
});
75+
76+
it("returns undefined for current tool options outside task execution context", function () {
77+
expect(ai.currentToolOptions()).toBeUndefined();
78+
});
79+
});

packages/ai/src/ai.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
AnyTask,
3+
isSchemaZodEsque,
4+
runMetadata,
5+
Task,
6+
type inferSchemaIn,
7+
type TaskSchema,
8+
type TaskWithSchema,
9+
} from "@trigger.dev/core/v3";
10+
import {
11+
dynamicTool,
12+
jsonSchema,
13+
JSONSchema7,
14+
Schema,
15+
Tool,
16+
ToolCallOptions,
17+
zodSchema,
18+
} from "ai";
19+
20+
const METADATA_KEY = "tool.execute.options";
21+
22+
export type ToolCallExecutionOptions = Omit<ToolCallOptions, "abortSignal">;
23+
24+
type ToolResultContent = Array<
25+
| {
26+
type: "text";
27+
text: string;
28+
}
29+
| {
30+
type: "image";
31+
data: string;
32+
mimeType?: string;
33+
}
34+
>;
35+
36+
export type ToolOptions<TResult> = {
37+
experimental_toToolResultContent?: (result: TResult) => ToolResultContent;
38+
};
39+
40+
function toolFromTask<TIdentifier extends string, TInput = void, TOutput = unknown>(
41+
task: Task<TIdentifier, TInput, TOutput>,
42+
options?: ToolOptions<TOutput>
43+
): Tool<TInput, TOutput>;
44+
function toolFromTask<
45+
TIdentifier extends string,
46+
TTaskSchema extends TaskSchema | undefined = undefined,
47+
TOutput = unknown,
48+
>(
49+
task: TaskWithSchema<TIdentifier, TTaskSchema, TOutput>,
50+
options?: ToolOptions<TOutput>
51+
): Tool<inferSchemaIn<TTaskSchema>, TOutput>;
52+
function toolFromTask<
53+
TIdentifier extends string,
54+
TTaskSchema extends TaskSchema | undefined = undefined,
55+
TInput = void,
56+
TOutput = unknown,
57+
>(
58+
task: TaskWithSchema<TIdentifier, TTaskSchema, TOutput> | Task<TIdentifier, TInput, TOutput>,
59+
options?: ToolOptions<TOutput>
60+
): TTaskSchema extends TaskSchema
61+
? Tool<inferSchemaIn<TTaskSchema>, TOutput>
62+
: Tool<TInput, TOutput> {
63+
if (("schema" in task && !task.schema) || ("jsonSchema" in task && !task.jsonSchema)) {
64+
throw new Error(
65+
"Cannot convert this task to to a tool because the task has no schema. Make sure to either use schemaTask or a task with an input jsonSchema."
66+
);
67+
}
68+
69+
const toolDefinition = dynamicTool({
70+
description: task.description,
71+
inputSchema: convertTaskSchemaToToolParameters(task),
72+
execute: async (input, options) => {
73+
const serializedOptions = options ? JSON.parse(JSON.stringify(options)) : undefined;
74+
75+
return await task
76+
.triggerAndWait(input as inferSchemaIn<TTaskSchema>, {
77+
metadata: {
78+
[METADATA_KEY]: serializedOptions,
79+
},
80+
})
81+
.unwrap();
82+
},
83+
...options,
84+
});
85+
86+
return toolDefinition as TTaskSchema extends TaskSchema
87+
? Tool<inferSchemaIn<TTaskSchema>, TOutput>
88+
: Tool<TInput, TOutput>;
89+
}
90+
91+
function getToolOptionsFromMetadata(): ToolCallExecutionOptions | undefined {
92+
let tool: unknown;
93+
try {
94+
tool = runMetadata.getKey(METADATA_KEY);
95+
} catch {
96+
return undefined;
97+
}
98+
99+
if (!tool) {
100+
return undefined;
101+
}
102+
103+
return tool as ToolCallExecutionOptions;
104+
}
105+
106+
function convertTaskSchemaToToolParameters(
107+
task: AnyTask | TaskWithSchema<any, any, any>
108+
): Schema<unknown> {
109+
if ("schema" in task) {
110+
if ("toJsonSchema" in task.schema && typeof task.schema.toJsonSchema === "function") {
111+
return jsonSchema((task.schema as any).toJsonSchema());
112+
}
113+
114+
if (isSchemaZodEsque(task.schema)) {
115+
return zodSchema(task.schema as any);
116+
}
117+
}
118+
119+
if ("jsonSchema" in task) {
120+
return jsonSchema(task.jsonSchema as JSONSchema7);
121+
}
122+
123+
throw new Error(
124+
"Cannot convert task to a tool. Make sure to use a task with a schema or jsonSchema."
125+
);
126+
}
127+
128+
export const ai = {
129+
tool: toolFromTask,
130+
currentToolOptions: getToolOptionsFromMetadata,
131+
};

0 commit comments

Comments
 (0)