@librechat/agents 3.4.3 → 3.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +27 -14
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/hitl/askUserQuestions.cjs +66 -0
- package/dist/cjs/hitl/askUserQuestions.cjs.map +1 -0
- package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs +46 -0
- package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs.map +1 -0
- package/dist/cjs/hitl/index.cjs +2 -0
- package/dist/cjs/instrumentation.cjs +3 -3
- package/dist/cjs/langfuse.cjs +3 -3
- package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
- package/dist/cjs/main.cjs +11 -1
- package/dist/cjs/messages/assistantPhase.cjs +59 -0
- package/dist/cjs/messages/assistantPhase.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +1 -0
- package/dist/cjs/prompts/activityLabel.cjs +76 -0
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +200 -10
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/stream.cjs +45 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +81 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/types/hitl.cjs +13 -0
- package/dist/cjs/types/hitl.cjs.map +1 -0
- package/dist/cjs/utils/callbacks.cjs +8 -0
- package/dist/cjs/utils/callbacks.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +27 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/hitl/askUserQuestions.mjs +66 -0
- package/dist/esm/hitl/askUserQuestions.mjs.map +1 -0
- package/dist/esm/hitl/askUserQuestionsInterrupt.mjs +43 -0
- package/dist/esm/hitl/askUserQuestionsInterrupt.mjs.map +1 -0
- package/dist/esm/hitl/index.mjs +2 -0
- package/dist/esm/instrumentation.mjs +3 -3
- package/dist/esm/langfuse.mjs +3 -3
- package/dist/esm/langfuseRuntimeScope.mjs +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
- package/dist/esm/main.mjs +5 -2
- package/dist/esm/messages/assistantPhase.mjs +57 -0
- package/dist/esm/messages/assistantPhase.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +1 -0
- package/dist/esm/prompts/activityLabel.mjs +74 -1
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +202 -12
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/stream.mjs +45 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +81 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/types/hitl.mjs +13 -0
- package/dist/esm/types/hitl.mjs.map +1 -0
- package/dist/esm/utils/callbacks.mjs +8 -1
- package/dist/esm/utils/callbacks.mjs.map +1 -1
- package/dist/types/hitl/askUserQuestions.d.ts +24 -0
- package/dist/types/hitl/askUserQuestionsInterrupt.d.ts +11 -0
- package/dist/types/hitl/index.d.ts +2 -0
- package/dist/types/messages/assistantPhase.d.ts +22 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/prompts/activityLabel.d.ts +21 -1
- package/dist/types/run.d.ts +15 -2
- package/dist/types/types/activityLabel.d.ts +63 -0
- package/dist/types/types/assistantPhase.d.ts +6 -0
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/hitl.d.ts +31 -2
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/stream.d.ts +11 -0
- package/dist/types/utils/callbacks.d.ts +1 -0
- package/package.json +2 -1
- package/src/graphs/Graph.ts +33 -9
- package/src/graphs/__tests__/Graph.reasoning.test.ts +57 -0
- package/src/hitl/askUserQuestions.ts +126 -0
- package/src/hitl/askUserQuestionsInterrupt.ts +115 -0
- package/src/hitl/index.ts +6 -0
- package/src/messages/assistantPhase.test.ts +75 -0
- package/src/messages/assistantPhase.ts +91 -0
- package/src/messages/index.ts +1 -0
- package/src/prompts/activityLabel.ts +177 -1
- package/src/run.ts +403 -21
- package/src/specs/activity-label-prompt.test.ts +123 -1
- package/src/specs/activity-phase-label.test.ts +306 -0
- package/src/specs/ask-user-questions.live.test.ts +185 -0
- package/src/specs/ask-user-questions.test.ts +293 -0
- package/src/stream.ts +69 -12
- package/src/tools/__tests__/SubagentExecutor.test.ts +436 -0
- package/src/tools/subagent/SubagentExecutor.ts +160 -8
- package/src/types/activityLabel.ts +65 -0
- package/src/types/assistantPhase.ts +6 -0
- package/src/types/graph.ts +8 -0
- package/src/types/hitl.ts +36 -2
- package/src/types/index.ts +1 -0
- package/src/types/stream.ts +9 -0
- package/src/utils/callbacks.ts +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/types/hitl.ts
|
|
2
|
+
/**
|
|
3
|
+
* Type guard narrowing an arbitrary value to an
|
|
4
|
+
* `AskUserQuestionInterruptPayload`. Same `unknown`-tolerant contract
|
|
5
|
+
* as `isToolApprovalInterrupt`.
|
|
6
|
+
*/
|
|
7
|
+
function isAskUserQuestionInterrupt(payload) {
|
|
8
|
+
return typeof payload === "object" && payload !== null && payload.type === "ask_user_question";
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { isAskUserQuestionInterrupt };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=hitl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hitl.mjs","names":[],"sources":["../../../src/types/hitl.ts"],"sourcesContent":["/**\n * First-class human-in-the-loop (HITL) types for `@librechat/agents`.\n * Surfaces the interrupt payload that `ToolNode` raises when a `PreToolUse`\n * hook returns `decision: 'ask'` and HITL is enabled on the run, plus the\n * resume-decision shape the host returns to continue or reject the tool.\n *\n * Mirrors the LangChain HITL middleware shape (action_requests /\n * review_configs) so hosts and clients can share rendering/UI semantics\n * across the langchain ecosystem.\n */\n\n/** Per-tool approval request emitted inside an interrupt payload. */\nexport interface ToolApprovalRequest {\n /** Stable id of the tool call (matches LangGraph `ToolCall.id`). */\n tool_call_id: string;\n /** Tool name being invoked. */\n name: string;\n /**\n * Arguments the tool is about to be invoked with — already resolved by\n * any `{{tool<i>turn<n>}}` references and any `updatedInput` returned\n * by the firing PreToolUse hook.\n */\n arguments: Record<string, unknown>;\n /**\n * Optional reason the hook supplied for asking (e.g., \"destructive\n * filesystem write\"). Hosts can render this verbatim.\n */\n description?: string;\n}\n\n/** Allowed host-side decisions for a `tool_approval` interrupt. */\nexport type ToolApprovalDecisionType =\n | 'approve'\n | 'reject'\n | 'edit'\n | 'respond';\n\n/** Per-action review configuration paired with each action_request. */\nexport interface ToolApprovalReviewConfig {\n /** Tool name (matches the `name` field on the corresponding action_request). */\n action_name: string;\n /**\n * Stable id of the tool call this review_config applies to (matches\n * the `tool_call_id` of the corresponding action_request). Lets a UI\n * map review_configs → action_requests directly when a batch\n * contains the same tool called more than once — by-position\n * mapping breaks down with duplicates.\n */\n tool_call_id: string;\n /** Decisions the host UI is allowed to surface for this action. */\n allowed_decisions: ToolApprovalDecisionType[];\n}\n\n/**\n * Resume value the host returns through `Run.resume(decisions)` after a\n * `tool_approval` interrupt. One entry per action_request, in the same\n * order. Hosts may also return a record keyed by `tool_call_id`; the SDK\n * handles either shape.\n *\n * Variants:\n * - `approve`: run the tool with its original (or hook-rewritten) args.\n * - `reject`: skip the tool, emit a blocked error `ToolMessage` with\n * `reason` surfaced to the model.\n * - `edit`: replace the tool's args with `updatedInput` (re-resolves\n * any `{{tool<i>turn<n>}}` placeholders) and run the tool.\n * - `respond`: skip the tool entirely and emit `responseText` as a\n * successful `ToolMessage`. Mirrors LangChain HITL middleware's\n * `respond` semantic — the human supplies the result the model sees,\n * bypassing tool execution. Useful when the user wants to short-circuit\n * a tool call with a hand-written answer (e.g., \"don't actually run\n * the search, just tell the model 'no relevant results'\").\n *\n * Note on hook semantics: `respond` does NOT fire the per-tool\n * `PostToolUse` hook (no real tool execution happened, so the\n * \"post-tool\" semantic doesn't apply). It DOES appear in the\n * `PostToolBatch` entry array with `status: 'success'` and the\n * user-supplied text as `toolOutput`, so batch-level audit /\n * convention hooks see the full set of outcomes.\n */\nexport type ToolApprovalDecision =\n | { type: 'approve' }\n | { type: 'reject'; reason?: string }\n | { type: 'edit'; updatedInput: Record<string, unknown> }\n | { type: 'respond'; responseText: string };\n\n/** Map form of resume decisions, keyed by tool call id. */\nexport type ToolApprovalDecisionMap = Record<string, ToolApprovalDecision>;\n\n/**\n * Categories of human-in-the-loop interrupts the SDK can raise. Hosts\n * narrow on `HumanInterruptPayload.type` to determine which payload\n * shape they're handling and which resume value to send back through\n * `Run.resume()`.\n *\n * Exported as a discrete type so downstream consumers (notably\n * LibreChat's wire types in `librechat-data-provider`) can mirror\n * the discriminator alongside their own host-side `PendingAction`\n * record without re-declaring the union themselves. Internal SDK\n * code narrows directly on the literal strings via the type guards\n * below; this type alias is primarily an integration-layer contract.\n */\nexport type HumanInterruptType = 'tool_approval' | 'ask_user_question';\n\n/** Identifies an interrupt that originated inside a checkpointed subagent. */\nexport interface SubagentInterruptScope {\n /** Child execution run id used by subagent update and usage events. */\n run_id: string;\n /** Child agent id that owns the interrupted tool call. */\n agent_id: string;\n /** Configured subagent type selected by the parent tool call. */\n subagent_type: string;\n /** Parent `subagent` tool call that launched this child. */\n parent_tool_call_id?: string;\n}\n\n/**\n * Structured payload the SDK passes to `interrupt()` when one or more\n * pending tool calls require host approval. All `ask`-decision tool calls\n * from a single ToolNode batch are bundled into one interrupt so the host\n * can render and resolve them together.\n *\n * Resume value: `ToolApprovalDecision[]` (in `action_requests` order) or\n * `ToolApprovalDecisionMap` (keyed by `tool_call_id`).\n */\nexport interface ToolApprovalInterruptPayload {\n type: 'tool_approval';\n action_requests: ToolApprovalRequest[];\n review_configs: ToolApprovalReviewConfig[];\n /** Hook-registry session whose policy raised this interrupt. */\n hook_session_id?: string;\n /** Present when the approval request was bridged from a child graph. */\n subagent?: SubagentInterruptScope;\n}\n\n/**\n * Pre-defined option the user can pick when answering an\n * `ask_user_question` interrupt. The selected option's `value` becomes\n * the resume value's `answer` field.\n */\nexport interface AskUserQuestionOption {\n /** Human-readable label rendered in the host UI. */\n label: string;\n /** Value returned via `AskUserQuestionResolution.answer` if picked. */\n value: string;\n}\n\n/** Question request emitted inside an `ask_user_question` interrupt. */\nexport interface AskUserQuestionRequest {\n /** The question to ask the human. */\n question: string;\n /** Optional context / description rendered alongside the question. */\n description?: string;\n /**\n * Optional pre-defined response options. When present, hosts can render\n * a picker; the user may still type a free-form answer when the host\n * UI allows it. Omit to require a free-form answer.\n */\n options?: AskUserQuestionOption[];\n /**\n * When `true`, the host UI may let the user pick several options; the\n * resulting `AskUserQuestionResolution.answer` is the selected option\n * values joined by `\", \"`. When omitted or `false`, hosts render a\n * single-select picker. Only meaningful alongside `options`.\n */\n multiSelect?: boolean;\n}\n\n/** One independently answerable question in a batched question request. */\nexport interface AskUserQuestionBatchItem extends AskUserQuestionRequest {\n /** Batch-unique identifier (`[A-Za-z][A-Za-z0-9_-]{0,63}`). */\n id: string;\n /** Optional short heading rendered above the question. */\n header?: string;\n}\n\n/** Input shape for one tool call that asks one to four questions together. */\nexport interface AskUserQuestionsRequest {\n questions: AskUserQuestionBatchItem[];\n}\n\n/**\n * Structured payload the SDK passes to `interrupt()` when an agent (or\n * a custom node) needs to ask the user a clarifying question. Mirrors\n * Claude Code's `AskUserQuestion` semantic. Resume value is\n * `AskUserQuestionResolution` for a single question, or\n * `AskUserQuestionsResolution` when `questions` is present.\n */\nexport interface AskUserQuestionInterruptPayload {\n type: 'ask_user_question';\n /**\n * Single-question request, or the first question as a compatibility\n * fallback when `questions` contains a batch. This lets existing hosts show\n * a useful preview during a staged rollout, but they must support `questions`\n * and `AskUserQuestionsResolution` before enabling a batched tool schema.\n */\n question: AskUserQuestionRequest;\n /** One to four questions collected by one `ask_user_question` tool call. */\n questions?: AskUserQuestionsRequest['questions'];\n /**\n * The `tool_call_id` of the ask-tool call that raised this interrupt,\n * when the tool body supplied it (see `askUserQuestion`'s `options`).\n * Lets hosts attribute the question — and later the answer — to the\n * exact tool-call content part instead of guessing by position, which\n * mislabels cards when a model emits several ask calls in one turn.\n */\n tool_call_id?: string;\n}\n\n/** Batch-specialized ask payload for hosts that render several questions. */\nexport interface AskUserQuestionsInterruptPayload\n extends AskUserQuestionInterruptPayload {\n questions: AskUserQuestionsRequest['questions'];\n}\n\n/**\n * Discriminated union of every interrupt payload the SDK raises. New\n * variants can be added without breaking existing handlers as long as\n * those handlers check `payload.type` before reading variant-specific\n * fields. Use the `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt`\n * type guards for ergonomic narrowing.\n */\nexport type HumanInterruptPayload =\n | ToolApprovalInterruptPayload\n | AskUserQuestionInterruptPayload;\n\n/** Resume value the host returns for an `ask_user_question` interrupt. */\nexport interface AskUserQuestionResolution {\n /**\n * The human's answer. Free-form text, or — when `options` were\n * provided — one of the option `value`s (or, when the request set\n * `multiSelect`, several option `value`s joined by `\", \"`). Hosts may\n * also send any structured object their custom UI defines; see the\n * host docs for what your downstream consumer expects.\n */\n answer: string;\n}\n\n/** Resume value for a batched `ask_user_question` interrupt. */\nexport interface AskUserQuestionsResolution {\n /** Human answers keyed by each `AskUserQuestionBatchItem.id`. */\n answers: Record<string, string>;\n}\n\n/**\n * Type guard narrowing an arbitrary value to a `ToolApprovalInterruptPayload`.\n * Accepts `unknown` (not just `HumanInterruptPayload`) because hosts can\n * raise custom interrupt payloads from custom nodes — `getInterrupt()`\n * surfaces them as-is, and downstream code must validate the shape at\n * runtime before reading variant-specific fields.\n */\nexport function isToolApprovalInterrupt(\n payload: unknown\n): payload is ToolApprovalInterruptPayload {\n return (\n typeof payload === 'object' &&\n payload !== null &&\n (payload as { type?: unknown }).type === 'tool_approval'\n );\n}\n\n/**\n * Type guard narrowing an arbitrary value to an\n * `AskUserQuestionInterruptPayload`. Same `unknown`-tolerant contract\n * as `isToolApprovalInterrupt`.\n */\nexport function isAskUserQuestionInterrupt(\n payload: unknown\n): payload is AskUserQuestionInterruptPayload {\n return (\n typeof payload === 'object' &&\n payload !== null &&\n (payload as { type?: unknown }).type === 'ask_user_question'\n );\n}\n\n/**\n * Run-level configuration controlling HITL semantics. **HITL is OFF by\n * default** for now — the SDK ships the interrupt machinery, but the\n * default stays opt-in until host UIs (notably LibreChat) ship the\n * approval-rendering affordances needed to surface interrupts to end\n * users. Without that UI, an interrupt with no resolver looks like a\n * hung tool-call card. Hosts opt in explicitly with\n * `{ enabled: true }`. The intent is to flip this default to ON in a\n * future minor once the consumer ecosystem is ready to render\n * interrupts end-to-end.\n *\n * When enabled (`{ enabled: true }`):\n *\n * - `PreToolUse` hooks returning `decision: 'ask'` raise a real\n * LangGraph `interrupt()` instead of being treated as a synchronous\n * deny.\n * - `Run.create` installs a `MemorySaver` checkpointer fallback on the\n * run's compile options if the host did not provide one, since\n * LangGraph requires a checkpointer to suspend and resume.\n *\n * When disabled (the default — omitted, or `{ enabled: false }`):\n * `ask` decisions are fail-closed (blocked with an error\n * `ToolMessage`) and no checkpointer is implicitly attached. This\n * matches the pre-HITL behavior so existing hosts upgrading the SDK\n * see no change until they're ready to wire the resume UI.\n *\n * ## Scope: every tool the ToolNode runs\n *\n * The interrupt path is wired into both `dispatchToolEvents` (the\n * event-driven path) and `runDirectToolWithLifecycleHooks` (the\n * direct path used by `directToolNames` entries — graph-managed\n * handoff/subagent tools and every in-process `graphTool` instance).\n * `PreToolUse` hooks fire for every tool the ToolNode invokes, and\n * HITL approval gates every tool whose hook returns `'ask'` —\n * regardless of whether the tool is dispatched as an event or\n * invoked in-process. This convergence happened in two follow-up\n * commits to the original HITL surface (see `Graph.ts` —\n * `hookRegistry`/`humanInTheLoop` are passed in both\n * event-driven and legacy branches; and `ToolNode.runDirectToolWithLifecycleHooks`\n * — direct-path tools build their own single-tool `tool_approval`\n * payload and raise `interrupt()` the same way the event path does).\n *\n * Practical implications:\n * - Every host gets the full HITL surface across every tool the\n * model calls — event-dispatched, direct, mixed.\n * - `createToolPolicyHook` and `createWorkspacePolicyHook` apply\n * uniformly. A hook can be registered without knowing or caring\n * which path the tool will take.\n * - Direct tools that the host opted into via `directToolNames` no\n * longer bypass policy. If you need a tool to skip the hook\n * surface entirely, omit it from any registered matcher.\n *\n * ## Resume re-execution: every tool in the interrupted batch\n *\n * LangGraph rolls back to the start of the interrupted node on\n * resume. That means **every tool in the same batch as the one that\n * interrupted re-runs from the top on the resume pass**, not just\n * the interrupting tool, and not just the direct half (this used to\n * be framed as a direct-tool-specific concern; it is not — it\n * applies to event-dispatched siblings too). Practical contract:\n *\n * - The body of the interrupting tool itself runs **once** total\n * (the first pass interrupted *before* the body, the resume pass\n * ran the body after the host's decision was applied).\n * - The body of any sibling tool that already executed in the\n * same batch before the interrupting tool runs **twice** — once\n * on the first pass, once on the resume pass.\n * - `PreToolUse` hooks fire **once per pass per tool**. A hook\n * that always returns `'ask'` will loop forever on resume; real\n * hooks should be deterministic w.r.t. inputs and use the\n * `'ask' → host approves → resume → hook returns 'allow'`\n * pattern, where the second-pass `allow` reflects the host\n * having recorded the approval (e.g., a session-scoped approved-\n * paths set keyed by `runId`).\n *\n * Consequence: any tool with side effects MUST be idempotent if\n * there's any chance another tool in the same batch could trigger\n * an interrupt. This applies equally to direct tools (handoffs,\n * subagents) and to event tools.\n *\n * ### Guarding non-idempotent siblings via `interruptingToolNames`\n *\n * The \"must be idempotent\" rule above is unavoidable in the general\n * case, but the SDK can protect siblings against the one interrupt\n * shape it can predict: a tool whose *body* raises `interrupt()`\n * mid-execution — the `ask_user_question` shape, where the tool\n * suspends the run to collect a human answer. Declare such tools in\n * `RunConfig.interruptingToolNames`\n * ({@link ToolNodeOptions.interruptingToolNames}) and the ToolNode\n * schedules them, within each batch, **ahead of** their\n * non-interrupting direct siblings. When one interrupts, the batch\n * unwinds before any declared-safe sibling has run, so the sibling\n * executes exactly once (on resume) instead of twice. Empirically:\n *\n * - A **direct** sibling sharing the interrupter's in-process\n * `Promise.all` is the only shape that double-executes; declaring\n * the interrupter closes it.\n * - An **event-dispatched** sibling is already safe without any\n * config: the ToolNode awaits the whole direct group (where the\n * body interrupt unwinds) before it dispatches event tools, so a\n * dispatched sibling never runs on the first pass.\n *\n * This is a *scheduling* guard, not full resume idempotency: it only\n * covers tools that interrupt from their own body and only protects\n * siblings scheduled after them. It does not retroactively make a\n * `PreToolUse` `'ask'` gate on tool B stop tool A (already executed)\n * from re-running — unless B is itself declared interrupting, so it\n * runs first. Tools with side effects should still be written\n * idempotent as defense in depth.\n *\n * The guard only REORDERS the direct group — declaring a name does not\n * force it onto the direct path. The interrupting tool must already be a\n * real in-process graphTool (the only kind whose body can reach\n * `interrupt()`). A name that resolves to a schema-only event stub (an\n * inherited `toolDefinition` with no executable instance, e.g. in a\n * self-spawned child that scrubs `graphTools`) stays event-dispatched\n * and the ordering is a no-op for it.\n *\n * ## Note on idempotency\n *\n * Same root cause as the resume re-execution above: LangGraph\n * re-runs the interrupted node from the start on resume, which\n * fires `PreToolUse` hooks again. Hooks that produce side effects\n * (logging, external calls) will see at least two invocations per\n * paused turn — exactly two for the interrupting tool, possibly\n * more across siblings.\n */\nexport interface HumanInTheLoopConfig {\n /**\n * Master switch. Defaults to `false` — omit the field (or pass\n * `false`) to keep HITL off, or set `true` to opt in once the host\n * UI is ready to render and resolve `tool_approval` interrupts.\n */\n enabled?: boolean;\n}\n\n/**\n * Snapshot of an in-flight interrupt surfaced from `Run.processStream`\n * via `run.getInterrupt()`. Hosts persist this alongside their job\n * record so they can later call `Run.resume(decisions)` against a Run\n * compiled with the same `thread_id` / checkpointer.\n *\n * The `payload` type defaults to `HumanInterruptPayload` (the SDK's\n * built-in `tool_approval` / `ask_user_question` discriminated union)\n * for ergonomic narrowing in the common case. Hosts that raise custom\n * interrupt payloads from custom graph nodes can pass the type\n * parameter (`run.getInterrupt<MyCustom>()` or\n * `RunInterruptResult<MyCustom>`) — the SDK does not validate the\n * runtime shape, it just transports whatever the node passed to\n * `interrupt()`. Use the `isToolApprovalInterrupt` /\n * `isAskUserQuestionInterrupt` guards (which accept `unknown`) when\n * the source of the interrupt isn't statically known.\n */\nexport interface RunInterruptResult<TPayload = HumanInterruptPayload> {\n /** Stable id of the LangGraph interrupt (from `Interrupt.id`). */\n interruptId: string;\n /** `thread_id` the run was bound to — required to resume. */\n threadId?: string;\n /** LangGraph checkpoint id that contains the paused interrupt task. */\n checkpointId?: string;\n /** LangGraph checkpoint namespace for the paused interrupt task. */\n checkpointNs?: string;\n /** Structured payload describing what needs human input. */\n payload: TPayload;\n}\n"],"mappings":";;;;;;AAyQA,SAAgB,2BACd,SAC4C;CAC5C,OACE,OAAO,YAAY,YACnB,YAAY,QACX,QAA+B,SAAS;AAE7C"}
|
|
@@ -10,7 +10,14 @@ function findCallback(callbacks, predicate) {
|
|
|
10
10
|
if (callbacks == null) return;
|
|
11
11
|
return (Array.isArray(callbacks) ? callbacks : callbacks.handlers).find(predicate);
|
|
12
12
|
}
|
|
13
|
+
function filterCallbacks(callbacks, predicate) {
|
|
14
|
+
if (callbacks == null) return [];
|
|
15
|
+
if (Array.isArray(callbacks)) return callbacks.filter(predicate);
|
|
16
|
+
const filtered = callbacks.copy();
|
|
17
|
+
for (const handler of [...filtered.handlers]) if (!predicate(handler)) filtered.removeHandler(handler);
|
|
18
|
+
return filtered;
|
|
19
|
+
}
|
|
13
20
|
//#endregion
|
|
14
|
-
export { appendCallbacks, findCallback };
|
|
21
|
+
export { appendCallbacks, filterCallbacks, findCallback };
|
|
15
22
|
|
|
16
23
|
//# sourceMappingURL=callbacks.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callbacks.mjs","names":[],"sources":["../../../src/utils/callbacks.ts"],"sourcesContent":["import { ensureHandler } from '@langchain/core/callbacks/manager';\nimport type {\n BaseCallbackHandler,\n CallbackHandlerMethods,\n} from '@langchain/core/callbacks/base';\nimport type { Callbacks } from '@langchain/core/callbacks/manager';\n\nexport type CallbackEntry = BaseCallbackHandler | CallbackHandlerMethods;\n\nexport function appendCallbacks(\n callbacks: Callbacks | undefined,\n additions: readonly CallbackEntry[]\n): Callbacks {\n if (additions.length === 0) {\n return callbacks ?? [];\n }\n\n if (callbacks == null) {\n return [...additions];\n }\n\n if (Array.isArray(callbacks)) {\n return callbacks.concat(additions);\n }\n\n return callbacks.copy(additions.map(ensureHandler));\n}\n\nexport function findCallback(\n callbacks: Callbacks | undefined,\n predicate: (callback: CallbackEntry) => boolean\n): CallbackEntry | undefined {\n if (callbacks == null) {\n return undefined;\n }\n\n const handlers = Array.isArray(callbacks) ? callbacks : callbacks.handlers;\n return handlers.find(predicate);\n}\n"],"mappings":";;AASA,SAAgB,gBACd,WACA,WACW;CACX,IAAI,UAAU,WAAW,GACvB,OAAO,aAAa,CAAC;CAGvB,IAAI,aAAa,MACf,OAAO,CAAC,GAAG,SAAS;CAGtB,IAAI,MAAM,QAAQ,SAAS,GACzB,OAAO,UAAU,OAAO,SAAS;CAGnC,OAAO,UAAU,KAAK,UAAU,IAAI,aAAa,CAAC;AACpD;AAEA,SAAgB,aACd,WACA,WAC2B;CAC3B,IAAI,aAAa,MACf;CAIF,QADiB,MAAM,QAAQ,SAAS,IAAI,YAAY,UAAU,SAAA,CAClD,KAAK,SAAS;AAChC"}
|
|
1
|
+
{"version":3,"file":"callbacks.mjs","names":[],"sources":["../../../src/utils/callbacks.ts"],"sourcesContent":["import { ensureHandler } from '@langchain/core/callbacks/manager';\nimport type {\n BaseCallbackHandler,\n CallbackHandlerMethods,\n} from '@langchain/core/callbacks/base';\nimport type { Callbacks } from '@langchain/core/callbacks/manager';\n\nexport type CallbackEntry = BaseCallbackHandler | CallbackHandlerMethods;\n\nexport function appendCallbacks(\n callbacks: Callbacks | undefined,\n additions: readonly CallbackEntry[]\n): Callbacks {\n if (additions.length === 0) {\n return callbacks ?? [];\n }\n\n if (callbacks == null) {\n return [...additions];\n }\n\n if (Array.isArray(callbacks)) {\n return callbacks.concat(additions);\n }\n\n return callbacks.copy(additions.map(ensureHandler));\n}\n\nexport function findCallback(\n callbacks: Callbacks | undefined,\n predicate: (callback: CallbackEntry) => boolean\n): CallbackEntry | undefined {\n if (callbacks == null) {\n return undefined;\n }\n\n const handlers = Array.isArray(callbacks) ? callbacks : callbacks.handlers;\n return handlers.find(predicate);\n}\n\nexport function filterCallbacks(\n callbacks: Callbacks | undefined,\n predicate: (callback: CallbackEntry) => boolean\n): Callbacks {\n if (callbacks == null) {\n return [];\n }\n\n if (Array.isArray(callbacks)) {\n return callbacks.filter(predicate);\n }\n\n const filtered = callbacks.copy();\n for (const handler of [...filtered.handlers]) {\n if (!predicate(handler)) {\n filtered.removeHandler(handler);\n }\n }\n return filtered;\n}\n"],"mappings":";;AASA,SAAgB,gBACd,WACA,WACW;CACX,IAAI,UAAU,WAAW,GACvB,OAAO,aAAa,CAAC;CAGvB,IAAI,aAAa,MACf,OAAO,CAAC,GAAG,SAAS;CAGtB,IAAI,MAAM,QAAQ,SAAS,GACzB,OAAO,UAAU,OAAO,SAAS;CAGnC,OAAO,UAAU,KAAK,UAAU,IAAI,aAAa,CAAC;AACpD;AAEA,SAAgB,aACd,WACA,WAC2B;CAC3B,IAAI,aAAa,MACf;CAIF,QADiB,MAAM,QAAQ,SAAS,IAAI,YAAY,UAAU,SAAA,CAClD,KAAK,SAAS;AAChC;AAEA,SAAgB,gBACd,WACA,WACW;CACX,IAAI,aAAa,MACf,OAAO,CAAC;CAGV,IAAI,MAAM,QAAQ,SAAS,GACzB,OAAO,UAAU,OAAO,SAAS;CAGnC,MAAM,WAAW,UAAU,KAAK;CAChC,KAAK,MAAM,WAAW,CAAC,GAAG,SAAS,QAAQ,GACzC,IAAI,CAAC,UAAU,OAAO,GACpB,SAAS,cAAc,OAAO;CAGlC,OAAO;AACT"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AskUserQuestionsRequest, AskUserQuestionsResolution } from '@/types/hitl';
|
|
2
|
+
/**
|
|
3
|
+
* Suspend once to collect answers to several related questions. The first
|
|
4
|
+
* question is also included in the legacy `question` field so existing hosts
|
|
5
|
+
* can render a useful fallback during a staged rollout.
|
|
6
|
+
*
|
|
7
|
+
* Question ids must be non-empty and unique within the batch. The helper
|
|
8
|
+
* accepts at most four questions so hosts can render the interaction as one
|
|
9
|
+
* focused decision surface rather than an unbounded form.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { answers } = askUserQuestions({
|
|
14
|
+
* questions: [
|
|
15
|
+
* { id: 'environment', question: 'Which environment?' },
|
|
16
|
+
* { id: 'region', question: 'Which region?' },
|
|
17
|
+
* ],
|
|
18
|
+
* });
|
|
19
|
+
* return `Deploy to ${answers.environment} in ${answers.region}`;
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function askUserQuestions(request: AskUserQuestionsRequest, options?: {
|
|
23
|
+
toolCallId?: string;
|
|
24
|
+
}): AskUserQuestionsResolution;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AskUserQuestionRequest, AskUserQuestionsInterruptPayload } from '@/types/hitl';
|
|
2
|
+
/** Maximum questions supported by one batched clarification interaction. */
|
|
3
|
+
export declare const MAX_ASK_USER_QUESTIONS = 4;
|
|
4
|
+
/** Safe identifier format for answer-map keys in a batched question. */
|
|
5
|
+
export declare const ASK_USER_QUESTION_ID_PATTERN: RegExp;
|
|
6
|
+
export declare function isAskUserQuestionRequest(value: unknown): value is AskUserQuestionRequest;
|
|
7
|
+
/**
|
|
8
|
+
* Type guard for the batched form of an `ask_user_question` interrupt. Hosts
|
|
9
|
+
* use this to select the multi-question UI and `AskUserQuestionsResolution`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isAskUserQuestionsInterrupt(payload: unknown): payload is AskUserQuestionsInterruptPayload;
|
|
@@ -4,3 +4,5 @@
|
|
|
4
4
|
* `askUserQuestion()`) live here.
|
|
5
5
|
*/
|
|
6
6
|
export { askUserQuestion } from './askUserQuestion';
|
|
7
|
+
export { askUserQuestions } from './askUserQuestions';
|
|
8
|
+
export { ASK_USER_QUESTION_ID_PATTERN, isAskUserQuestionsInterrupt, MAX_ASK_USER_QUESTIONS, } from './askUserQuestionsInterrupt';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AssistantTextPhase } from '@/types/assistantPhase';
|
|
2
|
+
import type { MessageContentComplex } from '@/types/stream';
|
|
3
|
+
import { ContentTypes } from '@/common';
|
|
4
|
+
export type MessageCreationContentMetadata = {
|
|
5
|
+
content_type?: ContentTypes.TEXT | ContentTypes.THINK;
|
|
6
|
+
phase?: AssistantTextPhase;
|
|
7
|
+
};
|
|
8
|
+
/** Reads both provider-native and LangChain standard-content phase fields. */
|
|
9
|
+
export declare function getAssistantTextPhase(contentPart: MessageContentComplex): AssistantTextPhase | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Keeps provider-authored text phases in distinct message-creation steps.
|
|
12
|
+
* Open Responses may return commentary and final-answer blocks in one chunk;
|
|
13
|
+
* collapsing the array into one step would assign the first block's phase to
|
|
14
|
+
* every block and hide the boundary that closes an activity phase.
|
|
15
|
+
*/
|
|
16
|
+
export declare function splitAssistantTextContentByPhase(content: MessageContentComplex[]): MessageContentComplex[][];
|
|
17
|
+
/**
|
|
18
|
+
* Derives additive message-creation metadata before a content delta is
|
|
19
|
+
* dispatched. The fallback covers string-only providers whose semantic lane
|
|
20
|
+
* is tracked by the stream handler rather than represented on a block.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getMessageCreationContentMetadata(content: string | MessageContentComplex[] | undefined, fallbackContentType?: ContentTypes.TEXT | ContentTypes.THINK): MessageCreationContentMetadata;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { ActivityLabelToolEntry, ActivityPhaseEntry } from '@/types/activityLabel';
|
|
1
2
|
import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
|
|
2
|
-
import type { ActivityLabelToolEntry } from '@/types/activityLabel';
|
|
3
3
|
/**
|
|
4
4
|
* Default system prompt for fast-model activity labeling.
|
|
5
5
|
*
|
|
@@ -9,8 +9,13 @@ import type { ActivityLabelToolEntry } from '@/types/activityLabel';
|
|
|
9
9
|
* "Synthesized version data and curated comparative framework").
|
|
10
10
|
*/
|
|
11
11
|
export declare const ACTIVITY_LABEL_PROMPT = "Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.\n\nRules:\n- 5 to 9 words, past-tense verb first\n- Name the most distinctive subject (file, API, topic); drop articles and filler\n- Describe outcomes, not mechanics; if something failed, say so plainly\n- Output only the label \u2014 no quotes, no punctuation at the end, no preamble\n\nExamples:\n- Searched Node.js release notes and changelogs\n- Compared runtime versions across official sources\n- Fixed failing auth middleware tests\n- Read project config and dependency manifests\n- Attempted database migration, hit permission errors";
|
|
12
|
+
/** Default system prompt for a run-wide parent activity phase. */
|
|
13
|
+
export declare const ACTIVITY_PHASE_LABEL_PROMPT = "Summarize what this phase of an agent run accomplished. The result appears as the header of one collapsed parent group containing several activities.\n\nRules:\n- One line, 8 to 18 words, past tense\n- Lead with the concrete outcome and name the most distinctive subject\n- Synthesize the phase; do not enumerate, count, or restate individual activities\n- Describe failures plainly when they are the phase's material outcome\n- Never mention tool names, calls, arguments, reasoning, commentary, or activity counts\n- Output only the summary \u2014 no quotes, no trailing punctuation, no preamble\n\nExamples:\n- Reconciled authentication behavior and fixed the failing session refresh path\n- Compared deployment options and documented the safest production rollout\n- Investigated database latency but could not confirm the suspected index regression\n\nBad examples:\n- Used three tools to inspect files and run tests\n- Searched code, read configuration, and updated middleware";
|
|
14
|
+
/** Hard ceiling across every activity/context section in one phase request. */
|
|
15
|
+
export declare const ACTIVITY_PHASE_PROMPT_MAX_LENGTH = 12000;
|
|
12
16
|
/** Truncates a serialized value for the label prompt. */
|
|
13
17
|
export declare function truncateForLabel(value: string, maxLength: number): string;
|
|
18
|
+
export declare const ACTIVITY_PHASE_LABEL_MAX_LENGTH = 160;
|
|
14
19
|
export type BuildActivityLabelPromptParams = {
|
|
15
20
|
entries: ActivityLabelToolEntry[];
|
|
16
21
|
charLimit: number;
|
|
@@ -36,3 +41,18 @@ export type BuildActivityLabelPromptParams = {
|
|
|
36
41
|
* for direct testing of redaction and truncation behavior.
|
|
37
42
|
*/
|
|
38
43
|
export declare function buildActivityLabelPrompt({ entries, charLimit, thinkingExcerpts, lastAssistantText, previousLabels, redaction, }: BuildActivityLabelPromptParams): string;
|
|
44
|
+
export type BuildActivityPhaseLabelPromptParams = {
|
|
45
|
+
activities: ActivityPhaseEntry[];
|
|
46
|
+
totalActivityCount?: number;
|
|
47
|
+
charLimit: number;
|
|
48
|
+
assistantContext?: string[];
|
|
49
|
+
redaction?: ResolvedLangfuseToolOutputTracingConfig;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Builds bounded, redaction-aware evidence for a parent activity phase.
|
|
53
|
+
* Committed child labels are preferred; raw tool/reasoning evidence is only
|
|
54
|
+
* used when no child label exists.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildActivityPhaseLabelPrompt({ activities, totalActivityCount, charLimit, assistantContext, redaction, }: BuildActivityPhaseLabelPromptParams): string;
|
|
57
|
+
/** Normalizes a model result for safe single-row persistence and display. */
|
|
58
|
+
export declare function normalizeActivityPhaseLabel(label: string): string;
|
package/dist/types/run.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseMessage } from '@langchain/core/messages';
|
|
2
1
|
import { Command } from '@langchain/langgraph';
|
|
2
|
+
import { BaseMessage } from '@langchain/core/messages';
|
|
3
3
|
import type { MessageContentComplex } from '@langchain/core/messages';
|
|
4
4
|
import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
|
|
5
5
|
import type { StandardGraph } from '@/graphs/Graph';
|
|
@@ -44,6 +44,10 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
44
44
|
private _interrupt;
|
|
45
45
|
/** Per-run sequence for batch-unique activity-label trace-seed fallbacks. */
|
|
46
46
|
private activityLabelSeq;
|
|
47
|
+
/** Per-run sequence for parent activity-phase trace and invocation ids. */
|
|
48
|
+
private activityPhaseLabelSeq;
|
|
49
|
+
/** Latest user turn used to keep detached phase roots conversation-shaped. */
|
|
50
|
+
private activityPhaseTraceInput?;
|
|
47
51
|
/** Distinguishes sibling forks started from the same explicit checkpoint. */
|
|
48
52
|
private checkpointForkSeq;
|
|
49
53
|
private _haltedReason;
|
|
@@ -238,7 +242,16 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
238
242
|
* comes from `lastAssistantText`, content from reasoning excerpts and
|
|
239
243
|
* tool entries.
|
|
240
244
|
*/
|
|
241
|
-
generateActivityLabel({ provider, clientOptions, entries, thinkingExcerpts, lastAssistantText, previousLabels, prompt, charLimit, chainOptions, traceSeed, agentId, }: t.RunActivityLabelOptions): Promise<{
|
|
245
|
+
generateActivityLabel({ provider, clientOptions, entries, thinkingExcerpts, lastAssistantText, lastAssistantPhase, previousLabels, prompt, charLimit, chainOptions, traceSeed, agentId, }: t.RunActivityLabelOptions): Promise<{
|
|
246
|
+
label?: string;
|
|
247
|
+
}>;
|
|
248
|
+
/**
|
|
249
|
+
* Generates one parent summary for two or more logical activities. The
|
|
250
|
+
* summary model is traced as a dedicated activity-phase agent root in the
|
|
251
|
+
* conversation session, with the model callback recorded as its generation
|
|
252
|
+
* child. No session id means no phase trace, avoiding orphan observations.
|
|
253
|
+
*/
|
|
254
|
+
generateActivityPhaseLabel({ provider, clientOptions, activities, totalActivityCount, assistantContext, closingTextPhase, prompt, charLimit, chainOptions, traceSeed, sourceRunId, sourceTraceId, responseId, phaseIndex, status, agentIds, }: t.RunActivityPhaseLabelOptions): Promise<{
|
|
242
255
|
label?: string;
|
|
243
256
|
}>;
|
|
244
257
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
2
|
+
import type { AssistantTextPhase } from '@/types/assistantPhase';
|
|
2
3
|
import type { ClientOptions } from '@/types/llm';
|
|
3
4
|
import type { Providers } from '@/common';
|
|
4
5
|
/** One tool call's contribution to the label payload (host-assembled). */
|
|
@@ -36,6 +37,11 @@ export type RunActivityLabelOptions = {
|
|
|
36
37
|
thinkingExcerpts?: string[];
|
|
37
38
|
/** Assistant's last text before the block (~200 chars), as intent context. */
|
|
38
39
|
lastAssistantText?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Provider-authored semantic phase for `lastAssistantText`. Hosts can use
|
|
42
|
+
* this to pass commentary as intent while excluding final-answer text.
|
|
43
|
+
*/
|
|
44
|
+
lastAssistantPhase?: AssistantTextPhase;
|
|
39
45
|
/**
|
|
40
46
|
* Headers already committed for earlier batches in this run (run order,
|
|
41
47
|
* most recent last). Continuity context: the prompt shows them so the new
|
|
@@ -59,3 +65,60 @@ export type RunActivityLabelOptions = {
|
|
|
59
65
|
*/
|
|
60
66
|
traceSeed?: string;
|
|
61
67
|
};
|
|
68
|
+
/** One logical activity contributing to a run-wide phase summary. */
|
|
69
|
+
export type ActivityPhaseEntry = {
|
|
70
|
+
/** Already committed child activity label, preferred over raw evidence. */
|
|
71
|
+
label?: string;
|
|
72
|
+
/** Raw fallback for a child label that was disabled, pending, or dropped. */
|
|
73
|
+
entries?: ActivityLabelToolEntry[];
|
|
74
|
+
/** Standalone or tool-attached reasoning excerpts for this activity. */
|
|
75
|
+
thinkingExcerpts?: string[];
|
|
76
|
+
/** Agent lane that produced the activity. */
|
|
77
|
+
agentId?: string;
|
|
78
|
+
/** Failed activities remain useful evidence and still count. */
|
|
79
|
+
status?: 'success' | 'partial' | 'error';
|
|
80
|
+
};
|
|
81
|
+
/** Options for `Run.generateActivityPhaseLabel`. */
|
|
82
|
+
export type RunActivityPhaseLabelOptions = {
|
|
83
|
+
provider: Providers;
|
|
84
|
+
clientOptions?: ClientOptions;
|
|
85
|
+
/**
|
|
86
|
+
* Logical activities in run order. The SDK requires at least two so hosts
|
|
87
|
+
* cannot accidentally spend a model call summarizing a single activity.
|
|
88
|
+
*/
|
|
89
|
+
activities: ActivityPhaseEntry[];
|
|
90
|
+
/** Full count when the host retained only bounded prompt evidence. */
|
|
91
|
+
totalActivityCount?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Bounded assistant commentary emitted inside the phase. Human messages
|
|
94
|
+
* must not be supplied through this low-scrutiny summarization path.
|
|
95
|
+
*/
|
|
96
|
+
assistantContext?: string[];
|
|
97
|
+
/** Semantic phase of the text boundary that closed the activity phase. */
|
|
98
|
+
closingTextPhase?: AssistantTextPhase;
|
|
99
|
+
/** Override for the dedicated phase-label system prompt. */
|
|
100
|
+
prompt?: string;
|
|
101
|
+
/** Per-evidence serialization cap for the prompt. Default 600. */
|
|
102
|
+
charLimit?: number;
|
|
103
|
+
/** LangChain runnable config carrier (signal, callbacks, thread/user ids). */
|
|
104
|
+
chainOptions?: Partial<RunnableConfig> & {
|
|
105
|
+
configurable?: Record<string, unknown>;
|
|
106
|
+
};
|
|
107
|
+
/** Deterministic seed for this phase label trace. */
|
|
108
|
+
traceSeed?: string;
|
|
109
|
+
/** Stable source run identifier recorded on the phase observation. */
|
|
110
|
+
sourceRunId?: string;
|
|
111
|
+
/** Source Langfuse trace id for linking this detached summary trace. */
|
|
112
|
+
sourceTraceId?: string;
|
|
113
|
+
/** Host response/message identifier recorded on the phase observation. */
|
|
114
|
+
responseId?: string;
|
|
115
|
+
/** Zero-based index of the phase within the source run. */
|
|
116
|
+
phaseIndex?: number;
|
|
117
|
+
/** Completion state recorded on the phase observation. */
|
|
118
|
+
status?: 'completed' | 'partial' | 'failed';
|
|
119
|
+
/**
|
|
120
|
+
* Contributing agents. Their Langfuse redaction policies are combined into
|
|
121
|
+
* the strictest union before any phase evidence enters the model or trace.
|
|
122
|
+
*/
|
|
123
|
+
agentIds?: string[];
|
|
124
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic phase attached to assistant text by Open Responses-compatible
|
|
3
|
+
* providers. Commentary is intermediate agent narration; final_answer marks
|
|
4
|
+
* the user-facing answer that closes the preceding activity phase.
|
|
5
|
+
*/
|
|
6
|
+
export type AssistantTextPhase = 'commentary' | 'final_answer';
|
|
@@ -5,7 +5,7 @@ import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
|
|
|
5
5
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
6
6
|
import type { GoogleAIToolType } from '@langchain/google-common';
|
|
7
7
|
import type { SummarizationNodeInput, SummarizeCompleteEvent, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
|
|
8
|
-
import type { ToolMap, ToolEndEvent, GenericTool, LCTool, ToolExecuteBatchRequest } from '@/types/tools';
|
|
8
|
+
import type { ToolMap, ToolSessionMap, ToolEndEvent, GenericTool, LCTool, ToolExecuteBatchRequest } from '@/types/tools';
|
|
9
9
|
import type { RunStep, RunStepDeltaEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
|
|
10
10
|
import type { TokenCounter, StreamLimits, StreamPreemption, TokenBudgetBreakdown } from '@/types/run';
|
|
11
11
|
import type { Providers, Callback, GraphNodeKeys } from '@/common';
|
|
@@ -680,6 +680,13 @@ export interface AgentInputs {
|
|
|
680
680
|
subagentConfigs?: SubagentConfigEntry[];
|
|
681
681
|
/** Maximum subagent nesting depth. Default 1 means top-level agents can spawn subagents but subagents cannot nest further. */
|
|
682
682
|
maxSubagentDepth?: number;
|
|
683
|
+
/**
|
|
684
|
+
* Initial tool-session state owned by this agent when it runs as a
|
|
685
|
+
* subagent. The executor copies these sessions into the isolated child
|
|
686
|
+
* graph before its first invocation. Top-level runs continue to use
|
|
687
|
+
* `RunConfig.initialSessions`.
|
|
688
|
+
*/
|
|
689
|
+
initialSessions?: ToolSessionMap;
|
|
683
690
|
/**
|
|
684
691
|
* Host-supplied tool instances that must execute IN-PROCESS inside the graph's
|
|
685
692
|
* ToolNode even when the run is event-driven (`toolDefinitions` non-empty). Each
|
|
@@ -157,15 +157,35 @@ export interface AskUserQuestionRequest {
|
|
|
157
157
|
*/
|
|
158
158
|
multiSelect?: boolean;
|
|
159
159
|
}
|
|
160
|
+
/** One independently answerable question in a batched question request. */
|
|
161
|
+
export interface AskUserQuestionBatchItem extends AskUserQuestionRequest {
|
|
162
|
+
/** Batch-unique identifier (`[A-Za-z][A-Za-z0-9_-]{0,63}`). */
|
|
163
|
+
id: string;
|
|
164
|
+
/** Optional short heading rendered above the question. */
|
|
165
|
+
header?: string;
|
|
166
|
+
}
|
|
167
|
+
/** Input shape for one tool call that asks one to four questions together. */
|
|
168
|
+
export interface AskUserQuestionsRequest {
|
|
169
|
+
questions: AskUserQuestionBatchItem[];
|
|
170
|
+
}
|
|
160
171
|
/**
|
|
161
172
|
* Structured payload the SDK passes to `interrupt()` when an agent (or
|
|
162
173
|
* a custom node) needs to ask the user a clarifying question. Mirrors
|
|
163
|
-
* Claude Code's `AskUserQuestion` semantic. Resume value
|
|
164
|
-
* `AskUserQuestionResolution
|
|
174
|
+
* Claude Code's `AskUserQuestion` semantic. Resume value is
|
|
175
|
+
* `AskUserQuestionResolution` for a single question, or
|
|
176
|
+
* `AskUserQuestionsResolution` when `questions` is present.
|
|
165
177
|
*/
|
|
166
178
|
export interface AskUserQuestionInterruptPayload {
|
|
167
179
|
type: 'ask_user_question';
|
|
180
|
+
/**
|
|
181
|
+
* Single-question request, or the first question as a compatibility
|
|
182
|
+
* fallback when `questions` contains a batch. This lets existing hosts show
|
|
183
|
+
* a useful preview during a staged rollout, but they must support `questions`
|
|
184
|
+
* and `AskUserQuestionsResolution` before enabling a batched tool schema.
|
|
185
|
+
*/
|
|
168
186
|
question: AskUserQuestionRequest;
|
|
187
|
+
/** One to four questions collected by one `ask_user_question` tool call. */
|
|
188
|
+
questions?: AskUserQuestionsRequest['questions'];
|
|
169
189
|
/**
|
|
170
190
|
* The `tool_call_id` of the ask-tool call that raised this interrupt,
|
|
171
191
|
* when the tool body supplied it (see `askUserQuestion`'s `options`).
|
|
@@ -175,6 +195,10 @@ export interface AskUserQuestionInterruptPayload {
|
|
|
175
195
|
*/
|
|
176
196
|
tool_call_id?: string;
|
|
177
197
|
}
|
|
198
|
+
/** Batch-specialized ask payload for hosts that render several questions. */
|
|
199
|
+
export interface AskUserQuestionsInterruptPayload extends AskUserQuestionInterruptPayload {
|
|
200
|
+
questions: AskUserQuestionsRequest['questions'];
|
|
201
|
+
}
|
|
178
202
|
/**
|
|
179
203
|
* Discriminated union of every interrupt payload the SDK raises. New
|
|
180
204
|
* variants can be added without breaking existing handlers as long as
|
|
@@ -194,6 +218,11 @@ export interface AskUserQuestionResolution {
|
|
|
194
218
|
*/
|
|
195
219
|
answer: string;
|
|
196
220
|
}
|
|
221
|
+
/** Resume value for a batched `ask_user_question` interrupt. */
|
|
222
|
+
export interface AskUserQuestionsResolution {
|
|
223
|
+
/** Human answers keyed by each `AskUserQuestionBatchItem.id`. */
|
|
224
|
+
answers: Record<string, string>;
|
|
225
|
+
}
|
|
197
226
|
/**
|
|
198
227
|
* Type guard narrowing an arbitrary value to a `ToolApprovalInterruptPayload`.
|
|
199
228
|
* Accepts `unknown` (not just `HumanInterruptPayload`) because hosts can
|
|
@@ -3,6 +3,7 @@ import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
|
|
|
3
3
|
import type { LLMResult, Generation } from '@langchain/core/outputs';
|
|
4
4
|
import type { Command } from '@langchain/langgraph';
|
|
5
5
|
import type { AnthropicContentBlock } from '@/llm/anthropic/types';
|
|
6
|
+
import type { AssistantTextPhase } from '@/types/assistantPhase';
|
|
6
7
|
import type { SummarizeCompleteEvent } from '@/types/summarize';
|
|
7
8
|
import type { ToolEndEvent } from '@/types/tools';
|
|
8
9
|
import { StepTypes, ContentTypes, GraphEvents } from '@/common/enum';
|
|
@@ -80,6 +81,10 @@ export type MessageCreationDetails = {
|
|
|
80
81
|
type: StepTypes.MESSAGE_CREATION;
|
|
81
82
|
message_creation: {
|
|
82
83
|
message_id: string;
|
|
84
|
+
/** Content lane announced before its first delta. */
|
|
85
|
+
content_type?: ContentTypes.TEXT | ContentTypes.THINK;
|
|
86
|
+
/** Provider-authored assistant text phase, when available. */
|
|
87
|
+
phase?: AssistantTextPhase;
|
|
83
88
|
};
|
|
84
89
|
};
|
|
85
90
|
export type ToolEndData = {
|
|
@@ -323,6 +328,12 @@ export type MessageContentComplex = (ToolResultContent | ThinkingContentText | S
|
|
|
323
328
|
}) | (Record<string, any> & {
|
|
324
329
|
type?: never;
|
|
325
330
|
})) & {
|
|
331
|
+
/** Open Responses-compatible semantic phase for assistant text. */
|
|
332
|
+
phase?: AssistantTextPhase;
|
|
333
|
+
/** LangChain standard-content form of provider-specific block fields. */
|
|
334
|
+
extras?: {
|
|
335
|
+
phase?: AssistantTextPhase;
|
|
336
|
+
} & Record<string, unknown>;
|
|
326
337
|
tool_call_ids?: string[];
|
|
327
338
|
agentId?: string;
|
|
328
339
|
groupId?: number;
|
|
@@ -3,3 +3,4 @@ import type { Callbacks } from '@langchain/core/callbacks/manager';
|
|
|
3
3
|
export type CallbackEntry = BaseCallbackHandler | CallbackHandlerMethods;
|
|
4
4
|
export declare function appendCallbacks(callbacks: Callbacks | undefined, additions: readonly CallbackEntry[]): Callbacks;
|
|
5
5
|
export declare function findCallback(callbacks: Callbacks | undefined, predicate: (callback: CallbackEntry) => boolean): CallbackEntry | undefined;
|
|
6
|
+
export declare function filterCallbacks(callbacks: Callbacks | undefined, predicate: (callback: CallbackEntry) => boolean): Callbacks;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.5",
|
|
4
4
|
"reova": {
|
|
5
5
|
"enabled": true,
|
|
6
6
|
"endpoint": "https://telemetry.reo.dev/data"
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"supervised": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/supervised.ts --provider anthropic --name Jo --location \"New York, NY\"",
|
|
200
200
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
201
201
|
"test:live:handoffs": "RUN_HANDOFF_LIVE_TESTS=1 NODE_OPTIONS='--experimental-vm-modules' jest src/specs/agent-handoffs.live.test.ts --runInBand",
|
|
202
|
+
"test:live:ask-user-questions": "RUN_ASK_USER_QUESTIONS_LIVE_TESTS=1 NODE_OPTIONS='--experimental-vm-modules' jest src/specs/ask-user-questions.live.test.ts --runInBand",
|
|
202
203
|
"test:memory": "NODE_OPTIONS='--expose-gc' npx jest src/specs/title.memory-leak.test.ts",
|
|
203
204
|
"test:all": "npm test -- --testPathIgnorePatterns=title.memory-leak.test.ts && npm run test:memory",
|
|
204
205
|
"reinstall": "npm run clean && npm ci && rm -rf ./dist && npm run build",
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -61,6 +61,8 @@ import {
|
|
|
61
61
|
supportsBedrockToolCache,
|
|
62
62
|
isSyntheticProviderContextMessage,
|
|
63
63
|
getMessageId,
|
|
64
|
+
getMessageCreationContentMetadata,
|
|
65
|
+
splitAssistantTextContentByPhase,
|
|
64
66
|
makeIsDeferred,
|
|
65
67
|
partitionAndMarkAnthropicToolCache,
|
|
66
68
|
DEFAULT_RETAIN_RECENT_TURNS,
|
|
@@ -507,18 +509,25 @@ async function dispatchMessageCreationStep({
|
|
|
507
509
|
graph,
|
|
508
510
|
stepKey,
|
|
509
511
|
messageId,
|
|
512
|
+
content,
|
|
513
|
+
contentType,
|
|
510
514
|
metadata,
|
|
511
515
|
}: {
|
|
512
516
|
graph: Graph<t.BaseGraphState>;
|
|
513
517
|
stepKey: string;
|
|
514
518
|
messageId: string;
|
|
519
|
+
content?: string | t.MessageContentComplex[];
|
|
520
|
+
contentType?: ContentTypes.TEXT | ContentTypes.THINK;
|
|
515
521
|
metadata: Record<string, unknown>;
|
|
516
522
|
}): Promise<string> {
|
|
517
523
|
await graph.dispatchRunStep(
|
|
518
524
|
stepKey,
|
|
519
525
|
{
|
|
520
526
|
type: StepTypes.MESSAGE_CREATION,
|
|
521
|
-
message_creation: {
|
|
527
|
+
message_creation: {
|
|
528
|
+
message_id: messageId,
|
|
529
|
+
...getMessageCreationContentMetadata(content, contentType),
|
|
530
|
+
},
|
|
522
531
|
},
|
|
523
532
|
metadata
|
|
524
533
|
);
|
|
@@ -548,6 +557,7 @@ async function dispatchTextMessageContent({
|
|
|
548
557
|
graph,
|
|
549
558
|
stepKey,
|
|
550
559
|
messageId,
|
|
560
|
+
content: [contentPart],
|
|
551
561
|
metadata,
|
|
552
562
|
});
|
|
553
563
|
await graph.dispatchMessageDelta(
|
|
@@ -558,13 +568,24 @@ async function dispatchTextMessageContent({
|
|
|
558
568
|
}
|
|
559
569
|
return true;
|
|
560
570
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
571
|
+
const contentGroups = Array.isArray(content)
|
|
572
|
+
? splitAssistantTextContentByPhase(content)
|
|
573
|
+
: [content];
|
|
574
|
+
for (const contentGroup of contentGroups) {
|
|
575
|
+
const stepId = await dispatchMessageCreationStep({
|
|
576
|
+
graph,
|
|
577
|
+
stepKey,
|
|
578
|
+
messageId,
|
|
579
|
+
content: contentGroup,
|
|
580
|
+
contentType: ContentTypes.TEXT,
|
|
581
|
+
metadata,
|
|
582
|
+
});
|
|
583
|
+
await graph.dispatchMessageDelta(
|
|
584
|
+
stepId,
|
|
585
|
+
{ content: contentGroup },
|
|
586
|
+
metadata
|
|
587
|
+
);
|
|
588
|
+
}
|
|
568
589
|
return true;
|
|
569
590
|
}
|
|
570
591
|
|
|
@@ -599,7 +620,10 @@ async function dispatchReasoningContent({
|
|
|
599
620
|
stepKey,
|
|
600
621
|
{
|
|
601
622
|
type: StepTypes.MESSAGE_CREATION,
|
|
602
|
-
message_creation: {
|
|
623
|
+
message_creation: {
|
|
624
|
+
message_id: messageId,
|
|
625
|
+
content_type: ContentTypes.THINK,
|
|
626
|
+
},
|
|
603
627
|
},
|
|
604
628
|
metadata
|
|
605
629
|
);
|
|
@@ -275,6 +275,63 @@ describe('StandardGraph final response reasoning fallback', () => {
|
|
|
275
275
|
streamUsage: false,
|
|
276
276
|
};
|
|
277
277
|
|
|
278
|
+
it('starts a new message step when streamed assistant phases change', async () => {
|
|
279
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
280
|
+
const messageDeltas: t.MessageDeltaEvent[] = [];
|
|
281
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
282
|
+
const run = await Run.create<t.IState>({
|
|
283
|
+
runId: 'separate-streamed-assistant-phases',
|
|
284
|
+
graphConfig: {
|
|
285
|
+
type: 'standard',
|
|
286
|
+
llmConfig,
|
|
287
|
+
},
|
|
288
|
+
returnContent: true,
|
|
289
|
+
skipCleanup: true,
|
|
290
|
+
customHandlers: createReasoningHandlers(
|
|
291
|
+
aggregateContent,
|
|
292
|
+
reasoningDeltas,
|
|
293
|
+
messageDeltas
|
|
294
|
+
),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!run.Graph) {
|
|
298
|
+
throw new Error('Expected graph to be initialized');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
run.Graph.overrideModel = new StreamingReasoningModel([
|
|
302
|
+
new AIMessageChunk({
|
|
303
|
+
content: [
|
|
304
|
+
{ type: 'text', text: 'Checking the session.', phase: 'commentary' },
|
|
305
|
+
] as never,
|
|
306
|
+
}),
|
|
307
|
+
new AIMessageChunk({
|
|
308
|
+
content: [
|
|
309
|
+
{ type: 'text', text: 'The session is fixed.', phase: 'final_answer' },
|
|
310
|
+
] as never,
|
|
311
|
+
}),
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
await run.processStream(
|
|
315
|
+
{ messages: [new HumanMessage('fix the session')] },
|
|
316
|
+
config
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(new Set(messageDeltas.map((delta) => delta.id)).size).toBe(2);
|
|
320
|
+
expect(contentParts).toEqual([
|
|
321
|
+
{ type: 'text', text: 'Checking the session.' },
|
|
322
|
+
{ type: 'text', text: 'The session is fixed.' },
|
|
323
|
+
]);
|
|
324
|
+
expect(
|
|
325
|
+
run.Graph.getRunSteps()
|
|
326
|
+
.filter((step) => step.stepDetails.type === StepTypes.MESSAGE_CREATION)
|
|
327
|
+
.map((step) =>
|
|
328
|
+
step.stepDetails.type === StepTypes.MESSAGE_CREATION
|
|
329
|
+
? step.stepDetails.message_creation.phase
|
|
330
|
+
: undefined
|
|
331
|
+
)
|
|
332
|
+
).toEqual(['commentary', 'final_answer']);
|
|
333
|
+
});
|
|
334
|
+
|
|
278
335
|
it('emits reasoning_content from invoke-only final responses', async () => {
|
|
279
336
|
const reasoningText = 'Need to inspect the Home Assistant tool state.';
|
|
280
337
|
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|