create-langgraph-app 1.5.1 → 1.5.2

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 +49 -13
  2. package/package.json +1 -1
package/dist/create.js CHANGED
@@ -477,10 +477,21 @@ ${imports}
477
477
  interface AppGraph {
478
478
  invoke(input: unknown, config?: unknown): Promise<{ messages: BaseMessage[] } & Record<string, unknown>>;
479
479
  stream(input: unknown, config?: unknown): Promise<AsyncIterable<[unknown, { langgraph_node?: string } | undefined]>>;
480
- getState(config: unknown): Promise<{ values?: unknown; next?: string[]; tasks?: unknown[] } | undefined>;
480
+ getState(config: unknown): Promise<ThreadState | undefined>;
481
+ }
482
+ interface ThreadState {
483
+ values?: unknown;
484
+ next?: string[];
485
+ tasks?: { interrupts?: { value?: unknown }[] }[];
486
+ createdAt?: string | null;
481
487
  }
482
488
  const asApp = (g: unknown) => g as AppGraph;
483
489
 
490
+ /** What each pending interrupt() was given. Empty when the thread isn't paused. */
491
+ function pendingInterrupts(state: ThreadState | undefined): unknown[] {
492
+ return (state?.tasks ?? []).flatMap((t) => t.interrupts ?? []).map((i) => i.value);
493
+ }
494
+
484
495
  function httpError(status: number, message: string) {
485
496
  return Object.assign(new Error(message), { statusCode: status });
486
497
  }
@@ -532,18 +543,29 @@ ${entries}${ragEntry}
532
543
  return app;
533
544
  };
534
545
 
546
+ // A thread that never ran still has a snapshot (values: {}); a checkpoint
547
+ // (createdAt) is what says it exists.
548
+ const getThread = async (app: AppGraph, thread_id: string) => {
549
+ const state = await app.getState({ configurable: { thread_id } });
550
+ if (!state?.createdAt) throw httpError(404, \`Thread "\${thread_id}" not found\`);
551
+ return state;
552
+ };
553
+
535
554
  server.post<{ Params: { app: string } }>("/:app/invoke", async (req, reply) => {
536
555
  const app = getApp(req.params.app);
537
556
  const body = (req.body ?? {}) as { messages?: unknown; thread_id?: unknown };
538
557
  const messages = validateMessages(body.messages);
539
558
  const thread_id = validateThreadId(body.thread_id);
540
559
 
541
- const result = await app.invoke({ messages }, { configurable: { thread_id } });
560
+ const config = { configurable: { thread_id } };
561
+ const result = await app.invoke({ messages }, config);
542
562
  const last = result.messages.at(-1);
543
563
  return reply.send({
544
564
  messages: result.messages,
545
565
  structuredResponse: (result as Record<string, unknown>).structuredResponse ?? null,
546
566
  lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
567
+ // Non-empty = paused for approval; answer via POST /:app/resume.
568
+ interrupts: pendingInterrupts(await app.getState(config)),
547
569
  });
548
570
  });
549
571
 
@@ -558,17 +580,27 @@ ${entries}${ragEntry}
558
580
  "Cache-Control": "no-cache",
559
581
  Connection: "keep-alive",
560
582
  });
561
- const send = (e: unknown) => reply.raw.write(\`data: \${JSON.stringify(e)}\\n\\n\`);
583
+ const controller = new AbortController();
584
+ reply.raw.on("close", () => {
585
+ if (!reply.raw.writableFinished) controller.abort();
586
+ });
587
+ const send = (e: unknown) => {
588
+ if (controller.signal.aborted) return;
589
+ reply.raw.write(\`data: \${JSON.stringify(e)}\\n\\n\`);
590
+ };
562
591
 
563
592
  try {
564
- const stream = await app.stream({ messages }, { configurable: { thread_id }, streamMode: "messages" });
593
+ const stream = await app.stream({ messages }, { configurable: { thread_id }, streamMode: "messages", signal: controller.signal });
565
594
  for await (const [chunk, metadata] of stream) {
566
595
  if (chunk instanceof AIMessageChunk) {
567
596
  send({ type: "token", content: chunk.content, node: metadata?.langgraph_node ?? "unknown" });
568
597
  }
569
598
  }
599
+ const interrupts = pendingInterrupts(await app.getState({ configurable: { thread_id } }));
600
+ if (interrupts.length > 0) send({ type: "interrupt", content: interrupts });
570
601
  send({ type: "done" });
571
602
  } catch (err) {
603
+ if (controller.signal.aborted) return;
572
604
  send({ type: "error", content: err instanceof Error ? err.message : "Stream failed" });
573
605
  } finally {
574
606
  reply.raw.end();
@@ -577,25 +609,29 @@ ${entries}${ragEntry}
577
609
 
578
610
  server.post<{ Params: { app: string } }>("/:app/resume", async (req, reply) => {
579
611
  const app = getApp(req.params.app);
580
- const body = (req.body ?? {}) as { thread_id?: unknown; decision?: string };
612
+ const body = (req.body ?? {}) as { thread_id?: unknown; decision?: unknown };
581
613
  const thread_id = validateThreadId(body.thread_id);
582
- if (body.decision === undefined) {
583
- return reply.status(400).send({ error: '"decision" field is required' });
614
+ if (typeof body.decision !== "string") throw httpError(400, '"decision" must be a string');
615
+
616
+ // Resuming a thread that isn't paused is a silent no-op — refuse it so an
617
+ // approval UI can't report "approved" for something that never waited.
618
+ if (pendingInterrupts(await getThread(app, thread_id)).length === 0) {
619
+ throw httpError(409, \`Thread "\${thread_id}" is not waiting for a decision\`);
584
620
  }
585
- const result = await app.invoke(new Command({ resume: body.decision }), {
586
- configurable: { thread_id },
587
- });
621
+
622
+ const config = { configurable: { thread_id } };
623
+ const result = await app.invoke(new Command({ resume: body.decision }), config);
588
624
  const last = result.messages.at(-1);
589
625
  return reply.send({
590
626
  messages: result.messages,
591
627
  lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
628
+ // A resumed run can pause again (e.g. a second approval).
629
+ interrupts: pendingInterrupts(await app.getState(config)),
592
630
  });
593
631
  });
594
632
 
595
633
  server.get<{ Params: { app: string; threadId: string } }>("/:app/threads/:threadId", async (req, reply) => {
596
- const app = getApp(req.params.app);
597
- const state = await app.getState({ configurable: { thread_id: req.params.threadId } });
598
- if (!state?.values) return reply.status(404).send({ error: "Thread not found" });
634
+ const state = await getThread(getApp(req.params.app), req.params.threadId);
599
635
  return reply.send({ values: state.values, next: state.next ?? [], tasks: state.tasks ?? [] });
600
636
  });
601
637
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-langgraph-app",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Scaffold a new LangGraph multi-agent project in seconds",
5
5
  "type": "module",
6
6
  "bin": {