LangChain Reference home pageLangChain ReferenceLangChain Reference
  • GitHub
  • Main Docs
Deep Agents
LangChain
LangGraph
Integrations
LangSmith
  • Overview
  • Getting started
  • useStream
  • Selectors
  • Interrupts & headless tools
  • Subagents & subgraphs
  • Fork & edit from a checkpoint
  • Submission queue
  • Multimodal media
  • Transports
  • Suspense
  • provideStream & context
  • Type safety
  • Migrating to v1
LangGraph SDK
  • Client
  • Auth
  • React
  • Logging
  • React Ui
  • Server
LangGraph Checkpoint
LangGraph Checkpoint MongoDB
LangGraph Checkpoint Postgres
LangGraph Checkpoint Redis
  • Shallow
  • Store
LangGraph Checkpoint SQLite
LangGraph Checkpoint Validation
  • Cli
LangGraph API
LangGraph CLI
LangGraph CUA
  • Utils
LangGraph Supervisor
LangGraph Swarm
⌘I

LangChain Assistant

Ask a question to get started

Enter to send•Shift+Enter new line

Menu

OverviewGetting starteduseStreamSelectorsInterrupts & headless toolsSubagents & subgraphsFork & edit from a checkpointSubmission queueMultimodal mediaTransportsSuspenseprovideStream & contextType safetyMigrating to v1
LangGraph SDK
ClientAuthReactLoggingReact UiServer
LangGraph Checkpoint
LangGraph Checkpoint MongoDB
LangGraph Checkpoint Postgres
LangGraph Checkpoint Redis
ShallowStore
LangGraph Checkpoint SQLite
LangGraph Checkpoint Validation
Cli
LangGraph API
LangGraph CLI
LangGraph CUA
Utils
LangGraph Supervisor
LangGraph Swarm
Language
Theme
JavaScript@langchain/vueInterrupts & headless tools

Interrupts & headless tools

Interrupts pause graph execution and wait for input. @langchain/vue surfaces them on the root hook, lets you resume them imperatively, and can auto-resolve tool-interrupts via registered headless tool implementations.

Learn more: For HITL UX patterns, see the Human-in-the-loop documentation.

Runnable example: The streaming package in the streaming cookbook has hitl:* scripts that pause on interrupts and resume with a Command.

Reading interrupts

The root composable exposes the latest interrupt and the full list. Each entry is an SDK Interrupt<TValue> object:

interface Interrupt<TValue = unknown> {
  id?: string;
  value?: TValue; // your interrupt payload lives here
}

Type the payload with the second generic to useStream:

<script setup lang="ts">
import { useStream } from "@langchain/vue";
import type { BaseMessage } from "@langchain/core/messages";

const stream = useStream<
  { messages: BaseMessage[] },
  { question: string } // InterruptType
>({
  assistantId: "agent",
  apiUrl: "http://localhost:2024",
});

function onResume() {
  void stream.respond("Approved");
}
</script>

<template>
  <div v-if="stream.interrupt">
    <p>{{ stream.interrupt.value.question }}</p>
    <button @click="onResume">Approve</button>
  </div>
</template>

stream.interrupt is stream.interrupts[0] — the most recent root interrupt mirrored for UI convenience. It is not always the interrupt respond() would pick when target is omitted (see below).

Script vs template access

Unlike React, Vue wraps reactive stream fields in ShallowRef / ComputedRef. That adds one extra .value in <script setup> when you read the interrupt payload.

Location Access pattern Resolves to
<template> stream.interrupt.value.question Payload field (question)
<script setup> stream.interrupt.value?.value Full payload object
<script setup> (recommended) computed(() => stream.interrupt.value?.value) Reactive payload

In templates, Vue auto-unwraps refs on the stream handle, so the first .value reads the SDK Interrupt.value payload. In script, the first .value unwraps the Vue ComputedRef and returns the Interrupt object; read the payload with a second .value:

<script setup lang="ts">
import { computed } from "vue";

const question = computed(() => stream.interrupt.value?.value?.question);

// Wrong in script: this is the Interrupt wrapper, not the payload.
// const question = stream.interrupt.value?.question;
</script>

Prefer a computed for anything you render or pass to handlers so the payload stays reactive and you avoid repeating the double unwrap.

Human-in-the-loop (HITL)

When using LangChain's humanInTheLoopMiddleware, the interrupt payload is a HITLRequest (action requests, review configs, allowed decisions). Import the types from langchain and unwrap the payload in script with stream.interrupt.value?.value:

<script setup lang="ts">
import { computed, ref } from "vue";
import { useStream } from "@langchain/vue";
import { AIMessage, HumanMessage, type HITLRequest, type HITLResponse } from "langchain";
import type { BaseMessage } from "@langchain/core/messages";

const stream = useStream<{ messages: BaseMessage[] }, HITLRequest>({
  assistantId: "agent",
  apiUrl: "http://localhost:2024",
});

const isProcessing = ref(false);
const hitlRequest = computed(() => stream.interrupt.value?.value as HITLRequest | undefined);
const actionRequests = computed(() => hitlRequest.value?.actionRequests ?? []);

async function onApprove() {
  if (!hitlRequest.value) return;
  isProcessing.value = true;
  try {
    const resume: HITLResponse = {
      decisions: actionRequests.value.map(() => ({ type: "approve" })),
    };
    await stream.respond(resume);
  } finally {
    isProcessing.value = false;
  }
}
</script>

<template>
  <div v-for="msg in stream.messages" :key="msg.id">
    <div v-if="HumanMessage.isInstance(msg)">{{ msg.text }}</div>
    <div v-else-if="AIMessage.isInstance(msg) && msg.text">{{ msg.text }}</div>
  </div>

  <div v-if="hitlRequest && actionRequests.length > 0 && !isProcessing">
    <!-- render ApprovalCard per actionRequests[i] -->
    <button @click="onApprove">Approve</button>
  </div>
</template>

Use hitlRequest (the unwrapped payload) for :disabled, :placeholder, and conditional UI — not stream.interrupt itself, which is always a ref object and stays truthy even when no interrupt is pending.

Resuming an interrupt

Call stream.respond(value) when exactly one interrupt is pending:

void stream.respond({ approved: true });

When more than one interrupt can be active, pass an explicit target.

Multiple pending interrupts

When options.interruptId is omitted, respond() walks stream.getThread()?.interrupts from newest to oldest and resumes the first entry whose interruptId has not already been resolved by a prior respond() call.

That list includes root and subgraph interrupts. It is not the same as stream.interrupt / stream.interrupts[0], which only mirror root-namespace interrupts.

Surface What it contains Use for
stream.interrupts Root-namespace interrupts ({ id, value }) Rendering root HITL UI
stream.getThread()?.interrupts All protocol interrupts ({ interruptId, payload, namespace }) Targeting + namespace for respond()

When several root interrupts are pending, target by id:

for (const intr of stream.interrupts.value) {
  await stream.respond({ approved: true }, { interruptId: intr.id! });
}

Subgraph interrupts and namespace

Interrupts raised inside a subagent or nested graph carry a non-empty protocol namespace tuple (for example ["task:research"]). The server validates that tuple when you resume. Read namespace from the thread stream entry — do not guess it from UI state:

const thread = stream.getThread();

for (const entry of thread?.interrupts ?? []) {
  await stream.respond(buildResponse(entry.payload), {
    interruptId: entry.interruptId,
    namespace: entry.namespace,
  });
}

Pass both interruptId and namespace for subgraph interrupts; omitting namespace assumes root ([]) and the server will reject the resume if the pending interrupt lives in a subgraph.

respond(response, options?)

options.interruptId Behavior
Omitted Newest unresolved entry in getThread()?.interrupts. Safe when one interrupt is pending.
interruptId set Resume that id at root (namespace: []).
interruptId + namespace Resume that id in the given subgraph namespace. Required when the interrupt is not at root.

options.config / options.metadata are folded into the run that services the resume — the same config / metadata you'd pass to submit().

respondAll(responsesById, options?)

When a run pauses on several interrupts at the same checkpoint (e.g. parallel tool-authorization prompts), resume them in one command with respondAll. Sequential respond() calls would fail — the first resume starts a run, leaving the rest with no interrupted run to respond to.

// Distinct payloads per interrupt:
await stream.respondAll({
  [interruptA.id]: { approved: true },
  [interruptB.id]: { approved: false },
});

// Same payload to every pending interrupt:
await stream.respondAll(
  Object.fromEntries(stream.interrupts.value.map((i) => [i.id!, { approved: true }])),
);

Stopping a run

stream.stop() aborts the in-flight run. The transport AbortController fires, the messages / toolCalls projections stop receiving deltas, and values reconciles with the server's authoritative snapshot. Safe to call unconditionally — when no run is active it is a no-op.

<button :disabled="!stream.isLoading" @click="() => void stream.stop()">
  Stop
</button>

Headless tools

Register tool implementations on the hook and the SDK will auto-resume matching interrupts with the handler's return value. The user never sees the tool interrupt — it resolves transparently:

import { useStream } from "@langchain/vue";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const getCurrentLocation = tool(async () => ({ latitude: 47.61, longitude: -122.33 }), {
  name: "get_current_location",
  description: "Get the user's current location",
  schema: z.object({}),
});

const stream = useStream({
  assistantId: "deep-agent",
  apiUrl: "http://localhost:2024",
  tools: [getCurrentLocation],
  onTool: (event) => {
    if (event.type === "error") console.error(event.error);
  },
});

onTool lifecycle events:

event.type Description
start The SDK matched an interrupt to a registered tool.
success Tool returned a value; the run is about to resume.
error Tool threw; the error is surfaced to the server.

Dedupe is automatic: the same interrupt observed twice (for example under <StrictMode>) is invoked once.

Lower-level helpers

For advanced composition (custom interrupt routers, background workers, tests) the SDK also exports flushPendingHeadlessToolInterrupts, findHeadlessTool, handleHeadlessToolInterrupt, executeHeadlessTool, headlessToolResumeCommand, applyHeadlessToolResumeCommand, and the isHeadlessToolInterrupt / parseHeadlessToolInterruptPayload / filterOutHeadlessToolInterrupts predicates. These are the same primitives useStream uses internally to service tools / onTool.

API reference

Functions

Function

findHeadlessTool

Function

executeHeadlessTool

Function

handleHeadlessToolInterrupt

Function

flushPendingHeadlessToolInterrupts

Execute and resume all newly seen headless-tool interrupts from a values

Function

isHeadlessToolInterrupt

Function

parseHeadlessToolInterruptPayload

Parses a headless-tool interrupt value from the graph. Accepts both

Function

filterOutHeadlessToolInterrupts

Strip headless-tool interrupts from a user-facing interrupt list.

Function

headlessToolResumeCommand

Interfaces

Interface

HeadlessToolImplementation

Client-side implementation returned by headlessTool.implement(...).

Interface

ToolEvent

Interface

HeadlessToolInterrupt

Represents a headless tool interrupt payload emitted by LangChain's

Types

Type

OnToolCallback