feat(langgraph): narrow Runtime types to remove optionality from writer, interrupt and signal (#1616)

This commit is contained in:
David Duong
2025-09-09 00:35:17 +02:00
committed by GitHub
parent 2311efcb68
commit e8f5084d0c
6 changed files with 76 additions and 66 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@langchain/langgraph": patch
---
`writer`, `interrupt` and `signal` is no longer an optional property of `Runtime`
+1 -5
View File
@@ -157,11 +157,7 @@ type NodeAction<
> = RunnableLike<
S,
U extends object ? U & Record<string, any> : U, // eslint-disable-line @typescript-eslint/no-explicit-any
LangGraphRunnableConfig<
StateType<ToStateDefinition<C>>,
InterruptType,
WriterType
>
Runtime<StateType<ToStateDefinition<C>>, InterruptType, WriterType>
>;
type StrictNodeAction<
@@ -14,7 +14,6 @@ import {
} from "@langchain/core/messages";
import {
Runnable,
RunnableConfig,
RunnableLambda,
RunnableToolLike,
RunnableSequence,
@@ -742,10 +741,10 @@ export function createReactAgent<
const _getDynamicModel = async (
llm: (
state: AgentState<StructuredResponseFormat> & PreHookAnnotation["State"],
runtime: LangGraphRunnableConfig
runtime: Runtime<ToAnnotationRoot<C>["State"]>
) => Promise<LanguageModelLike> | LanguageModelLike,
state: AgentState<StructuredResponseFormat> & PreHookAnnotation["State"],
config: LangGraphRunnableConfig
config: Runtime<ToAnnotationRoot<C>["State"]>
) => {
const model = await llm(state, config);
@@ -777,7 +776,7 @@ export function createReactAgent<
const generateStructuredResponse = async (
state: AgentState<StructuredResponseFormat> & PreHookAnnotation["State"],
config: RunnableConfig
config: Runtime<ToAnnotationRoot<C>["State"]>
) => {
if (responseFormat == null) {
throw new Error(
@@ -816,7 +815,7 @@ export function createReactAgent<
const callModel = async (
state: AgentState<StructuredResponseFormat> & PreHookAnnotation["State"],
config: RunnableConfig
config: Runtime<ToAnnotationRoot<C>["State"]>
) => {
// NOTE: we're dynamically creating the model runnable here
// to ensure that we can validate ConfigurableModel properly
+18 -17
View File
@@ -573,7 +573,7 @@ export class Pregel<
* @returns A new Pregel instance with the merged configuration
*/
override withConfig(
config: Omit<LangGraphRunnableConfig, "store" | "writer">
config: Omit<LangGraphRunnableConfig, "store" | "writer" | "interrupt">
): typeof this {
const mergedConfig = mergeConfigs(this.config, config);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2025,24 +2025,25 @@ export class Pregel<
}
}
// setup custom stream mode
if (streamMode.includes("custom")) {
config.writer = (chunk: unknown) => {
const ns = (
getConfig()?.configurable?.[CONFIG_KEY_CHECKPOINT_NS] as
| string
| undefined
)
?.split(CHECKPOINT_NAMESPACE_SEPARATOR)
.slice(0, -1);
config.writer ??= (chunk: unknown) => {
if (!streamMode.includes("custom")) return;
const ns = (
getConfig()?.configurable?.[CONFIG_KEY_CHECKPOINT_NS] as
| string
| undefined
)
?.split(CHECKPOINT_NAMESPACE_SEPARATOR)
.slice(0, -1);
stream.push([ns ?? [], "custom", chunk]);
};
}
stream.push([ns ?? [], "custom", chunk]);
};
if (this.checkpointer != null) {
config.interrupt = interrupt;
}
config.interrupt ??=
this.checkpointer != null
? interrupt
: () => {
throw new GraphValueError("No checkpointer set");
};
const callbackManager = await getCallbackManagerForConfig(config);
const runManager = await callbackManager?.handleChainStart(
+35 -22
View File
@@ -26,41 +26,54 @@ export type RunnableLike<
type IsEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;
export interface LangGraphRunnableConfig<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ContextType extends Record<string, any> = Record<string, any>,
InterruptType = unknown,
WriterType = unknown
> extends RunnableConfig<ContextType> {
context?: ContextType;
store?: BaseStore;
writer?: IsEqual<WriterType, unknown> extends true
? (chunk: unknown) => void
: WriterType;
interrupt?: IsEqual<InterruptType, unknown> extends true
? (value: unknown) => unknown
: InterruptType;
}
export interface Runtime<
ContextType = Record<string, unknown>,
InterruptType = unknown,
WriterType = unknown
> {
configurable?: ContextType;
/** User provided context */
context?: ContextType;
/** Persistent key-value store */
store?: BaseStore;
writer?: IsEqual<WriterType, unknown> extends true
/** Callback to send custom data chunks via the `custom` stream mode */
writer: IsEqual<WriterType, unknown> extends true
? (chunk: unknown) => void
: WriterType;
interrupt?: IsEqual<InterruptType, unknown> extends true
/**
* Interrupts the execution of a graph node.
*
* This function can be used to pause execution of a node, and return the value of the `resume`
* input when the graph is re-invoked using `Command`.
* Multiple interrupts can be called within a single node, and each will be handled sequentially.
*
* When an interrupt is called:
* 1. If there's a `resume` value available (from a previous `Command`), it returns that value.
* 2. Otherwise, it throws a `GraphInterrupt` with the provided value
* 3. The graph can be resumed by passing a `Command` with a `resume` value
*
* Because the `interrupt` function propagates by throwing a special `GraphInterrupt` error,
* you should avoid using `try/catch` blocks around the `interrupt` function,
* or if you do, ensure that the `GraphInterrupt` error is thrown again within your `catch` block.
*
* @param value - The value to include in the interrupt.
* @returns The `resume` value provided when the graph is re-invoked with a Command.
*/
interrupt: IsEqual<InterruptType, unknown> extends true
? (value: unknown) => unknown
: InterruptType;
signal?: AbortSignal;
/** Abort signal to cancel the run. */
signal: AbortSignal;
}
export interface LangGraphRunnableConfig<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ContextType extends Record<string, any> = Record<string, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
> extends RunnableConfig<ContextType>,
Partial<Runtime<ContextType, unknown, unknown>> {}
+13 -17
View File
@@ -550,33 +550,29 @@ it("state graph builder", async () => {
const hello: typeof builder.Node = async (_, runtime) => {
// @ts-expect-error - Invalid interrupt input
if (false) runtime.interrupt?.({ reason: false });
if (false) runtime.interrupt({ reason: false });
// @ts-expect-error - Invalid writer input
if (false) runtime.interrupt?.();
if (false) runtime.interrupt();
const result = runtime.interrupt?.({ reason: "hello" });
const result = runtime.interrupt({ reason: "hello" });
if (result != null) {
runtime.writer?.({ custom: "hello" });
runtime.writer({ custom: "hello" });
// @ts-expect-error - Invalid writer value
if (false) runtime.writer?.({ invalid: "hello" });
// @ts-expect-error - Invalid writer value
if (false) runtime.writer({ invalid: "hello" });
// @ts-expect-error - Invalid interrupt value
if (false) runtime.writer?.();
// @ts-expect-error - Invalid interrupt value
if (false) runtime.writer();
return new Command({
update: { messages: result.messages },
goto: "what",
});
}
return new Command({ goto: "what" });
return new Command({
update: { messages: result.messages },
goto: "what",
});
};
const what: typeof builder.Node = async (_, runtime) => {
const result = runtime.interrupt?.({ reason: "what" });
const result = runtime.interrupt({ reason: "what" });
if (result != null) return { messages: result.messages };
return {};
};