create-langgraph-app 1.1.0 → 1.3.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 (2) hide show
  1. package/dist/create.js +484 -88
  2. package/package.json +1 -1
package/dist/create.js CHANGED
@@ -72,18 +72,14 @@ function generateEnv(config) {
72
72
  }
73
73
  function generatePackageJson(config) {
74
74
  const deps = {
75
- "@langchain/core": "^1.1.48",
76
- "@langchain/langgraph": "^1.3.2",
77
- "@langchain/langgraph-supervisor": "^1.0.3",
75
+ "@langchain/core": "^1.2.3",
76
+ "@langchain/langgraph": "^1.4.8",
78
77
  "@langchain/mcp-adapters": "^1.1.3",
79
78
  dotenv: "^17.4.2",
80
- fastify: "^5.8.5",
81
- langchain: "^1.3.0",
79
+ fastify: "^5.10.0",
80
+ langchain: "^1.5.3",
82
81
  zod: "^4.4.3",
83
82
  };
84
- if (config.patterns.includes("swarm")) {
85
- deps["@langchain/langgraph-swarm"] = "^1.0.2";
86
- }
87
83
  if (config.patterns.includes("rag")) {
88
84
  deps["@langchain/textsplitters"] = "^1.0.1";
89
85
  }
@@ -105,6 +101,7 @@ function generatePackageJson(config) {
105
101
  dev: "tsx src/index.ts",
106
102
  "dev:http": "tsx src/server.ts",
107
103
  test: "vitest run",
104
+ "test:watch": "vitest",
108
105
  typecheck: "tsc --noEmit",
109
106
  },
110
107
  dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),
@@ -130,7 +127,7 @@ function generateTsConfig() {
130
127
  types: ["node"],
131
128
  outDir: "dist",
132
129
  },
133
- include: ["src"],
130
+ include: ["src", "tests"],
134
131
  }, null, 2) + "\n";
135
132
  }
136
133
  function generateEnvConfig(config) {
@@ -219,50 +216,372 @@ export function getLlm(opts?: LlmOptions): Promise<BaseChatModel> {
219
216
  `;
220
217
  }
221
218
  function generateAgentFactory() {
222
- return `import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
223
- import type { StructuredToolInterface } from "@langchain/core/tools";
224
- import { createReactAgent, type CreateReactAgentParams } from "@langchain/langgraph/prebuilt";
219
+ return `import { createAgent, type ResponseFormat, type TypedToolStrategy } from "langchain";
220
+ import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
221
+ import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
222
+
223
+ type CreateAgentParams = Parameters<typeof createAgent>[0];
225
224
 
226
225
  export interface MakeAgentParams {
227
226
  name: string;
228
227
  llm: BaseChatModel;
229
- tools?: StructuredToolInterface[];
228
+ tools?: CreateAgentParams["tools"];
230
229
  system?: string;
231
- responseFormat?: CreateReactAgentParams["responseFormat"];
230
+ responseFormat?: ResponseFormat | TypedToolStrategy<Record<string, unknown>>;
231
+ /**
232
+ * Only the outermost agent of a multi-agent setup should get a
233
+ * checkpointer — subagents inherit it at runtime, which is what lets
234
+ * interrupt() calls bubble up to the top-level graph.
235
+ */
236
+ checkpointer?: BaseCheckpointSaver;
232
237
  }
233
238
 
234
- export function makeAgent({ name, llm, tools = [], system, responseFormat }: MakeAgentParams) {
235
- return createReactAgent({
239
+ /** Wraps LangChain's createAgent with a simpler interface. */
240
+ export function makeAgent({
241
+ name,
242
+ llm,
243
+ tools = [],
244
+ system,
245
+ responseFormat,
246
+ checkpointer,
247
+ }: MakeAgentParams) {
248
+ return createAgent({
236
249
  name,
237
- llm,
250
+ model: llm,
238
251
  tools,
239
- ...(system ? { prompt: system } : {}),
252
+ ...(system ? { systemPrompt: system } : {}),
240
253
  ...(responseFormat ? { responseFormat } : {}),
254
+ ...(checkpointer ? { checkpointer } : {}),
241
255
  });
242
256
  }
257
+
258
+ export type AgentGraph = ReturnType<typeof makeAgent>;
243
259
  `;
244
260
  }
245
261
  function generateSupervisorHelper() {
246
- return `import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
247
- import type { BaseCheckpointSaver, BaseStore } from "@langchain/langgraph-checkpoint";
248
- import { createSupervisor } from "@langchain/langgraph-supervisor";
249
- import { MemorySaver, InMemoryStore } from "@langchain/langgraph";
262
+ return `import { z } from "zod";
263
+ import { tool } from "@langchain/core/tools";
264
+ import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
265
+ import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
266
+ import { MemorySaver } from "@langchain/langgraph";
267
+ import { makeAgent, type AgentGraph } from "./factory";
250
268
 
251
- type SupervisorParams = Parameters<typeof createSupervisor>[0];
269
+ /**
270
+ * Supervisor via the "subagents" pattern: a main agent coordinates workers
271
+ * by calling them as tools. Subagents are stateless and run in isolated
272
+ * context windows; only the supervisor gets a checkpointer, so interrupt()
273
+ * inside a subagent tool pauses the whole graph.
274
+ */
275
+
276
+ export interface SubagentSpec {
277
+ /** Tool name the supervisor calls, e.g. "math_expert". */
278
+ name: string;
279
+ /** Tells the supervisor's model when to delegate to this agent. */
280
+ description: string;
281
+ agent: AgentGraph;
282
+ }
283
+
284
+ /** Wraps a compiled agent as a tool the supervisor can call. */
285
+ export function subagentTool({ name, description, agent }: SubagentSpec) {
286
+ return tool(
287
+ async ({ task }) => {
288
+ const result = await agent.invoke({
289
+ messages: [{ role: "user", content: task }],
290
+ });
291
+ const last = result.messages.at(-1);
292
+ if (!last) return "(no response)";
293
+ return typeof last.content === "string"
294
+ ? last.content
295
+ : JSON.stringify(last.content);
296
+ },
297
+ {
298
+ name,
299
+ description,
300
+ schema: z.object({
301
+ task: z
302
+ .string()
303
+ .describe("A self-contained task with all context the agent needs"),
304
+ }),
305
+ }
306
+ );
307
+ }
252
308
 
253
- export interface MakeSupervisorParams extends SupervisorParams {
309
+ export interface MakeSupervisorParams {
310
+ subagents: SubagentSpec[];
311
+ llm: BaseChatModel;
312
+ supervisorName?: string;
313
+ prompt?: string;
254
314
  checkpointer?: BaseCheckpointSaver;
255
- store?: BaseStore;
256
315
  }
257
316
 
258
- export function makeSupervisor({ checkpointer, store, ...params }: MakeSupervisorParams) {
259
- return createSupervisor(params).compile({
317
+ export function makeSupervisor({
318
+ subagents,
319
+ llm,
320
+ supervisorName = "supervisor",
321
+ prompt,
322
+ checkpointer,
323
+ }: MakeSupervisorParams) {
324
+ const defaultPrompt =
325
+ "You coordinate a team of specialists. Delegate work to them via " +
326
+ \`your tools (\${subagents.map((s) => s.name).join(", ")}) and answer \` +
327
+ "the user only once the delegated work is done.";
328
+
329
+ return makeAgent({
330
+ name: supervisorName,
331
+ llm,
332
+ tools: subagents.map(subagentTool),
333
+ system: prompt ?? defaultPrompt,
260
334
  checkpointer: checkpointer ?? new MemorySaver(),
261
- store: store ?? new InMemoryStore(),
262
335
  });
263
336
  }
264
337
  `;
265
338
  }
339
+ // Maps a selected pattern to the app it generates, so the server and demos
340
+ // only reference files that actually exist.
341
+ const PATTERN_APPS = {
342
+ supervisor: { route: "supervisor", fn: "createSupervisorApp", path: "./apps/supervisor" },
343
+ swarm: { route: "swarm", fn: "createSwarmApp", path: "./apps/swarm" },
344
+ hitl: { route: "interrupt", fn: "createInterruptApp", path: "./apps/interrupt" },
345
+ structured: { route: "analyst", fn: "createAnalystApp", path: "./apps/analyst" },
346
+ rag: { route: "rag", fn: "createRagApp", path: "./apps/rag" },
347
+ };
348
+ function selectedApps(config) {
349
+ return config.patterns.map((p) => PATTERN_APPS[p]).filter(Boolean);
350
+ }
351
+ function generateServer(config) {
352
+ const apps = selectedApps(config);
353
+ const imports = apps.map((a) => `import { ${a.fn} } from "${a.path}";`).join("\n");
354
+ const entries = apps.map((a) => ` "${a.route}": asApp(await ${a.fn}()),`).join("\n");
355
+ return `import "./config/env";
356
+ import { fastify, type FastifyError, type FastifyReply, type FastifyRequest } from "fastify";
357
+ import { Command } from "@langchain/langgraph";
358
+ import { AIMessageChunk, type BaseMessage } from "@langchain/core/messages";
359
+ import { PORT } from "./config/env";
360
+ ${imports}
361
+
362
+ // The apps are a mix of agents and graphs whose generic types don't unify;
363
+ // this is the structural slice the routes actually need.
364
+ interface AppGraph {
365
+ invoke(input: unknown, config?: unknown): Promise<{ messages: BaseMessage[] } & Record<string, unknown>>;
366
+ stream(input: unknown, config?: unknown): Promise<AsyncIterable<[unknown, { langgraph_node?: string } | undefined]>>;
367
+ getState(config: unknown): Promise<{ values?: unknown; next?: string[]; tasks?: unknown[] } | undefined>;
368
+ }
369
+ const asApp = (g: unknown) => g as AppGraph;
370
+
371
+ function httpError(status: number, message: string) {
372
+ return Object.assign(new Error(message), { statusCode: status });
373
+ }
374
+
375
+ /** Reject malformed bodies before they reach the agent. */
376
+ function validateMessages(raw: unknown): { role: string; content: string }[] {
377
+ if (raw === undefined) return [];
378
+ if (!Array.isArray(raw)) throw httpError(400, '"messages" must be an array');
379
+ return raw.map((m, i) => {
380
+ const { role, content } = (m ?? {}) as Record<string, unknown>;
381
+ if (typeof role !== "string" || typeof content !== "string") {
382
+ throw httpError(400, \`messages[\${i}] needs string "role" and "content"\`);
383
+ }
384
+ return { role, content };
385
+ });
386
+ }
387
+
388
+ const server = fastify({ logger: false });
389
+
390
+ server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply: FastifyReply) => {
391
+ const status = error.statusCode ?? 500;
392
+ return reply.status(status).send({ error: error.message });
393
+ });
394
+
395
+ async function start() {
396
+ const apps: Record<string, AppGraph> = {
397
+ ${entries}
398
+ };
399
+
400
+ const getApp = (name: string) => {
401
+ const app = apps[name];
402
+ if (!app) throw httpError(404, \`Unknown app: "\${name}". Available: \${Object.keys(apps).join(", ")}\`);
403
+ return app;
404
+ };
405
+
406
+ server.post<{ Params: { app: string } }>("/:app/invoke", async (req, reply) => {
407
+ const app = getApp(req.params.app);
408
+ const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
409
+ const messages = validateMessages(body.messages);
410
+ const thread_id = body.thread_id ?? "default";
411
+
412
+ const result = await app.invoke({ messages }, { configurable: { thread_id } });
413
+ const last = result.messages.at(-1);
414
+ return reply.send({
415
+ messages: result.messages,
416
+ structuredResponse: (result as Record<string, unknown>).structuredResponse ?? null,
417
+ lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
418
+ });
419
+ });
420
+
421
+ server.post<{ Params: { app: string } }>("/:app/stream", async (req, reply) => {
422
+ const app = getApp(req.params.app);
423
+ const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
424
+ const messages = validateMessages(body.messages);
425
+ const thread_id = body.thread_id ?? "default";
426
+
427
+ reply.raw.writeHead(200, {
428
+ "Content-Type": "text/event-stream",
429
+ "Cache-Control": "no-cache",
430
+ Connection: "keep-alive",
431
+ });
432
+ const send = (e: unknown) => reply.raw.write(\`data: \${JSON.stringify(e)}\\n\\n\`);
433
+
434
+ try {
435
+ const stream = await app.stream({ messages }, { configurable: { thread_id }, streamMode: "messages" });
436
+ for await (const [chunk, metadata] of stream) {
437
+ if (chunk instanceof AIMessageChunk) {
438
+ send({ type: "token", content: chunk.content, node: metadata?.langgraph_node ?? "unknown" });
439
+ }
440
+ }
441
+ send({ type: "done" });
442
+ } catch (err) {
443
+ send({ type: "error", content: err instanceof Error ? err.message : "Stream failed" });
444
+ } finally {
445
+ reply.raw.end();
446
+ }
447
+ });
448
+
449
+ server.post<{ Params: { app: string } }>("/:app/resume", async (req, reply) => {
450
+ const app = getApp(req.params.app);
451
+ const body = (req.body ?? {}) as { thread_id?: string; decision?: string };
452
+ if (body.decision === undefined) {
453
+ return reply.status(400).send({ error: '"decision" field is required' });
454
+ }
455
+ const result = await app.invoke(new Command({ resume: body.decision }), {
456
+ configurable: { thread_id: body.thread_id ?? "default" },
457
+ });
458
+ const last = result.messages.at(-1);
459
+ return reply.send({
460
+ messages: result.messages,
461
+ lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
462
+ });
463
+ });
464
+
465
+ server.get<{ Params: { app: string; threadId: string } }>("/:app/threads/:threadId", async (req, reply) => {
466
+ const app = getApp(req.params.app);
467
+ const state = await app.getState({ configurable: { thread_id: req.params.threadId } });
468
+ if (!state?.values) return reply.status(404).send({ error: "Thread not found" });
469
+ return reply.send({ values: state.values, next: state.next ?? [], tasks: state.tasks ?? [] });
470
+ });
471
+
472
+ server.get("/health", async () => ({ status: "ok", apps: Object.keys(apps) }));
473
+
474
+ // Container runtimes send SIGTERM on stop — drain instead of dying mid-request.
475
+ let shuttingDown = false;
476
+ for (const signal of ["SIGTERM", "SIGINT"] as const) {
477
+ process.on(signal, () => {
478
+ if (shuttingDown) return;
479
+ shuttingDown = true;
480
+ server.close().then(() => process.exit(0)).catch(() => process.exit(1));
481
+ });
482
+ }
483
+
484
+ await server.listen({ port: PORT, host: "0.0.0.0" });
485
+ console.log(\`Server running at http://localhost:\${PORT}\`);
486
+ console.log(\`Apps: \${Object.keys(apps).join(", ")}\`);
487
+ }
488
+
489
+ start().catch((err) => {
490
+ console.error("Failed to start server:", err);
491
+ process.exit(1);
492
+ });
493
+ `;
494
+ }
495
+ function generateScriptedModel() {
496
+ return `import { BaseChatModel } from "@langchain/core/language_models/chat_models";
497
+ import { AIMessage, type BaseMessage } from "@langchain/core/messages";
498
+ import type { ChatResult } from "@langchain/core/outputs";
499
+
500
+ /**
501
+ * A model that replays a queued list of responses, so tests can drive real
502
+ * graphs offline — no API key, no network, fully deterministic.
503
+ */
504
+ export class ScriptedToolCallingModel extends BaseChatModel {
505
+ private queue: AIMessage[];
506
+
507
+ constructor(queue: AIMessage[]) {
508
+ super({});
509
+ this.queue = [...queue];
510
+ }
511
+
512
+ _llmType(): string {
513
+ return "scripted-tool-calling";
514
+ }
515
+
516
+ override bindTools(): this {
517
+ return this;
518
+ }
519
+
520
+ async _generate(_messages: BaseMessage[]): Promise<ChatResult> {
521
+ const message = this.queue.shift();
522
+ if (!message) throw new Error("Scripted model ran out of responses");
523
+ return { generations: [{ message, text: "" }] };
524
+ }
525
+ }
526
+ `;
527
+ }
528
+ function generateAgentTest(config) {
529
+ const hasSupervisor = config.patterns.includes("supervisor");
530
+ const supervisorTest = hasSupervisor
531
+ ? `
532
+ it("delegates to a subagent and returns its answer", async () => {
533
+ const llm = new ScriptedToolCallingModel([
534
+ // supervisor delegates
535
+ new AIMessage({
536
+ content: "",
537
+ tool_calls: [{ id: "c1", name: "worker", args: { task: "do the thing" } }],
538
+ }),
539
+ // subagent answers
540
+ new AIMessage("worker done"),
541
+ // supervisor wraps up
542
+ new AIMessage("All finished."),
543
+ ]);
544
+
545
+ const worker = makeAgent({ name: "worker", llm, tools: [] });
546
+ const app = await makeSupervisor({
547
+ subagents: [{ name: "worker", description: "Does the thing.", agent: worker }],
548
+ llm,
549
+ checkpointer: new MemorySaver(),
550
+ });
551
+
552
+ const result = await app.invoke(
553
+ { messages: [{ role: "user", content: "do the thing" }] },
554
+ { configurable: { thread_id: "t1" } }
555
+ );
556
+
557
+ expect(result.messages.at(-1)?.content).toBe("All finished.");
558
+ // the supervisor sees the subagent's final answer, not its internals
559
+ const toolMessages = result.messages.filter((m) => m.getType() === "tool");
560
+ expect(toolMessages).toHaveLength(1);
561
+ expect(toolMessages[0].content).toBe("worker done");
562
+ });
563
+ `
564
+ : "";
565
+ const supervisorImports = hasSupervisor
566
+ ? `import { makeSupervisor } from "../src/agents/supervisor";\nimport { MemorySaver } from "@langchain/langgraph";\n`
567
+ : "";
568
+ return `import { describe, expect, it } from "vitest";
569
+ import { AIMessage } from "@langchain/core/messages";
570
+ import { makeAgent } from "../src/agents/factory";
571
+ ${supervisorImports}import { ScriptedToolCallingModel } from "./helpers/scripted-model";
572
+
573
+ describe("agents", () => {
574
+ it("runs a single agent to a final answer", async () => {
575
+ const llm = new ScriptedToolCallingModel([new AIMessage("Hello there.")]);
576
+ const agent = makeAgent({ name: "assistant", llm, tools: [], system: "Be brief." });
577
+
578
+ const result = await agent.invoke({ messages: [{ role: "user", content: "hi" }] });
579
+
580
+ expect(result.messages.at(-1)?.content).toBe("Hello there.");
581
+ });
582
+ ${supervisorTest}});
583
+ `;
584
+ }
266
585
  // Pattern-specific: only files for selected patterns are generated
267
586
  function getPatternFiles(config) {
268
587
  const files = [];
@@ -303,8 +622,11 @@ export async function createSupervisorApp() {
303
622
  });
304
623
 
305
624
  return makeSupervisor({
306
- agents: [math, writer], llm,
307
- outputMode: "last_message",
625
+ subagents: [
626
+ { name: "math_expert", description: "Delegate calculations to the math expert.", agent: math },
627
+ { name: "writer", description: "Delegate writing. Include all facts the text should contain.", agent: writer },
628
+ ],
629
+ llm,
308
630
  supervisorName: "supervisor",
309
631
  });
310
632
  }
@@ -322,56 +644,131 @@ export async function createSupervisorApp() {
322
644
  if (config.patterns.includes("swarm")) {
323
645
  files.push({
324
646
  path: "src/agents/swarm.ts",
325
- content: `import type { BaseCheckpointSaver, BaseStore } from "@langchain/langgraph-checkpoint";
326
- import { MessagesAnnotation, Annotation, MemorySaver } from "@langchain/langgraph";
327
- import { createSwarm } from "@langchain/langgraph-swarm";
647
+ content: `import { z } from "zod";
648
+ import { AIMessage } from "@langchain/core/messages";
649
+ import {
650
+ StateGraph,
651
+ StateSchema,
652
+ MessagesValue,
653
+ MemorySaver,
654
+ START,
655
+ END,
656
+ } from "@langchain/langgraph";
657
+ import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
658
+ import type { AgentGraph } from "./factory";
659
+
660
+ /**
661
+ * Swarm via the "handoffs" pattern: each agent is a graph node, and handoff
662
+ * tools (see ./handoff.ts) jump between nodes while flipping activeAgent.
663
+ * activeAgent is checkpointed, so the conversation resumes with whichever
664
+ * agent last held it.
665
+ */
328
666
 
329
- export const SwarmState = Annotation.Root({
330
- ...MessagesAnnotation.spec,
331
- activeAgent: Annotation<string>(),
667
+ export const SwarmState = new StateSchema({
668
+ messages: MessagesValue,
669
+ activeAgent: z.string().optional(),
332
670
  });
333
671
 
334
- type SwarmParams = Parameters<typeof createSwarm>[0];
672
+ export type SwarmStateType = typeof SwarmState.State;
335
673
 
336
- export function makeSwarm({
337
- agents, defaultActiveAgent, checkpointer,
338
- }: {
339
- agents: SwarmParams["agents"];
674
+ export interface SwarmAgentSpec {
675
+ /** Node name; handoff tools reference it as transfer_to_<name>. */
676
+ name: string;
677
+ agent: AgentGraph;
678
+ }
679
+
680
+ export interface MakeSwarmParams {
681
+ agents: SwarmAgentSpec[];
340
682
  defaultActiveAgent: string;
341
683
  checkpointer?: BaseCheckpointSaver;
342
- store?: BaseStore;
343
- }) {
344
- return createSwarm({ agents, defaultActiveAgent, stateSchema: SwarmState })
345
- .compile({ checkpointer: checkpointer ?? new MemorySaver() });
684
+ }
685
+
686
+ export function makeSwarm({
687
+ agents,
688
+ defaultActiveAgent,
689
+ checkpointer,
690
+ }: MakeSwarmParams) {
691
+ const names = agents.map((a) => a.name);
692
+
693
+ const routeToActive = (state: SwarmStateType) =>
694
+ state.activeAgent ?? defaultActiveAgent;
695
+
696
+ // After an agent runs: done if it answered (no pending tool calls);
697
+ // otherwise a handoff moved activeAgent, so continue there.
698
+ const routeAfterAgent = (state: SwarmStateType) => {
699
+ const last = state.messages.at(-1);
700
+ if (last && AIMessage.isInstance(last) && !last.tool_calls?.length) {
701
+ return END;
702
+ }
703
+ return state.activeAgent ?? defaultActiveAgent;
704
+ };
705
+
706
+ // Node names are only known at runtime, so the graph is built with
707
+ // string keys and the routers are cast to the node-name union.
708
+ let builder = new StateGraph(SwarmState) as StateGraph<
709
+ typeof SwarmState,
710
+ SwarmStateType
711
+ >;
712
+ for (const { name, agent } of agents) {
713
+ builder = builder.addNode(name, async (state: SwarmStateType) =>
714
+ agent.invoke(state)
715
+ ) as typeof builder;
716
+ }
717
+
718
+ builder.addConditionalEdges(START, routeToActive as never, names as never);
719
+ for (const name of names) {
720
+ builder.addConditionalEdges(
721
+ name as never,
722
+ routeAfterAgent as never,
723
+ [...names, END] as never
724
+ );
725
+ }
726
+
727
+ return builder.compile({
728
+ checkpointer: checkpointer ?? new MemorySaver(),
729
+ });
346
730
  }
347
731
  `,
348
732
  });
349
733
  files.push({
350
734
  path: "src/agents/handoff.ts",
351
735
  content: `import { z } from "zod";
352
- import { ToolMessage } from "@langchain/core/messages";
353
- import { tool } from "@langchain/core/tools";
354
- import { Command, MessagesAnnotation, getCurrentTaskInput } from "@langchain/langgraph";
736
+ import { tool, type ToolRuntime } from "@langchain/core/tools";
737
+ import { AIMessage, ToolMessage } from "@langchain/core/messages";
738
+ import { Command } from "@langchain/langgraph";
739
+ import type { SwarmStateType } from "./swarm";
355
740
 
741
+ /**
742
+ * Handoff tool for the handoffs pattern. Returns a Command targeting the
743
+ * PARENT graph: it flips activeAgent and jumps to the target agent's node.
744
+ * The calling agent's last AI message (the one containing this tool call)
745
+ * plus a ToolMessage are copied into parent state so the conversation
746
+ * history stays well-formed.
747
+ */
356
748
  export function createHandoffTool({ agentName, description }: { agentName: string; description?: string }) {
357
- const toolName = \`transfer_to_\${agentName.replace(/\\s+/g, "_").toLowerCase()}\`;
358
-
359
749
  return tool(
360
- async (_args, cfg) => {
361
- const state = getCurrentTaskInput() as (typeof MessagesAnnotation)["State"];
362
- const messages = state.messages ?? [];
363
- const tm = new ToolMessage({
750
+ async (_, runtime: ToolRuntime<SwarmStateType>) => {
751
+ const lastAiMessage = [...(runtime.state.messages ?? [])]
752
+ .reverse()
753
+ .find(AIMessage.isInstance);
754
+ const transferMessage = new ToolMessage({
364
755
  content: \`Transferred to \${agentName}\`,
365
- name: toolName,
366
- tool_call_id: cfg.toolCall.id,
756
+ tool_call_id: runtime.toolCallId ?? "",
367
757
  });
368
758
  return new Command({
369
759
  goto: agentName,
760
+ update: {
761
+ activeAgent: agentName,
762
+ messages: [lastAiMessage, transferMessage].filter(Boolean),
763
+ },
370
764
  graph: Command.PARENT,
371
- update: { messages: messages.concat(tm), activeAgent: agentName },
372
765
  });
373
766
  },
374
- { name: toolName, description: description ?? \`Ask \${agentName} for help\`, schema: z.object({}) }
767
+ {
768
+ name: \`transfer_to_\${agentName}\`,
769
+ description: description ?? \`Transfer the conversation to \${agentName}.\`,
770
+ schema: z.object({}),
771
+ }
375
772
  );
376
773
  }
377
774
  `,
@@ -383,7 +780,7 @@ import { z } from "zod";
383
780
  import { tool } from "@langchain/core/tools";
384
781
  import { makeAgent } from "../agents/factory";
385
782
  import { createHandoffTool } from "../agents/handoff";
386
- import { makeSwarm, type SwarmState } from "../agents/swarm";
783
+ import { makeSwarm } from "../agents/swarm";
387
784
 
388
785
  const add = tool(async ({ a, b }) => String(a + b), {
389
786
  name: "add", description: "Add two numbers",
@@ -411,7 +808,10 @@ export async function createSwarmApp() {
411
808
  });
412
809
 
413
810
  return makeSwarm({
414
- agents: [alice, bob] as any,
811
+ agents: [
812
+ { name: "alice", agent: alice },
813
+ { name: "bob", agent: bob },
814
+ ],
415
815
  defaultActiveAgent: "alice",
416
816
  });
417
817
  }
@@ -431,10 +831,9 @@ export async function createSwarmApp() {
431
831
  path: "src/apps/interrupt.ts",
432
832
  content: `import { z } from "zod";
433
833
  import { tool } from "@langchain/core/tools";
434
- import { interrupt } from "@langchain/langgraph";
834
+ import { interrupt, MemorySaver } from "@langchain/langgraph";
435
835
  import { getLlm } from "../config/llm";
436
836
  import { makeAgent } from "../agents/factory";
437
- import { makeSupervisor } from "../agents/supervisor";
438
837
 
439
838
  const deleteRecord = tool(
440
839
  async (args) => {
@@ -457,16 +856,13 @@ const deleteRecord = tool(
457
856
  export async function createInterruptApp() {
458
857
  const llm = await getLlm();
459
858
 
460
- const dbAdmin = makeAgent({
859
+ // A single agent with a checkpointer — interrupt() inside delete_record
860
+ // pauses the graph; resume with Command({ resume: "yes" }) on the thread.
861
+ return makeAgent({
461
862
  name: "db_admin", llm,
462
863
  tools: [deleteRecord],
463
864
  system: "You are a database administrator.",
464
- });
465
-
466
- return makeSupervisor({
467
- agents: [dbAdmin], llm,
468
- outputMode: "last_message",
469
- supervisorName: "interrupt_supervisor",
865
+ checkpointer: new MemorySaver(),
470
866
  });
471
867
  }
472
868
  `,
@@ -491,9 +887,10 @@ export async function createInterruptApp() {
491
887
  files.push({
492
888
  path: "src/apps/analyst.ts",
493
889
  content: `import { z } from "zod";
890
+ import { toolStrategy } from "langchain";
891
+ import { MemorySaver } from "@langchain/langgraph";
494
892
  import { getLlm } from "../config/llm";
495
893
  import { makeAgent } from "../agents/factory";
496
- import { makeSupervisor } from "../agents/supervisor";
497
894
 
498
895
  const SummarySchema = z.object({
499
896
  title: z.string(),
@@ -504,16 +901,14 @@ const SummarySchema = z.object({
504
901
  export async function createAnalystApp() {
505
902
  const llm = await getLlm();
506
903
 
507
- const analyst = makeAgent({
904
+ // The structured result is returned on the structuredResponse key of the
905
+ // final state. toolStrategy works with every provider; models with native
906
+ // structured output support could use providerStrategy instead.
907
+ return makeAgent({
508
908
  name: "analyst", llm, tools: [],
509
909
  system: "Analyze text and produce structured summaries.",
510
- responseFormat: SummarySchema,
511
- });
512
-
513
- return makeSupervisor({
514
- agents: [analyst], llm,
515
- outputMode: "last_message",
516
- supervisorName: "analyst_supervisor",
910
+ responseFormat: toolStrategy(SummarySchema),
911
+ checkpointer: new MemorySaver(),
517
912
  });
518
913
  }
519
914
  `,
@@ -525,14 +920,15 @@ export async function createAnalystApp() {
525
920
  { messages: [{ role: "user", content: "Analyze: Revenue grew 25% but churn increased 8%." }] },
526
921
  { configurable: { thread_id: "analyst-demo" } }
527
922
  );
528
- console.log("Result:", analysis.messages.at(-1)?.content);`);
923
+ const structured = (analysis as Record<string, unknown>).structuredResponse;
924
+ console.log("Result:", JSON.stringify(structured ?? analysis.messages.at(-1)?.content));`);
529
925
  }
530
926
  if (config.patterns.includes("rag")) {
531
927
  files.push({
532
928
  path: "src/apps/rag.ts",
533
- content: `import { getLlm } from "../config/llm";
929
+ content: `import { MemorySaver } from "@langchain/langgraph";
930
+ import { getLlm } from "../config/llm";
534
931
  import { makeAgent } from "../agents/factory";
535
- import { makeSupervisor } from "../agents/supervisor";
536
932
  // TODO: Add your vector store, embeddings, and retrieval tool here.
537
933
  // See the full starter kit for a complete RAG implementation:
538
934
  // https://github.com/ac12644/langgraph-starter-kit
@@ -540,15 +936,10 @@ import { makeSupervisor } from "../agents/supervisor";
540
936
  export async function createRagApp() {
541
937
  const llm = await getLlm();
542
938
 
543
- const ragAgent = makeAgent({
939
+ return makeAgent({
544
940
  name: "rag_agent", llm, tools: [],
545
941
  system: "You are a knowledgeable assistant. Answer questions based on your knowledge.",
546
- });
547
-
548
- return makeSupervisor({
549
- agents: [ragAgent], llm,
550
- outputMode: "last_message",
551
- supervisorName: "rag_supervisor",
942
+ checkpointer: new MemorySaver(),
552
943
  });
553
944
  }
554
945
  `,
@@ -562,6 +953,11 @@ export async function createRagApp() {
562
953
  );
563
954
  console.log("Result:", rag.messages.at(-1)?.content);`);
564
955
  }
956
+ // HTTP server — routes only the apps that were generated
957
+ files.push({ path: "src/server.ts", content: generateServer(config) });
958
+ // Tests — offline, no API key needed
959
+ files.push({ path: "tests/helpers/scripted-model.ts", content: generateScriptedModel() });
960
+ files.push({ path: "tests/agents.test.ts", content: generateAgentTest(config) });
565
961
  // Generate index.ts
566
962
  files.push({
567
963
  path: "src/index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-langgraph-app",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Scaffold a new LangGraph multi-agent project in seconds",
5
5
  "type": "module",
6
6
  "bin": {