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.
- package/dist/create.js +49 -13
- 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<
|
|
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
|
|
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
|
|
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?:
|
|
612
|
+
const body = (req.body ?? {}) as { thread_id?: unknown; decision?: unknown };
|
|
581
613
|
const thread_id = validateThreadId(body.thread_id);
|
|
582
|
-
if (body.decision
|
|
583
|
-
|
|
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
|
-
|
|
586
|
-
|
|
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
|
|
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
|
|