import { Agent } from "agents";
import type { GeneratedEnv } from "@ayjnt/env";
type Message = { id: string; role: "user" | "assistant"; text: string; at: number };
type State = { messages: Message[]; streaming: boolean; streamingId: string | null };
export default class ChatAgent extends Agent<GeneratedEnv, State> {
override initialState: State = { messages: [], streaming: false, streamingId: null };
override async onRequest(request: Request): Promise<Response> {
if (request.method !== "POST") return Response.json({ instance: this.name, ...this.state });
const { text } = (await request.json()) as { text: string };
const userMsg: Message = { id: crypto.randomUUID(), role: "user", text, at: Date.now() };
const assistantId = crypto.randomUUID();
const assistantMsg: Message = { id: assistantId, role: "assistant", text: "", at: Date.now() };
this.setState({
messages: [...this.state.messages, userMsg, assistantMsg],
streaming: true, streamingId: assistantId,
});
// Fire-and-forget: HTTP returns now, generation continues in the background.
// ctx.waitUntil keeps the worker alive until the promise resolves.
this.ctx.waitUntil(this.streamReply(assistantId));
return Response.json({ ok: true, assistantId });
}
private async streamReply(assistantId: string) {
const history = this.state.messages
.filter((m) => m.id !== assistantId)
.map((m) => ({ role: m.role === "user" ? "user" : "model", parts: [{ text: m.text }] }));
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=${this.env.GOOGLE_API_KEY}`;
const res = await fetch(url, { method: "POST", headers: {"content-type":"application/json"}, body: JSON.stringify({ contents: history }) });
const reader = res.body!.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n"); buf = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
try {
const chunk = JSON.parse(line.slice(6));
const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) {
this.setState({
...this.state,
messages: this.state.messages.map((m) =>
m.id === assistantId ? { ...m, text: m.text + text } : m),
});
}
} catch { /* chunk boundary; keep buffering */ }
}
}
this.setState({ ...this.state, streaming: false, streamingId: null });
}
}