@alexanderolsen/create-deepagent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +661 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +59 -0
  5. package/registry/frameworks/deno/.env.example +6 -0
  6. package/registry/frameworks/deno/README.md +137 -0
  7. package/registry/frameworks/deno/client/index.html +23 -0
  8. package/registry/frameworks/deno/client/package.json +30 -0
  9. package/registry/frameworks/deno/client/public/favicon.ico +0 -0
  10. package/registry/frameworks/deno/client/src/components/Chat.tsx +124 -0
  11. package/registry/frameworks/deno/client/src/components/ChatApp.tsx +129 -0
  12. package/registry/frameworks/deno/client/src/components/Conversation.tsx +91 -0
  13. package/registry/frameworks/deno/client/src/components/MessageBubbles.tsx +88 -0
  14. package/registry/frameworks/deno/client/src/components/MessageReasoning.tsx +71 -0
  15. package/registry/frameworks/deno/client/src/components/MessageThread.tsx +135 -0
  16. package/registry/frameworks/deno/client/src/components/StreamingIndicator.tsx +36 -0
  17. package/registry/frameworks/deno/client/src/components/Subagents.tsx +120 -0
  18. package/registry/frameworks/deno/client/src/components/ThemeIcons.tsx +31 -0
  19. package/registry/frameworks/deno/client/src/components/ThreadHistory.tsx +73 -0
  20. package/registry/frameworks/deno/client/src/components/ToolCall.tsx +89 -0
  21. package/registry/frameworks/deno/client/src/lib/agent-type.ts +4 -0
  22. package/registry/frameworks/deno/client/src/lib/chat/threads-client.ts +51 -0
  23. package/registry/frameworks/deno/client/src/main.tsx +11 -0
  24. package/registry/frameworks/deno/client/src/styles/globals.css +714 -0
  25. package/registry/frameworks/deno/client/src/vite-env.d.ts +1 -0
  26. package/registry/frameworks/deno/client/tsconfig.app.json +7 -0
  27. package/registry/frameworks/deno/client/tsconfig.json +24 -0
  28. package/registry/frameworks/deno/client/tsconfig.node.json +19 -0
  29. package/registry/frameworks/deno/client/vite.config.ts +24 -0
  30. package/registry/frameworks/deno/deno.json +16 -0
  31. package/registry/frameworks/deno/main.ts +23 -0
  32. package/registry/frameworks/deno/package.json +14 -0
  33. package/registry/frameworks/deno/server/agent/index.ts +60 -0
  34. package/registry/frameworks/deno/server/agent/middleware.ts +24 -0
  35. package/registry/frameworks/deno/server/agent/tools.ts +64 -0
  36. package/registry/frameworks/deno/server/registry.ts +40 -0
  37. package/registry/frameworks/deno/server/routes.ts +114 -0
  38. package/registry/frameworks/deno/server/serialize.ts +30 -0
  39. package/registry/frameworks/deno/server/session.ts +210 -0
  40. package/registry/frameworks/deno/server/threads.ts +404 -0
  41. package/registry/frameworks/deno.ts +17 -0
  42. package/registry/frameworks/hono/.env.example +6 -0
  43. package/registry/frameworks/hono/README.md +186 -0
  44. package/registry/frameworks/hono/index.html +22 -0
  45. package/registry/frameworks/hono/package.json +42 -0
  46. package/registry/frameworks/hono/src/components/Chat.tsx +124 -0
  47. package/registry/frameworks/hono/src/components/ChatApp.tsx +129 -0
  48. package/registry/frameworks/hono/src/components/Conversation.tsx +90 -0
  49. package/registry/frameworks/hono/src/components/MessageBubbles.tsx +88 -0
  50. package/registry/frameworks/hono/src/components/MessageReasoning.tsx +71 -0
  51. package/registry/frameworks/hono/src/components/MessageThread.tsx +135 -0
  52. package/registry/frameworks/hono/src/components/StreamingIndicator.tsx +36 -0
  53. package/registry/frameworks/hono/src/components/Subagents.tsx +120 -0
  54. package/registry/frameworks/hono/src/components/ThemeIcons.tsx +31 -0
  55. package/registry/frameworks/hono/src/components/ThreadHistory.tsx +73 -0
  56. package/registry/frameworks/hono/src/components/ToolCall.tsx +89 -0
  57. package/registry/frameworks/hono/src/lib/agent/types.ts +4 -0
  58. package/registry/frameworks/hono/src/lib/chat/threads-client.ts +57 -0
  59. package/registry/frameworks/hono/src/main.tsx +11 -0
  60. package/registry/frameworks/hono/src/styles/globals.css +714 -0
  61. package/registry/frameworks/hono/src/vite-env.d.ts +1 -0
  62. package/registry/frameworks/hono/tsconfig.app.json +22 -0
  63. package/registry/frameworks/hono/tsconfig.json +7 -0
  64. package/registry/frameworks/hono/tsconfig.worker.json +18 -0
  65. package/registry/frameworks/hono/vite.config.ts +16 -0
  66. package/registry/frameworks/hono/worker/agent/index.ts +53 -0
  67. package/registry/frameworks/hono/worker/agent/middleware.ts +20 -0
  68. package/registry/frameworks/hono/worker/agent/tools.ts +55 -0
  69. package/registry/frameworks/hono/worker/durable-objects/thread-session.ts +159 -0
  70. package/registry/frameworks/hono/worker/env.d.ts +17 -0
  71. package/registry/frameworks/hono/worker/index.ts +140 -0
  72. package/registry/frameworks/hono/worker/server/registry.ts +39 -0
  73. package/registry/frameworks/hono/worker/server/runs.ts +82 -0
  74. package/registry/frameworks/hono/worker/server/serialize.ts +30 -0
  75. package/registry/frameworks/hono/worker/server/threads.ts +404 -0
  76. package/registry/frameworks/hono/worker/tsconfig.json +4 -0
  77. package/registry/frameworks/hono/wrangler.jsonc +28 -0
  78. package/registry/frameworks/hono.ts +35 -0
  79. package/registry/frameworks/next/.env.example +6 -0
  80. package/registry/frameworks/next/README.md +173 -0
  81. package/registry/frameworks/next/app/api/threads/[threadId]/commands/route.ts +21 -0
  82. package/registry/frameworks/next/app/api/threads/[threadId]/history/route.ts +35 -0
  83. package/registry/frameworks/next/app/api/threads/[threadId]/route.ts +13 -0
  84. package/registry/frameworks/next/app/api/threads/[threadId]/state/route.ts +51 -0
  85. package/registry/frameworks/next/app/api/threads/[threadId]/stream/route.ts +30 -0
  86. package/registry/frameworks/next/app/api/threads/route.ts +11 -0
  87. package/registry/frameworks/next/app/favicon.ico +0 -0
  88. package/registry/frameworks/next/app/globals.css +712 -0
  89. package/registry/frameworks/next/app/layout.tsx +34 -0
  90. package/registry/frameworks/next/app/page.tsx +5 -0
  91. package/registry/frameworks/next/components/Chat.tsx +124 -0
  92. package/registry/frameworks/next/components/ChatApp.tsx +129 -0
  93. package/registry/frameworks/next/components/Conversation.tsx +90 -0
  94. package/registry/frameworks/next/components/MessageBubbles.tsx +88 -0
  95. package/registry/frameworks/next/components/MessageReasoning.tsx +71 -0
  96. package/registry/frameworks/next/components/MessageThread.tsx +135 -0
  97. package/registry/frameworks/next/components/StreamingIndicator.tsx +36 -0
  98. package/registry/frameworks/next/components/Subagents.tsx +120 -0
  99. package/registry/frameworks/next/components/ThemeIcons.tsx +31 -0
  100. package/registry/frameworks/next/components/ThreadHistory.tsx +73 -0
  101. package/registry/frameworks/next/components/ToolCall.tsx +89 -0
  102. package/registry/frameworks/next/eslint.config.mjs +18 -0
  103. package/registry/frameworks/next/lib/agent/index.ts +95 -0
  104. package/registry/frameworks/next/lib/agent/middleware.ts +40 -0
  105. package/registry/frameworks/next/lib/agent/tools.ts +66 -0
  106. package/registry/frameworks/next/lib/chat/threads-client.ts +57 -0
  107. package/registry/frameworks/next/lib/server/registry.ts +57 -0
  108. package/registry/frameworks/next/lib/server/serialize.ts +32 -0
  109. package/registry/frameworks/next/lib/server/session.ts +212 -0
  110. package/registry/frameworks/next/lib/server/threads.ts +406 -0
  111. package/registry/frameworks/next/next.config.ts +7 -0
  112. package/registry/frameworks/next/package.json +37 -0
  113. package/registry/frameworks/next/postcss.config.mjs +7 -0
  114. package/registry/frameworks/next/public/file.svg +1 -0
  115. package/registry/frameworks/next/public/globe.svg +1 -0
  116. package/registry/frameworks/next/public/next.svg +1 -0
  117. package/registry/frameworks/next/public/vercel.svg +1 -0
  118. package/registry/frameworks/next/public/window.svg +1 -0
  119. package/registry/frameworks/next/tsconfig.json +34 -0
  120. package/registry/frameworks/next.ts +17 -0
  121. package/registry/frameworks/nuxt/.env.example +3 -0
  122. package/registry/frameworks/nuxt/README.md +133 -0
  123. package/registry/frameworks/nuxt/app/app.vue +26 -0
  124. package/registry/frameworks/nuxt/app/assets/css/main.css +707 -0
  125. package/registry/frameworks/nuxt/app/components/Chat.vue +105 -0
  126. package/registry/frameworks/nuxt/app/components/ChatApp.vue +89 -0
  127. package/registry/frameworks/nuxt/app/components/ChatThread.vue +27 -0
  128. package/registry/frameworks/nuxt/app/components/MessageBubble.vue +60 -0
  129. package/registry/frameworks/nuxt/app/components/MessageBubbles.vue +213 -0
  130. package/registry/frameworks/nuxt/app/components/MessageList.vue +51 -0
  131. package/registry/frameworks/nuxt/app/components/MessageReasoning.vue +53 -0
  132. package/registry/frameworks/nuxt/app/components/StreamingIndicator.vue +9 -0
  133. package/registry/frameworks/nuxt/app/components/SubagentDetail.vue +51 -0
  134. package/registry/frameworks/nuxt/app/components/SubagentList.vue +49 -0
  135. package/registry/frameworks/nuxt/app/components/ThemeToggle.vue +43 -0
  136. package/registry/frameworks/nuxt/app/components/ThreadHistory.vue +65 -0
  137. package/registry/frameworks/nuxt/app/components/ToolCall.vue +81 -0
  138. package/registry/frameworks/nuxt/app/components/TypingDots.vue +14 -0
  139. package/registry/frameworks/nuxt/app/composables/useTheme.ts +14 -0
  140. package/registry/frameworks/nuxt/app/utils/streaming.ts +44 -0
  141. package/registry/frameworks/nuxt/app/utils/threads.ts +57 -0
  142. package/registry/frameworks/nuxt/nuxt.config.ts +6 -0
  143. package/registry/frameworks/nuxt/package.json +28 -0
  144. package/registry/frameworks/nuxt/public/favicon.ico +0 -0
  145. package/registry/frameworks/nuxt/public/robots.txt +2 -0
  146. package/registry/frameworks/nuxt/server/agent/index.ts +89 -0
  147. package/registry/frameworks/nuxt/server/agent/middleware.ts +38 -0
  148. package/registry/frameworks/nuxt/server/agent/tools.ts +66 -0
  149. package/registry/frameworks/nuxt/server/api/threads/[threadId]/commands.post.ts +16 -0
  150. package/registry/frameworks/nuxt/server/api/threads/[threadId]/history.post.ts +37 -0
  151. package/registry/frameworks/nuxt/server/api/threads/[threadId]/index.delete.ts +12 -0
  152. package/registry/frameworks/nuxt/server/api/threads/[threadId]/state.get.ts +22 -0
  153. package/registry/frameworks/nuxt/server/api/threads/[threadId]/state.post.ts +32 -0
  154. package/registry/frameworks/nuxt/server/api/threads/[threadId]/stream.post.ts +24 -0
  155. package/registry/frameworks/nuxt/server/api/threads/index.get.ts +13 -0
  156. package/registry/frameworks/nuxt/server/utils/runtime.ts +42 -0
  157. package/registry/frameworks/nuxt/server/utils/serialize.ts +30 -0
  158. package/registry/frameworks/nuxt/server/utils/session.ts +210 -0
  159. package/registry/frameworks/nuxt/server/utils/threads.ts +404 -0
  160. package/registry/frameworks/nuxt/tsconfig.json +18 -0
  161. package/registry/frameworks/nuxt.ts +17 -0
  162. package/registry/frameworks/vite/.env.example +20 -0
  163. package/registry/frameworks/vite/README.md +149 -0
  164. package/registry/frameworks/vite/agent/index.ts +59 -0
  165. package/registry/frameworks/vite/agent/middleware.ts +24 -0
  166. package/registry/frameworks/vite/agent/tools.ts +64 -0
  167. package/registry/frameworks/vite/index.html +23 -0
  168. package/registry/frameworks/vite/langgraph.json +16 -0
  169. package/registry/frameworks/vite/package.json +39 -0
  170. package/registry/frameworks/vite/public/favicon.ico +0 -0
  171. package/registry/frameworks/vite/scripts/vite-langgraph-proxy.ts +34 -0
  172. package/registry/frameworks/vite/src/components/Chat.tsx +124 -0
  173. package/registry/frameworks/vite/src/components/ChatApp.tsx +122 -0
  174. package/registry/frameworks/vite/src/components/Conversation.tsx +91 -0
  175. package/registry/frameworks/vite/src/components/MessageBubbles.tsx +88 -0
  176. package/registry/frameworks/vite/src/components/MessageReasoning.tsx +71 -0
  177. package/registry/frameworks/vite/src/components/MessageThread.tsx +135 -0
  178. package/registry/frameworks/vite/src/components/StreamingIndicator.tsx +36 -0
  179. package/registry/frameworks/vite/src/components/Subagents.tsx +120 -0
  180. package/registry/frameworks/vite/src/components/ThemeIcons.tsx +31 -0
  181. package/registry/frameworks/vite/src/components/ThreadHistory.tsx +73 -0
  182. package/registry/frameworks/vite/src/components/ToolCall.tsx +89 -0
  183. package/registry/frameworks/vite/src/lib/agent-type.ts +4 -0
  184. package/registry/frameworks/vite/src/lib/chat/threads-client.ts +114 -0
  185. package/registry/frameworks/vite/src/main.tsx +11 -0
  186. package/registry/frameworks/vite/src/styles/globals.css +714 -0
  187. package/registry/frameworks/vite/src/vite-env.d.ts +11 -0
  188. package/registry/frameworks/vite/tsconfig.app.json +24 -0
  189. package/registry/frameworks/vite/tsconfig.json +7 -0
  190. package/registry/frameworks/vite/tsconfig.node.json +21 -0
  191. package/registry/frameworks/vite/vercel.json +3 -0
  192. package/registry/frameworks/vite/vite.config.ts +24 -0
  193. package/registry/frameworks/vite.ts +17 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.worker.json" }
6
+ ]
7
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "composite": true,
15
+ "types": ["@cloudflare/workers-types/latest", "vite/client"]
16
+ },
17
+ "include": ["worker"]
18
+ }
@@ -0,0 +1,16 @@
1
+ /// <reference types="node" />
2
+ import path from "node:path";
3
+
4
+ import { cloudflare } from "@cloudflare/vite-plugin";
5
+ import tailwindcss from "@tailwindcss/vite";
6
+ import react from "@vitejs/plugin-react";
7
+ import { defineConfig } from "vite";
8
+
9
+ export default defineConfig({
10
+ plugins: [react(), tailwindcss(), cloudflare()],
11
+ resolve: {
12
+ alias: {
13
+ "@": path.resolve(__dirname, "src"),
14
+ },
15
+ },
16
+ });
@@ -0,0 +1,53 @@
1
+ import { MemorySaver } from "@langchain/langgraph";
2
+ import { ChatOpenAI } from "@langchain/openai";
3
+ import { createDeepAgent } from "deepagents";
4
+
5
+ import { stripReasoningReplay } from "./middleware";
6
+ import { calculator, searchWeb } from "./tools";
7
+
8
+ const coordinatorModel = new ChatOpenAI({
9
+ model: "gpt-5.4-mini",
10
+ reasoning: { effort: "low", summary: "auto" },
11
+ });
12
+
13
+ const subagentModel = new ChatOpenAI({ model: "gpt-5.4-mini" });
14
+
15
+ export const checkpointer = new MemorySaver();
16
+
17
+ export const agent = createDeepAgent({
18
+ model: coordinatorModel,
19
+ middleware: [stripReasoningReplay],
20
+ checkpointer,
21
+ subagents: [
22
+ {
23
+ name: "researcher",
24
+ description:
25
+ "Researches a topic using the search_web tool and reports concise findings.",
26
+ tools: [searchWeb],
27
+ model: subagentModel,
28
+ systemPrompt:
29
+ "You are the researcher subagent. Use the search_web tool to look up " +
30
+ "the requested topic, then summarize the findings in two or three " +
31
+ "sentences. Always call search_web at least once before answering.",
32
+ },
33
+ {
34
+ name: "math-whiz",
35
+ description:
36
+ "Performs calculations using the calculator tool and explains the result.",
37
+ tools: [calculator],
38
+ model: subagentModel,
39
+ systemPrompt:
40
+ "You are the math-whiz subagent. Use the calculator tool to evaluate " +
41
+ "the requested expression, then state the result clearly. Always call " +
42
+ "the calculator tool before answering.",
43
+ },
44
+ ],
45
+ systemPrompt:
46
+ "You are a helpful coordinator. When a request involves looking something " +
47
+ "up, delegate it to the `researcher` subagent. When it involves math, " +
48
+ "delegate it to the `math-whiz` subagent. You may run both subagents for a " +
49
+ "single request. After the subagents respond, combine their results into a " +
50
+ "short, clearly labeled final answer.",
51
+ });
52
+
53
+ export type Agent = typeof agent;
@@ -0,0 +1,20 @@
1
+ import { AIMessage, type BaseMessage } from "@langchain/core/messages";
2
+ import { createMiddleware } from "langchain";
3
+
4
+ function sanitizeForReplay(message: BaseMessage): BaseMessage {
5
+ if (!AIMessage.isInstance(message)) return message;
6
+
7
+ return new AIMessage({
8
+ id: message.id,
9
+ content: message.content,
10
+ tool_calls: message.tool_calls,
11
+ invalid_tool_calls: message.invalid_tool_calls,
12
+ usage_metadata: message.usage_metadata,
13
+ });
14
+ }
15
+
16
+ export const stripReasoningReplay = createMiddleware({
17
+ name: "StripReasoningReplay",
18
+ wrapModelCall: async (request, handler) =>
19
+ handler({ ...request, messages: request.messages.map(sanitizeForReplay) }),
20
+ });
@@ -0,0 +1,55 @@
1
+ import { tool } from "langchain";
2
+ import { z } from "zod";
3
+
4
+ export const searchWeb = tool(
5
+ async ({ query }) => {
6
+ await new Promise((resolve) => setTimeout(resolve, 300));
7
+ return JSON.stringify({
8
+ results: [
9
+ {
10
+ title: `Result for: ${query}`,
11
+ snippet:
12
+ "LangGraph streaming sends token deltas on the messages channel " +
13
+ "and tool lifecycle events on the tools channel.",
14
+ },
15
+ ],
16
+ });
17
+ },
18
+ {
19
+ name: "search_web",
20
+ description: "Search the web for information about a topic.",
21
+ schema: z.object({ query: z.string().describe("Search query.") }),
22
+ },
23
+ );
24
+
25
+ function evaluateExpression(expression: string): number {
26
+ if (!/^[\d+\-*/().\s]+$/.test(expression)) {
27
+ throw new Error("Only basic arithmetic is supported.");
28
+ }
29
+ const compute = new Function(
30
+ `"use strict"; return (${expression});`,
31
+ ) as () => unknown;
32
+ const result = compute();
33
+ if (typeof result !== "number" || !Number.isFinite(result)) {
34
+ throw new Error("Expression did not evaluate to a finite number.");
35
+ }
36
+ return result;
37
+ }
38
+
39
+ export const calculator = tool(
40
+ async ({ expression }) => {
41
+ await new Promise((resolve) => setTimeout(resolve, 100));
42
+ try {
43
+ return String(evaluateExpression(expression));
44
+ } catch (error) {
45
+ return `Error evaluating: ${expression} (${String(error)})`;
46
+ }
47
+ },
48
+ {
49
+ name: "calculator",
50
+ description: "Evaluate a math expression.",
51
+ schema: z.object({
52
+ expression: z.string().describe("Math expression to evaluate."),
53
+ }),
54
+ },
55
+ );
@@ -0,0 +1,159 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ // `StreamChannel` buffers events; `matchesSubscription` is the shared protocol
3
+ // predicate from `@langchain/langgraph/stream` — the same one langgraph-api
4
+ // uses, so this custom transport stays aligned with the production server.
5
+ import {
6
+ StreamChannel,
7
+ matchesSubscription,
8
+ type ProtocolEvent,
9
+ } from "@langchain/langgraph/stream";
10
+ import type { SubscribeParams } from "@langchain/protocol";
11
+
12
+ import { sanitizeForJson } from "../server/serialize";
13
+
14
+ /**
15
+ * Make an event safe to `JSON.stringify` onto the SSE wire.
16
+ *
17
+ * Only the protocol payload (`params.data`) and any `params.interrupts` can
18
+ * carry LangChain message instances, so those are the fields we sanitize into
19
+ * the plain, role-keyed protocol message shape the SDK expects.
20
+ */
21
+ function sanitizeEvent(event: ProtocolEvent): ProtocolEvent {
22
+ const params = event.params as Record<string, unknown>;
23
+ const sanitizedParams: Record<string, unknown> = {
24
+ ...params,
25
+ data: sanitizeForJson(params.data),
26
+ };
27
+ if ("interrupts" in params) {
28
+ sanitizedParams.interrupts = sanitizeForJson(params.interrupts);
29
+ }
30
+ return { ...event, params: sanitizedParams } as ProtocolEvent;
31
+ }
32
+
33
+ /**
34
+ * Encode an Agent Protocol event as a Server-Sent Event frame.
35
+ *
36
+ * When available, `event_id` is mirrored into the SSE `id:` field for
37
+ * transport-level reconnection. The SDK primarily deduplicates by `event_id`
38
+ * and replays by `seq`; if an event has no `event_id`, this example falls back
39
+ * to `seq` as a stable frame id.
40
+ */
41
+ function encodeSse(event: ProtocolEvent) {
42
+ const eventId = (event as { event_id?: string }).event_id;
43
+ const id = eventId ?? (typeof event.seq === "number" ? `${event.seq}` : "");
44
+ const idLine = id ? `id: ${id}\n` : "";
45
+ return new TextEncoder().encode(
46
+ `${idLine}event: message\ndata: ${JSON.stringify(event)}\n\n`,
47
+ );
48
+ }
49
+
50
+ const SSE_HEADERS = {
51
+ "cache-control": "no-cache, no-transform",
52
+ "content-type": "text/event-stream",
53
+ connection: "keep-alive",
54
+ "x-accel-buffering": "no",
55
+ };
56
+
57
+ /**
58
+ * Per-thread Durable Object that owns the Agent Streaming Protocol event log.
59
+ *
60
+ * Cloudflare Workers cannot keep an in-memory session map like the Node
61
+ * examples. Instead the Worker runs the LangGraph agent and POSTs each protocol
62
+ * each protocol event here; browser clients subscribe via `/stream`, which
63
+ * replays buffered events and stays attached for live frames.
64
+ *
65
+ * - `POST /publish` appends a protocol event to the replay log.
66
+ * - `POST /stream` opens a connection-scoped SSE subscription described by
67
+ * `SubscribeParams`.
68
+ * - `POST /clear` resets the replay buffer when a thread is deleted.
69
+ *
70
+ * The implementation is intentionally small. Production servers should persist
71
+ * threads, enforce concurrency policies, and coordinate replay buffers across
72
+ * workers.
73
+ */
74
+ export class ThreadSession extends DurableObject {
75
+ /**
76
+ * Per-thread protocol event log.
77
+ *
78
+ * A {@link StreamChannel} is LangGraph's buffered, append-only stream with
79
+ * independent per-consumer cursors. Every event ever published stays
80
+ * buffered, and each SSE subscription gets its own cursor via
81
+ * {@link StreamChannel.iterate}, so buffered replay and live delivery are the
82
+ * same iteration.
83
+ */
84
+ readonly #log = StreamChannel.local<ProtocolEvent>();
85
+
86
+ /** Monotonic seq across all runs on this thread (graph runs reset at 0). */
87
+ #nextSeq = 0;
88
+
89
+ async fetch(request: Request): Promise<Response> {
90
+ const url = new URL(request.url);
91
+
92
+ if (request.method === "POST" && url.pathname === "/publish") {
93
+ const event = (await request.json()) as ProtocolEvent;
94
+ this.#publish(event);
95
+ return new Response(null, { status: 204 });
96
+ }
97
+
98
+ if (request.method === "POST" && url.pathname === "/stream") {
99
+ const params = (await request.json()) as SubscribeParams;
100
+ return new Response(this.#stream(params), { headers: SSE_HEADERS });
101
+ }
102
+
103
+ if (request.method === "POST" && url.pathname === "/clear") {
104
+ this.#nextSeq = 0;
105
+ await this.ctx.storage.deleteAll();
106
+ return new Response(null, { status: 204 });
107
+ }
108
+
109
+ return new Response("Not Found", { status: 404 });
110
+ }
111
+
112
+ #publish(rawEvent: ProtocolEvent) {
113
+ const seq = this.#nextSeq;
114
+ this.#nextSeq += 1;
115
+ const event = sanitizeEvent({
116
+ ...rawEvent,
117
+ type: "event",
118
+ seq,
119
+ } as ProtocolEvent);
120
+ this.#log.push(event);
121
+ }
122
+
123
+ /**
124
+ * Open a connection-scoped SSE subscription for this thread.
125
+ *
126
+ * The returned `ReadableStream` first replays buffered events matching the
127
+ * requested `channels`, `namespaces`, `depth`, and optional `since` cursor,
128
+ * then stays attached for live events. Closing the HTTP connection releases
129
+ * this subscription's event-log cursor.
130
+ */
131
+ #stream(params: SubscribeParams) {
132
+ const cursor = this.#log.iterate();
133
+
134
+ return new ReadableStream<Uint8Array>({
135
+ pull: async (controller) => {
136
+ // Scan forward until we find an event matching this subscription's
137
+ // filter, enqueue exactly one frame, and return so the channel honors
138
+ // the consumer's backpressure. `cursor.next()` resolves immediately for
139
+ // buffered events and suspends once the live edge is reached.
140
+ for (;;) {
141
+ const { value: event, done } = await cursor.next();
142
+ if (done) {
143
+ controller.close();
144
+ return;
145
+ }
146
+ if (matchesSubscription(event, params)) {
147
+ controller.enqueue(encodeSse(event));
148
+ return;
149
+ }
150
+ }
151
+ },
152
+ cancel: () => {
153
+ void cursor.return?.(undefined);
154
+ },
155
+ });
156
+ }
157
+ }
158
+
159
+ export default ThreadSession;
@@ -0,0 +1,17 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ import type {
4
+ Command,
5
+ CommandResponse,
6
+ ErrorResponse,
7
+ } from "@langchain/protocol";
8
+
9
+ declare global {
10
+ interface Env {
11
+ ASSETS: Fetcher;
12
+ SESSIONS: DurableObjectNamespace;
13
+ OPENAI_API_KEY?: string;
14
+ }
15
+ }
16
+
17
+ export type { Command, CommandResponse, ErrorResponse };
@@ -0,0 +1,140 @@
1
+ import { Hono } from "hono";
2
+ import type {
3
+ Command,
4
+ CommandResponse,
5
+ ErrorResponse,
6
+ } from "@langchain/protocol";
7
+
8
+ import { ThreadSession } from "./durable-objects/thread-session";
9
+ import {
10
+ deleteThread,
11
+ getAgent,
12
+ getCheckpointer,
13
+ getSessionStub,
14
+ } from "./server/registry";
15
+ import { parseRunInput, startAgentRun } from "./server/runs";
16
+ import {
17
+ ThreadNotFoundError,
18
+ getThreadHistory,
19
+ getThreadState,
20
+ listThreads,
21
+ updateThreadState,
22
+ } from "./server/threads";
23
+
24
+ export { ThreadSession };
25
+
26
+ const app = new Hono<{ Bindings: Env }>();
27
+
28
+ app.get("/api/threads", async (c) => {
29
+ const threads = await listThreads(getAgent().graph, getCheckpointer());
30
+ return c.json(threads);
31
+ });
32
+
33
+ app.delete("/api/threads/:threadId", async (c) => {
34
+ const threadId = c.req.param("threadId");
35
+ await deleteThread(c.env, threadId);
36
+ return c.body(null, 204);
37
+ });
38
+
39
+ app.post("/api/threads/:threadId/commands", async (c) => {
40
+ const threadId = c.req.param("threadId");
41
+ const command = (await c.req.json()) as Command;
42
+
43
+ if (command.method !== "run.start") {
44
+ const error: ErrorResponse = {
45
+ type: "error",
46
+ id: command.id,
47
+ error: "unknown_command",
48
+ message: `Unsupported command: ${command.method}`,
49
+ };
50
+ return c.json(error);
51
+ }
52
+
53
+ const runId = crypto.randomUUID();
54
+ const input = parseRunInput(command);
55
+ c.executionCtx.waitUntil(startAgentRun(c.env, threadId, input, runId));
56
+
57
+ const response: CommandResponse = {
58
+ type: "success",
59
+ id: command.id,
60
+ result: { run_id: runId },
61
+ };
62
+ return c.json(response);
63
+ });
64
+
65
+ app.post("/api/threads/:threadId/stream", async (c) => {
66
+ const threadId = c.req.param("threadId");
67
+ const params = await c.req.json();
68
+ const stub = getSessionStub(c.env, threadId);
69
+ return stub.fetch(
70
+ new Request("https://session/stream", {
71
+ method: "POST",
72
+ body: JSON.stringify(params),
73
+ }),
74
+ );
75
+ });
76
+
77
+ app.get("/api/threads/:threadId/state", async (c) => {
78
+ const threadId = c.req.param("threadId");
79
+ try {
80
+ const state = await getThreadState(getAgent().graph, threadId);
81
+ return c.json(state);
82
+ } catch (error) {
83
+ if (error instanceof ThreadNotFoundError) {
84
+ return c.json({ error: "not_found", message: error.message }, 404);
85
+ }
86
+ throw error;
87
+ }
88
+ });
89
+
90
+ app.post("/api/threads/:threadId/state", async (c) => {
91
+ const threadId = c.req.param("threadId");
92
+ const body = (await c.req.json().catch(() => ({}))) as {
93
+ values?: Record<string, unknown> | null;
94
+ checkpoint?: Record<string, unknown> | null;
95
+ as_node?: string;
96
+ };
97
+ try {
98
+ const state = await updateThreadState(getAgent().graph, threadId, {
99
+ values: body.values ?? null,
100
+ checkpoint: body.checkpoint ?? null,
101
+ asNode: body.as_node,
102
+ });
103
+ return c.json(state);
104
+ } catch (error) {
105
+ return c.json(
106
+ { error: "invalid_state_update", message: String(error) },
107
+ 422,
108
+ );
109
+ }
110
+ });
111
+
112
+ app.post("/api/threads/:threadId/history", async (c) => {
113
+ const threadId = c.req.param("threadId");
114
+ const body = (await c.req.json().catch(() => ({}))) as {
115
+ limit?: number;
116
+ before?: unknown;
117
+ metadata?: Record<string, unknown>;
118
+ checkpoint?: Record<string, unknown>;
119
+ };
120
+ try {
121
+ const history = await getThreadHistory(getAgent().graph, threadId, {
122
+ limit: typeof body.limit === "number" ? body.limit : 10,
123
+ before: body.before,
124
+ metadata: body.metadata,
125
+ checkpoint: body.checkpoint,
126
+ });
127
+ return c.json(history);
128
+ } catch (error) {
129
+ if (error instanceof ThreadNotFoundError) {
130
+ return c.json({ error: "not_found", message: error.message }, 404);
131
+ }
132
+ throw error;
133
+ }
134
+ });
135
+
136
+ export default {
137
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
138
+ return app.fetch(request, env, ctx);
139
+ },
140
+ };
@@ -0,0 +1,39 @@
1
+ import { agent, checkpointer } from "../agent";
2
+
3
+ /**
4
+ * Worker-level registry for the compiled agent and checkpointer.
5
+ *
6
+ * Unlike Next.js route handlers (one Node process), Cloudflare Workers are
7
+ * short-lived isolates. The agent and `MemorySaver` checkpointer live here so
8
+ * thread state routes can read/write checkpoints. SSE replay buffers live in
9
+ * per-thread Durable Objects (see `durable-objects/thread-session.ts`) rather
10
+ * than an in-memory session map used by the Node examples.
11
+ *
12
+ * NOTE: This is in-memory and process-local within each isolate. Production
13
+ * deployments need a durable checkpointer and a Durable Object session store.
14
+ * The wiring here stays the same; only the checkpointer in `worker/agent` and
15
+ * the session store change.
16
+ */
17
+
18
+ /** The shared, compiled agent (and its checkpointer). */
19
+ export function getAgent() {
20
+ return agent;
21
+ }
22
+
23
+ /** The shared checkpointer — the single source of truth for threads. */
24
+ export function getCheckpointer() {
25
+ return checkpointer;
26
+ }
27
+
28
+ /** Delete a thread: remove its checkpointed state and clear the replay buffer. */
29
+ export async function deleteThread(env: Env, threadId: string): Promise<void> {
30
+ await checkpointer.deleteThread(threadId);
31
+ const stub = getSessionStub(env, threadId);
32
+ await stub.fetch(new Request("https://session/clear", { method: "POST" }));
33
+ }
34
+
35
+ /** Resolve the per-thread Durable Object stub for SSE replay. */
36
+ export function getSessionStub(env: Env, threadId: string) {
37
+ const id = env.SESSIONS.idFromName(threadId);
38
+ return env.SESSIONS.get(id);
39
+ }
@@ -0,0 +1,82 @@
1
+ import type { ReactAgent } from "langchain";
2
+ import type { ProtocolEvent } from "@langchain/langgraph/stream";
3
+
4
+ import { getAgent, getSessionStub } from "./registry";
5
+ import { isRecord, sanitizeForJson } from "./serialize";
6
+
7
+ // `ReactAgent<any>` accepts both `createAgent` results and `DeepAgent`
8
+ // instances (which carry a specific, non-default type config).
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ type AnyReactAgent = ReactAgent<any>;
11
+ type AgentRunInput = Parameters<AnyReactAgent["streamEvents"]>[0];
12
+
13
+ /**
14
+ * Make an event safe to `JSON.stringify` onto the SSE wire.
15
+ *
16
+ * Only the protocol payload (`params.data`) and any `params.interrupts` can
17
+ * carry LangChain message instances, so those are the fields we sanitize into
18
+ * the plain, role-keyed protocol message shape the SDK expects.
19
+ */
20
+ function sanitizeEvent(event: ProtocolEvent): ProtocolEvent {
21
+ const params = event.params as Record<string, unknown>;
22
+ const sanitizedParams: Record<string, unknown> = {
23
+ ...params,
24
+ data: sanitizeForJson(params.data),
25
+ };
26
+ if ("interrupts" in params) {
27
+ sanitizedParams.interrupts = sanitizeForJson(params.interrupts);
28
+ }
29
+ return { ...event, params: sanitizedParams } as ProtocolEvent;
30
+ }
31
+
32
+ /**
33
+ * Start an agent run on the Worker and fan protocol events into the thread's
34
+ * Durable Object for SSE replay.
35
+ *
36
+ * The SDK sends `run.start` to the `/commands` route, which calls this helper
37
+ * and immediately returns a generated `run_id`. Streamed events are published
38
+ * asynchronously to the thread's Durable Object while clients consume them
39
+ * through active `/stream` subscriptions.
40
+ */
41
+ export async function startAgentRun(
42
+ env: Env,
43
+ threadId: string,
44
+ input: unknown,
45
+ runId: string,
46
+ ) {
47
+ const stub = getSessionStub(env, threadId);
48
+ const activeAgent = getAgent() as AnyReactAgent;
49
+
50
+ // Thread the `thread_id` / `run_id` into the run config so the checkpointer
51
+ // persists conversation state per thread and downstream events carry the
52
+ // run identity.
53
+ const run = await activeAgent.streamEvents(input as AgentRunInput, {
54
+ version: "v3",
55
+ configurable: { thread_id: threadId, run_id: runId },
56
+ });
57
+
58
+ try {
59
+ for await (const rawEvent of run) {
60
+ const event = sanitizeEvent({
61
+ ...(rawEvent as ProtocolEvent),
62
+ type: "event",
63
+ } as ProtocolEvent);
64
+ await stub.fetch(
65
+ new Request("https://session/publish", {
66
+ method: "POST",
67
+ body: JSON.stringify(event),
68
+ }),
69
+ );
70
+ }
71
+ } catch (error) {
72
+ console.error(error);
73
+ }
74
+ }
75
+
76
+ /** Parse the `run.start` command payload accepted by `/commands`. */
77
+ export function parseRunInput(command: {
78
+ params?: unknown;
79
+ }): AgentRunInput | undefined {
80
+ if (!isRecord(command.params)) return undefined;
81
+ return command.params.input as AgentRunInput;
82
+ }
@@ -0,0 +1,30 @@
1
+ import { BaseMessage } from "@langchain/core/messages";
2
+
3
+ export function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === "object" && value !== null;
5
+ }
6
+
7
+ /**
8
+ * Recursively replace LangChain message instances with plain protocol dicts.
9
+ *
10
+ * Uses {@link BaseMessage.isInstance} and {@link BaseMessage.toDict} from
11
+ * `@langchain/core/messages` — the canonical LangChain serialization primitive.
12
+ * Message instances surface in `values`/`updates` stream data and in the
13
+ * checkpointer state snapshot returned by the thread-state routes, often nested
14
+ * under `messages`.
15
+ */
16
+ export function sanitizeForJson(value: unknown): unknown {
17
+ if (BaseMessage.isInstance(value)) {
18
+ const { type, data } = value.toDict();
19
+ return sanitizeForJson({ ...data, type });
20
+ }
21
+ if (Array.isArray(value)) return value.map(sanitizeForJson);
22
+ if (isRecord(value)) {
23
+ const result: Record<string, unknown> = {};
24
+ for (const [key, item] of Object.entries(value)) {
25
+ result[key] = sanitizeForJson(item);
26
+ }
27
+ return result;
28
+ }
29
+ return value;
30
+ }