@langchain/langgraph-sdk 1.9.9 → 1.9.11

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 (51) hide show
  1. package/dist/client/stream/index.cjs +37 -5
  2. package/dist/client/stream/index.cjs.map +1 -1
  3. package/dist/client/stream/index.d.cts +7 -14
  4. package/dist/client/stream/index.d.cts.map +1 -1
  5. package/dist/client/stream/index.d.ts +7 -14
  6. package/dist/client/stream/index.d.ts.map +1 -1
  7. package/dist/client/stream/index.js +37 -5
  8. package/dist/client/stream/index.js.map +1 -1
  9. package/dist/client/stream/types.d.cts +8 -3
  10. package/dist/client/stream/types.d.cts.map +1 -1
  11. package/dist/client/stream/types.d.ts +8 -3
  12. package/dist/client/stream/types.d.ts.map +1 -1
  13. package/dist/headless-tools.cjs +0 -24
  14. package/dist/headless-tools.cjs.map +1 -1
  15. package/dist/headless-tools.d.cts.map +1 -1
  16. package/dist/headless-tools.d.ts.map +1 -1
  17. package/dist/headless-tools.js +1 -24
  18. package/dist/headless-tools.js.map +1 -1
  19. package/dist/stream/controller.cjs +156 -19
  20. package/dist/stream/controller.cjs.map +1 -1
  21. package/dist/stream/controller.d.cts +113 -10
  22. package/dist/stream/controller.d.cts.map +1 -1
  23. package/dist/stream/controller.d.ts +113 -10
  24. package/dist/stream/controller.d.ts.map +1 -1
  25. package/dist/stream/controller.js +157 -20
  26. package/dist/stream/controller.js.map +1 -1
  27. package/dist/stream/index.d.cts +2 -2
  28. package/dist/stream/index.d.ts +2 -2
  29. package/dist/stream/message-metadata-tracker.cjs +1 -1
  30. package/dist/stream/message-metadata-tracker.cjs.map +1 -1
  31. package/dist/stream/message-metadata-tracker.d.cts +1 -1
  32. package/dist/stream/message-metadata-tracker.d.ts +1 -1
  33. package/dist/stream/message-metadata-tracker.js +1 -1
  34. package/dist/stream/message-metadata-tracker.js.map +1 -1
  35. package/dist/stream/submit-coordinator.cjs +45 -79
  36. package/dist/stream/submit-coordinator.cjs.map +1 -1
  37. package/dist/stream/submit-coordinator.d.cts.map +1 -1
  38. package/dist/stream/submit-coordinator.d.ts.map +1 -1
  39. package/dist/stream/submit-coordinator.js +45 -79
  40. package/dist/stream/submit-coordinator.js.map +1 -1
  41. package/dist/stream/types.d.cts +52 -30
  42. package/dist/stream/types.d.cts.map +1 -1
  43. package/dist/stream/types.d.ts +52 -30
  44. package/dist/stream/types.d.ts.map +1 -1
  45. package/dist/ui/manager.cjs +47 -34
  46. package/dist/ui/manager.cjs.map +1 -1
  47. package/dist/ui/manager.d.cts.map +1 -1
  48. package/dist/ui/manager.d.ts.map +1 -1
  49. package/dist/ui/manager.js +47 -34
  50. package/dist/ui/manager.js.map +1 -1
  51. package/package.json +3 -3
@@ -4,9 +4,7 @@ import { AgentResult, Channel, Event, InputInjectParams, InputRespondParams, Lis
4
4
 
5
5
  //#region src/client/stream/types.d.ts
6
6
  interface ExtendedRunStartParams extends RunStartParams {
7
- forkFrom?: {
8
- checkpointId: string;
9
- };
7
+ forkFrom?: string;
10
8
  multitaskStrategy?: "reject" | "rollback" | "interrupt" | "enqueue";
11
9
  }
12
10
  type SubscribeOptions = Omit<SubscribeParams, "channels">;
@@ -146,10 +144,17 @@ interface ThreadModules {
146
144
  /**
147
145
  * Human-in-the-loop interrupt payload surfaced from lifecycle events.
148
146
  * Matches the in-process `InterruptPayload` type.
147
+ *
148
+ * {@link ThreadStream.interrupts} collects these entries in arrival order.
149
+ * Use them (via {@link StreamController.getThread `getThread()`}) when
150
+ * you need the protocol `namespace` tuple for
151
+ * {@link StreamController.respond `respond()`} — for example subgraph
152
+ * interrupts that are not mirrored on {@link RootSnapshot.interrupts}.
149
153
  */
150
154
  interface InterruptPayload<TPayload = unknown> {
151
155
  interruptId: string;
152
156
  payload: TPayload;
157
+ /** Protocol namespace tuple the server validates on resume (`[]` at root). */
153
158
  namespace: string[];
154
159
  }
155
160
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/client/stream/types.ts"],"mappings":";;;;;UAoBiB,sBAAA,SAA+B,cAAA;EAC9C,QAAA;IAAa,YAAA;EAAA;EACb,iBAAA;AAAA;AAAA,KAGU,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,KAExB,oBAAA;EACV,MAAA;EACA,OAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,KAAA;EACA,KAAA;EACA,WAAA;EACA,KAAA;AAAA;AAAA,KAGU,eAAA,kBAAiC,OAAA,IAC3C,QAAA,eAAuB,oBAAA,GACnB,OAAA,CAAQ,KAAA;EAAS,MAAA,EAAQ,oBAAA,CAAqB,QAAA;AAAA,KAC9C,QAAA,8BACE,OAAA,CAAQ,KAAA;EAAS,MAAA;AAAA;AAAA,KAGb,gBAAA,4BAA4C,OAAA,MACtD,eAAA,CAAgB,SAAA;;;;;;;KAQN,eAAA,kBAAiC,OAAA,IAC3C,QAAA,wCAAgD,eAAA,CAAgB,QAAA;AAAA,KAEtD,gBAAA,4BAA4C,OAAA,MACtD,eAAA,CAAgB,SAAA;AApBlB;;;;;;;;AAAA,KA8BY,yBAAA;;;;;;;;;KAUA,qBAAA,GACR,yBAAA,GACA,kBAAA;;;;UAKa,mBAAA;EA7Cc;;;;;EAmD7B,WAAA;EAjD6B;;AAG/B;;;;;;;;;;;EA4DE,SAAA,GAAY,qBAAA;EA3Da;;AAQ3B;;EAwDE,iBAAA;EAxD2C;;;;;;;EAgE3C,KAAA,UAAe,KAAA;EA/Df;;;;;AAEF;EAoEE,gBAAA,IAAoB,GAAA,aAAgB,SAAA;AAAA;AAAA,UAGrB,oBAAA;EACf,WAAA;EACA,qBAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA,UACN,KAAA,UACD,aAAA,CAAc,MAAA;EAAA,SACb,cAAA;EAAA,SACA,MAAA,EAAQ,eAAA;EACjB,WAAA,IAAe,OAAA;AAAA;AAAA,UAGA,mBAAA,SAA4B,aAAA,CAAc,gBAAA;EAAA,SAChD,cAAA;EAAA,SACA,MAAA,EAAQ,eAAA;EACjB,WAAA,IAAe,OAAA;AAAA;AAAA,UAGA,WAAA;EACf,OAAA,CAAQ,MAAA,EAAQ,kBAAA,GAAqB,OAAA;EACrC,MAAA,CAAO,MAAA,EAAQ,iBAAA,GAAoB,OAAA;AAAA;AAAA,UAGpB,WAAA;EACf,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,cAAA;EACrC,eAAA,CACE,MAAA,EAAQ,qBAAA,GACP,OAAA,CAAQ,qBAAA;EACX,IAAA,CAAK,MAAA,EAAQ,eAAA,GAAkB,OAAA,CAAQ,eAAA;AAAA;;;;UAMxB,aAAA;EACf,GAAA;IA1EA;;;;;IAgFE,KAAA,CACE,MAAA,EAAQ,IAAA,CAAK,sBAAA,oBACZ,OAAA,CAAQ,SAAA;EAAA;EAEb,KAAA;IACE,OAAA,CAAQ,MAAA;MAAW,MAAA;IAAA,IAAoB,OAAA,CAAQ,WAAA;EAAA;EAEjD,KAAA,EAAO,WAAA;EACP,KAAA,EAAO,WAAA;AAAA;;;;;UAOQ,gBAAA;EACf,WAAA;EACA,OAAA,EAAS,QAAA;EACT,SAAA;AAAA;;;;;;;;;;;;;;;;;;AA/CF;UAoEiB,eAAA,sBACP,aAAA,CAAc,CAAA,GAAI,WAAA,CAAY,CAAA;;;;;;;;;;;;;;KAe5B,eAAA,MACV,CAAA,SAAU,WAAA,YAAuB,CAAA,GAAI,CAAA,SAAU,aAAA,YAAyB,CAAA,GAAI,CAAA;;AA/E9E;;;;;;;;;;;;;;KAgGY,gBAAA,qBACU,MAAA,oBAA0B,MAAA,4CAEzB,WAAA,GAAc,eAAA,CACjC,eAAA,CAAgB,WAAA,CAAY,CAAA;EAAA,UAGpB,IAAA,WAAe,eAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/client/stream/types.ts"],"mappings":";;;;;UAoBiB,sBAAA,SAA+B,cAAA;EAC9C,QAAA;EACA,iBAAA;AAAA;AAAA,KAGU,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,KAExB,oBAAA;EACV,MAAA;EACA,OAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,KAAA;EACA,KAAA;EACA,WAAA;EACA,KAAA;AAAA;AAAA,KAGU,eAAA,kBAAiC,OAAA,IAC3C,QAAA,eAAuB,oBAAA,GACnB,OAAA,CAAQ,KAAA;EAAS,MAAA,EAAQ,oBAAA,CAAqB,QAAA;AAAA,KAC9C,QAAA,8BACE,OAAA,CAAQ,KAAA;EAAS,MAAA;AAAA;AAAA,KAGb,gBAAA,4BAA4C,OAAA,MACtD,eAAA,CAAgB,SAAA;;;;;;;KAQN,eAAA,kBAAiC,OAAA,IAC3C,QAAA,wCAAgD,eAAA,CAAgB,QAAA;AAAA,KAEtD,gBAAA,4BAA4C,OAAA,MACtD,eAAA,CAAgB,SAAA;;AApBlB;;;;;;;KA8BY,yBAAA;;;;;;;;;KAUA,qBAAA,GACR,yBAAA,GACA,kBAAA;;;;UAKa,mBAAA;EA7CM;;;;;EAmDrB,WAAA;EAjDuB;;;AAGzB;;;;;;;;;;EA4DE,SAAA,GAAY,qBAAA;EA3DI;;;AAQlB;EAwDE,iBAAA;EAxDyB;;;;;;;EAgEzB,KAAA,UAAe,KAAA;EAhE4B;;;;;;EAuE3C,gBAAA,IAAoB,GAAA,aAAgB,SAAA;AAAA;AAAA,UAGrB,oBAAA;EACf,WAAA;EACA,qBAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA,UACN,KAAA,UACD,aAAA,CAAc,MAAA;EAAA,SACb,cAAA;EAAA,SACA,MAAA,EAAQ,eAAA;EACjB,WAAA,IAAe,OAAA;AAAA;AAAA,UAGA,mBAAA,SAA4B,aAAA,CAAc,gBAAA;EAAA,SAChD,cAAA;EAAA,SACA,MAAA,EAAQ,eAAA;EACjB,WAAA,IAAe,OAAA;AAAA;AAAA,UAGA,WAAA;EACf,OAAA,CAAQ,MAAA,EAAQ,kBAAA,GAAqB,OAAA;EACrC,MAAA,CAAO,MAAA,EAAQ,iBAAA,GAAoB,OAAA;AAAA;AAAA,UAGpB,WAAA;EACf,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,cAAA;EACrC,eAAA,CACE,MAAA,EAAQ,qBAAA,GACP,OAAA,CAAQ,qBAAA;EACX,IAAA,CAAK,MAAA,EAAQ,eAAA,GAAkB,OAAA,CAAQ,eAAA;AAAA;;;;UAMxB,aAAA;EACf,GAAA;IAxC6C;;;;;IA8C3C,KAAA,CACE,MAAA,EAAQ,IAAA,CAAK,sBAAA,oBACZ,OAAA,CAAQ,SAAA;EAAA;EAEb,KAAA;IACE,OAAA,CAAQ,MAAA;MAAW,MAAA;IAAA,IAAoB,OAAA,CAAQ,WAAA;EAAA;EAEjD,KAAA,EAAO,WAAA;EACP,KAAA,EAAO,WAAA;AAAA;;;;;;;AA7CT;;;;UA0DiB,gBAAA;EACf,WAAA;EACA,OAAA,EAAS,QAAA;EA1DD;EA4DR,SAAA;AAAA;;;;;;;;;;;;AAtDF;;;;;;;UA2EiB,eAAA,sBACP,aAAA,CAAc,CAAA,GAAI,WAAA,CAAY,CAAA;;;;;;;;;;AAtExC;;;;KAqFY,eAAA,MACV,CAAA,SAAU,WAAA,YAAuB,CAAA,GAAI,CAAA,SAAU,aAAA,YAAyB,CAAA,GAAI,CAAA;;;;;;;;;;;;;;;AAjF9E;KAkGY,gBAAA,qBACU,MAAA,oBAA0B,MAAA,4CAEzB,WAAA,GAAc,eAAA,CACjC,eAAA,CAAgB,WAAA,CAAY,CAAA;EAAA,UAGpB,IAAA,WAAe,eAAA;AAAA"}
@@ -121,29 +121,6 @@ function headlessToolsBatchResumeCommand(entries) {
121
121
  return { resume };
122
122
  }
123
123
  /**
124
- * True when every top-level resume key is a graph task interrupt id
125
- * (32-char hex from `values.__interrupt__`).
126
- */
127
- function isInterruptIdKeyedResume(resume) {
128
- if (resume == null || typeof resume !== "object" || Array.isArray(resume)) return false;
129
- const keys = Object.keys(resume);
130
- if (keys.length === 0) return false;
131
- return keys.every((key) => /^[0-9a-f]{32}$/i.test(key));
132
- }
133
- /**
134
- * Normalize `command.resume` into the `run.start` input the API turns
135
- * into `Command({ resume })`. Interrupt-id keyed payloads pass through;
136
- * tool-call-keyed and generic payloads are wrapped under the matching
137
- * protocol interrupt id.
138
- */
139
- function buildResumeRunInput(resume, interrupts, resolvedInterruptIds) {
140
- if (resume == null) return null;
141
- if (isInterruptIdKeyedResume(resume)) return resume;
142
- const target = resolveInterruptTargetForHeadlessResume(resume, interrupts, resolvedInterruptIds);
143
- if (target == null) return null;
144
- return { [target.interruptId]: resume };
145
- }
146
- /**
147
124
  * Reads the tool-call id from a headless-tool resume command shaped as
148
125
  * `{ [toolCallId]: result }`.
149
126
  */
@@ -246,7 +223,6 @@ function flushPendingHeadlessToolInterrupts(values, tools, handledIds, options)
246
223
  });
247
224
  }
248
225
  //#endregion
249
- exports.buildResumeRunInput = buildResumeRunInput;
250
226
  exports.executeHeadlessTool = executeHeadlessTool;
251
227
  exports.filterOutHeadlessToolInterrupts = filterOutHeadlessToolInterrupts;
252
228
  exports.findHeadlessTool = findHeadlessTool;
@@ -1 +1 @@
1
- {"version":3,"file":"headless-tools.cjs","names":[],"sources":["../src/headless-tools.ts"],"sourcesContent":["import type { Interrupt } from \"./schema.js\";\n\n/**\n * Represents a headless tool interrupt payload emitted by LangChain's\n * schema-only `tool({ ... })` overload.\n *\n * Servers may serialize the nested tool call as `toolCall` (JS) or\n * `tool_call` (Python). Use {@link parseHeadlessToolInterruptPayload} to\n * normalize either shape before reading fields.\n */\nexport interface HeadlessToolInterrupt {\n type: \"tool\";\n toolCall: {\n id: string | undefined;\n name: string;\n args: unknown;\n };\n}\n\n/**\n * Parses a headless-tool interrupt `value` from the graph. Accepts both\n * `toolCall` (LangChain JS) and `tool_call` (Python / JSON snake_case).\n */\nexport function parseHeadlessToolInterruptPayload(\n value: unknown\n): HeadlessToolInterrupt | null {\n if (typeof value !== \"object\" || value == null) {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (v.type !== \"tool\") {\n return null;\n }\n\n const rawTc = v.toolCall ?? v.tool_call;\n if (typeof rawTc !== \"object\" || rawTc == null) {\n return null;\n }\n const tc = rawTc as Record<string, unknown>;\n if (typeof tc.name !== \"string\") {\n return null;\n }\n\n const id = typeof tc.id === \"string\" ? tc.id : undefined;\n\n return {\n type: \"tool\",\n toolCall: {\n id,\n name: tc.name,\n args: tc.args,\n },\n };\n}\n\n/**\n * Client-side implementation returned by `headlessTool.implement(...)`.\n */\nexport interface HeadlessToolImplementation<Args = unknown, Output = unknown> {\n tool: {\n name: string;\n };\n execute: (args: Args) => Promise<Output>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyHeadlessToolImplementation = HeadlessToolImplementation<\n any,\n any\n>;\n\nexport interface ToolEvent {\n phase: \"start\" | \"success\" | \"error\";\n name: string;\n args: unknown;\n result?: unknown;\n error?: Error;\n duration?: number;\n}\n\nexport type OnToolCallback = (event: ToolEvent) => void;\n\n/**\n * Strip headless-tool interrupts from a user-facing interrupt list.\n */\nexport function filterOutHeadlessToolInterrupts<T extends { value?: unknown }>(\n interrupts: readonly T[]\n): T[] {\n return interrupts.filter(\n (interrupt) =>\n interrupt.value == null || !isHeadlessToolInterrupt(interrupt.value)\n );\n}\n\nexport function isHeadlessToolInterrupt(\n interrupt: unknown\n): interrupt is HeadlessToolInterrupt {\n return parseHeadlessToolInterruptPayload(interrupt) != null;\n}\n\nexport function findHeadlessTool<Args = unknown, Output = unknown>(\n tools: HeadlessToolImplementation[],\n name: string\n): HeadlessToolImplementation<Args, Output> | undefined {\n return tools.find((tool) => tool.tool.name === name) as\n | HeadlessToolImplementation<Args, Output>\n | undefined;\n}\n\nexport async function executeHeadlessTool<Args = unknown, Output = unknown>(\n implementation: HeadlessToolImplementation<Args, Output>,\n args: Args,\n onTool?: OnToolCallback\n): Promise<\n { success: true; result: Output } | { success: false; error: Error }\n> {\n const startTime = Date.now();\n\n onTool?.({\n phase: \"start\",\n name: implementation.tool.name,\n args,\n });\n\n try {\n const result = await implementation.execute(args);\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"success\",\n name: implementation.tool.name,\n args,\n result,\n duration,\n });\n\n return { success: true, result };\n } catch (err) {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n const error = err instanceof Error ? err : new Error(String(err));\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"error\",\n name: implementation.tool.name,\n args,\n error,\n duration,\n });\n\n return { success: false, error };\n }\n}\n\nexport async function handleHeadlessToolInterrupt(\n interrupt: HeadlessToolInterrupt,\n tools: HeadlessToolImplementation[],\n onTool?: OnToolCallback\n): Promise<{ toolCallId: string | undefined; value: unknown }> {\n const { toolCall } = interrupt;\n const implementation = findHeadlessTool(tools, toolCall.name);\n\n if (!implementation) {\n const error = new Error(\n `Headless tool \"${toolCall.name}\" is not registered. ` +\n `Available tools: ${tools.map((tool) => tool.tool.name).join(\", \") || \"none\"}`\n );\n\n onTool?.({\n phase: \"error\",\n name: toolCall.name,\n args: toolCall.args,\n error,\n duration: 0,\n });\n\n return {\n toolCallId: toolCall.id,\n value: { error: error.message },\n };\n }\n\n const result = await executeHeadlessTool(\n implementation,\n toolCall.args as never,\n onTool\n );\n\n if (result.success) {\n return {\n toolCallId: toolCall.id,\n value: result.result,\n };\n }\n\n return {\n toolCallId: toolCall.id,\n value: { error: result.error.message },\n };\n}\n\nexport function headlessToolResumeCommand(result: {\n toolCallId: string | undefined;\n value: unknown;\n}): { resume: unknown } {\n return {\n resume: result.toolCallId\n ? { [result.toolCallId]: result.value }\n : result.value,\n };\n}\n\n/**\n * Merge headless-tool results into one resume command. Use interrupt-id keys\n * whenever the stream provided them so the resume does not need to rediscover\n * a pending interrupt from mutable client state.\n */\nexport function headlessToolsBatchResumeCommand(\n entries: ReadonlyArray<{\n interruptId: string;\n toolCallId: string | undefined;\n value: unknown;\n }>\n): { resume: unknown } {\n if (entries.length === 0) {\n return { resume: {} };\n }\n\n const hasInterruptIds = entries.every(\n (entry) => entry.interruptId.length > 0\n );\n if (!hasInterruptIds && entries.length === 1) {\n const [entry] = entries;\n return headlessToolResumeCommand({\n toolCallId: entry.toolCallId,\n value: entry.value,\n });\n }\n\n const resume: Record<string, unknown> = {};\n for (const entry of entries) {\n if (entry.interruptId.length === 0) continue;\n resume[entry.interruptId] =\n entry.toolCallId != null && entry.toolCallId.length > 0\n ? { [entry.toolCallId]: entry.value }\n : entry.value;\n }\n return { resume };\n}\n\n/**\n * True when every top-level resume key is a graph task interrupt id\n * (32-char hex from `values.__interrupt__`).\n */\nexport function isInterruptIdKeyedResume(resume: unknown): boolean {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return false;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length === 0) return false;\n return keys.every((key) => /^[0-9a-f]{32}$/i.test(key));\n}\n\n/**\n * Normalize `command.resume` into the `run.start` input the API turns\n * into `Command({ resume })`. Interrupt-id keyed payloads pass through;\n * tool-call-keyed and generic payloads are wrapped under the matching\n * protocol interrupt id.\n */\nexport function buildResumeRunInput(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): Record<string, unknown> | null {\n if (resume == null) return null;\n if (isInterruptIdKeyedResume(resume)) {\n return resume as Record<string, unknown>;\n }\n\n const target = resolveInterruptTargetForHeadlessResume(\n resume,\n interrupts,\n resolvedInterruptIds\n );\n if (target == null) return null;\n\n return { [target.interruptId]: resume };\n}\n\n/**\n * Reads the tool-call id from a headless-tool resume command shaped as\n * `{ [toolCallId]: result }`.\n */\nexport function extractHeadlessToolCallIdFromResumeCommand(\n resume: unknown\n): string | undefined {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return undefined;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length !== 1) return undefined;\n return keys[0];\n}\n\nexport interface ProtocolInterruptEntry {\n interruptId: string;\n namespace: string[];\n payload: unknown;\n}\n\n/**\n * Pick the protocol interrupt that matches a headless-tool resume payload.\n * Falls back to the newest unresolved interrupt for non-keyed resumes.\n */\nexport function resolveInterruptTargetForHeadlessResume(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): { interruptId: string; namespace: string[] } | null {\n const toolCallId = extractHeadlessToolCallIdFromResumeCommand(resume);\n if (toolCallId != null) {\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n const headless = parseHeadlessToolInterruptPayload(entry.payload);\n if (headless?.toolCall.id === toolCallId) {\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n }\n }\n\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n return null;\n}\n\nexport interface FlushPendingHeadlessToolInterruptsOptions {\n onTool?: OnToolCallback;\n resumeSubmit: (command: { resume: unknown }) => void | Promise<void>;\n defer?: (run: () => void) => void;\n}\n\nconst coalescedHeadlessFlushes = new WeakMap<\n Set<string>,\n { scheduled: boolean; run: () => void }\n>();\n\n/**\n * Coalesce rapid headless-tool flush triggers into one microtask so parallel\n * `input.requested` events observed back-to-back batch into a single resume.\n * Vue/Svelte/Angular watchers run synchronously per event; without this,\n * the first interrupt can be claimed before the second arrives and resume\n * splits into staggered single-tool commands.\n */\nexport function scheduleCoalescedHeadlessToolFlush(\n handledIds: Set<string>,\n run: () => void\n): void {\n let state = coalescedHeadlessFlushes.get(handledIds);\n if (state == null) {\n state = { scheduled: false, run: () => {} };\n coalescedHeadlessFlushes.set(handledIds, state);\n }\n state.run = run;\n if (state.scheduled) return;\n state.scheduled = true;\n void Promise.resolve().then(() => {\n state!.scheduled = false;\n state!.run();\n });\n}\n\n/**\n * Execute and resume all newly seen headless-tool interrupts from a values\n * payload. Callers own `handledIds` and should clear it when the thread changes.\n */\nexport function flushPendingHeadlessToolInterrupts(\n values: Record<string, unknown> | null | undefined,\n tools: HeadlessToolImplementation[] | undefined,\n handledIds: Set<string>,\n options: FlushPendingHeadlessToolInterruptsOptions\n): void {\n if (!tools?.length || !values) return;\n\n const interrupts = values.__interrupt__;\n if (!Array.isArray(interrupts) || interrupts.length === 0) return;\n\n const defer = options.defer ?? ((run) => run());\n const pending: Array<{\n interruptId: string;\n headlessInterrupt: HeadlessToolInterrupt;\n toolCallId: string;\n }> = [];\n const seenToolCallIds = new Set<string>();\n\n for (const interrupt of interrupts as Interrupt[]) {\n const headlessInterrupt = parseHeadlessToolInterruptPayload(\n interrupt.value\n );\n if (!headlessInterrupt) continue;\n\n const interruptId = interrupt.id ?? headlessInterrupt.toolCall.id ?? \"\";\n const toolCallId = headlessInterrupt.toolCall.id ?? \"\";\n if (handledIds.has(interruptId)) continue;\n // v2 protocol runs mirror the same headless-tool interrupt in both\n // `values.__interrupt__` and `rootStore.interrupts` with different\n // ids (graph/task id vs protocol interrupt_id). The headless-tool\n // effect can also re-run after the first resume clears\n // `rootStore.interrupts` while `values.__interrupt__` is still\n // present — persist tool call ids in the caller-owned set so we\n // only execute + resume once per pending tool call.\n if (toolCallId && handledIds.has(toolCallId)) continue;\n if (toolCallId && seenToolCallIds.has(toolCallId)) continue;\n if (toolCallId) seenToolCallIds.add(toolCallId);\n\n // Claim before defer so a second flush in the same tick cannot\n // schedule a duplicate execute/resume for the same interrupt.\n handledIds.add(interruptId);\n if (toolCallId) handledIds.add(toolCallId);\n\n pending.push({ interruptId, headlessInterrupt, toolCallId });\n }\n\n if (pending.length === 0) return;\n\n defer(() => {\n void (async () => {\n const results = await Promise.all(\n pending.map(async ({ interruptId, headlessInterrupt, toolCallId }) => {\n const result = await handleHeadlessToolInterrupt(\n headlessInterrupt,\n tools,\n options.onTool\n );\n return {\n interruptId,\n toolCallId: result.toolCallId ?? toolCallId,\n value: result.value,\n };\n })\n );\n await Promise.resolve(\n options.resumeSubmit(headlessToolsBatchResumeCommand(results))\n );\n })();\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"headless-tools.cjs","names":[],"sources":["../src/headless-tools.ts"],"sourcesContent":["import type { Interrupt } from \"./schema.js\";\n\n/**\n * Represents a headless tool interrupt payload emitted by LangChain's\n * schema-only `tool({ ... })` overload.\n *\n * Servers may serialize the nested tool call as `toolCall` (JS) or\n * `tool_call` (Python). Use {@link parseHeadlessToolInterruptPayload} to\n * normalize either shape before reading fields.\n */\nexport interface HeadlessToolInterrupt {\n type: \"tool\";\n toolCall: {\n id: string | undefined;\n name: string;\n args: unknown;\n };\n}\n\n/**\n * Parses a headless-tool interrupt `value` from the graph. Accepts both\n * `toolCall` (LangChain JS) and `tool_call` (Python / JSON snake_case).\n */\nexport function parseHeadlessToolInterruptPayload(\n value: unknown\n): HeadlessToolInterrupt | null {\n if (typeof value !== \"object\" || value == null) {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (v.type !== \"tool\") {\n return null;\n }\n\n const rawTc = v.toolCall ?? v.tool_call;\n if (typeof rawTc !== \"object\" || rawTc == null) {\n return null;\n }\n const tc = rawTc as Record<string, unknown>;\n if (typeof tc.name !== \"string\") {\n return null;\n }\n\n const id = typeof tc.id === \"string\" ? tc.id : undefined;\n\n return {\n type: \"tool\",\n toolCall: {\n id,\n name: tc.name,\n args: tc.args,\n },\n };\n}\n\n/**\n * Client-side implementation returned by `headlessTool.implement(...)`.\n */\nexport interface HeadlessToolImplementation<Args = unknown, Output = unknown> {\n tool: {\n name: string;\n };\n execute: (args: Args) => Promise<Output>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyHeadlessToolImplementation = HeadlessToolImplementation<\n any,\n any\n>;\n\nexport interface ToolEvent {\n phase: \"start\" | \"success\" | \"error\";\n name: string;\n args: unknown;\n result?: unknown;\n error?: Error;\n duration?: number;\n}\n\nexport type OnToolCallback = (event: ToolEvent) => void;\n\n/**\n * Strip headless-tool interrupts from a user-facing interrupt list.\n */\nexport function filterOutHeadlessToolInterrupts<T extends { value?: unknown }>(\n interrupts: readonly T[]\n): T[] {\n return interrupts.filter(\n (interrupt) =>\n interrupt.value == null || !isHeadlessToolInterrupt(interrupt.value)\n );\n}\n\nexport function isHeadlessToolInterrupt(\n interrupt: unknown\n): interrupt is HeadlessToolInterrupt {\n return parseHeadlessToolInterruptPayload(interrupt) != null;\n}\n\nexport function findHeadlessTool<Args = unknown, Output = unknown>(\n tools: HeadlessToolImplementation[],\n name: string\n): HeadlessToolImplementation<Args, Output> | undefined {\n return tools.find((tool) => tool.tool.name === name) as\n | HeadlessToolImplementation<Args, Output>\n | undefined;\n}\n\nexport async function executeHeadlessTool<Args = unknown, Output = unknown>(\n implementation: HeadlessToolImplementation<Args, Output>,\n args: Args,\n onTool?: OnToolCallback\n): Promise<\n { success: true; result: Output } | { success: false; error: Error }\n> {\n const startTime = Date.now();\n\n onTool?.({\n phase: \"start\",\n name: implementation.tool.name,\n args,\n });\n\n try {\n const result = await implementation.execute(args);\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"success\",\n name: implementation.tool.name,\n args,\n result,\n duration,\n });\n\n return { success: true, result };\n } catch (err) {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n const error = err instanceof Error ? err : new Error(String(err));\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"error\",\n name: implementation.tool.name,\n args,\n error,\n duration,\n });\n\n return { success: false, error };\n }\n}\n\nexport async function handleHeadlessToolInterrupt(\n interrupt: HeadlessToolInterrupt,\n tools: HeadlessToolImplementation[],\n onTool?: OnToolCallback\n): Promise<{ toolCallId: string | undefined; value: unknown }> {\n const { toolCall } = interrupt;\n const implementation = findHeadlessTool(tools, toolCall.name);\n\n if (!implementation) {\n const error = new Error(\n `Headless tool \"${toolCall.name}\" is not registered. ` +\n `Available tools: ${tools.map((tool) => tool.tool.name).join(\", \") || \"none\"}`\n );\n\n onTool?.({\n phase: \"error\",\n name: toolCall.name,\n args: toolCall.args,\n error,\n duration: 0,\n });\n\n return {\n toolCallId: toolCall.id,\n value: { error: error.message },\n };\n }\n\n const result = await executeHeadlessTool(\n implementation,\n toolCall.args as never,\n onTool\n );\n\n if (result.success) {\n return {\n toolCallId: toolCall.id,\n value: result.result,\n };\n }\n\n return {\n toolCallId: toolCall.id,\n value: { error: result.error.message },\n };\n}\n\nexport function headlessToolResumeCommand(result: {\n toolCallId: string | undefined;\n value: unknown;\n}): { resume: unknown } {\n return {\n resume: result.toolCallId\n ? { [result.toolCallId]: result.value }\n : result.value,\n };\n}\n\n/**\n * Merge headless-tool results into one resume command. Use interrupt-id keys\n * whenever the stream provided them so the resume does not need to rediscover\n * a pending interrupt from mutable client state.\n */\nexport function headlessToolsBatchResumeCommand(\n entries: ReadonlyArray<{\n interruptId: string;\n toolCallId: string | undefined;\n value: unknown;\n }>\n): { resume: unknown } {\n if (entries.length === 0) {\n return { resume: {} };\n }\n\n const hasInterruptIds = entries.every(\n (entry) => entry.interruptId.length > 0\n );\n if (!hasInterruptIds && entries.length === 1) {\n const [entry] = entries;\n return headlessToolResumeCommand({\n toolCallId: entry.toolCallId,\n value: entry.value,\n });\n }\n\n const resume: Record<string, unknown> = {};\n for (const entry of entries) {\n if (entry.interruptId.length === 0) continue;\n resume[entry.interruptId] =\n entry.toolCallId != null && entry.toolCallId.length > 0\n ? { [entry.toolCallId]: entry.value }\n : entry.value;\n }\n return { resume };\n}\n\n/**\n * True when every top-level resume key is a graph task interrupt id\n * (32-char hex from `values.__interrupt__`).\n */\nexport function isInterruptIdKeyedResume(resume: unknown): boolean {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return false;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length === 0) return false;\n return keys.every((key) => /^[0-9a-f]{32}$/i.test(key));\n}\n\n/**\n * Normalize `command.resume` into the `run.start` input the API turns\n * into `Command({ resume })`. Interrupt-id keyed payloads pass through;\n * tool-call-keyed and generic payloads are wrapped under the matching\n * protocol interrupt id.\n */\nexport function buildResumeRunInput(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): Record<string, unknown> | null {\n if (resume == null) return null;\n if (isInterruptIdKeyedResume(resume)) {\n return resume as Record<string, unknown>;\n }\n\n const target = resolveInterruptTargetForHeadlessResume(\n resume,\n interrupts,\n resolvedInterruptIds\n );\n if (target == null) return null;\n\n return { [target.interruptId]: resume };\n}\n\n/**\n * Reads the tool-call id from a headless-tool resume command shaped as\n * `{ [toolCallId]: result }`.\n */\nexport function extractHeadlessToolCallIdFromResumeCommand(\n resume: unknown\n): string | undefined {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return undefined;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length !== 1) return undefined;\n return keys[0];\n}\n\n/**\n * Protocol interrupt entry tracked on {@link ThreadStream.interrupts}.\n * Used by {@link resolveInterruptTargetForHeadlessResume} when `respond()`\n * omits an explicit target.\n */\nexport interface ProtocolInterruptEntry {\n interruptId: string;\n namespace: string[];\n payload: unknown;\n}\n\n/**\n * Pick the protocol interrupt that matches a headless-tool resume payload.\n * Falls back to the newest unresolved interrupt for non-keyed resumes.\n */\nexport function resolveInterruptTargetForHeadlessResume(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): { interruptId: string; namespace: string[] } | null {\n const toolCallId = extractHeadlessToolCallIdFromResumeCommand(resume);\n if (toolCallId != null) {\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n const headless = parseHeadlessToolInterruptPayload(entry.payload);\n if (headless?.toolCall.id === toolCallId) {\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n }\n }\n\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n return null;\n}\n\nexport interface FlushPendingHeadlessToolInterruptsOptions {\n onTool?: OnToolCallback;\n resumeSubmit: (command: { resume: unknown }) => void | Promise<void>;\n defer?: (run: () => void) => void;\n}\n\nconst coalescedHeadlessFlushes = new WeakMap<\n Set<string>,\n { scheduled: boolean; run: () => void }\n>();\n\n/**\n * Coalesce rapid headless-tool flush triggers into one microtask so parallel\n * `input.requested` events observed back-to-back batch into a single resume.\n * Vue/Svelte/Angular watchers run synchronously per event; without this,\n * the first interrupt can be claimed before the second arrives and resume\n * splits into staggered single-tool commands.\n */\nexport function scheduleCoalescedHeadlessToolFlush(\n handledIds: Set<string>,\n run: () => void\n): void {\n let state = coalescedHeadlessFlushes.get(handledIds);\n if (state == null) {\n state = { scheduled: false, run: () => {} };\n coalescedHeadlessFlushes.set(handledIds, state);\n }\n state.run = run;\n if (state.scheduled) return;\n state.scheduled = true;\n void Promise.resolve().then(() => {\n state!.scheduled = false;\n state!.run();\n });\n}\n\n/**\n * Execute and resume all newly seen headless-tool interrupts from a values\n * payload. Callers own `handledIds` and should clear it when the thread changes.\n */\nexport function flushPendingHeadlessToolInterrupts(\n values: Record<string, unknown> | null | undefined,\n tools: HeadlessToolImplementation[] | undefined,\n handledIds: Set<string>,\n options: FlushPendingHeadlessToolInterruptsOptions\n): void {\n if (!tools?.length || !values) return;\n\n const interrupts = values.__interrupt__;\n if (!Array.isArray(interrupts) || interrupts.length === 0) return;\n\n const defer = options.defer ?? ((run) => run());\n const pending: Array<{\n interruptId: string;\n headlessInterrupt: HeadlessToolInterrupt;\n toolCallId: string;\n }> = [];\n const seenToolCallIds = new Set<string>();\n\n for (const interrupt of interrupts as Interrupt[]) {\n const headlessInterrupt = parseHeadlessToolInterruptPayload(\n interrupt.value\n );\n if (!headlessInterrupt) continue;\n\n const interruptId = interrupt.id ?? headlessInterrupt.toolCall.id ?? \"\";\n const toolCallId = headlessInterrupt.toolCall.id ?? \"\";\n if (handledIds.has(interruptId)) continue;\n // v2 protocol runs mirror the same headless-tool interrupt in both\n // `values.__interrupt__` and `rootStore.interrupts` with different\n // ids (graph/task id vs protocol interrupt_id). The headless-tool\n // effect can also re-run after the first resume clears\n // `rootStore.interrupts` while `values.__interrupt__` is still\n // present — persist tool call ids in the caller-owned set so we\n // only execute + resume once per pending tool call.\n if (toolCallId && handledIds.has(toolCallId)) continue;\n if (toolCallId && seenToolCallIds.has(toolCallId)) continue;\n if (toolCallId) seenToolCallIds.add(toolCallId);\n\n // Claim before defer so a second flush in the same tick cannot\n // schedule a duplicate execute/resume for the same interrupt.\n handledIds.add(interruptId);\n if (toolCallId) handledIds.add(toolCallId);\n\n pending.push({ interruptId, headlessInterrupt, toolCallId });\n }\n\n if (pending.length === 0) return;\n\n defer(() => {\n void (async () => {\n const results = await Promise.all(\n pending.map(async ({ interruptId, headlessInterrupt, toolCallId }) => {\n const result = await handleHeadlessToolInterrupt(\n headlessInterrupt,\n tools,\n options.onTool\n );\n return {\n interruptId,\n toolCallId: result.toolCallId ?? toolCallId,\n value: result.value,\n };\n })\n );\n await Promise.resolve(\n options.resumeSubmit(headlessToolsBatchResumeCommand(results))\n );\n })();\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"headless-tools.d.cts","names":[],"sources":["../src/headless-tools.ts"],"mappings":";;AAUA;;;;;;;UAAiB,qBAAA;EACf,IAAA;EACA,QAAA;IACE,EAAA;IACA,IAAA;IACA,IAAA;EAAA;AAAA;;AA2CJ;;;iBAnCgB,iCAAA,CACd,KAAA,YACC,qBAAA;;;;UAiCc,0BAAA;EACf,IAAA;IACE,IAAA;EAAA;EAEF,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,MAAA;AAAA;AAAA,KAIvB,6BAAA,GAAgC,0BAAA;AAAA,UAK3B,SAAA;EACf,KAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA,GAAQ,KAAA;EACR,QAAA;AAAA;AAAA,KAGU,cAAA,IAAkB,KAAA,EAAO,SAAA;;AATrC;;iBAcgB,+BAAA;EAA4C,KAAA;AAAA,EAAA,CAC1D,UAAA,WAAqB,CAAA,KACpB,CAAA;AAAA,iBAOa,uBAAA,CACd,SAAA,YACC,SAAA,IAAa,qBAAA;AAAA,iBAIA,gBAAA,kCAAA,CACd,KAAA,EAAO,0BAAA,IACP,IAAA,WACC,0BAAA,CAA2B,IAAA,EAAM,MAAA;AAAA,iBAMd,mBAAA,kCAAA,CACpB,cAAA,EAAgB,0BAAA,CAA2B,IAAA,EAAM,MAAA,GACjD,IAAA,EAAM,IAAA,EACN,MAAA,GAAS,cAAA,GACR,OAAA;EACC,OAAA;EAAe,MAAA,EAAQ,MAAA;AAAA;EAAa,OAAA;EAAgB,KAAA,EAAO,KAAA;AAAA;AAAA,iBAwCzC,2BAAA,CACpB,SAAA,EAAW,qBAAA,EACX,KAAA,EAAO,0BAAA,IACP,MAAA,GAAS,cAAA,GACR,OAAA;EAAU,UAAA;EAAgC,KAAA;AAAA;AAAA,iBA2C7B,yBAAA,CAA0B,MAAA;EACxC,UAAA;EACA,KAAA;AAAA;EACI,MAAA;AAAA;AAAA,UAiJW,yCAAA;EACf,MAAA,GAAS,cAAA;EACT,YAAA,GAAe,OAAA;IAAW,MAAA;EAAA,aAA6B,OAAA;EACvD,KAAA,IAAS,GAAA;AAAA;;;;;;;;iBAeK,kCAAA,CACd,UAAA,EAAY,GAAA,UACZ,GAAA;;;;;iBAoBc,kCAAA,CACd,MAAA,EAAQ,MAAA,sCACR,KAAA,EAAO,0BAAA,gBACP,UAAA,EAAY,GAAA,UACZ,OAAA,EAAS,yCAAA"}
1
+ {"version":3,"file":"headless-tools.d.cts","names":[],"sources":["../src/headless-tools.ts"],"mappings":";;AAUA;;;;;;;UAAiB,qBAAA;EACf,IAAA;EACA,QAAA;IACE,EAAA;IACA,IAAA;IACA,IAAA;EAAA;AAAA;;AA2CJ;;;iBAnCgB,iCAAA,CACd,KAAA,YACC,qBAAA;;;;UAiCc,0BAAA;EACf,IAAA;IACE,IAAA;EAAA;EAEF,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,MAAA;AAAA;AAAA,KAIvB,6BAAA,GAAgC,0BAAA;AAAA,UAK3B,SAAA;EACf,KAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA,GAAQ,KAAA;EACR,QAAA;AAAA;AAAA,KAGU,cAAA,IAAkB,KAAA,EAAO,SAAA;;AATrC;;iBAcgB,+BAAA;EAA4C,KAAA;AAAA,EAAA,CAC1D,UAAA,WAAqB,CAAA,KACpB,CAAA;AAAA,iBAOa,uBAAA,CACd,SAAA,YACC,SAAA,IAAa,qBAAA;AAAA,iBAIA,gBAAA,kCAAA,CACd,KAAA,EAAO,0BAAA,IACP,IAAA,WACC,0BAAA,CAA2B,IAAA,EAAM,MAAA;AAAA,iBAMd,mBAAA,kCAAA,CACpB,cAAA,EAAgB,0BAAA,CAA2B,IAAA,EAAM,MAAA,GACjD,IAAA,EAAM,IAAA,EACN,MAAA,GAAS,cAAA,GACR,OAAA;EACC,OAAA;EAAe,MAAA,EAAQ,MAAA;AAAA;EAAa,OAAA;EAAgB,KAAA,EAAO,KAAA;AAAA;AAAA,iBAwCzC,2BAAA,CACpB,SAAA,EAAW,qBAAA,EACX,KAAA,EAAO,0BAAA,IACP,MAAA,GAAS,cAAA,GACR,OAAA;EAAU,UAAA;EAAgC,KAAA;AAAA;AAAA,iBA2C7B,yBAAA,CAA0B,MAAA;EACxC,UAAA;EACA,KAAA;AAAA;EACI,MAAA;AAAA;AAAA,UAsJW,yCAAA;EACf,MAAA,GAAS,cAAA;EACT,YAAA,GAAe,OAAA;IAAW,MAAA;EAAA,aAA6B,OAAA;EACvD,KAAA,IAAS,GAAA;AAAA;;;;;;;;iBAeK,kCAAA,CACd,UAAA,EAAY,GAAA,UACZ,GAAA;;;;AA5NF;iBAgPgB,kCAAA,CACd,MAAA,EAAQ,MAAA,sCACR,KAAA,EAAO,0BAAA,gBACP,UAAA,EAAY,GAAA,UACZ,OAAA,EAAS,yCAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"headless-tools.d.ts","names":[],"sources":["../src/headless-tools.ts"],"mappings":";;AAUA;;;;;;;UAAiB,qBAAA;EACf,IAAA;EACA,QAAA;IACE,EAAA;IACA,IAAA;IACA,IAAA;EAAA;AAAA;;AA2CJ;;;iBAnCgB,iCAAA,CACd,KAAA,YACC,qBAAA;;;;UAiCc,0BAAA;EACf,IAAA;IACE,IAAA;EAAA;EAEF,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,MAAA;AAAA;AAAA,KAIvB,6BAAA,GAAgC,0BAAA;AAAA,UAK3B,SAAA;EACf,KAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA,GAAQ,KAAA;EACR,QAAA;AAAA;AAAA,KAGU,cAAA,IAAkB,KAAA,EAAO,SAAA;;AATrC;;iBAcgB,+BAAA;EAA4C,KAAA;AAAA,EAAA,CAC1D,UAAA,WAAqB,CAAA,KACpB,CAAA;AAAA,iBAOa,uBAAA,CACd,SAAA,YACC,SAAA,IAAa,qBAAA;AAAA,iBAIA,gBAAA,kCAAA,CACd,KAAA,EAAO,0BAAA,IACP,IAAA,WACC,0BAAA,CAA2B,IAAA,EAAM,MAAA;AAAA,iBAMd,mBAAA,kCAAA,CACpB,cAAA,EAAgB,0BAAA,CAA2B,IAAA,EAAM,MAAA,GACjD,IAAA,EAAM,IAAA,EACN,MAAA,GAAS,cAAA,GACR,OAAA;EACC,OAAA;EAAe,MAAA,EAAQ,MAAA;AAAA;EAAa,OAAA;EAAgB,KAAA,EAAO,KAAA;AAAA;AAAA,iBAwCzC,2BAAA,CACpB,SAAA,EAAW,qBAAA,EACX,KAAA,EAAO,0BAAA,IACP,MAAA,GAAS,cAAA,GACR,OAAA;EAAU,UAAA;EAAgC,KAAA;AAAA;AAAA,iBA2C7B,yBAAA,CAA0B,MAAA;EACxC,UAAA;EACA,KAAA;AAAA;EACI,MAAA;AAAA;AAAA,UAiJW,yCAAA;EACf,MAAA,GAAS,cAAA;EACT,YAAA,GAAe,OAAA;IAAW,MAAA;EAAA,aAA6B,OAAA;EACvD,KAAA,IAAS,GAAA;AAAA;;;;;;;;iBAeK,kCAAA,CACd,UAAA,EAAY,GAAA,UACZ,GAAA;;;;;iBAoBc,kCAAA,CACd,MAAA,EAAQ,MAAA,sCACR,KAAA,EAAO,0BAAA,gBACP,UAAA,EAAY,GAAA,UACZ,OAAA,EAAS,yCAAA"}
1
+ {"version":3,"file":"headless-tools.d.ts","names":[],"sources":["../src/headless-tools.ts"],"mappings":";;AAUA;;;;;;;UAAiB,qBAAA;EACf,IAAA;EACA,QAAA;IACE,EAAA;IACA,IAAA;IACA,IAAA;EAAA;AAAA;;AA2CJ;;;iBAnCgB,iCAAA,CACd,KAAA,YACC,qBAAA;;;;UAiCc,0BAAA;EACf,IAAA;IACE,IAAA;EAAA;EAEF,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,MAAA;AAAA;AAAA,KAIvB,6BAAA,GAAgC,0BAAA;AAAA,UAK3B,SAAA;EACf,KAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA,GAAQ,KAAA;EACR,QAAA;AAAA;AAAA,KAGU,cAAA,IAAkB,KAAA,EAAO,SAAA;;AATrC;;iBAcgB,+BAAA;EAA4C,KAAA;AAAA,EAAA,CAC1D,UAAA,WAAqB,CAAA,KACpB,CAAA;AAAA,iBAOa,uBAAA,CACd,SAAA,YACC,SAAA,IAAa,qBAAA;AAAA,iBAIA,gBAAA,kCAAA,CACd,KAAA,EAAO,0BAAA,IACP,IAAA,WACC,0BAAA,CAA2B,IAAA,EAAM,MAAA;AAAA,iBAMd,mBAAA,kCAAA,CACpB,cAAA,EAAgB,0BAAA,CAA2B,IAAA,EAAM,MAAA,GACjD,IAAA,EAAM,IAAA,EACN,MAAA,GAAS,cAAA,GACR,OAAA;EACC,OAAA;EAAe,MAAA,EAAQ,MAAA;AAAA;EAAa,OAAA;EAAgB,KAAA,EAAO,KAAA;AAAA;AAAA,iBAwCzC,2BAAA,CACpB,SAAA,EAAW,qBAAA,EACX,KAAA,EAAO,0BAAA,IACP,MAAA,GAAS,cAAA,GACR,OAAA;EAAU,UAAA;EAAgC,KAAA;AAAA;AAAA,iBA2C7B,yBAAA,CAA0B,MAAA;EACxC,UAAA;EACA,KAAA;AAAA;EACI,MAAA;AAAA;AAAA,UAsJW,yCAAA;EACf,MAAA,GAAS,cAAA;EACT,YAAA,GAAe,OAAA;IAAW,MAAA;EAAA,aAA6B,OAAA;EACvD,KAAA,IAAS,GAAA;AAAA;;;;;;;;iBAeK,kCAAA,CACd,UAAA,EAAY,GAAA,UACZ,GAAA;;;;AA5NF;iBAgPgB,kCAAA,CACd,MAAA,EAAQ,MAAA,sCACR,KAAA,EAAO,0BAAA,gBACP,UAAA,EAAY,GAAA,UACZ,OAAA,EAAS,yCAAA"}
@@ -121,29 +121,6 @@ function headlessToolsBatchResumeCommand(entries) {
121
121
  return { resume };
122
122
  }
123
123
  /**
124
- * True when every top-level resume key is a graph task interrupt id
125
- * (32-char hex from `values.__interrupt__`).
126
- */
127
- function isInterruptIdKeyedResume(resume) {
128
- if (resume == null || typeof resume !== "object" || Array.isArray(resume)) return false;
129
- const keys = Object.keys(resume);
130
- if (keys.length === 0) return false;
131
- return keys.every((key) => /^[0-9a-f]{32}$/i.test(key));
132
- }
133
- /**
134
- * Normalize `command.resume` into the `run.start` input the API turns
135
- * into `Command({ resume })`. Interrupt-id keyed payloads pass through;
136
- * tool-call-keyed and generic payloads are wrapped under the matching
137
- * protocol interrupt id.
138
- */
139
- function buildResumeRunInput(resume, interrupts, resolvedInterruptIds) {
140
- if (resume == null) return null;
141
- if (isInterruptIdKeyedResume(resume)) return resume;
142
- const target = resolveInterruptTargetForHeadlessResume(resume, interrupts, resolvedInterruptIds);
143
- if (target == null) return null;
144
- return { [target.interruptId]: resume };
145
- }
146
- /**
147
124
  * Reads the tool-call id from a headless-tool resume command shaped as
148
125
  * `{ [toolCallId]: result }`.
149
126
  */
@@ -246,6 +223,6 @@ function flushPendingHeadlessToolInterrupts(values, tools, handledIds, options)
246
223
  });
247
224
  }
248
225
  //#endregion
249
- export { buildResumeRunInput, executeHeadlessTool, filterOutHeadlessToolInterrupts, findHeadlessTool, flushPendingHeadlessToolInterrupts, handleHeadlessToolInterrupt, headlessToolResumeCommand, isHeadlessToolInterrupt, parseHeadlessToolInterruptPayload, resolveInterruptTargetForHeadlessResume, scheduleCoalescedHeadlessToolFlush };
226
+ export { executeHeadlessTool, filterOutHeadlessToolInterrupts, findHeadlessTool, flushPendingHeadlessToolInterrupts, handleHeadlessToolInterrupt, headlessToolResumeCommand, isHeadlessToolInterrupt, parseHeadlessToolInterruptPayload, resolveInterruptTargetForHeadlessResume, scheduleCoalescedHeadlessToolFlush };
250
227
 
251
228
  //# sourceMappingURL=headless-tools.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless-tools.js","names":[],"sources":["../src/headless-tools.ts"],"sourcesContent":["import type { Interrupt } from \"./schema.js\";\n\n/**\n * Represents a headless tool interrupt payload emitted by LangChain's\n * schema-only `tool({ ... })` overload.\n *\n * Servers may serialize the nested tool call as `toolCall` (JS) or\n * `tool_call` (Python). Use {@link parseHeadlessToolInterruptPayload} to\n * normalize either shape before reading fields.\n */\nexport interface HeadlessToolInterrupt {\n type: \"tool\";\n toolCall: {\n id: string | undefined;\n name: string;\n args: unknown;\n };\n}\n\n/**\n * Parses a headless-tool interrupt `value` from the graph. Accepts both\n * `toolCall` (LangChain JS) and `tool_call` (Python / JSON snake_case).\n */\nexport function parseHeadlessToolInterruptPayload(\n value: unknown\n): HeadlessToolInterrupt | null {\n if (typeof value !== \"object\" || value == null) {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (v.type !== \"tool\") {\n return null;\n }\n\n const rawTc = v.toolCall ?? v.tool_call;\n if (typeof rawTc !== \"object\" || rawTc == null) {\n return null;\n }\n const tc = rawTc as Record<string, unknown>;\n if (typeof tc.name !== \"string\") {\n return null;\n }\n\n const id = typeof tc.id === \"string\" ? tc.id : undefined;\n\n return {\n type: \"tool\",\n toolCall: {\n id,\n name: tc.name,\n args: tc.args,\n },\n };\n}\n\n/**\n * Client-side implementation returned by `headlessTool.implement(...)`.\n */\nexport interface HeadlessToolImplementation<Args = unknown, Output = unknown> {\n tool: {\n name: string;\n };\n execute: (args: Args) => Promise<Output>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyHeadlessToolImplementation = HeadlessToolImplementation<\n any,\n any\n>;\n\nexport interface ToolEvent {\n phase: \"start\" | \"success\" | \"error\";\n name: string;\n args: unknown;\n result?: unknown;\n error?: Error;\n duration?: number;\n}\n\nexport type OnToolCallback = (event: ToolEvent) => void;\n\n/**\n * Strip headless-tool interrupts from a user-facing interrupt list.\n */\nexport function filterOutHeadlessToolInterrupts<T extends { value?: unknown }>(\n interrupts: readonly T[]\n): T[] {\n return interrupts.filter(\n (interrupt) =>\n interrupt.value == null || !isHeadlessToolInterrupt(interrupt.value)\n );\n}\n\nexport function isHeadlessToolInterrupt(\n interrupt: unknown\n): interrupt is HeadlessToolInterrupt {\n return parseHeadlessToolInterruptPayload(interrupt) != null;\n}\n\nexport function findHeadlessTool<Args = unknown, Output = unknown>(\n tools: HeadlessToolImplementation[],\n name: string\n): HeadlessToolImplementation<Args, Output> | undefined {\n return tools.find((tool) => tool.tool.name === name) as\n | HeadlessToolImplementation<Args, Output>\n | undefined;\n}\n\nexport async function executeHeadlessTool<Args = unknown, Output = unknown>(\n implementation: HeadlessToolImplementation<Args, Output>,\n args: Args,\n onTool?: OnToolCallback\n): Promise<\n { success: true; result: Output } | { success: false; error: Error }\n> {\n const startTime = Date.now();\n\n onTool?.({\n phase: \"start\",\n name: implementation.tool.name,\n args,\n });\n\n try {\n const result = await implementation.execute(args);\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"success\",\n name: implementation.tool.name,\n args,\n result,\n duration,\n });\n\n return { success: true, result };\n } catch (err) {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n const error = err instanceof Error ? err : new Error(String(err));\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"error\",\n name: implementation.tool.name,\n args,\n error,\n duration,\n });\n\n return { success: false, error };\n }\n}\n\nexport async function handleHeadlessToolInterrupt(\n interrupt: HeadlessToolInterrupt,\n tools: HeadlessToolImplementation[],\n onTool?: OnToolCallback\n): Promise<{ toolCallId: string | undefined; value: unknown }> {\n const { toolCall } = interrupt;\n const implementation = findHeadlessTool(tools, toolCall.name);\n\n if (!implementation) {\n const error = new Error(\n `Headless tool \"${toolCall.name}\" is not registered. ` +\n `Available tools: ${tools.map((tool) => tool.tool.name).join(\", \") || \"none\"}`\n );\n\n onTool?.({\n phase: \"error\",\n name: toolCall.name,\n args: toolCall.args,\n error,\n duration: 0,\n });\n\n return {\n toolCallId: toolCall.id,\n value: { error: error.message },\n };\n }\n\n const result = await executeHeadlessTool(\n implementation,\n toolCall.args as never,\n onTool\n );\n\n if (result.success) {\n return {\n toolCallId: toolCall.id,\n value: result.result,\n };\n }\n\n return {\n toolCallId: toolCall.id,\n value: { error: result.error.message },\n };\n}\n\nexport function headlessToolResumeCommand(result: {\n toolCallId: string | undefined;\n value: unknown;\n}): { resume: unknown } {\n return {\n resume: result.toolCallId\n ? { [result.toolCallId]: result.value }\n : result.value,\n };\n}\n\n/**\n * Merge headless-tool results into one resume command. Use interrupt-id keys\n * whenever the stream provided them so the resume does not need to rediscover\n * a pending interrupt from mutable client state.\n */\nexport function headlessToolsBatchResumeCommand(\n entries: ReadonlyArray<{\n interruptId: string;\n toolCallId: string | undefined;\n value: unknown;\n }>\n): { resume: unknown } {\n if (entries.length === 0) {\n return { resume: {} };\n }\n\n const hasInterruptIds = entries.every(\n (entry) => entry.interruptId.length > 0\n );\n if (!hasInterruptIds && entries.length === 1) {\n const [entry] = entries;\n return headlessToolResumeCommand({\n toolCallId: entry.toolCallId,\n value: entry.value,\n });\n }\n\n const resume: Record<string, unknown> = {};\n for (const entry of entries) {\n if (entry.interruptId.length === 0) continue;\n resume[entry.interruptId] =\n entry.toolCallId != null && entry.toolCallId.length > 0\n ? { [entry.toolCallId]: entry.value }\n : entry.value;\n }\n return { resume };\n}\n\n/**\n * True when every top-level resume key is a graph task interrupt id\n * (32-char hex from `values.__interrupt__`).\n */\nexport function isInterruptIdKeyedResume(resume: unknown): boolean {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return false;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length === 0) return false;\n return keys.every((key) => /^[0-9a-f]{32}$/i.test(key));\n}\n\n/**\n * Normalize `command.resume` into the `run.start` input the API turns\n * into `Command({ resume })`. Interrupt-id keyed payloads pass through;\n * tool-call-keyed and generic payloads are wrapped under the matching\n * protocol interrupt id.\n */\nexport function buildResumeRunInput(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): Record<string, unknown> | null {\n if (resume == null) return null;\n if (isInterruptIdKeyedResume(resume)) {\n return resume as Record<string, unknown>;\n }\n\n const target = resolveInterruptTargetForHeadlessResume(\n resume,\n interrupts,\n resolvedInterruptIds\n );\n if (target == null) return null;\n\n return { [target.interruptId]: resume };\n}\n\n/**\n * Reads the tool-call id from a headless-tool resume command shaped as\n * `{ [toolCallId]: result }`.\n */\nexport function extractHeadlessToolCallIdFromResumeCommand(\n resume: unknown\n): string | undefined {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return undefined;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length !== 1) return undefined;\n return keys[0];\n}\n\nexport interface ProtocolInterruptEntry {\n interruptId: string;\n namespace: string[];\n payload: unknown;\n}\n\n/**\n * Pick the protocol interrupt that matches a headless-tool resume payload.\n * Falls back to the newest unresolved interrupt for non-keyed resumes.\n */\nexport function resolveInterruptTargetForHeadlessResume(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): { interruptId: string; namespace: string[] } | null {\n const toolCallId = extractHeadlessToolCallIdFromResumeCommand(resume);\n if (toolCallId != null) {\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n const headless = parseHeadlessToolInterruptPayload(entry.payload);\n if (headless?.toolCall.id === toolCallId) {\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n }\n }\n\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n return null;\n}\n\nexport interface FlushPendingHeadlessToolInterruptsOptions {\n onTool?: OnToolCallback;\n resumeSubmit: (command: { resume: unknown }) => void | Promise<void>;\n defer?: (run: () => void) => void;\n}\n\nconst coalescedHeadlessFlushes = new WeakMap<\n Set<string>,\n { scheduled: boolean; run: () => void }\n>();\n\n/**\n * Coalesce rapid headless-tool flush triggers into one microtask so parallel\n * `input.requested` events observed back-to-back batch into a single resume.\n * Vue/Svelte/Angular watchers run synchronously per event; without this,\n * the first interrupt can be claimed before the second arrives and resume\n * splits into staggered single-tool commands.\n */\nexport function scheduleCoalescedHeadlessToolFlush(\n handledIds: Set<string>,\n run: () => void\n): void {\n let state = coalescedHeadlessFlushes.get(handledIds);\n if (state == null) {\n state = { scheduled: false, run: () => {} };\n coalescedHeadlessFlushes.set(handledIds, state);\n }\n state.run = run;\n if (state.scheduled) return;\n state.scheduled = true;\n void Promise.resolve().then(() => {\n state!.scheduled = false;\n state!.run();\n });\n}\n\n/**\n * Execute and resume all newly seen headless-tool interrupts from a values\n * payload. Callers own `handledIds` and should clear it when the thread changes.\n */\nexport function flushPendingHeadlessToolInterrupts(\n values: Record<string, unknown> | null | undefined,\n tools: HeadlessToolImplementation[] | undefined,\n handledIds: Set<string>,\n options: FlushPendingHeadlessToolInterruptsOptions\n): void {\n if (!tools?.length || !values) return;\n\n const interrupts = values.__interrupt__;\n if (!Array.isArray(interrupts) || interrupts.length === 0) return;\n\n const defer = options.defer ?? ((run) => run());\n const pending: Array<{\n interruptId: string;\n headlessInterrupt: HeadlessToolInterrupt;\n toolCallId: string;\n }> = [];\n const seenToolCallIds = new Set<string>();\n\n for (const interrupt of interrupts as Interrupt[]) {\n const headlessInterrupt = parseHeadlessToolInterruptPayload(\n interrupt.value\n );\n if (!headlessInterrupt) continue;\n\n const interruptId = interrupt.id ?? headlessInterrupt.toolCall.id ?? \"\";\n const toolCallId = headlessInterrupt.toolCall.id ?? \"\";\n if (handledIds.has(interruptId)) continue;\n // v2 protocol runs mirror the same headless-tool interrupt in both\n // `values.__interrupt__` and `rootStore.interrupts` with different\n // ids (graph/task id vs protocol interrupt_id). The headless-tool\n // effect can also re-run after the first resume clears\n // `rootStore.interrupts` while `values.__interrupt__` is still\n // present — persist tool call ids in the caller-owned set so we\n // only execute + resume once per pending tool call.\n if (toolCallId && handledIds.has(toolCallId)) continue;\n if (toolCallId && seenToolCallIds.has(toolCallId)) continue;\n if (toolCallId) seenToolCallIds.add(toolCallId);\n\n // Claim before defer so a second flush in the same tick cannot\n // schedule a duplicate execute/resume for the same interrupt.\n handledIds.add(interruptId);\n if (toolCallId) handledIds.add(toolCallId);\n\n pending.push({ interruptId, headlessInterrupt, toolCallId });\n }\n\n if (pending.length === 0) return;\n\n defer(() => {\n void (async () => {\n const results = await Promise.all(\n pending.map(async ({ interruptId, headlessInterrupt, toolCallId }) => {\n const result = await handleHeadlessToolInterrupt(\n headlessInterrupt,\n tools,\n options.onTool\n );\n return {\n interruptId,\n toolCallId: result.toolCallId ?? toolCallId,\n value: result.value,\n };\n })\n );\n await Promise.resolve(\n options.resumeSubmit(headlessToolsBatchResumeCommand(results))\n );\n })();\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"headless-tools.js","names":[],"sources":["../src/headless-tools.ts"],"sourcesContent":["import type { Interrupt } from \"./schema.js\";\n\n/**\n * Represents a headless tool interrupt payload emitted by LangChain's\n * schema-only `tool({ ... })` overload.\n *\n * Servers may serialize the nested tool call as `toolCall` (JS) or\n * `tool_call` (Python). Use {@link parseHeadlessToolInterruptPayload} to\n * normalize either shape before reading fields.\n */\nexport interface HeadlessToolInterrupt {\n type: \"tool\";\n toolCall: {\n id: string | undefined;\n name: string;\n args: unknown;\n };\n}\n\n/**\n * Parses a headless-tool interrupt `value` from the graph. Accepts both\n * `toolCall` (LangChain JS) and `tool_call` (Python / JSON snake_case).\n */\nexport function parseHeadlessToolInterruptPayload(\n value: unknown\n): HeadlessToolInterrupt | null {\n if (typeof value !== \"object\" || value == null) {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (v.type !== \"tool\") {\n return null;\n }\n\n const rawTc = v.toolCall ?? v.tool_call;\n if (typeof rawTc !== \"object\" || rawTc == null) {\n return null;\n }\n const tc = rawTc as Record<string, unknown>;\n if (typeof tc.name !== \"string\") {\n return null;\n }\n\n const id = typeof tc.id === \"string\" ? tc.id : undefined;\n\n return {\n type: \"tool\",\n toolCall: {\n id,\n name: tc.name,\n args: tc.args,\n },\n };\n}\n\n/**\n * Client-side implementation returned by `headlessTool.implement(...)`.\n */\nexport interface HeadlessToolImplementation<Args = unknown, Output = unknown> {\n tool: {\n name: string;\n };\n execute: (args: Args) => Promise<Output>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyHeadlessToolImplementation = HeadlessToolImplementation<\n any,\n any\n>;\n\nexport interface ToolEvent {\n phase: \"start\" | \"success\" | \"error\";\n name: string;\n args: unknown;\n result?: unknown;\n error?: Error;\n duration?: number;\n}\n\nexport type OnToolCallback = (event: ToolEvent) => void;\n\n/**\n * Strip headless-tool interrupts from a user-facing interrupt list.\n */\nexport function filterOutHeadlessToolInterrupts<T extends { value?: unknown }>(\n interrupts: readonly T[]\n): T[] {\n return interrupts.filter(\n (interrupt) =>\n interrupt.value == null || !isHeadlessToolInterrupt(interrupt.value)\n );\n}\n\nexport function isHeadlessToolInterrupt(\n interrupt: unknown\n): interrupt is HeadlessToolInterrupt {\n return parseHeadlessToolInterruptPayload(interrupt) != null;\n}\n\nexport function findHeadlessTool<Args = unknown, Output = unknown>(\n tools: HeadlessToolImplementation[],\n name: string\n): HeadlessToolImplementation<Args, Output> | undefined {\n return tools.find((tool) => tool.tool.name === name) as\n | HeadlessToolImplementation<Args, Output>\n | undefined;\n}\n\nexport async function executeHeadlessTool<Args = unknown, Output = unknown>(\n implementation: HeadlessToolImplementation<Args, Output>,\n args: Args,\n onTool?: OnToolCallback\n): Promise<\n { success: true; result: Output } | { success: false; error: Error }\n> {\n const startTime = Date.now();\n\n onTool?.({\n phase: \"start\",\n name: implementation.tool.name,\n args,\n });\n\n try {\n const result = await implementation.execute(args);\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"success\",\n name: implementation.tool.name,\n args,\n result,\n duration,\n });\n\n return { success: true, result };\n } catch (err) {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n const error = err instanceof Error ? err : new Error(String(err));\n const duration = Date.now() - startTime;\n\n onTool?.({\n phase: \"error\",\n name: implementation.tool.name,\n args,\n error,\n duration,\n });\n\n return { success: false, error };\n }\n}\n\nexport async function handleHeadlessToolInterrupt(\n interrupt: HeadlessToolInterrupt,\n tools: HeadlessToolImplementation[],\n onTool?: OnToolCallback\n): Promise<{ toolCallId: string | undefined; value: unknown }> {\n const { toolCall } = interrupt;\n const implementation = findHeadlessTool(tools, toolCall.name);\n\n if (!implementation) {\n const error = new Error(\n `Headless tool \"${toolCall.name}\" is not registered. ` +\n `Available tools: ${tools.map((tool) => tool.tool.name).join(\", \") || \"none\"}`\n );\n\n onTool?.({\n phase: \"error\",\n name: toolCall.name,\n args: toolCall.args,\n error,\n duration: 0,\n });\n\n return {\n toolCallId: toolCall.id,\n value: { error: error.message },\n };\n }\n\n const result = await executeHeadlessTool(\n implementation,\n toolCall.args as never,\n onTool\n );\n\n if (result.success) {\n return {\n toolCallId: toolCall.id,\n value: result.result,\n };\n }\n\n return {\n toolCallId: toolCall.id,\n value: { error: result.error.message },\n };\n}\n\nexport function headlessToolResumeCommand(result: {\n toolCallId: string | undefined;\n value: unknown;\n}): { resume: unknown } {\n return {\n resume: result.toolCallId\n ? { [result.toolCallId]: result.value }\n : result.value,\n };\n}\n\n/**\n * Merge headless-tool results into one resume command. Use interrupt-id keys\n * whenever the stream provided them so the resume does not need to rediscover\n * a pending interrupt from mutable client state.\n */\nexport function headlessToolsBatchResumeCommand(\n entries: ReadonlyArray<{\n interruptId: string;\n toolCallId: string | undefined;\n value: unknown;\n }>\n): { resume: unknown } {\n if (entries.length === 0) {\n return { resume: {} };\n }\n\n const hasInterruptIds = entries.every(\n (entry) => entry.interruptId.length > 0\n );\n if (!hasInterruptIds && entries.length === 1) {\n const [entry] = entries;\n return headlessToolResumeCommand({\n toolCallId: entry.toolCallId,\n value: entry.value,\n });\n }\n\n const resume: Record<string, unknown> = {};\n for (const entry of entries) {\n if (entry.interruptId.length === 0) continue;\n resume[entry.interruptId] =\n entry.toolCallId != null && entry.toolCallId.length > 0\n ? { [entry.toolCallId]: entry.value }\n : entry.value;\n }\n return { resume };\n}\n\n/**\n * True when every top-level resume key is a graph task interrupt id\n * (32-char hex from `values.__interrupt__`).\n */\nexport function isInterruptIdKeyedResume(resume: unknown): boolean {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return false;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length === 0) return false;\n return keys.every((key) => /^[0-9a-f]{32}$/i.test(key));\n}\n\n/**\n * Normalize `command.resume` into the `run.start` input the API turns\n * into `Command({ resume })`. Interrupt-id keyed payloads pass through;\n * tool-call-keyed and generic payloads are wrapped under the matching\n * protocol interrupt id.\n */\nexport function buildResumeRunInput(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): Record<string, unknown> | null {\n if (resume == null) return null;\n if (isInterruptIdKeyedResume(resume)) {\n return resume as Record<string, unknown>;\n }\n\n const target = resolveInterruptTargetForHeadlessResume(\n resume,\n interrupts,\n resolvedInterruptIds\n );\n if (target == null) return null;\n\n return { [target.interruptId]: resume };\n}\n\n/**\n * Reads the tool-call id from a headless-tool resume command shaped as\n * `{ [toolCallId]: result }`.\n */\nexport function extractHeadlessToolCallIdFromResumeCommand(\n resume: unknown\n): string | undefined {\n if (resume == null || typeof resume !== \"object\" || Array.isArray(resume)) {\n return undefined;\n }\n const keys = Object.keys(resume as Record<string, unknown>);\n if (keys.length !== 1) return undefined;\n return keys[0];\n}\n\n/**\n * Protocol interrupt entry tracked on {@link ThreadStream.interrupts}.\n * Used by {@link resolveInterruptTargetForHeadlessResume} when `respond()`\n * omits an explicit target.\n */\nexport interface ProtocolInterruptEntry {\n interruptId: string;\n namespace: string[];\n payload: unknown;\n}\n\n/**\n * Pick the protocol interrupt that matches a headless-tool resume payload.\n * Falls back to the newest unresolved interrupt for non-keyed resumes.\n */\nexport function resolveInterruptTargetForHeadlessResume(\n resume: unknown,\n interrupts: readonly ProtocolInterruptEntry[],\n resolvedInterruptIds: ReadonlySet<string>\n): { interruptId: string; namespace: string[] } | null {\n const toolCallId = extractHeadlessToolCallIdFromResumeCommand(resume);\n if (toolCallId != null) {\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n const headless = parseHeadlessToolInterruptPayload(entry.payload);\n if (headless?.toolCall.id === toolCallId) {\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n }\n }\n\n for (let i = interrupts.length - 1; i >= 0; i -= 1) {\n const entry = interrupts[i];\n if (entry == null || resolvedInterruptIds.has(entry.interruptId)) {\n continue;\n }\n return {\n interruptId: entry.interruptId,\n namespace: [...entry.namespace],\n };\n }\n return null;\n}\n\nexport interface FlushPendingHeadlessToolInterruptsOptions {\n onTool?: OnToolCallback;\n resumeSubmit: (command: { resume: unknown }) => void | Promise<void>;\n defer?: (run: () => void) => void;\n}\n\nconst coalescedHeadlessFlushes = new WeakMap<\n Set<string>,\n { scheduled: boolean; run: () => void }\n>();\n\n/**\n * Coalesce rapid headless-tool flush triggers into one microtask so parallel\n * `input.requested` events observed back-to-back batch into a single resume.\n * Vue/Svelte/Angular watchers run synchronously per event; without this,\n * the first interrupt can be claimed before the second arrives and resume\n * splits into staggered single-tool commands.\n */\nexport function scheduleCoalescedHeadlessToolFlush(\n handledIds: Set<string>,\n run: () => void\n): void {\n let state = coalescedHeadlessFlushes.get(handledIds);\n if (state == null) {\n state = { scheduled: false, run: () => {} };\n coalescedHeadlessFlushes.set(handledIds, state);\n }\n state.run = run;\n if (state.scheduled) return;\n state.scheduled = true;\n void Promise.resolve().then(() => {\n state!.scheduled = false;\n state!.run();\n });\n}\n\n/**\n * Execute and resume all newly seen headless-tool interrupts from a values\n * payload. Callers own `handledIds` and should clear it when the thread changes.\n */\nexport function flushPendingHeadlessToolInterrupts(\n values: Record<string, unknown> | null | undefined,\n tools: HeadlessToolImplementation[] | undefined,\n handledIds: Set<string>,\n options: FlushPendingHeadlessToolInterruptsOptions\n): void {\n if (!tools?.length || !values) return;\n\n const interrupts = values.__interrupt__;\n if (!Array.isArray(interrupts) || interrupts.length === 0) return;\n\n const defer = options.defer ?? ((run) => run());\n const pending: Array<{\n interruptId: string;\n headlessInterrupt: HeadlessToolInterrupt;\n toolCallId: string;\n }> = [];\n const seenToolCallIds = new Set<string>();\n\n for (const interrupt of interrupts as Interrupt[]) {\n const headlessInterrupt = parseHeadlessToolInterruptPayload(\n interrupt.value\n );\n if (!headlessInterrupt) continue;\n\n const interruptId = interrupt.id ?? headlessInterrupt.toolCall.id ?? \"\";\n const toolCallId = headlessInterrupt.toolCall.id ?? \"\";\n if (handledIds.has(interruptId)) continue;\n // v2 protocol runs mirror the same headless-tool interrupt in both\n // `values.__interrupt__` and `rootStore.interrupts` with different\n // ids (graph/task id vs protocol interrupt_id). The headless-tool\n // effect can also re-run after the first resume clears\n // `rootStore.interrupts` while `values.__interrupt__` is still\n // present — persist tool call ids in the caller-owned set so we\n // only execute + resume once per pending tool call.\n if (toolCallId && handledIds.has(toolCallId)) continue;\n if (toolCallId && seenToolCallIds.has(toolCallId)) continue;\n if (toolCallId) seenToolCallIds.add(toolCallId);\n\n // Claim before defer so a second flush in the same tick cannot\n // schedule a duplicate execute/resume for the same interrupt.\n handledIds.add(interruptId);\n if (toolCallId) handledIds.add(toolCallId);\n\n pending.push({ interruptId, headlessInterrupt, toolCallId });\n }\n\n if (pending.length === 0) return;\n\n defer(() => {\n void (async () => {\n const results = await Promise.all(\n pending.map(async ({ interruptId, headlessInterrupt, toolCallId }) => {\n const result = await handleHeadlessToolInterrupt(\n headlessInterrupt,\n tools,\n options.onTool\n );\n return {\n interruptId,\n toolCallId: result.toolCallId ?? toolCallId,\n value: result.value,\n };\n })\n );\n await Promise.resolve(\n options.resumeSubmit(headlessToolsBatchResumeCommand(results))\n );\n })();\n });\n}\n"],"mappings":"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"}
@@ -208,14 +208,6 @@ var StreamController = class {
208
208
  },
209
209
  waitForRootPumpReady: () => this.#rootPumpReady,
210
210
  awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
211
- buildResumeRunInput: (resume) => {
212
- const thread = this.#thread;
213
- if (thread == null) return null;
214
- return require_headless_tools.buildResumeRunInput(resume, thread.interrupts, this.#resolvedInterrupts);
215
- },
216
- markInterruptResolved: (interruptId) => {
217
- this.#resolvedInterrupts.add(interruptId);
218
- },
219
211
  onSubmitStart: () => {
220
212
  this.#hydratedActiveInterruptIds = null;
221
213
  this.#submitGeneration += 1;
@@ -441,9 +433,11 @@ var StreamController = class {
441
433
  if (threadExists) thread.startLifecycleWatcher();
442
434
  }
443
435
  /**
444
- * Submit input or a resume command to the active thread.
436
+ * Submit input to the active thread.
437
+ *
438
+ * To resume a pending interrupt, use {@link respond} instead.
445
439
  *
446
- * @param input - Input payload for a new run; `null`/`undefined` submits no input.
440
+ * @param input - Input payload for a new run.
447
441
  * @param options - Per-run config, metadata, multitask behavior, and callbacks.
448
442
  */
449
443
  async submit(input, options) {
@@ -527,25 +521,152 @@ var StreamController = class {
527
521
  await this.#submitter.clearQueue();
528
522
  }
529
523
  /**
530
- * Respond to a pending protocol interrupt.
524
+ * Respond to a single pending protocol interrupt.
525
+ *
526
+ * When `options.interruptId` is omitted, resolution walks
527
+ * {@link ThreadStream.interrupts `thread.interrupts`} from newest to
528
+ * oldest and picks the first entry whose `interruptId` has not already
529
+ * been resolved by a prior `respond()` call. That entry may be at the
530
+ * root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
531
+ * This is **not** the same as {@link RootSnapshot.interrupts
532
+ * `rootStore.interrupts[0]`} / framework `stream.interrupt`, which only
533
+ * mirrors root-namespace interrupts for UI convenience.
534
+ *
535
+ * Omitting `interruptId` is fine when exactly one interrupt is pending.
536
+ * When several can be active (parallel subagents, fan-out, nested
537
+ * graphs), pass an explicit `interruptId` (and `namespace` for subgraph
538
+ * interrupts) so you resume the interrupt the user acted on.
539
+ *
540
+ * To resume several interrupts pending at the same checkpoint in one
541
+ * command, use {@link respondAll} — sequential single `respond()` calls
542
+ * would not work, since the first resume starts a run, leaving the
543
+ * others with no interrupted run to respond to.
544
+ *
545
+ * The server validates `namespace` against the pending interrupt. Root
546
+ * interrupts use `namespace: []` (the default when `namespace` is
547
+ * omitted). Subgraph interrupts require the exact tuple from
548
+ * `getThread()?.interrupts` — see the example below.
549
+ *
550
+ * @param response - Payload sent back to the interrupted namespace.
551
+ * @param options - Optional target (`interruptId` / `namespace`) and
552
+ * run-level `config` / `metadata` folded into the run that services
553
+ * the resume (model/user config, trigger source, test flags, …).
554
+ * Equivalent to the same fields on {@link StreamSubmitOptions}.
555
+ *
556
+ * @example Single pending interrupt (safe to omit a target)
557
+ * ```ts
558
+ * await controller.respond({ approved: true });
559
+ * ```
560
+ *
561
+ * @example Carry run config / metadata onto the resume
562
+ * ```ts
563
+ * await controller.respond(
564
+ * { approved: true },
565
+ * { config: { configurable: { model: "gpt-4o" } }, metadata: { source: "ui" } },
566
+ * );
567
+ * ```
531
568
  *
532
- * @param response - Payload to send back to the interrupted namespace.
533
- * @param target - Optional explicit interrupt id and namespace; defaults to the latest unresolved interrupt.
569
+ * @example Multiple root interrupts target by id
570
+ * ```tsx
571
+ * for (const intr of stream.interrupts) {
572
+ * await stream.respond(decide(intr.value), { interruptId: intr.id! });
573
+ * }
574
+ * ```
575
+ *
576
+ * @example Subgraph interrupt — read `namespace` from the thread stream
577
+ * ```tsx
578
+ * const thread = stream.getThread();
579
+ * for (const entry of thread?.interrupts ?? []) {
580
+ * await stream.respond(buildResponse(entry.payload), {
581
+ * interruptId: entry.interruptId,
582
+ * namespace: entry.namespace,
583
+ * });
584
+ * }
585
+ * ```
586
+ *
587
+ * Each {@link InterruptPayload} on `thread.interrupts` mirrors an
588
+ * `input.requested` event: `{ interruptId, payload, namespace }`.
589
+ * Nested interrupts may appear here but not on `stream.interrupts`.
534
590
  */
535
- async respond(response, target) {
591
+ async respond(response, options) {
536
592
  if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
537
- const resolved = target != null ? {
538
- interruptId: target.interruptId,
539
- namespace: target.namespace ?? [...ROOT_NAMESPACE]
593
+ const resolved = options?.interruptId != null ? {
594
+ interruptId: options.interruptId,
595
+ namespace: options.namespace ?? [...ROOT_NAMESPACE]
540
596
  } : this.#resolveInterruptForResume();
541
597
  if (resolved == null) throw new Error("No pending interrupt to respond to.");
542
598
  try {
543
599
  await this.#thread.respondInput({
544
600
  namespace: resolved.namespace,
545
601
  interrupt_id: resolved.interruptId,
546
- response
602
+ response,
603
+ config: options?.config,
604
+ metadata: options?.metadata
605
+ });
606
+ this.#markInterruptResolvedInRootStore(resolved.interruptId);
607
+ } catch (error) {
608
+ if (this.#disposed && isAbortLikeError(error)) return;
609
+ throw error;
610
+ }
611
+ }
612
+ /**
613
+ * Resume several pending interrupts at the same checkpoint in a single
614
+ * command.
615
+ *
616
+ * Required when a run pauses on multiple interrupts simultaneously
617
+ * (e.g. parallel tool-authorization prompts): a single
618
+ * `Command({ resume })` carrying every interrupt's payload resumes them
619
+ * together. Sequential {@link respond} calls would fail because the
620
+ * first resume starts a run, leaving the rest with no interrupted run to
621
+ * respond to.
622
+ *
623
+ * `responsesById` maps each pending `interruptId` to the payload sent
624
+ * back to it, so different interrupts can receive different responses
625
+ * (approve one, deny another). To send the *same* payload to several
626
+ * interrupts, build the map with that value for each id, e.g.
627
+ * `Object.fromEntries(ids.map((id) => [id, response]))`.
628
+ *
629
+ * The server resumes by `interruptId`, so namespaces are resolved
630
+ * internally from `getThread()?.interrupts` and need not be supplied.
631
+ *
632
+ * @param responsesById - Map of pending `interruptId` to its response
633
+ * payload. Must contain at least one entry.
634
+ * @param options - Optional run-level `config` / `metadata` folded into
635
+ * the single run that services the batched resume. Equivalent to the
636
+ * same fields on {@link StreamSubmitOptions}.
637
+ *
638
+ * @example Distinct payloads per interrupt
639
+ * ```tsx
640
+ * await stream.respondAll({
641
+ * [interruptA.id]: { approved: true },
642
+ * [interruptB.id]: { approved: false },
643
+ * });
644
+ * ```
645
+ *
646
+ * @example Same payload to every pending interrupt
647
+ * ```tsx
648
+ * await stream.respondAll(
649
+ * Object.fromEntries(stream.interrupts.map((i) => [i.id!, { approved: true }])),
650
+ * );
651
+ * ```
652
+ */
653
+ async respondAll(responsesById, options) {
654
+ if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
655
+ const entries = Object.entries(responsesById);
656
+ if (entries.length === 0) throw new Error("respondAll() requires at least one response.");
657
+ const pending = this.#thread.interrupts;
658
+ const responses = entries.map(([interruptId, response]) => ({
659
+ interrupt_id: interruptId,
660
+ response,
661
+ namespace: pending.find((entry) => entry.interruptId === interruptId)?.namespace ?? [...ROOT_NAMESPACE]
662
+ }));
663
+ try {
664
+ await this.#thread.respondInput({
665
+ responses,
666
+ config: options?.config,
667
+ metadata: options?.metadata
547
668
  });
548
- this.#resolvedInterrupts.add(resolved.interruptId);
669
+ for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
549
670
  } catch (error) {
550
671
  if (this.#disposed && isAbortLikeError(error)) return;
551
672
  throw error;
@@ -1083,6 +1204,22 @@ var StreamController = class {
1083
1204
  });
1084
1205
  }
1085
1206
  /**
1207
+ * Mark an interrupt resolved for replay filtering and mirror the
1208
+ * removal into the root snapshot the framework hooks read.
1209
+ */
1210
+ #markInterruptResolvedInRootStore(interruptId) {
1211
+ this.#resolvedInterrupts.add(interruptId);
1212
+ this.rootStore.setState((s) => {
1213
+ const interrupts = s.interrupts.filter((entry) => entry.id !== interruptId);
1214
+ if (interrupts.length === s.interrupts.length && s.interrupt?.id !== interruptId) return s;
1215
+ return {
1216
+ ...s,
1217
+ interrupts,
1218
+ interrupt: interrupts[0]
1219
+ };
1220
+ });
1221
+ }
1222
+ /**
1086
1223
  * Resolve on the next root-namespace terminal lifecycle event
1087
1224
  * (`completed` / `failed` / `interrupted`) or on abort.
1088
1225
  *