Added tool example

This commit is contained in:
Eric Allam
2026-02-21 13:42:09 +00:00
parent 076c32b2f6
commit 735e845d8b
5 changed files with 507 additions and 78 deletions
+285
View File
@@ -916,6 +916,189 @@ describe("TriggerChatTransport", () => {
});
});
describe("lastEventId tracking", () => {
it("should pass lastEventId to SSE subscription on subsequent turns", async () => {
const controlChunk = {
type: "__trigger_waitpoint_ready",
tokenId: "wp_token_eid",
publicAccessToken: "wp_access_eid",
};
let triggerCallCount = 0;
const streamFetchCalls: { url: string; headers: Record<string, string> }[] = [];
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
triggerCallCount++;
return new Response(
JSON.stringify({ id: "run_eid" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "pub_token_eid",
},
}
);
}
if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) {
return new Response(
JSON.stringify({ success: true }),
{
status: 200,
headers: { "content-type": "application/json" },
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
streamFetchCalls.push({
url: urlStr,
headers: (init?.headers as Record<string, string>) ?? {},
});
const chunks = [
...sampleChunks,
{ type: "finish" as const, id: "part-1" } as UIMessageChunk,
controlChunk,
];
return new Response(createSSEStream(sseEncode(chunks)), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
task: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
// First message — triggers a new run
const stream1 = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-eid",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
const reader1 = stream1.getReader();
while (true) {
const { done } = await reader1.read();
if (done) break;
}
// Second message — completes the waitpoint
const stream2 = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-eid",
messageId: undefined,
messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("What's up?")],
abortSignal: undefined,
});
const reader2 = stream2.getReader();
while (true) {
const { done } = await reader2.read();
if (done) break;
}
// The second stream subscription should include a Last-Event-ID header
expect(streamFetchCalls.length).toBe(2);
const secondStreamHeaders = streamFetchCalls[1]!.headers;
// SSEStreamSubscription passes lastEventId as the Last-Event-ID header
expect(secondStreamHeaders["Last-Event-ID"]).toBeDefined();
});
});
describe("AbortController cleanup", () => {
it("should terminate SSE connection after intercepting control chunk", async () => {
const controlChunk = {
type: "__trigger_waitpoint_ready",
tokenId: "wp_token_abort",
publicAccessToken: "wp_access_abort",
};
let streamAborted = false;
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_abort_cleanup" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "pub_token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
// Track abort signal
const signal = init?.signal;
if (signal) {
signal.addEventListener("abort", () => {
streamAborted = true;
});
}
const chunks = [
...sampleChunks,
{ type: "finish" as const, id: "part-1" } as UIMessageChunk,
controlChunk,
];
return new Response(createSSEStream(sseEncode(chunks)), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
task: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
const stream = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-abort-cleanup",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
// Consume all chunks
const reader = stream.getReader();
while (true) {
const { done } = await reader.read();
if (done) break;
}
// The internal AbortController should have aborted the fetch
expect(streamAborted).toBe(true);
});
});
describe("async accessToken", () => {
it("should accept an async function for accessToken", async () => {
let tokenCallCount = 0;
@@ -974,6 +1157,108 @@ describe("TriggerChatTransport", () => {
expect(tokenCallCount).toBe(1);
});
it("should resolve async token for waitpoint completion flow", async () => {
const controlChunk = {
type: "__trigger_waitpoint_ready",
tokenId: "wp_token_async",
publicAccessToken: "wp_access_async",
};
let tokenCallCount = 0;
let completeWaitpointCalled = false;
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_async_wp" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "stream-token",
},
}
);
}
if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) {
completeWaitpointCalled = true;
return new Response(
JSON.stringify({ success: true }),
{
status: 200,
headers: { "content-type": "application/json" },
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
const chunks = [
...sampleChunks,
{ type: "finish" as const, id: "part-1" } as UIMessageChunk,
controlChunk,
];
return new Response(createSSEStream(sseEncode(chunks)), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
task: "my-task",
accessToken: async () => {
tokenCallCount++;
await new Promise((r) => setTimeout(r, 1));
return `async-wp-token-${tokenCallCount}`;
},
baseURL: "https://api.test.trigger.dev",
});
// First message — triggers a new run (calls async token)
const stream1 = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-async-wp",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
const reader1 = stream1.getReader();
while (true) {
const { done } = await reader1.read();
if (done) break;
}
const firstTokenCount = tokenCallCount;
// Second message — should complete waitpoint (does NOT call async token)
const stream2 = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-async-wp",
messageId: undefined,
messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("More")],
abortSignal: undefined,
});
const reader2 = stream2.getReader();
while (true) {
const { done } = await reader2.read();
if (done) break;
}
// Token function should NOT have been called again for the waitpoint path
expect(tokenCallCount).toBe(firstTokenCount);
expect(completeWaitpointCalled).toBe(true);
});
});
describe("single-run mode (waitpoint loop)", () => {
+87 -76
View File
@@ -456,8 +456,8 @@ importers:
specifier: ^0.1.3
version: 0.1.3(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))
'@s2-dev/streamstore':
specifier: ^0.22.5
version: 0.22.5(supports-color@10.0.0)
specifier: ^0.17.2
version: 0.17.3(typescript@5.5.4)
'@sentry/remix':
specifier: 9.46.0
version: 9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.1.0(typescript@5.5.4))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(encoding@0.1.13)(react@18.2.0)
@@ -1101,7 +1101,7 @@ importers:
version: 18.3.1
react-email:
specifier: ^2.1.1
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0)
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0)
resend:
specifier: ^3.2.0
version: 3.2.0
@@ -1452,8 +1452,8 @@ importers:
specifier: 1.36.0
version: 1.36.0
'@s2-dev/streamstore':
specifier: ^0.22.5
version: 0.22.5(supports-color@10.0.0)
specifier: ^0.17.6
version: 0.17.6
'@trigger.dev/build':
specifier: workspace:4.4.1
version: link:../build
@@ -1729,8 +1729,8 @@ importers:
specifier: 1.36.0
version: 1.36.0
'@s2-dev/streamstore':
specifier: 0.22.5
version: 0.22.5(supports-color@10.0.0)
specifier: 0.17.3
version: 0.17.3(typescript@5.5.4)
dequal:
specifier: ^2.0.3
version: 2.0.3
@@ -2112,10 +2112,10 @@ importers:
dependencies:
'@ai-sdk/openai':
specifier: ^3.0.0
version: 3.0.27(zod@3.25.76)
version: 3.0.19(zod@3.25.76)
'@ai-sdk/react':
specifier: ^3.0.0
version: 3.0.84(react@19.1.0)(zod@3.25.76)
version: 3.0.51(react@19.1.0)(zod@3.25.76)
'@trigger.dev/sdk':
specifier: workspace:*
version: link:../../packages/trigger-sdk
@@ -2131,6 +2131,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.1.0(react@19.1.0)
zod:
specifier: 3.25.76
version: 3.25.76
devDependencies:
'@tailwindcss/postcss':
specifier: ^4
@@ -2916,8 +2919,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.42':
resolution: {integrity: sha512-Il9lZWPUQMX59H5yJvA08gxfL2Py8oHwvAYRnK0Mt91S+JgPcyk/yEmXNDZG9ghJrwSawtK5Yocy8OnzsTOGsw==}
'@ai-sdk/gateway@3.0.22':
resolution: {integrity: sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -2952,8 +2955,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@3.0.27':
resolution: {integrity: sha512-pLMxWOypwroXiK9dxNpn60/HGhWWWDEOJ3lo9vZLoxvpJNtKnLKojwVIvlW3yEjlD7ll1+jUO2uzsABNTaP5Yg==}
'@ai-sdk/openai@3.0.19':
resolution: {integrity: sha512-qpMGKV6eYfW8IzErk/OppchQwVui3GPc4BEfg/sQGRzR89vf2Sa8qvSavXeZi5w/oUF56d+VtobwSH0FRooFCQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -3006,8 +3009,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.14':
resolution: {integrity: sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==}
'@ai-sdk/provider-utils@4.0.9':
resolution: {integrity: sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -3036,8 +3039,8 @@ packages:
resolution: {integrity: sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==}
engines: {node: '>=18'}
'@ai-sdk/provider@3.0.8':
resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==}
'@ai-sdk/provider@3.0.5':
resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==}
engines: {node: '>=18'}
'@ai-sdk/react@1.0.0':
@@ -3072,8 +3075,8 @@ packages:
zod:
optional: true
'@ai-sdk/react@3.0.84':
resolution: {integrity: sha512-caX8dsXGHDctQsFGgq05sdaw9YD2C8Y9SfnOk0b0LPPi4J7/V54tq22MPTGVO9zS3LmsfFQf0GDM4WFZNC5XZA==}
'@ai-sdk/react@3.0.51':
resolution: {integrity: sha512-7nmCwEJM52NQZB4/ED8qJ4wbDg7EEWh94qJ7K9GSJxD6sWF3GOKrRZ5ivm4qNmKhY+JfCxCAxfghGY5mTKOsxw==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
@@ -9544,8 +9547,13 @@ packages:
'@rushstack/eslint-patch@1.2.0':
resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==}
'@s2-dev/streamstore@0.22.5':
resolution: {integrity: sha512-GqdOKIbIoIxT+40fnKzHbrsHB6gBqKdECmFe7D3Ojk4FoN1Hu0LhFzZv6ZmVMjoHHU+55debS1xSWjZwQmbIyQ==}
'@s2-dev/streamstore@0.17.3':
resolution: {integrity: sha512-UeXL5+MgZQfNkbhCgEDVm7PrV5B3bxh6Zp4C5pUzQQwaoA+iGh2QiiIptRZynWgayzRv4vh0PYfnKpTzJEXegQ==}
peerDependencies:
typescript: 5.5.4
'@s2-dev/streamstore@0.17.6':
resolution: {integrity: sha512-ocjZfKaPKmo2yhudM58zVNHv3rBLSbTKkabVoLFn9nAxU6iLrR2CO3QmSo7/waohI3EZHAWxF/Pw8kA8d6QH2g==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -11604,8 +11612,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ai@6.0.82:
resolution: {integrity: sha512-WLml1ab2IXtREgkxrq2Pl6lFO6NKgC17MqTzmK5mO1UO6tMAJiVjkednw9p0j4+/LaUIZQoRiIT8wA37LswZ9Q==}
ai@6.0.49:
resolution: {integrity: sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -14388,7 +14396,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
deprecated: Glob versions prior to v9 are no longer supported
glob@9.3.5:
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
@@ -19123,22 +19131,21 @@ packages:
tar@6.1.13:
resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
tar@7.4.3:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
tar@7.5.6:
resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==}
engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
@@ -20421,10 +20428,10 @@ snapshots:
'@vercel/oidc': 3.0.5
zod: 3.25.76
'@ai-sdk/gateway@3.0.42(zod@3.25.76)':
'@ai-sdk/gateway@3.0.22(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
'@ai-sdk/provider': 3.0.5
'@ai-sdk/provider-utils': 4.0.9(zod@3.25.76)
'@vercel/oidc': 3.1.0
zod: 3.25.76
@@ -20458,10 +20465,10 @@ snapshots:
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/openai@3.0.27(zod@3.25.76)':
'@ai-sdk/openai@3.0.19(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
'@ai-sdk/provider': 3.0.5
'@ai-sdk/provider-utils': 4.0.9(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/provider-utils@1.0.22(zod@3.25.76)':
@@ -20518,9 +20525,9 @@ snapshots:
eventsource-parser: 3.0.6
zod: 3.25.76
'@ai-sdk/provider-utils@4.0.14(zod@3.25.76)':
'@ai-sdk/provider-utils@4.0.9(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider': 3.0.5
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.6
zod: 3.25.76
@@ -20549,7 +20556,7 @@ snapshots:
dependencies:
json-schema: 0.4.0
'@ai-sdk/provider@3.0.8':
'@ai-sdk/provider@3.0.5':
dependencies:
json-schema: 0.4.0
@@ -20583,10 +20590,10 @@ snapshots:
optionalDependencies:
zod: 3.25.76
'@ai-sdk/react@3.0.84(react@19.1.0)(zod@3.25.76)':
'@ai-sdk/react@3.0.51(react@19.1.0)(zod@3.25.76)':
dependencies:
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
ai: 6.0.82(zod@3.25.76)
'@ai-sdk/provider-utils': 4.0.9(zod@3.25.76)
ai: 6.0.49(zod@3.25.76)
react: 19.1.0
swr: 2.2.5(react@19.1.0)
throttleit: 2.1.0
@@ -23320,8 +23327,8 @@ snapshots:
'@epic-web/test-server@0.1.0(bufferutil@4.0.9)':
dependencies:
'@hono/node-server': 1.12.2(hono@4.11.8)
'@hono/node-ws': 1.0.4(@hono/node-server@1.12.2(hono@4.11.8))(bufferutil@4.0.9)
'@hono/node-server': 1.12.2(hono@4.5.11)
'@hono/node-ws': 1.0.4(@hono/node-server@1.12.2(hono@4.5.11))(bufferutil@4.0.9)
'@open-draft/deferred-promise': 2.2.0
'@types/ws': 8.5.12
hono: 4.5.11
@@ -24068,17 +24075,17 @@ snapshots:
dependencies:
react: 18.2.0
'@hono/node-server@1.12.2(hono@4.11.8)':
'@hono/node-server@1.12.2(hono@4.5.11)':
dependencies:
hono: 4.11.8
hono: 4.5.11
'@hono/node-server@1.19.9(hono@4.11.8)':
dependencies:
hono: 4.11.8
'@hono/node-ws@1.0.4(@hono/node-server@1.12.2(hono@4.11.8))(bufferutil@4.0.9)':
'@hono/node-ws@1.0.4(@hono/node-server@1.12.2(hono@4.5.11))(bufferutil@4.0.9)':
dependencies:
'@hono/node-server': 1.12.2(hono@4.11.8)
'@hono/node-server': 1.12.2(hono@4.5.11)
ws: 8.18.3(bufferutil@4.0.9)
transitivePeerDependencies:
- bufferutil
@@ -25862,7 +25869,7 @@ snapshots:
'@puppeteer/browsers@2.10.6':
dependencies:
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
@@ -29425,12 +29432,14 @@ snapshots:
'@rushstack/eslint-patch@1.2.0': {}
'@s2-dev/streamstore@0.22.5(supports-color@10.0.0)':
'@s2-dev/streamstore@0.17.3(typescript@5.5.4)':
dependencies:
'@protobuf-ts/runtime': 2.11.1
typescript: 5.5.4
'@s2-dev/streamstore@0.17.6':
dependencies:
'@protobuf-ts/runtime': 2.11.1
debug: 4.4.3(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
'@sec-ant/readable-stream@0.4.1': {}
@@ -31511,7 +31520,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4)
'@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4)
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
eslint: 8.31.0
tsutils: 3.21.0(typescript@5.5.4)
optionalDependencies:
@@ -31525,7 +31534,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 5.59.6
'@typescript-eslint/visitor-keys': 5.59.6
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.7.3
@@ -32119,11 +32128,11 @@ snapshots:
'@opentelemetry/api': 1.9.0
zod: 3.25.76
ai@6.0.82(zod@3.25.76):
ai@6.0.49(zod@3.25.76):
dependencies:
'@ai-sdk/gateway': 3.0.42(zod@3.25.76)
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
'@ai-sdk/gateway': 3.0.22(zod@3.25.76)
'@ai-sdk/provider': 3.0.5
'@ai-sdk/provider-utils': 4.0.9(zod@3.25.76)
'@opentelemetry/api': 1.9.0
zod: 3.25.76
@@ -33546,9 +33555,11 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.1:
debug@4.4.1(supports-color@10.0.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.0.0
debug@4.4.3(supports-color@10.0.0):
dependencies:
@@ -34916,7 +34927,7 @@ snapshots:
extract-zip@2.0.1:
dependencies:
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
get-stream: 5.2.0
yauzl: 2.10.0
optionalDependencies:
@@ -35350,7 +35361,7 @@ snapshots:
dependencies:
basic-ftp: 5.0.3
data-uri-to-buffer: 5.0.1
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
fs-extra: 8.1.0
transitivePeerDependencies:
- supports-color
@@ -35509,7 +35520,7 @@ snapshots:
'@types/node': 20.14.14
'@types/semver': 7.5.1
chalk: 4.1.2
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
interpret: 3.1.1
semver: 7.7.3
tslib: 2.8.1
@@ -35793,7 +35804,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
@@ -35813,7 +35824,7 @@ snapshots:
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
@@ -36178,7 +36189,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -38411,7 +38422,7 @@ snapshots:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
get-uri: 6.0.1
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
@@ -39164,7 +39175,7 @@ snapshots:
proxy-agent@6.5.0:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 7.18.3
@@ -39204,7 +39215,7 @@ snapshots:
dependencies:
'@puppeteer/browsers': 2.10.6
chromium-bidi: 7.2.0(devtools-protocol@0.0.1464554)
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
devtools-protocol: 0.0.1464554
typed-query-selector: 2.12.0
ws: 8.18.3(bufferutil@4.0.9)
@@ -39419,7 +39430,7 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0):
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0):
dependencies:
'@babel/parser': 7.24.1
'@radix-ui/colors': 1.0.1
@@ -39456,8 +39467,8 @@ snapshots:
react: 18.3.1
react-dom: 18.2.0(react@18.3.1)
shelljs: 0.8.5
socket.io: 4.7.3
socket.io-client: 4.7.3
socket.io: 4.7.3(bufferutil@4.0.9)
socket.io-client: 4.7.3(bufferutil@4.0.9)
sonner: 1.3.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1)
source-map-js: 1.0.2
stacktrace-parser: 0.1.10
@@ -40089,7 +40100,7 @@ snapshots:
require-in-the-middle@7.1.1(supports-color@10.0.0):
dependencies:
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
module-details-from-path: 1.0.3
resolve: 1.22.8
transitivePeerDependencies:
@@ -40657,7 +40668,7 @@ snapshots:
- supports-color
- utf-8-validate
socket.io-client@4.7.3:
socket.io-client@4.7.3(bufferutil@4.0.9):
dependencies:
'@socket.io/component-emitter': 3.1.0
debug: 4.3.7(supports-color@10.0.0)
@@ -40686,7 +40697,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
socket.io@4.7.3:
socket.io@4.7.3(bufferutil@4.0.9):
dependencies:
accepts: 1.3.8
base64id: 2.0.0
@@ -40717,7 +40728,7 @@ snapshots:
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
socks: 2.8.3
transitivePeerDependencies:
- supports-color
@@ -41089,7 +41100,7 @@ snapshots:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.3(supports-color@10.0.0)
debug: 4.4.1(supports-color@10.0.0)
fast-safe-stringify: 2.1.1
form-data: 4.0.4
formidable: 3.5.1
@@ -42295,7 +42306,7 @@ snapshots:
'@vitest/spy': 3.1.4
'@vitest/utils': 3.1.4
chai: 5.2.0
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
expect-type: 1.2.1
magic-string: 0.30.21
pathe: 2.0.3
+2 -1
View File
@@ -15,7 +15,8 @@
"ai": "^6.0.0",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"zod": "3.25.76"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -5,6 +5,70 @@ import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
import { useMemo, useState } from "react";
import { getChatToken } from "@/app/actions";
function ToolInvocation({ part }: { part: any }) {
const [expanded, setExpanded] = useState(false);
// Static tools: type is "tool-{name}", dynamic tools have toolName property
const toolName =
part.type === "dynamic-tool"
? (part.toolName ?? "tool")
: part.type.startsWith("tool-")
? part.type.slice(5)
: "tool";
const state = part.state ?? "input-available";
const args = part.input;
const result = part.output;
const isLoading = state === "input-streaming" || state === "input-available";
const isError = state === "output-error";
return (
<div className="my-1 rounded border border-gray-200 bg-gray-50 text-xs">
<button
type="button"
onClick={() => setExpanded(!expanded)}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left font-medium text-gray-700 hover:bg-gray-100"
>
{isLoading && (
<span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
)}
{!isLoading && !isError && <span className="text-green-600">&#10003;</span>}
{isError && <span className="text-red-600">&#10007;</span>}
<span>{toolName}</span>
<span className="ml-auto text-gray-400">{expanded ? "▲" : "▼"}</span>
</button>
{expanded && (
<div className="border-t border-gray-200 px-3 py-2 space-y-2">
{args && Object.keys(args).length > 0 && (
<div>
<div className="mb-1 font-medium text-gray-500">Input</div>
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-white p-2 text-gray-800">
{JSON.stringify(args, null, 2)}
</pre>
</div>
)}
{state === "output-available" && result !== undefined && (
<div>
<div className="mb-1 font-medium text-gray-500">Output</div>
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-white p-2 text-gray-800">
{JSON.stringify(result, null, 2)}
</pre>
</div>
)}
{isError && result !== undefined && (
<div>
<div className="mb-1 font-medium text-red-500">Error</div>
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-red-50 p-2 text-red-700">
{typeof result === "string" ? result : JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
);
}
export function Chat() {
const [input, setInput] = useState("");
@@ -54,6 +118,12 @@ export function Chat() {
if (part.type === "text") {
return <span key={i}>{part.text}</span>;
}
// Static tools: "tool-{toolName}", dynamic tools: "dynamic-tool"
if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
return <ToolInvocation key={i} part={part} />;
}
return null;
})}
</div>
+63 -1
View File
@@ -1,6 +1,66 @@
import { chatTask } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";
import { streamText, convertToModelMessages, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import os from "node:os";
const inspectEnvironment = tool({
description:
"Inspect the current execution environment. Returns runtime info (Node.js/Bun/Deno version), " +
"OS details, CPU architecture, memory usage, environment variables, and platform metadata.",
inputSchema: z.object({}),
execute: async () => {
const memUsage = process.memoryUsage();
return {
runtime: {
name: typeof Bun !== "undefined" ? "bun" : typeof Deno !== "undefined" ? "deno" : "node",
version: process.version,
versions: {
v8: process.versions.v8,
openssl: process.versions.openssl,
modules: process.versions.modules,
},
},
os: {
platform: process.platform,
arch: process.arch,
release: os.release(),
type: os.type(),
hostname: os.hostname(),
uptime: `${Math.floor(os.uptime())}s`,
},
cpus: {
count: os.cpus().length,
model: os.cpus()[0]?.model,
},
memory: {
total: `${Math.round(os.totalmem() / 1024 / 1024)}MB`,
free: `${Math.round(os.freemem() / 1024 / 1024)}MB`,
process: {
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
},
},
env: {
NODE_ENV: process.env.NODE_ENV,
TZ: process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
LANG: process.env.LANG,
},
process: {
pid: process.pid,
cwd: process.cwd(),
execPath: process.execPath,
argv: process.argv.slice(0, 3),
},
};
},
});
// Silence TS errors for Bun/Deno global checks
declare const Bun: unknown;
declare const Deno: unknown;
export const chat = chatTask({
id: "ai-chat",
@@ -9,6 +69,8 @@ export const chat = chatTask({
model: openai("gpt-4o-mini"),
system: "You are a helpful assistant. Be concise and friendly.",
messages: await convertToModelMessages(messages),
tools: { inspectEnvironment },
maxSteps: 3,
});
},
});