@librechat/agents 3.9.7 → 4.0.0

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 (74) hide show
  1. package/dist/cjs/common/enum.cjs +1 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +4 -3
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/instrumentation.cjs +3 -3
  6. package/dist/cjs/langfuse.cjs +3 -3
  7. package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
  8. package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
  9. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +20 -9
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +1 -1
  14. package/dist/cjs/run.cjs +4 -3
  15. package/dist/cjs/run.cjs.map +1 -1
  16. package/dist/cjs/stream.cjs +7 -4
  17. package/dist/cjs/stream.cjs.map +1 -1
  18. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +20 -6
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/handlers.cjs +2 -2
  22. package/dist/cjs/tools/handlers.cjs.map +1 -1
  23. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +22 -0
  24. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  25. package/dist/cjs/utils/events.cjs +20 -2
  26. package/dist/cjs/utils/events.cjs.map +1 -1
  27. package/dist/esm/common/enum.mjs +1 -0
  28. package/dist/esm/common/enum.mjs.map +1 -1
  29. package/dist/esm/graphs/Graph.mjs +4 -3
  30. package/dist/esm/graphs/Graph.mjs.map +1 -1
  31. package/dist/esm/instrumentation.mjs +3 -3
  32. package/dist/esm/langfuse.mjs +3 -3
  33. package/dist/esm/langfuseRuntimeScope.mjs +1 -1
  34. package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
  35. package/dist/esm/llm/bedrock/index.mjs +1 -1
  36. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  37. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +20 -9
  38. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  39. package/dist/esm/llm/invoke.mjs +1 -1
  40. package/dist/esm/run.mjs +4 -3
  41. package/dist/esm/run.mjs.map +1 -1
  42. package/dist/esm/stream.mjs +11 -7
  43. package/dist/esm/stream.mjs.map +1 -1
  44. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  45. package/dist/esm/tools/ToolNode.mjs +24 -9
  46. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  47. package/dist/esm/tools/handlers.mjs +2 -2
  48. package/dist/esm/tools/handlers.mjs.map +1 -1
  49. package/dist/esm/tools/subagent/SubagentExecutor.mjs +22 -0
  50. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  51. package/dist/esm/utils/events.mjs +20 -3
  52. package/dist/esm/utils/events.mjs.map +1 -1
  53. package/dist/types/common/enum.d.ts +2 -0
  54. package/dist/types/graphs/Graph.d.ts +2 -2
  55. package/dist/types/llm/bedrock/utils/message_inputs.d.ts +13 -1
  56. package/dist/types/tools/handlers.d.ts +2 -1
  57. package/dist/types/types/graph.d.ts +3 -3
  58. package/dist/types/types/stream.d.ts +24 -0
  59. package/dist/types/types/tools.d.ts +2 -0
  60. package/dist/types/utils/events.d.ts +4 -0
  61. package/package.json +1 -1
  62. package/src/common/enum.ts +2 -0
  63. package/src/graphs/Graph.ts +5 -2
  64. package/src/llm/bedrock/index.ts +4 -2
  65. package/src/llm/bedrock/utils/message_inputs.ts +62 -15
  66. package/src/run.ts +9 -7
  67. package/src/stream.ts +12 -3
  68. package/src/tools/ToolNode.ts +32 -6
  69. package/src/tools/handlers.ts +4 -1
  70. package/src/tools/subagent/SubagentExecutor.ts +24 -0
  71. package/src/types/graph.ts +3 -0
  72. package/src/types/stream.ts +21 -0
  73. package/src/types/tools.ts +2 -0
  74. package/src/utils/events.ts +21 -2
@@ -10,9 +10,14 @@ async function safeDispatchCustomEvent(event, payload, config) {
10
10
  payload = {
11
11
  ...request,
12
12
  resolve: (results) => {
13
- traceHostToolResults(request, results, config).then(() => request.resolve(results), () => {
13
+ const receivedAt = Date.now();
14
+ const stamped = results.map((result) => ({
15
+ ...result,
16
+ received_at: receivedAt
17
+ }));
18
+ traceHostToolResults(request, results, config).then(() => request.resolve(stamped), () => {
14
19
  console.warn("Failed to record host tool execution metadata");
15
- request.resolve(results);
20
+ request.resolve(stamped);
16
21
  });
17
22
  }
18
23
  };
@@ -25,6 +30,18 @@ async function safeDispatchCustomEvent(event, payload, config) {
25
30
  return false;
26
31
  }
27
32
  }
33
+ function createToolCallsDispatchedEvent(config, calls) {
34
+ const runId = config.configurable?.run_id;
35
+ return {
36
+ dispatched_at: Date.now(),
37
+ ...typeof runId === "string" ? { runId } : {},
38
+ toolCalls: calls.map(({ id, name, stepId }) => ({
39
+ id,
40
+ name,
41
+ stepId
42
+ }))
43
+ };
44
+ }
28
45
  function emitAgentLog(config, level, scope, message, data, meta, options) {
29
46
  if (!config) return;
30
47
  if (level === "debug" && !(options?.force ?? false) && process.env.AGENT_DEBUG_LOGGING !== "true") return;
@@ -37,6 +54,6 @@ function emitAgentLog(config, level, scope, message, data, meta, options) {
37
54
  }, config);
38
55
  }
39
56
  //#endregion
40
- export { emitAgentLog, safeDispatchCustomEvent };
57
+ export { createToolCallsDispatchedEvent, emitAgentLog, safeDispatchCustomEvent };
41
58
 
42
59
  //# sourceMappingURL=events.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.mjs","names":[],"sources":["../../../src/utils/events.ts"],"mappings":";;;;;AAaA,eAAsB,wBACpB,OACA,SACA,QACyB;CACzB,IAAI;EACF,IAAI,UAAA,mBAAuC;GACzC,MAAM,UAAU;GAChB,UAAU;IACR,GAAG;IACH,UACE,YACS;KACT,qBAA0B,SAAS,SAAS,MAAM,CAAC,CAAC,WAC5C,QAAQ,QAAQ,OAAO,SACvB;MACJ,QAAQ,KAAK,+CAA+C;MAC5D,QAAQ,QAAQ,OAAO;KACzB,CACF;IACF;GACF;EACF;EACA,MAAM,oBAAoB,OAAO,SAAS,MAAM;EAChD,OAAO;CACT,SAAS,GAAG;EAEV,IACE,aAAa,SACb,EAAE,QAAQ,SAAS,2BAA2B,KAC9C,EAAE,QAAQ,SAAS,sBAAsB,GAKzC,OAAO;EAGT,QAAQ,MAAM,mCAAmC,CAAC;EAClD,OAAO;CACT;AACF;AAQA,SAAgB,aACd,QACA,OACA,OACA,SACA,MACA,MACA,SACM;CACN,IAAI,CAAC,QAAQ;CACb,IACE,UAAU,WACV,EAAE,SAAS,SAAS,UACpB,QAAQ,IAAI,wBAAwB,QAEpC;CACF,wBAAK,gBAEH;EAAE;EAAO;EAAO;EAAS;EAAM,GAAG;CAAK,GACvC,MACF;AACF"}
1
+ {"version":3,"file":"events.mjs","names":[],"sources":["../../../src/utils/events.ts"],"mappings":";;;;;AAcA,eAAsB,wBACpB,OACA,SACA,QACyB;CACzB,IAAI;EACF,IAAI,UAAA,mBAAuC;GACzC,MAAM,UAAU;GAChB,UAAU;IACR,GAAG;IACH,UACE,YACS;KACT,MAAM,aAAa,KAAK,IAAI;KAC5B,MAAM,UAAU,QAAQ,KAAK,YAAY;MAAE,GAAG;MAAQ,aAAa;KAAW,EAAE;KAChF,qBAA0B,SAAS,SAAS,MAAM,CAAC,CAAC,WAC5C,QAAQ,QAAQ,OAAO,SACvB;MACJ,QAAQ,KAAK,+CAA+C;MAC5D,QAAQ,QAAQ,OAAO;KACzB,CACF;IACF;GACF;EACF;EACA,MAAM,oBAAoB,OAAO,SAAS,MAAM;EAChD,OAAO;CACT,SAAS,GAAG;EAEV,IACE,aAAa,SACb,EAAE,QAAQ,SAAS,2BAA2B,KAC9C,EAAE,QAAQ,SAAS,sBAAsB,GAKzC,OAAO;EAGT,QAAQ,MAAM,mCAAmC,CAAC;EAClD,OAAO;CACT;AACF;AAGA,SAAgB,+BACd,QACA,OAI0B;CAC1B,MAAM,QAAQ,OAAO,cAAc;CACnC,OAAO;EACL,eAAe,KAAK,IAAI;EACxB,GAAI,OAAO,UAAU,WAAW,EAAE,MAAM,IAAI,CAAC;EAC7C,WAAW,MAAM,KAAK,EAAE,IAAI,MAAM,cAAc;GAAE;GAAI;GAAM;EAAO,EAAE;CACvE;AACF;AAQA,SAAgB,aACd,QACA,OACA,OACA,SACA,MACA,MACA,SACM;CACN,IAAI,CAAC,QAAQ;CACb,IACE,UAAU,WACV,EAAE,SAAS,SAAS,UACpB,QAAQ,IAAI,wBAAwB,QAEpC;CACF,wBAAK,gBAEH;EAAE;EAAO;EAAO;EAAS;EAAM,GAAG;CAAK,GACvC,MACF;AACF"}
@@ -23,6 +23,8 @@ export declare enum GraphEvents {
23
23
  ON_MESSAGE_DELTA = "on_message_delta",
24
24
  /** [Custom] Reasoning Delta events for messages */
25
25
  ON_REASONING_DELTA = "on_reasoning_delta",
26
+ /** [Custom] Per-call handoff boundary between streamed tool preparation and execution */
27
+ ON_TOOL_CALLS_DISPATCHED = "on_tool_calls_dispatched",
26
28
  /** [Custom] Request to execute tools - dispatched by ToolNode, handled by host */
27
29
  ON_TOOL_EXECUTE = "on_tool_execute",
28
30
  /** [Custom] Emitted when the summarize node begins generating a summary */
@@ -38,7 +38,7 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
38
38
  abstract getStepIdByKey(stepKey: string, index?: number): string;
39
39
  abstract getRunStep(stepId: string): t.RunStep | undefined;
40
40
  abstract dispatchRunStep(stepKey: string, stepDetails: t.StepDetails, metadata?: Record<string, unknown>): Promise<string>;
41
- abstract dispatchRunStepDelta(id: string, delta: t.ToolCallDelta, metadata?: Record<string, unknown>): Promise<void>;
41
+ abstract dispatchRunStepDelta(id: string, delta: t.ToolCallDelta, metadata?: Record<string, unknown>, observedAt?: number): Promise<void>;
42
42
  abstract dispatchMessageDelta(id: string, delta: t.MessageDelta, metadata?: Record<string, unknown>): Promise<void>;
43
43
  abstract dispatchReasoningDelta(stepId: string, delta: t.ReasoningDelta, metadata?: Record<string, unknown>): Promise<void>;
44
44
  abstract createCallModel(agentId?: string, currentModel?: t.ChatModel): (state: t.AgentSubgraphState, config?: RunnableConfig) => Promise<Partial<t.AgentSubgraphState>>;
@@ -761,7 +761,7 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
761
761
  * Kept for backward compatibility
762
762
  */
763
763
  handleToolCallError(data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<boolean>;
764
- dispatchRunStepDelta(id: string, delta: t.ToolCallDelta, metadata?: Record<string, unknown>): Promise<void>;
764
+ dispatchRunStepDelta(id: string, delta: t.ToolCallDelta, metadata?: Record<string, unknown>, observedAt?: number): Promise<void>;
765
765
  dispatchMessageDelta(id: string, delta: t.MessageDelta, metadata?: Record<string, unknown>): Promise<void>;
766
766
  dispatchReasoningDelta: (stepId: string, delta: t.ReasoningDelta, metadata?: Record<string, unknown>) => Promise<void>;
767
767
  }
@@ -22,10 +22,22 @@ export declare function concatenateLangchainReasoningBlocks(content: Array<Messa
22
22
  * Extract image info from a base64 string or URL.
23
23
  */
24
24
  export declare function extractImageInfo(base64: string): BedrockContentBlock;
25
+ /** Options for {@link convertToConverseMessages}. */
26
+ export interface ConvertToConverseMessagesOptions {
27
+ /** The model the request is for, which decides whether prior reasoning is replayed. */
28
+ model?: string;
29
+ }
30
+ /**
31
+ * OpenAI Bedrock models reject prior `reasoningContent`, including on Claude-to-GPT handoffs.
32
+ * Only suppress replay for identified OpenAI targets; other model families retain the existing
33
+ * behavior because their Bedrock reasoning-replay requirements have not been verified here.
34
+ * Missing model IDs and opaque resource ARNs also retain the existing behavior.
35
+ */
36
+ export declare function replaysBedrockReasoning(model?: string): boolean;
25
37
  /**
26
38
  * Convert LangChain messages to Bedrock Converse messages.
27
39
  */
28
- export declare function convertToConverseMessages(messages: BaseMessage[]): {
40
+ export declare function convertToConverseMessages(messages: BaseMessage[], options?: ConvertToConverseMessagesOptions): {
29
41
  converseMessages: BedrockMessage[];
30
42
  converseSystem: BedrockSystemContentBlock[];
31
43
  };
@@ -2,11 +2,12 @@ import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
2
2
  import type { Graph, MultiAgentGraph, StandardGraph } from '@/graphs';
3
3
  import type { AgentContext } from '@/agents/AgentContext';
4
4
  import type * as t from '@/types';
5
- export declare function handleToolCallChunks({ graph, stepKey, toolCallChunks, metadata, }: {
5
+ export declare function handleToolCallChunks({ graph, stepKey, toolCallChunks, metadata, observedAt, }: {
6
6
  graph: StandardGraph | MultiAgentGraph;
7
7
  stepKey: string;
8
8
  toolCallChunks: ToolCallChunk[];
9
9
  metadata?: Record<string, unknown>;
10
+ observedAt?: number;
10
11
  }): Promise<void>;
11
12
  export declare const handleToolCalls: (toolCalls?: ToolCall[], metadata?: Record<string, unknown>, graph?: Graph | StandardGraph | MultiAgentGraph) => Promise<void>;
12
13
  export declare const toolResultTypes: Set<string>;
@@ -4,7 +4,7 @@ import type { START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
4
4
  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
- import type { RunStep, ModelResponseEvent, ModelToolsClaimedEvent, RunStepDeltaEvent, RunStepResumeState, RunStepClosedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
7
+ import type { RunStep, ModelResponseEvent, ModelToolsClaimedEvent, RunStepDeltaEvent, RunStepResumeState, RunStepClosedEvent, ToolCallsDispatchedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
8
8
  import type { SummarizationNodeInput, SummarizeCompleteEvent, CompactionSemanticIndex, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
9
9
  import type { ToolMap, ToolSessionMap, ToolEndEvent, GenericTool, LCTool, ToolExecutionConfig, ToolExecuteBatchRequest } from '@/types/tools';
10
10
  import type { TokenCounter, StreamLimits, StreamPreemption, TokenBudgetBreakdown } from '@/types/run';
@@ -139,7 +139,7 @@ export interface FadingTier {
139
139
  /** Latched fading tiers keyed by agent ID. */
140
140
  export type FadingTiers = Record<string, FadingTier>;
141
141
  export interface EventHandler {
142
- handle(event: string, data: StreamEventData | ModelEndData | ModelResponseEvent | ModelToolsClaimedEvent | RunStep | RunStepDeltaEvent | RunStepClosedEvent | MessageDeltaEvent | ReasoningDeltaEvent | SummarizeStartEvent | SummarizeDeltaEvent | SummarizeCompleteEvent | SubagentUpdateEvent | AgentLogEvent | ContextUsageEvent | ToolExecuteBatchRequest | {
142
+ handle(event: string, data: StreamEventData | ModelEndData | ModelResponseEvent | ModelToolsClaimedEvent | RunStep | RunStepDeltaEvent | RunStepClosedEvent | ToolCallsDispatchedEvent | MessageDeltaEvent | ReasoningDeltaEvent | SummarizeStartEvent | SummarizeDeltaEvent | SummarizeCompleteEvent | SubagentUpdateEvent | AgentLogEvent | ContextUsageEvent | ToolExecuteBatchRequest | {
143
143
  result: ToolEndEvent;
144
144
  }, metadata?: Record<string, unknown>, graph?: StandardGraph | MultiAgentGraph): void | Promise<void>;
145
145
  }
@@ -531,7 +531,7 @@ export type ExecutableSubagentConfig = ResolvedSubagentConfig | LazySingleAgentS
531
531
  /** Graph-aware config accepted by the executor. Graph configs stay eager. */
532
532
  export type ExecutableSubagentConfigEntry = ExecutableSubagentConfig | GraphSubagentConfig;
533
533
  /** Lifecycle phase carried on {@link SubagentUpdateEvent}. */
534
- export type SubagentUpdatePhase = 'start' | 'run_step' | 'run_step_delta' | 'run_step_completed' | 'run_step_closed' | 'message_delta' | 'reasoning_delta' | 'control' | 'stop' | 'error';
534
+ export type SubagentUpdatePhase = 'start' | 'run_step' | 'run_step_delta' | 'tool_calls_dispatched' | 'run_step_completed' | 'run_step_closed' | 'message_delta' | 'reasoning_delta' | 'control' | 'stop' | 'error';
535
535
  export interface SubagentAncestryEntry {
536
536
  readonly subagentRunId: string;
537
537
  readonly subagentType: string;
@@ -127,6 +127,8 @@ export interface RunStepDeltaEvent {
127
127
  * The delta containing the fields that have changed on the run step.
128
128
  */
129
129
  delta: ToolCallDelta;
130
+ /** Epoch ms when the SDK received this tool-call fragment, before step dispatch. */
131
+ observed_at?: number;
130
132
  }
131
133
  /**
132
134
  * Terminal signal for a run step, emitted exactly once per step when it
@@ -218,6 +220,28 @@ export type ToolCallCompleted = {
218
220
  type: 'tool_call';
219
221
  tool_call: ProcessedToolCall;
220
222
  };
223
+ /**
224
+ * A tool call has crossed from model-authored argument preparation into SDK
225
+ * execution. Emitted immediately before host dispatch or direct invocation;
226
+ * it does not claim the host has begun its own work. Paired with the tool
227
+ * completion `completed_at`, the stamp measures SDK handoff-to-result time,
228
+ * not MCP or database execution time. The earliest `observed_at` tool-call
229
+ * delta for a call bounds its argument preparation time; consumers correlate
230
+ * deltas by step/index until an ID is known. For a call without streamed
231
+ * fragments, the preparation start is unknown. Never infer a missing start.
232
+ * An eager call may dispatch before it has a step ID, and this observational
233
+ * event may arrive after completion: correlate by tool call ID and timestamps,
234
+ * not arrival order.
235
+ */
236
+ export type ToolCallsDispatchedEvent = {
237
+ dispatched_at: number;
238
+ runId?: string;
239
+ toolCalls: Array<{
240
+ id: string;
241
+ name: string;
242
+ stepId?: string;
243
+ }>;
244
+ };
221
245
  export type ToolCompleteEvent = ToolCallCompleted & {
222
246
  /** The Step Id of the Tool Call */
223
247
  id: string;
@@ -644,6 +644,8 @@ export type OutcomePatch = {
644
644
  export type ToolExecuteResult = {
645
645
  /** Matches ToolCallRequest.id */
646
646
  toolCallId: string;
647
+ /** SDK-stamped result receipt time; never host-supplied or inferred from handler cleanup. */
648
+ received_at?: number;
647
649
  /** Tool output content */
648
650
  content: string | unknown[];
649
651
  /** Optional artifact (for content_and_artifact format) */
@@ -1,10 +1,14 @@
1
1
  import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type { ToolCallsDispatchedEvent } from '@/types/stream';
3
+ import type { ToolExecuteBatchRequest } from '@/types/tools';
2
4
  import type { AgentLogEvent } from '@/types/graph';
3
5
  /**
4
6
  * Safely dispatches a custom event and properly awaits it to avoid
5
7
  * race conditions where events are dispatched after run cleanup.
6
8
  */
7
9
  export declare function safeDispatchCustomEvent(event: string, payload: unknown, config?: RunnableConfig): Promise<boolean | void>;
10
+ /** Builds an argument-free snapshot once per handoff, not once per result. */
11
+ export declare function createToolCallsDispatchedEvent(config: RunnableConfig, calls: readonly Pick<ToolExecuteBatchRequest['toolCalls'][number], 'id' | 'name' | 'stepId'>[]): ToolCallsDispatchedEvent;
8
12
  /**
9
13
  * Fire-and-forget diagnostic log event.
10
14
  * Debug-level logs are gated behind AGENT_DEBUG_LOGGING=true to avoid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.9.7",
3
+ "version": "4.0.0",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -26,6 +26,8 @@ export enum GraphEvents {
26
26
  ON_MESSAGE_DELTA = 'on_message_delta',
27
27
  /** [Custom] Reasoning Delta events for messages */
28
28
  ON_REASONING_DELTA = 'on_reasoning_delta',
29
+ /** [Custom] Per-call handoff boundary between streamed tool preparation and execution */
30
+ ON_TOOL_CALLS_DISPATCHED = 'on_tool_calls_dispatched',
29
31
  /** [Custom] Request to execute tools - dispatched by ToolNode, handled by host */
30
32
  ON_TOOL_EXECUTE = 'on_tool_execute',
31
33
  /** [Custom] Emitted when the summarize node begins generating a summary */
@@ -766,7 +766,8 @@ export abstract class Graph<
766
766
  abstract dispatchRunStepDelta(
767
767
  id: string,
768
768
  delta: t.ToolCallDelta,
769
- metadata?: Record<string, unknown>
769
+ metadata?: Record<string, unknown>,
770
+ observedAt?: number
770
771
  ): Promise<void>;
771
772
  abstract dispatchMessageDelta(
772
773
  id: string,
@@ -6130,7 +6131,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
6130
6131
  async dispatchRunStepDelta(
6131
6132
  id: string,
6132
6133
  delta: t.ToolCallDelta,
6133
- metadata?: Record<string, unknown>
6134
+ metadata?: Record<string, unknown>,
6135
+ observedAt?: number
6134
6136
  ): Promise<void> {
6135
6137
  if (!this.config) {
6136
6138
  throw new Error('No config provided');
@@ -6140,6 +6142,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
6140
6142
  const runStepDelta: t.RunStepDeltaEvent = {
6141
6143
  id,
6142
6144
  delta,
6145
+ ...(observedAt != null && { observed_at: observedAt }),
6143
6146
  };
6144
6147
  const handler = this.handlerRegistry?.getHandler(
6145
6148
  GraphEvents.ON_RUN_STEP_DELTA
@@ -270,8 +270,10 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
270
270
  options: this['ParsedCallOptions'] & CustomChatBedrockConverseCallOptions,
271
271
  runManager?: CallbackManagerForLLMRun
272
272
  ): AsyncGenerator<ChatGenerationChunk> {
273
- const { converseMessages, converseSystem } =
274
- convertToConverseMessages(messages);
273
+ const { converseMessages, converseSystem } = convertToConverseMessages(
274
+ messages,
275
+ { model: this.model }
276
+ );
275
277
  const params = this.invocationParams(options);
276
278
 
277
279
  let { streamUsage } = this;
@@ -29,8 +29,8 @@ import type {
29
29
  MessageContentReasoningBlock,
30
30
  } from '../types';
31
31
  import { serializeStructuredValueBounded } from '@/utils/toolContent';
32
- import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
33
32
  import { isReasoningContentBlock } from '@/messages/reasoningTypes';
33
+ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
34
34
 
35
35
  /**
36
36
  * Reasoning blocks from other providers, relative to Bedrock. Bedrock's native
@@ -714,13 +714,16 @@ function coerceBedrockToolUseInput(input: unknown): Record<string, unknown> {
714
714
  : {};
715
715
  }
716
716
 
717
- function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
717
+ function convertAIMessageToConverseMessage(
718
+ msg: BaseMessage,
719
+ replayReasoning = true
720
+ ): BedrockMessage {
718
721
  // Check for v1 format from other providers (PR #9766 fix)
719
722
  const responseMetadata = msg.response_metadata as
720
723
  | { output_version?: string }
721
724
  | undefined;
722
725
  if (responseMetadata?.output_version === 'v1') {
723
- return convertFromV1ToChatBedrockConverseMessage(msg);
726
+ return convertFromV1ToChatBedrockConverseMessage(msg, replayReasoning);
724
727
  }
725
728
 
726
729
  const assistantMsg: BedrockMessage = {
@@ -733,9 +736,7 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
733
736
  } else if (Array.isArray(msg.content)) {
734
737
  const parsedToolCallIds = new Set(
735
738
  isAIMessage(msg)
736
- ? (msg.tool_calls ?? []).flatMap((toolCall) =>
737
- toolCall.id != null ? [toolCall.id] : []
738
- )
739
+ ? (msg.tool_calls ?? []).flatMap(({ id }) => id ?? [])
739
740
  : []
740
741
  );
741
742
  const concatenatedBlocks = concatenateLangchainReasoningBlocks(
@@ -775,10 +776,14 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
775
776
  } else if (block.type === 'reasoning_content') {
776
777
  const reasoningBlock = block as MessageContentReasoningBlock;
777
778
  // Bedrock Converse rejects reasoningContent whose reasoningText.text is
778
- // null/empty (a signature-only block that never merged with its text).
779
- // Drop it rather than emit an invalid request; the empty-turn
780
- // placeholder below covers a turn left with no content.
781
- if (!isSerializableBedrockReasoningBlock(reasoningBlock)) {
779
+ // null/empty (a signature-only block that never merged with its text),
780
+ // and OpenAI models reject reasoningContent outright. Drop
781
+ // it rather than emit an invalid request; the empty-turn placeholder
782
+ // below covers a turn left with no content.
783
+ if (
784
+ !replayReasoning ||
785
+ !isSerializableBedrockReasoningBlock(reasoningBlock)
786
+ ) {
782
787
  return;
783
788
  }
784
789
  contentBlocks.push({
@@ -856,7 +861,8 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
856
861
  * (Implements PR #9766 fix for output_version v1 detection)
857
862
  */
858
863
  function convertFromV1ToChatBedrockConverseMessage(
859
- msg: BaseMessage
864
+ msg: BaseMessage,
865
+ replayReasoning = true
860
866
  ): BedrockMessage {
861
867
  const contentBlocks: BedrockContentBlock[] = [];
862
868
  const assistantMsg: BedrockMessage = {
@@ -898,7 +904,11 @@ function convertFromV1ToChatBedrockConverseMessage(
898
904
  /** Bedrock Converse rejects `reasoningText` with a null/empty `text`
899
905
  * (`Member must not be null`), e.g. when the producing model omitted
900
906
  * reasoning text (`thinking.display: "omitted"`) — drop rather than send. */
901
- if (reasoning.reasoning == null || reasoning.reasoning === '') {
907
+ if (
908
+ !replayReasoning ||
909
+ reasoning.reasoning == null ||
910
+ reasoning.reasoning === ''
911
+ ) {
902
912
  droppedUnserializableContent = true;
903
913
  continue;
904
914
  }
@@ -909,7 +919,10 @@ function convertFromV1ToChatBedrockConverseMessage(
909
919
  } as BedrockContentBlock);
910
920
  } else if (block.type === 'reasoning_content') {
911
921
  const reasoningBlock = block as MessageContentReasoningBlock;
912
- if (!isSerializableBedrockReasoningBlock(reasoningBlock)) {
922
+ if (
923
+ !replayReasoning ||
924
+ !isSerializableBedrockReasoningBlock(reasoningBlock)
925
+ ) {
913
926
  droppedUnserializableContent = true;
914
927
  continue;
915
928
  }
@@ -1118,13 +1131,47 @@ function convertToolMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
1118
1131
  };
1119
1132
  }
1120
1133
 
1134
+ /** Options for {@link convertToConverseMessages}. */
1135
+ export interface ConvertToConverseMessagesOptions {
1136
+ /** The model the request is for, which decides whether prior reasoning is replayed. */
1137
+ model?: string;
1138
+ }
1139
+
1140
+ /**
1141
+ * OpenAI Bedrock models reject prior `reasoningContent`, including on Claude-to-GPT handoffs.
1142
+ * Only suppress replay for identified OpenAI targets; other model families retain the existing
1143
+ * behavior because their Bedrock reasoning-replay requirements have not been verified here.
1144
+ * Missing model IDs and opaque resource ARNs also retain the existing behavior.
1145
+ */
1146
+ export function replaysBedrockReasoning(model?: string): boolean {
1147
+ if (model == null || model === '') {
1148
+ return true;
1149
+ }
1150
+ let id = model.toLowerCase();
1151
+ if (id.startsWith('arn:')) {
1152
+ const modelId =
1153
+ /^arn:[^:]+:bedrock:[^:]*:[^:]*:(?:foundation-model|inference-profile)\/(.+)$/.exec(
1154
+ id
1155
+ )?.[1];
1156
+ if (modelId == null) {
1157
+ return true;
1158
+ }
1159
+ id = modelId;
1160
+ }
1161
+ return !/(?:^|\.)openai\./.test(id);
1162
+ }
1163
+
1121
1164
  /**
1122
1165
  * Convert LangChain messages to Bedrock Converse messages.
1123
1166
  */
1124
- export function convertToConverseMessages(messages: BaseMessage[]): {
1167
+ export function convertToConverseMessages(
1168
+ messages: BaseMessage[],
1169
+ options: ConvertToConverseMessagesOptions = {}
1170
+ ): {
1125
1171
  converseMessages: BedrockMessage[];
1126
1172
  converseSystem: BedrockSystemContentBlock[];
1127
1173
  } {
1174
+ const replayReasoning = replaysBedrockReasoning(options.model);
1128
1175
  const converseSystem = messages
1129
1176
  .filter((msg) => msg._getType() === 'system')
1130
1177
  .flatMap((msg) => convertSystemMessageToConverseMessage(msg));
@@ -1133,7 +1180,7 @@ export function convertToConverseMessages(messages: BaseMessage[]): {
1133
1180
  .filter((msg) => msg._getType() !== 'system')
1134
1181
  .map((msg) => {
1135
1182
  if (msg._getType() === 'ai') {
1136
- return convertAIMessageToConverseMessage(msg);
1183
+ return convertAIMessageToConverseMessage(msg, replayReasoning);
1137
1184
  } else if (msg._getType() === 'human' || msg._getType() === 'generic') {
1138
1185
  return convertHumanMessageToConverseMessage(msg);
1139
1186
  } else if (msg._getType() === 'tool') {
package/src/run.ts CHANGED
@@ -37,13 +37,6 @@ import {
37
37
  DEFAULT_MAX_STOP_CONTINUATIONS,
38
38
  DEFAULT_RECURSION_LIMIT,
39
39
  } from '@/common';
40
- import {
41
- ACTIVITY_PHASE_LABEL_PROMPT,
42
- ACTIVITY_LABEL_PROMPT,
43
- buildActivityLabelPrompt,
44
- buildActivityPhaseLabelPrompt,
45
- normalizeActivityPhaseLabel,
46
- } from '@/prompts/activityLabel';
47
40
  import {
48
41
  createLangfuseTraceMetadata,
49
42
  createLangfuseHandler,
@@ -53,6 +46,13 @@ import {
53
46
  isLangfuseCallbackHandler,
54
47
  withLangfuseAttributes,
55
48
  } from '@/langfuse';
49
+ import {
50
+ ACTIVITY_PHASE_LABEL_PROMPT,
51
+ ACTIVITY_LABEL_PROMPT,
52
+ buildActivityLabelPrompt,
53
+ buildActivityPhaseLabelPrompt,
54
+ normalizeActivityPhaseLabel,
55
+ } from '@/prompts/activityLabel';
56
56
  import {
57
57
  requireValidSubagentResumeManifest,
58
58
  SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
@@ -204,6 +204,7 @@ const CUSTOM_GRAPH_EVENTS = new Set<string>([
204
204
  GraphEvents.ON_RUN_STEP_DELTA,
205
205
  GraphEvents.ON_RUN_STEP_COMPLETED,
206
206
  GraphEvents.ON_RUN_STEP_CLOSED,
207
+ GraphEvents.ON_TOOL_CALLS_DISPATCHED,
207
208
  GraphEvents.ON_MESSAGE_DELTA,
208
209
  GraphEvents.ON_REASONING_DELTA,
209
210
  GraphEvents.ON_TOOL_EXECUTE,
@@ -1095,6 +1096,7 @@ export class Run<_T extends t.BaseGraphState> {
1095
1096
  | t.RunStep
1096
1097
  | t.RunStepDeltaEvent
1097
1098
  | t.RunStepClosedEvent
1099
+ | t.ToolCallsDispatchedEvent
1098
1100
  | t.MessageDeltaEvent
1099
1101
  | t.ReasoningDeltaEvent
1100
1102
  | { result: t.ToolEndEvent },
package/src/stream.ts CHANGED
@@ -55,13 +55,13 @@ import {
55
55
  calculateMaxToolResultChars,
56
56
  truncateToolResultContent,
57
57
  } from '@/utils/truncation';
58
+ import { createToolCallsDispatchedEvent, safeDispatchCustomEvent } from '@/utils/events';
58
59
  import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
59
- import { formatToolErrorContent } from '@/tools/toolErrorContent';
60
60
  import { snapshotValidatedModelChunk } from '@/graphs/acceptedModelResponse';
61
61
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
62
+ import { formatToolErrorContent } from '@/tools/toolErrorContent';
62
63
  import { PreparedSubagentError } from '@/tools/preparedSubagents';
63
64
  import { isReasoningContentBlock } from '@/messages/core';
64
- import { safeDispatchCustomEvent } from '@/utils/events';
65
65
  import { composeAbortSignals } from '@/utils/misc';
66
66
  import { isGoogleLike } from '@/utils/llm';
67
67
  import { getMessageId } from '@/messages';
@@ -871,6 +871,13 @@ function startEagerToolExecutions(args: {
871
871
  reject,
872
872
  };
873
873
 
874
+ if (graph.config != null) {
875
+ void safeDispatchCustomEvent(
876
+ GraphEvents.ON_TOOL_CALLS_DISPATCHED,
877
+ createToolCallsDispatchedEvent(graph.config, batchRequest.toolCalls),
878
+ graph.config
879
+ );
880
+ }
874
881
  void safeDispatchCustomEvent(
875
882
  GraphEvents.ON_TOOL_EXECUTE,
876
883
  batchRequest,
@@ -980,7 +987,7 @@ async function dispatchEagerToolCompletions(args: {
980
987
  progress: 1,
981
988
  ...(outcome != null && { outcome }),
982
989
  } as t.ProcessedToolCall,
983
- completed_at: Date.now(),
990
+ completed_at: result.received_at ?? Date.now(),
984
991
  },
985
992
  },
986
993
  graph.config
@@ -1701,6 +1708,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1701
1708
  // Callback delivery can beat the producer's iterator. Validate before
1702
1709
  // accounting, run steps, or eager dispatch reads raw tool descriptors.
1703
1710
  chunk = snapshotValidatedModelChunk(chunk as AIMessageChunk);
1711
+ const observedAt = Date.now();
1704
1712
 
1705
1713
  /**
1706
1714
  * Enforced before every content-specific early return below
@@ -1923,6 +1931,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1923
1931
  stepKey,
1924
1932
  toolCallChunks: chunk.tool_call_chunks,
1925
1933
  metadata,
1934
+ observedAt,
1926
1935
  });
1927
1936
  if (canStreamEager) {
1928
1937
  if (runScopeInvalidated()) {
@@ -170,7 +170,7 @@ function stripHostExecutionConfig(
170
170
  }
171
171
  import { convertInjectedMessages } from '@/messages/injected';
172
172
  import { stampSyntheticProviderMessage } from '@/messages/provenance';
173
- import { safeDispatchCustomEvent } from '@/utils/events';
173
+ import { createToolCallsDispatchedEvent, safeDispatchCustomEvent } from '@/utils/events';
174
174
  import { RunnableCallable, composeAbortSignals } from '@/utils';
175
175
  import {
176
176
  executeHooks,
@@ -1672,6 +1672,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1672
1672
  };
1673
1673
  this.throwIfBreakerTripped(config);
1674
1674
  config.signal?.throwIfAborted();
1675
+ if (call.id != null && call.id !== '') {
1676
+ void safeDispatchCustomEvent(
1677
+ GraphEvents.ON_TOOL_CALLS_DISPATCHED,
1678
+ createToolCallsDispatchedEvent(config, [
1679
+ { id: call.id, name: call.name, stepId: this.toolCallStepIds?.get(call.id) },
1680
+ ]),
1681
+ config
1682
+ );
1683
+ }
1675
1684
  return tool.invoke(invokeParams, runtime);
1676
1685
  }
1677
1686
 
@@ -4334,7 +4343,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4334
4343
  const dispatchRequestById = new Map(
4335
4344
  dispatchRequests.map((request) => [request.id, request])
4336
4345
  );
4346
+ const receivedAtById = new Map<string, number>();
4337
4347
  const onResult = (result: t.ToolExecuteResult): void => {
4348
+ const receivedAt = Date.now();
4338
4349
  /** Runtime-honest widening: hosts may omit the id despite the
4339
4350
  * field type. */
4340
4351
  const resultToolCallId = result.toolCallId as string | undefined;
@@ -4349,8 +4360,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4349
4360
  return;
4350
4361
  }
4351
4362
  earlyCompletionDispatchedIds.add(result.toolCallId);
4363
+ receivedAtById.set(result.toolCallId, receivedAt);
4352
4364
  earlyCompletionDispatches.push(
4353
- this.dispatchEarlyToolCompletion(result, request, config).then(
4365
+ this.dispatchEarlyToolCompletion({ ...result, received_at: receivedAt }, request, config).then(
4354
4366
  (dispatched) => {
4355
4367
  if (!dispatched) {
4356
4368
  earlyCompletionDispatchedIds.delete(result.toolCallId);
@@ -4402,6 +4414,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4402
4414
  | undefined,
4403
4415
  signal: config.signal,
4404
4416
  resolve: (results): void => {
4417
+ const receivedAt = Date.now();
4418
+ for (const result of results) {
4419
+ if (dispatchRequestById.has(result.toolCallId)) {
4420
+ receivedAtById.set(result.toolCallId, result.received_at ?? receivedAt);
4421
+ }
4422
+ }
4405
4423
  resultSettled = true;
4406
4424
  settledResults = results;
4407
4425
  maybeResolve();
@@ -4410,6 +4428,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4410
4428
  ...(canEmitEarlyCompletions && { onResult }),
4411
4429
  };
4412
4430
 
4431
+ void safeDispatchCustomEvent(
4432
+ GraphEvents.ON_TOOL_CALLS_DISPATCHED,
4433
+ createToolCallsDispatchedEvent(config, dispatchRequests),
4434
+ config
4435
+ );
4413
4436
  void safeDispatchCustomEvent(
4414
4437
  GraphEvents.ON_TOOL_EXECUTE,
4415
4438
  batchRequest,
@@ -4695,7 +4718,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4695
4718
  request?.turn,
4696
4719
  resolveToolOutcome(request?.args, outcomeFieldsFromResult(result), {
4697
4720
  isError: result.status === 'error',
4698
- })
4721
+ }),
4722
+ receivedAtById.get(result.toolCallId) ?? result.received_at
4699
4723
  );
4700
4724
  }
4701
4725
 
@@ -4997,7 +5021,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4997
5021
  output: string,
4998
5022
  config: RunnableConfig,
4999
5023
  turn?: number,
5000
- outcome?: string
5024
+ outcome?: string,
5025
+ receivedAt?: number
5001
5026
  ): Promise<boolean> {
5002
5027
  const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
5003
5028
  if (!stepId) {
@@ -5024,7 +5049,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5024
5049
  progress: 1,
5025
5050
  ...(outcome != null && { outcome }),
5026
5051
  } as t.ProcessedToolCall,
5027
- completed_at: Date.now(),
5052
+ completed_at: receivedAt ?? Date.now(),
5028
5053
  },
5029
5054
  },
5030
5055
  config
@@ -5064,7 +5089,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5064
5089
  request.turn,
5065
5090
  resolveToolOutcome(request.args, outcomeFieldsFromResult(result), {
5066
5091
  isError: result.status === 'error',
5067
- })
5092
+ }),
5093
+ result.received_at
5068
5094
  );
5069
5095
  }
5070
5096
 
@@ -26,11 +26,13 @@ export async function handleToolCallChunks({
26
26
  stepKey,
27
27
  toolCallChunks,
28
28
  metadata,
29
+ observedAt = Date.now(),
29
30
  }: {
30
31
  graph: StandardGraph | MultiAgentGraph;
31
32
  stepKey: string;
32
33
  toolCallChunks: ToolCallChunk[];
33
34
  metadata?: Record<string, unknown>;
35
+ observedAt?: number;
34
36
  }): Promise<void> {
35
37
  let prevStepId: string;
36
38
  let prevRunStep: t.RunStep | undefined;
@@ -124,7 +126,8 @@ export async function handleToolCallChunks({
124
126
  type: StepTypes.TOOL_CALLS,
125
127
  tool_calls: toolCallChunks,
126
128
  },
127
- metadata
129
+ metadata,
130
+ observedAt
128
131
  );
129
132
  }
130
133