1import * as Cloudflare from "@/Cloudflare";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Ref from "effect/Ref";
5import * as Stream from "effect/Stream";
6import { Chat } from "effect/ai";
7import * as RpcSerialization from "effect/rpc/RpcSerialization";
8import * as RpcServer from "effect/rpc/RpcServer";
9import { ChatBackendRpcs } from "./ChatRpcs.ts";
10import { Gateway } from "./Gateway.ts";
11
12// One DurableObject instance == one conversation thread. The chat
13// history lives in the DO's `state.storage` via
14// `DurableObjectChatPersistence`. Unlike the method-bridge DO, this one
15// serves a typed Effect `RpcGroup` on its own `fetch`, so the streaming
16// procedure can hand back real `effect/ai` `Response.StreamPart`
17// instances (the built-in bridge would `JSON.stringify` them and strip
18// the `Schema.Class` identity).
19export default class ChatBackendRpc extends Cloudflare.RpcDurableObject<ChatBackendRpc>()(
20 "ChatBackendRpc",
21 { schema: ChatBackendRpcs },
22 Effect.gen(function* () {
23 // Outer init: bind the AI Gateway and build the LanguageModel layer
24 // once — shared across every instance hosted by this Worker.
25 const aiGateway = yield* Cloudflare.AI.QueryGateway(Gateway);
26 const languageModel = aiGateway.model({
27 model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
28 parameters: {
29 temperature: 0.2,
30 maxTokens: 512,
31 },
32 });
33
34 return Effect.gen(function* () {
35 // Per-instance: chat persistence backed by this DO's storage.
36 const persistence = yield* Chat.makePersisted({
37 storeId: "alchemy.chat",
38 }).pipe(Effect.provide(Cloudflare.AI.DurableObjectChatPersistence));
39
40 const handlers = ChatBackendRpcs.toLayer({
41 send: ({ prompt }) =>
42 Effect.gen(function* () {
43 const chat = yield* persistence.getOrCreate("thread");
44 const response = yield* chat.generateText({ prompt });
45 const history = yield* Ref.get(chat.history);
46 return { text: response.text, turns: history.content.length };
47 }).pipe(Effect.provide(languageModel), Effect.orDie),
48 streamMessage: ({ prompt }) =>
49 persistence.getOrCreate("thread").pipe(
50 // `streamText` on a persisted chat saves the appended turn
51 // back to `state.storage` when the stream finalizes.
52 Effect.map((chat) =>
53 chat.streamText({ prompt }).pipe(Stream.provide(languageModel)),
54 ),
55 Stream.unwrap,
56 Stream.orDie,
57 ),
58 });
59
60 return RpcServer.toHttpEffect(ChatBackendRpcs).pipe(
61 Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)),
62 );
63 });
64 }).pipe(Effect.provide(Cloudflare.AI.QueryGatewayBinding)),
65) {}