mirror of
https://github.com/langchain-ai/langgraphjs.git
synced 2026-07-22 17:15:25 -04:00
feat(langgraph): narrow Runtime types to remove optionality from writer, interrupt and signal (#1616)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@langchain/langgraph": patch
|
||||
---
|
||||
|
||||
`writer`, `interrupt` and `signal` is no longer an optional property of `Runtime`
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>> {}
|
||||
|
||||
@@ -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 {};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user