Skip to content

Commit d94a6a0

Browse files
committed
test(webapp): agent e2e legs for tools, HITL, and snapshot restore
1 parent 13ef0b0 commit d94a6a0

3 files changed

Lines changed: 337 additions & 8 deletions

File tree

apps/webapp/test/helpers/agentHarness.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ export type RunRealChatAgentOptions = {
1515
model: LanguageModel;
1616
modelLocal: LocalsKey<LanguageModel>;
1717
runId?: string;
18+
/**
19+
* Boot as a continuation of a previous run for the same session. Gates the
20+
* snapshot + `.out`/`.in` replay boot path, so the agent restores prior
21+
* history instead of treating the chat as brand new.
22+
*/
23+
continuation?: boolean;
24+
previousRunId?: string;
1825
};
1926

2027
export type RunningAgent = {
@@ -54,10 +61,15 @@ export function runRealChatAgent(opts: RunRealChatAgentOptions): RunningAgent {
5461
const done = runInMockTaskContext(
5562
async (drivers) => {
5663
drivers.locals.set(opts.modelLocal, opts.model);
57-
await runFn(
58-
{ chatId: opts.addressingKey, trigger: "preload", metadata: {} },
59-
{ ctx: drivers.ctx, signal: runSignal.signal }
60-
);
64+
const payload = opts.continuation
65+
? {
66+
chatId: opts.addressingKey,
67+
continuation: true,
68+
metadata: {},
69+
...(opts.previousRunId ? { previousRunId: opts.previousRunId } : {}),
70+
}
71+
: { chatId: opts.addressingKey, trigger: "preload", metadata: {} };
72+
await runFn(payload, { ctx: drivers.ctx, signal: runSignal.signal });
6173
},
6274
{ ctx: { run: { id: runId } }, sessionStreamManager: manager }
6375
) as Promise<void>;

apps/webapp/test/helpers/testChatAgent.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { chat } from "@trigger.dev/sdk/ai";
22
import { locals } from "@trigger.dev/core/v3";
3-
import { streamText, type LanguageModel, type UIMessage } from "ai";
3+
import { stepCountIs, streamText, tool, type LanguageModel, type UIMessage } from "ai";
44
import { z } from "zod";
55

66
/**
@@ -61,3 +61,73 @@ export const testChatAgent = chat
6161
return streamText({ model, messages, abortSignal: signal });
6262
},
6363
});
64+
65+
/**
66+
* A minimal agent with no lifecycle hooks. With `hydrateMessages` absent, the
67+
* default snapshot + `.out`/`.in` replay boot path is what restores prior
68+
* history on a continuation run.
69+
*/
70+
export const testPlainChatAgent = chat.agent({
71+
id: "e2e-test-chat-plain",
72+
run: async ({ messages, signal }) => {
73+
const model = locals.get(testChatModelLocal);
74+
if (!model) {
75+
throw new Error("test model not injected via locals");
76+
}
77+
return streamText({ model, messages, abortSignal: signal });
78+
},
79+
});
80+
81+
/**
82+
* A tool with a server-side `execute`: the agent runs it automatically and
83+
* feeds the result back to the model, so a single turn covers the whole
84+
* tool loop.
85+
*/
86+
const weatherTool = tool({
87+
description: "Get the current weather for a city.",
88+
inputSchema: z.object({ city: z.string() }),
89+
execute: async ({ city }) => ({ city, tempC: 21, summary: "clear" }),
90+
});
91+
92+
export const testToolChatAgent = chat.agent({
93+
id: "e2e-test-chat-tool",
94+
run: async ({ messages, signal }) => {
95+
const model = locals.get(testChatModelLocal);
96+
if (!model) {
97+
throw new Error("test model not injected via locals");
98+
}
99+
return streamText({
100+
model,
101+
messages,
102+
tools: { getWeather: weatherTool },
103+
stopWhen: stepCountIs(5),
104+
abortSignal: signal,
105+
});
106+
},
107+
});
108+
109+
/**
110+
* A tool with no `execute`: the model's call parks the turn on a
111+
* human-in-the-loop round-trip. The client supplies the tool output on the
112+
* next message and the agent continues from there.
113+
*/
114+
const askUserTool = tool({
115+
description: "Ask the user a question and wait for their answer.",
116+
inputSchema: z.object({ question: z.string() }),
117+
});
118+
119+
export const testHitlChatAgent = chat.agent({
120+
id: "e2e-test-chat-hitl",
121+
run: async ({ messages, signal }) => {
122+
const model = locals.get(testChatModelLocal);
123+
if (!model) {
124+
throw new Error("test model not injected via locals");
125+
}
126+
return streamText({
127+
model,
128+
messages,
129+
tools: { askUser: askUserTool },
130+
abortSignal: signal,
131+
});
132+
},
133+
});

0 commit comments

Comments
 (0)