export { Sandbox } from "../../worker/sandbox"; import runtime from "../../worker/index"; import { PersonalAgent as RuntimePersonalAgent, type Env, } from "../../worker/personal-agent"; import { Conversation as RuntimeConversation } from "../../worker/conversation"; import { callable, getAgentByName } from "agents"; import { MockLanguageModelV3 } from "ai/test"; import { tool } from "ai"; import { action } from "@cloudflare/think"; import type { ToolActivityDescriptor } from "../../worker/tool-activity"; import { z } from "zod"; import type { ModelConfiguration } from "../../worker/model-settings"; import type { Secret } from "../../configuration/secrets"; export class PersonalAgent extends RuntimePersonalAgent { protected async generateConversationTitle(message: string) { this .sql`CREATE TABLE IF NOT EXISTS fixture_title_calls (message TEXT NOT NULL)`; this.sql`INSERT INTO fixture_title_calls VALUES (${message})`; if (message === "title:failure") throw new Error("Fixture title model failed"); if (message === "title:long-output") return "Useful ".repeat(20); if (message === "title:unsafe-output") return "Visit https://example.com/?token=private"; if (message === "title:bad-output") return "API key private-credential"; if (message === "title:opaque-output") return "Use abcdefghijklmnopqrst"; if (message === "title:prefixed-output") return "Use sk-short-value"; if (message === "title:persistence-failure") { this.sql`CREATE TRIGGER IF NOT EXISTS fixture_title_failure BEFORE UPDATE OF name ON flarebot_conversations WHEN NEW.name = 'Failed title persistence' BEGIN SELECT RAISE(ABORT, 'Fixture persistence failure'); END`; return "Failed title persistence"; } if (message === "title:slow") { await new Promise((resolve) => setTimeout(resolve, 1200)); return "Configure Flarebot locally"; } if (message.startsWith("title:")) return message.slice(6); return "New conversation"; } fixtureEnableOpenRouter() { // Signed receipts are exercised by bridge.test.mjs. this .sql`INSERT OR IGNORE INTO flarebot_enabled_providers (provider) VALUES ('openrouter')`; } fixtureLegacyKey() { this.sql`UPDATE flarebot_model_settings SET anthropic_key = ( SELECT encrypted_key FROM flarebot_provider_credentials WHERE provider = 'anthropic' ) WHERE singleton = 1`; this .sql`DELETE FROM flarebot_provider_credentials WHERE provider = 'anthropic'`; } fixtureRetiredOpenCode() { this .sql`UPDATE flarebot_model_settings SET configuration = ${JSON.stringify({ provider: "opencode-go", model: "fixture-retired-model", })} WHERE singleton = 1`; this.sql`INSERT INTO flarebot_provider_credentials (provider, encrypted_key) SELECT 'opencode-go', encrypted_key FROM flarebot_provider_credentials WHERE provider = 'anthropic'`; this .sql`INSERT INTO flarebot_enabled_providers (provider) VALUES ('opencode-go')`; } async fixtureDiagnostics(id: string, fail: boolean) { const child = await this.subAgent(Conversation, id); return child.fixtureDiagnostics(fail); } private failDeletion = false; private loseMemoryReply = false; private delayMemoryUpdate = false; configureMemoryFault(mode: string) { this.loseMemoryReply = mode === "lost-reply"; this.delayMemoryUpdate = mode === "late-update"; } async rememberForConversation( ...args: Parameters ) { const fact = await super.rememberForConversation(...args); if (this.loseMemoryReply) { this.loseMemoryReply = false; throw new Error("Fixture lost memory reply after commit"); } return fact; } async updateMemoryForConversation( ...args: Parameters ) { if (this.delayMemoryUpdate) { this.delayMemoryUpdate = false; await new Promise((resolve) => setTimeout(resolve, 1200)); } return super.updateMemoryForConversation(...args); } inspectConversations() { this .sql`CREATE TABLE IF NOT EXISTS fixture_title_calls (message TEXT NOT NULL)`; return { titleCalls: this.sql`SELECT message FROM fixture_title_calls`, facets: this.listSubAgents(RuntimeConversation) .map((facet) => facet.name) .sort(), metadata: this .sql`SELECT id, status FROM flarebot_conversations ORDER BY id`, settingsStorage: this.sql`SELECT * FROM flarebot_model_settings`, credentialsStorage: this .sql`SELECT * FROM flarebot_provider_credentials ORDER BY provider`, enabledProvidersStorage: this .sql`SELECT * FROM flarebot_enabled_providers ORDER BY provider`, browserLeases: this.sql`SELECT * FROM flarebot_browser_leases`, }; } async stageInterruptedCreation() { const id = crypto.randomUUID(); const now = new Date().toISOString(); this.sql`INSERT INTO flarebot_conversations VALUES (${id}, 'Interrupted creation', ${now}, ${now}, 'creating')`; await this.subAgent(RuntimeConversation, id); return id; } failNextDeletion() { this.failDeletion = true; } async deleteSubAgent( ...args: Parameters ) { if (this.failDeletion) { this.failDeletion = false; throw new Error("Fixture deletion failure"); } return super.deleteSubAgent(...args); } } type ModelStream = Awaited< ReturnType >["stream"]; type ModelChunk = ModelStream extends ReadableStream ? Chunk : never; // Only this test entry replaces inference. No environment flag or development // authentication/model bypass is present in the customer runtime artifact. export class Conversation extends RuntimeConversation { @callable() fixtureModelCalls() { this .sql`CREATE TABLE IF NOT EXISTS fixture_model_calls (configuration TEXT, options TEXT)`; return this.sql<{ configuration: string; options: string; }>`SELECT * FROM fixture_model_calls`.map((row) => ({ configuration: JSON.parse(row.configuration), options: JSON.parse(row.options), })); } fixtureDiagnostics(fail: boolean) { this.diagnosticSnapshot(); if (fail) { this.sql`DROP TABLE flarebot_diagnostics`; return []; } return this.sql`SELECT event FROM flarebot_diagnostics`; } protected createModel(configuration: ModelConfiguration, key?: Secret) { return new MockLanguageModelV3({ doStream: async ({ prompt, abortSignal, tools, providerOptions }) => { this.fixtureModelCalls(); this .sql`INSERT INTO fixture_model_calls VALUES (${JSON.stringify(configuration)}, ${JSON.stringify(providerOptions ?? {})})`; // Native recovery appends a synthetic continuation instruction. Select // the last fixture scenario from the real user history, not that prompt. const user = prompt.findLast( (message) => message.role === "user" && message.content.some( (part) => part.type === "text" && (part.text.startsWith("memory:") || part.text.startsWith("schedule:") || part.text.startsWith("memory-context ") || /^(slow-[a-z]+|second|tool|error|after-error|recover|configuration|instructions|credential-error|activity-[a-z]+)$/.test( part.text, )), ), ); const text = (user?.role === "user" ? user.content : []) .filter((part) => part.type === "text") .map((part) => part.text) .join("") ?? ""; const memoryCall: { tool: string; input: unknown; id?: string } | undefined = text.startsWith("memory:") ? JSON.parse(text.slice(7)) : text.startsWith("schedule:") ? JSON.parse(text.slice(9)) : undefined; const toolResult = prompt.at(-1)?.role === "tool"; if (text === "error") throw new Error("Fixture model unavailable"); if (text === "credential-error") throw Object.assign(new Error(`Provider echoed ${key?.reveal()}`), { statusCode: 401, responseBody: JSON.stringify({ name: "AiGatewayError", internalCode: 2009, message: key?.reveal(), }), responseHeaders: { "cf-ray": "0123456789abcdef-WAW", authorization: key?.reveal(), }, }); const attemptsKey = `fixture-attempts:${text}`; const attempts = ((await this.ctx.storage.get(attemptsKey)) ?? 0) + 1; await this.ctx.storage.put(attemptsKey, attempts); const toolCall = (memoryCall || text === "tool" || text.startsWith("activity-")) && (!toolResult || (text === "activity-replay" && attempts === 2)) && (text !== "activity-recover" || attempts === 1); if ( toolCall && (tools?.length !== 14 || tools.some( (t) => ![ "shell", "browser_read", "web_search", "read_url", "remember", "createSchedule", "updateMemory", "forget", "recall", "fixtureEcho", "fixtureActivity", "fixtureProgress", "fixtureUnknown", "fixtureAction", ].includes(t.name), )) ) throw new Error("Unexpected enabled tools"); const slow = text.startsWith("slow") || (text === "recover" && attempts === 1); const tokens = toolCall ? [] : [ text === "recover" && attempts > 1 ? "Recovered " : "Reply ", text === "instructions" || text.startsWith("memory-context ") ? JSON.stringify( prompt .filter((message) => message.role === "system") .map((message) => message.content), ) : text === "configuration" ? `${configuration.provider}/${configuration.model}` : text, " complete", ]; const replayIdKey = "fixture-replay-id"; const replayId = (await this.ctx.storage.get(replayIdKey)) ?? crypto.randomUUID(); if (text === "activity-replay") await this.ctx.storage.put(replayIdKey, replayId); const stream = new ReadableStream({ async start(controller) { const emit = (chunk: ModelChunk) => controller.enqueue(chunk); emit({ type: "stream-start", warnings: [] }); if (toolCall) { const count = text === "activity-many" ? 105 : text === "activity-parallel" ? 2 : 1; for (let index = 0; index < count; index++) { const toolCallId = memoryCall?.id ?? (text === "activity-replay" ? replayId : crypto.randomUUID()); const toolName = memoryCall?.tool ?? (text === "tool" ? "fixtureEcho" : text === "activity-action" ? "fixtureAction" : text === "activity-progress" ? "fixtureProgress" : text === "activity-unknown" ? "fixtureUnknown" : "fixtureActivity"); emit({ type: "tool-input-start", id: toolCallId, toolName }); if (text === "activity-pending") await new Promise((resolve) => setTimeout(resolve, 1000)); const input = memoryCall ? JSON.stringify(memoryCall.input) : text === "tool" ? '{"value":"native tool output"}' : JSON.stringify({ scenario: text, index: text === "activity-replay" ? attempts : index, secret: "fixture-private-credential", url: "https://user:password@example.com/private?token=secret#secret", }); emit({ type: "tool-input-delta", id: toolCallId, delta: input, }); emit({ type: "tool-input-end", id: toolCallId }); emit({ type: "tool-call", toolCallId, toolName, input }); } } else { emit({ type: "text-start", id: "text" }); for (let index = 0; index < tokens.length; index++) { if (abortSignal?.aborted) { controller.close(); return; } emit({ type: "text-delta", id: "text", delta: tokens[index] }); if (index === 0 && slow) await new Promise((resolve) => { const timer = setTimeout( resolve, text === "recover" ? 30_000 : 1_500, ); abortSignal?.addEventListener( "abort", () => { clearTimeout(timer); resolve(); }, { once: true }, ); }); } emit({ type: "text-end", id: "text" }); } emit({ type: "finish", finishReason: { unified: toolCall ? "tool-calls" : "stop", raw: undefined, }, usage: { inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0, }, outputTokens: { total: 1, text: 1, reasoning: 0 }, }, }); controller.close(); }, }); return { stream }; }, }); } protected getToolActivityDescriptors(): Record< string, ToolActivityDescriptor > { return { ...super.getToolActivityDescriptors(), fixtureEcho: { kind: "other", label: "Echo test", outputSummary: () => "Echo completed", }, fixtureActivity: { kind: "web", label: "Test research", inputSummary: (input) => (input as { scenario: string }).scenario === "activity-bounds" ? "Safe\n".repeat(100) : "Research requested", progress: () => ({ text: "Checking items", completed: 0, total: 1 }), outcome: (output) => (output as { ok: boolean }).ok ? "succeeded" : "failed", }, fixtureProgress: { kind: "shell", label: "Test progress", progress: (value) => ({ text: "Processing items", completed: (value as { completed: number }).completed, total: 1000, }), }, fixtureAction: { kind: "schedule", label: "Test action" }, }; } getActions() { const memoryActions = super.getActions(); return { ...memoryActions, remember: action({ ...memoryActions.remember.config, execute: async (input, ctx) => { if (input.content === "Cancelled memory should not be saved.") await new Promise((resolve) => setTimeout(resolve, 1000)); return memoryActions.remember.config.execute(input, ctx); }, }), fixtureAction: action({ description: "Native failing test action", inputSchema: z.object({ scenario: z.string() }), execute: async () => { throw new Error("Fixture action failed"); }, }), }; } getTools() { const inputSchema = z.object({ scenario: z.string(), index: z.number(), secret: z.string(), url: z.string(), }); return { ...super.getTools(), fixtureActivity: tool({ description: "Test activity outcomes", inputSchema, execute: async ({ scenario, index }, { abortSignal, toolCallId }) => { if (scenario === "activity-early") { this.reportToolProgress(toolCallId, { secret: "fixture-private-progress", }); await new Promise((resolve) => setTimeout(resolve, 800)); } if (scenario === "activity-error") throw new Error("Fixture execution failed"); if ( [ "activity-cancel", "activity-late", "activity-recover", "activity-parallel", "activity-detach", ].includes(scenario) || (scenario === "activity-replay" && index === 1) ) { await new Promise((resolve, reject) => { const timer = setTimeout( resolve, scenario === "activity-recover" || scenario === "activity-replay" ? 30_000 : scenario === "activity-parallel" ? 350 + index * 750 : 1500, ); if (scenario !== "activity-late") abortSignal?.addEventListener( "abort", () => { clearTimeout(timer); reject(new Error("Fixture aborted")); }, { once: true }, ); }); } return { ok: scenario !== "activity-failure", secret: "fixture-private-output", }; }, }), fixtureProgress: tool({ description: "Test native preliminary outputs", inputSchema, execute: async function* () { for (let completed = 0; completed < 1000; completed++) { if (completed % 100 === 0) await new Promise((resolve) => setTimeout(resolve, 80)); yield { completed, secret: "fixture-private-progress" }; } yield { completed: 1000, secret: "fixture-private-output" }; }, }), fixtureUnknown: tool({ description: "Test generic fallback", inputSchema, execute: async () => ({ secret: "fixture-private-output" }), }), fixtureEcho: tool({ description: "Test the native server tool loop", inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => ({ echoed: value }), }), }; } } export default { async fetch(request, env, ctx) { const path = new URL(request.url).pathname; if (path.startsWith("/__fixture/")) { // This test Worker exports the extended fixture class under the stable // production binding. Namespace types are invariant across subclasses. const personal = await getAgentByName( env.PersonalAgent as unknown as DurableObjectNamespace, "personal", ); if (path === "/__fixture/diagnostics") { const url = new URL(request.url); return Response.json( await personal.fixtureDiagnostics( url.searchParams.get("id")!, url.searchParams.get("fail") === "true", ), ); } if (path === "/__fixture/memory-fault") { await personal.configureMemoryFault( new URL(request.url).searchParams.get("mode") ?? "", ); return new Response(null, { status: 204 }); } if (path === "/__fixture/inspect") return Response.json(await personal.inspectConversations()); if ( path === "/__fixture/enable-openrouter" && request.method === "POST" ) { await personal.fixtureEnableOpenRouter(); return new Response(null, { status: 204 }); } if (path === "/__fixture/legacy-key") { await personal.fixtureLegacyKey(); return new Response(null, { status: 204 }); } if (path === "/__fixture/retired-opencode") { await personal.fixtureRetiredOpenCode(); return new Response(null, { status: 204 }); } if (path === "/__fixture/interrupted-create") return Response.json(await personal.stageInterruptedCreation()); if (path === "/__fixture/fail-delete") { await personal.failNextDeletion(); return new Response(null, { status: 204 }); } } return runtime.fetch(request, env, ctx); }, } satisfies ExportedHandler;