export { Sandbox } from "../../worker/sandbox";
import runtime from "../../worker/index";
import { Conversation as BaseConversation } from "./think-worker";
import { getAgentByName } from "agents";
import { PersonalAgent as BasePersonalAgent } from "./think-worker";
import { Conversation as RuntimeConversation } from "../../worker/conversation";
import { MockLanguageModelV3 } from "ai/test";
import { tool, type UIMessage } from "ai";
import { z } from "zod";
import type { Env } from "../../worker/personal-agent";
export class PersonalAgent extends BasePersonalAgent {
fixturePreviousModel() {
this
.sql`UPDATE flarebot_model_settings SET configuration = ${JSON.stringify({
provider: "openrouter",
model: "openai/gpt-5-mini",
})} WHERE singleton = 1`;
this.fixtureEnableOpenRouter();
}
async seedChat(id: string, count: number) {
const conversation = await this.subAgent(RuntimeConversation, id);
await (conversation as unknown as Conversation).seedHistory(count);
}
}
const rich = `## Research result\n\nThis is **clear** and *readable* with \`inline code\`.\n\n- First item\n- Second item\n\n> Quoted evidence\n\n\`\`\`javascript\nconst answer = "";\nconsole.log(answer);\n\`\`\`\n\n| Topic | Result |\n| --- | --- |\n| Native | Working |\n\n[Documentation](https://developers.cloudflare.com/agents/)\n\n\n\n[unsafe](javascript:alert(1))  [data](data:text/html,bad) [relative](//evil.invalid/)\n`;
type Chunk =
Awaited<
ReturnType
>["stream"] extends ReadableStream
? C
: never;
export class Conversation extends BaseConversation {
protected createModel() {
return new MockLanguageModelV3({
doStream: async ({ prompt, abortSignal }) => {
const user = prompt.findLast(
(message) =>
message.role === "user" &&
message.content.some(
(part) => part.type === "text" && part.text.startsWith("chat:"),
),
);
const text =
user?.role === "user"
? user.content
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("")
.slice(5)
: "default";
const continuing = prompt.some(
(message) =>
message.role === "user" &&
message.content.some(
(part) =>
part.type === "text" &&
part.text ===
"Continue your previous response from exactly where it left off. Do not repeat any of it.",
),
);
if (text === "error") throw new Error("Fixture unavailable");
if (text === "pending")
await new Promise((resolve) => setTimeout(resolve, 2500));
const resource = text.startsWith("resource:")
? (JSON.parse(text.slice("resource:".length)) as {
toolName: string;
input: unknown;
})
: undefined;
const calls =
(text === "rich" || resource) && prompt.at(-1)?.role !== "tool";
const tokens =
text === "restart" && continuing
? ["Recovered remaining response."]
: resource
? ["Resource saved."]
: text === "rich"
? [rich]
: [
"Starting ",
...Array.from({ length: 20 }, (_, i) => `${text}-${i} `),
"finished.",
];
return {
stream: new ReadableStream({
async start(controller) {
const emit = (value: Chunk) => controller.enqueue(value);
emit({ type: "stream-start", warnings: [] });
if (calls) {
for (const call of resource
? [resource]
: [false, true].map((fail) => ({
toolName: "fixtureSources",
input: { fail },
})))
emit({
type: "tool-call",
toolCallId: crypto.randomUUID(),
toolName: call.toolName,
input: JSON.stringify(call.input),
});
} else {
emit({ type: "text-start", id: "text" });
for (const token of tokens) {
if (abortSignal?.aborted) {
controller.close();
return;
}
emit({ type: "text-delta", id: "text", delta: token });
if (text !== "rich" && text !== "fast")
await new Promise((resolve) => {
const timer = setTimeout(
resolve,
text === "restart" ? 1500 : 150,
);
abortSignal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
resolve();
},
{ once: true },
);
});
}
emit({ type: "text-end", id: "text" });
}
emit({
type: "finish",
finishReason: {
unified: calls ? "tool-calls" : "stop",
raw: undefined,
},
usage: {
inputTokens: {
total: 1,
noCache: 1,
cacheRead: 0,
cacheWrite: 0,
},
outputTokens: { total: 1, text: 1, reasoning: 0 },
},
});
controller.close();
},
}),
};
},
});
}
protected getToolActivityDescriptors() {
return {
...super.getToolActivityDescriptors(),
fixtureSources: {
kind: "web" as const,
label: "Read evidence",
outcome: (output: unknown) =>
(output as { ok: boolean }).ok
? ("succeeded" as const)
: ("failed" as const),
outputSummary: (output: unknown) =>
(output as { ok: boolean }).ok
? "Evidence read"
: "Page could not be read",
},
};
}
getTools() {
return {
...super.getTools(),
fixtureSources: tool({
description: "Fixture source evidence",
inputSchema: z.object({ fail: z.boolean() }),
execute: async ({ fail }) => {
await new Promise((resolve) => setTimeout(resolve, 1500));
return fail
? {
ok: false,
message:
"Safe tool failure
",
}
: {
ok: true,
sources: [
{
id: "fixture-source",
title: "Agents documentation",
requestedUrl: "https://developers.cloudflare.com/agents/",
finalUrl: "https://developers.cloudflare.com/agents/",
fetchedAt: new Date().toISOString(),
sourceKind: "page",
content: "Native evidence",
truncated: false,
},
],
};
},
}),
};
}
async seedHistory(count: number) {
const messages: UIMessage[] = Array.from({ length: count }, (_, index) => ({
id: `seed-${index}`,
role: index % 2 ? "assistant" : "user",
parts: [
{
type: "text",
text: `Saved message ${index}\n\n${"Long conversation content. ".repeat(12)}`,
},
],
}));
await this.saveMessages(messages);
}
}
export default {
async fetch(request, env, ctx) {
if (new URL(request.url).pathname === "/__chat/previous-model") {
const owner = await getAgentByName(
env.PersonalAgent as unknown as DurableObjectNamespace,
"personal",
);
await owner.fixturePreviousModel();
return Response.json({ ok: true });
}
if (new URL(request.url).pathname === "/__chat/seed") {
const { id, count } = (await request.json()) as {
id: string;
count: number;
};
const owner = await getAgentByName(
env.PersonalAgent as unknown as DurableObjectNamespace,
"personal",
);
await owner.seedChat(id, count);
return Response.json({ ok: true });
}
return runtime.fetch(request, env, ctx);
},
} satisfies ExportedHandler;