@librechat/agents 3.4.5 → 3.4.7

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 (87) hide show
  1. package/dist/cjs/common/enum.cjs +2 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +268 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuseTraceShaping.cjs +9 -6
  6. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  7. package/dist/cjs/llm/google/utils/common.cjs +10 -5
  8. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  9. package/dist/cjs/run.cjs +147 -14
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/session/AgentSession.cjs +93 -31
  12. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  13. package/dist/cjs/session/handlers.cjs +10 -0
  14. package/dist/cjs/session/handlers.cjs.map +1 -1
  15. package/dist/cjs/stream.cjs +17 -3
  16. package/dist/cjs/stream.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +1 -0
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +4 -2
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/handlers.cjs +1 -0
  22. package/dist/cjs/tools/handlers.cjs.map +1 -1
  23. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +28 -0
  24. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  25. package/dist/cjs/utils/handlers.cjs +3 -0
  26. package/dist/cjs/utils/handlers.cjs.map +1 -1
  27. package/dist/esm/common/enum.mjs +2 -0
  28. package/dist/esm/common/enum.mjs.map +1 -1
  29. package/dist/esm/graphs/Graph.mjs +268 -14
  30. package/dist/esm/graphs/Graph.mjs.map +1 -1
  31. package/dist/esm/langfuseTraceShaping.mjs +9 -6
  32. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  33. package/dist/esm/llm/google/utils/common.mjs +10 -5
  34. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  35. package/dist/esm/run.mjs +147 -14
  36. package/dist/esm/run.mjs.map +1 -1
  37. package/dist/esm/session/AgentSession.mjs +93 -31
  38. package/dist/esm/session/AgentSession.mjs.map +1 -1
  39. package/dist/esm/session/handlers.mjs +10 -0
  40. package/dist/esm/session/handlers.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +17 -3
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/summarization/node.mjs +1 -0
  44. package/dist/esm/summarization/node.mjs.map +1 -1
  45. package/dist/esm/tools/ToolNode.mjs +4 -2
  46. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  47. package/dist/esm/tools/handlers.mjs +1 -0
  48. package/dist/esm/tools/handlers.mjs.map +1 -1
  49. package/dist/esm/tools/subagent/SubagentExecutor.mjs +28 -0
  50. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  51. package/dist/esm/utils/handlers.mjs +3 -0
  52. package/dist/esm/utils/handlers.mjs.map +1 -1
  53. package/dist/types/common/enum.d.ts +2 -0
  54. package/dist/types/graphs/Graph.d.ts +87 -0
  55. package/dist/types/langfuseTraceShaping.d.ts +4 -3
  56. package/dist/types/run.d.ts +9 -1
  57. package/dist/types/session/types.d.ts +1 -1
  58. package/dist/types/summarization/node.d.ts +6 -0
  59. package/dist/types/types/graph.d.ts +3 -3
  60. package/dist/types/types/stream.d.ts +57 -0
  61. package/dist/types/utils/handlers.d.ts +1 -0
  62. package/package.json +1 -1
  63. package/src/aggregator.test.ts +127 -0
  64. package/src/common/enum.ts +2 -0
  65. package/src/graphs/Graph.ts +365 -7
  66. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +385 -0
  67. package/src/langfuseTraceShaping.ts +14 -7
  68. package/src/llm/google/utils/common.test.ts +11 -0
  69. package/src/llm/google/utils/common.ts +6 -4
  70. package/src/run.ts +219 -41
  71. package/src/session/AgentSession.ts +105 -13
  72. package/src/session/handlers.ts +14 -2
  73. package/src/session/types.ts +1 -0
  74. package/src/specs/activity-label-observability.live.test.ts +262 -0
  75. package/src/specs/activity-label-observability.test.ts +167 -0
  76. package/src/specs/langfuse-trace-shaping.test.ts +32 -0
  77. package/src/specs/run-step-timestamps.test.ts +414 -0
  78. package/src/stream.ts +29 -6
  79. package/src/summarization/node.ts +11 -0
  80. package/src/tools/ToolNode.ts +2 -0
  81. package/src/tools/__tests__/SubagentExecutor.test.ts +52 -10
  82. package/src/tools/__tests__/handlers.test.ts +2 -0
  83. package/src/tools/handlers.ts +1 -0
  84. package/src/tools/subagent/SubagentExecutor.ts +46 -4
  85. package/src/types/graph.ts +9 -6
  86. package/src/types/stream.ts +65 -12
  87. package/src/utils/handlers.ts +13 -0
@@ -742,6 +742,17 @@ export abstract class Graph<
742
742
  stepKeyIds: Map<string, string[]> = new Map<string, string[]>();
743
743
  contentIndexMap: Map<string, number> = new Map();
744
744
  toolCallStepIds: Map<string, string> = new Map();
745
+ /** Step ID -> tool call IDs whose completions have not yet arrived. */
746
+ pendingToolCallsByStep: Map<string, Set<string>> = new Map();
747
+ /**
748
+ * Step ID -> latest producer completion time seen for that step. Parallel
749
+ * calls sharing a step can settle out of producer order when their host
750
+ * handlers differ in latency, so the call that happens to drain the set is
751
+ * not necessarily the one that finished last.
752
+ */
753
+ latestCompletionByStep: Map<string, number> = new Map();
754
+ /** Agent key ('' for single-agent) -> currently open MESSAGE_CREATION step ID. */
755
+ openMessageStepByAgent: Map<string, string> = new Map();
745
756
  /**
746
757
  * Step IDs dispatched through the handler registry during this run.
747
758
  * Event echo suppression is tracked separately so repeated deltas for
@@ -858,6 +869,9 @@ export abstract class Graph<
858
869
  this.contentIndexMap = new Map();
859
870
  this.stepKeyIds = new Map();
860
871
  this.toolCallStepIds.clear();
872
+ this.pendingToolCallsByStep.clear();
873
+ this.latestCompletionByStep.clear();
874
+ this.openMessageStepByAgent.clear();
861
875
  this.messageIdsByStepKey = new Map();
862
876
  this.messageStepHasTextDeltas = new Set();
863
877
  this.reasoningStepHasDeltas = new Set();
@@ -934,6 +948,30 @@ export abstract class Graph<
934
948
  }
935
949
  }
936
950
 
951
+ /**
952
+ * Tracks a tool call whose completion must arrive before its step can be
953
+ * considered finished. Registered wherever `toolCallStepIds` gains entries,
954
+ * except cross-process subagent resume restoration, where pending state
955
+ * cannot be faithfully rebuilt and closes fall back to completions/sweep.
956
+ */
957
+ registerPendingToolCall(toolCallId: string, stepId: string): void {
958
+ if (!toolCallId || !stepId) {
959
+ return;
960
+ }
961
+ this.getPendingToolCallSet(stepId).add(toolCallId);
962
+ }
963
+
964
+ /** Lazily creates a step's pending-completions set; callers registering a
965
+ * batch hoist this lookup out of their per-call loop. */
966
+ protected getPendingToolCallSet(stepId: string): Set<string> {
967
+ let pending = this.pendingToolCallsByStep.get(stepId);
968
+ if (!pending) {
969
+ pending = new Set();
970
+ this.pendingToolCallsByStep.set(stepId, pending);
971
+ }
972
+ return pending;
973
+ }
974
+
937
975
  markHandlerDispatchedEvent(eventName: string, stepId: string): () => void {
938
976
  const key = getHandlerDispatchedEventKey(eventName, stepId);
939
977
  this.handlerDispatchedEventCounts.set(
@@ -1441,6 +1479,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1441
1479
  * a stale reference on 2nd+ processStream calls.
1442
1480
  */
1443
1481
  this.toolCallStepIds.clear();
1482
+ this.pendingToolCallsByStep.clear();
1483
+ this.latestCompletionByStep.clear();
1484
+ this.openMessageStepByAgent.clear();
1444
1485
  this.runProducedAiMessageIds.clear();
1445
1486
  this.eagerEventToolExecutions.clear();
1446
1487
  this.clearEagerEventToolUsageCounts();
@@ -1704,6 +1745,288 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1704
1745
  return undefined;
1705
1746
  }
1706
1747
 
1748
+ /**
1749
+ * Derives the same lane key `dispatchRunStep` stamps as `runStep.agentId`.
1750
+ * The multi-agent check gates the lookup because `getAgentContext` signals
1751
+ * a miss by throwing: single-agent graphs key every step under `''`, so
1752
+ * resolving the context could only ever produce a thrown-and-discarded
1753
+ * Error on a per-model-call path.
1754
+ */
1755
+ protected getStepAgentKey(metadata?: Record<string, unknown>): string {
1756
+ if (!metadata || !this.isMultiAgentGraph()) {
1757
+ return '';
1758
+ }
1759
+ try {
1760
+ const agentContext = this.getAgentContext(metadata);
1761
+ if (agentContext.agentId) {
1762
+ return agentContext.agentId;
1763
+ }
1764
+ } catch (_e) {
1765
+ /** No agent context — fall back to the default lane */
1766
+ }
1767
+ return '';
1768
+ }
1769
+
1770
+ /**
1771
+ * O(1) reverse lookup: both dispatch funnels key the open-message map by
1772
+ * `runStep.agentId ?? ''`, so the entry is addressable without scanning.
1773
+ */
1774
+ private untrackRunStep(runStep: t.RunStep): void {
1775
+ this.pendingToolCallsByStep.delete(runStep.id);
1776
+ this.latestCompletionByStep.delete(runStep.id);
1777
+ const agentKey = runStep.agentId ?? '';
1778
+ if (this.openMessageStepByAgent.get(agentKey) === runStep.id) {
1779
+ this.openMessageStepByAgent.delete(agentKey);
1780
+ }
1781
+ }
1782
+
1783
+ /**
1784
+ * Shared step accounting for both dispatch funnels: a successor step in
1785
+ * the same agent lane marks the previous message step as finished — its
1786
+ * CLOSED event must precede the successor's ON_RUN_STEP so hosts observe
1787
+ * a consistent open-step timeline — then the step is registered in the
1788
+ * content maps and tracked as the lane's open message step when
1789
+ * applicable.
1790
+ */
1791
+ protected async trackDispatchedRunStep(
1792
+ runStep: t.RunStep,
1793
+ metadata?: Record<string, unknown>,
1794
+ /**
1795
+ * Summarization steps are typed MESSAGE_CREATION but own an explicit
1796
+ * completion, and their model call emits `CHAT_MODEL_END` well before the
1797
+ * summary is assembled. Tracking one as the lane's open step would let
1798
+ * model-end publish an authoritative `completed` closure early — the
1799
+ * measured duration would exclude the remaining work and a later
1800
+ * post-model failure could no longer change the status. They close
1801
+ * through `recordStepCompletion` instead.
1802
+ */
1803
+ trackAsOpenMessageStep: boolean = true
1804
+ ): Promise<void> {
1805
+ const agentKey = runStep.agentId ?? '';
1806
+ const openMessageStepId = this.openMessageStepByAgent.get(agentKey);
1807
+ /**
1808
+ * Reserve the content index and register the step SYNCHRONOUSLY, before
1809
+ * awaiting the predecessor's closure. Parallel agent lanes dispatch
1810
+ * successors concurrently: if both yielded here first, they would push
1811
+ * with the same `contentData.length` and `contentIndexMap` would resolve
1812
+ * one step id to the other lane's entry, so later deltas and completions
1813
+ * would mutate the wrong agent's step. Assigning the index at push time
1814
+ * also supersedes whatever the caller computed before this await point.
1815
+ */
1816
+ runStep.index = this.contentData.length;
1817
+ this.contentData.push(runStep);
1818
+ this.contentIndexMap.set(runStep.id, runStep.index);
1819
+ if (trackAsOpenMessageStep && runStep.type === StepTypes.MESSAGE_CREATION) {
1820
+ this.openMessageStepByAgent.set(agentKey, runStep.id);
1821
+ } else {
1822
+ this.openMessageStepByAgent.delete(agentKey);
1823
+ }
1824
+ /**
1825
+ * Awaited after registration but before the caller dispatches this step's
1826
+ * ON_RUN_STEP, so the predecessor's CLOSED event still precedes the
1827
+ * successor's start event.
1828
+ */
1829
+ if (openMessageStepId != null && openMessageStepId !== runStep.id) {
1830
+ /**
1831
+ * Isolated: this step is already registered in `contentData`, so a
1832
+ * predecessor delivery failure rejecting here would abort the caller
1833
+ * before it publishes this step's ON_RUN_STEP — leaving the sweep to
1834
+ * emit a terminal event for a step that never announced a start.
1835
+ */
1836
+ try {
1837
+ await this.closeRunStep(openMessageStepId, 'completed', { metadata });
1838
+ } catch (_e) {
1839
+ /** Predecessor delivery must not abort the successor's lifecycle */
1840
+ }
1841
+ }
1842
+ /**
1843
+ * Stamped last, after the predecessor's closure has been delivered and
1844
+ * immediately before the caller publishes this step's ON_RUN_STEP.
1845
+ * `created_at` documents when the step was dispatched, so it must not
1846
+ * absorb the latency of an arbitrarily slow predecessor handler.
1847
+ */
1848
+ runStep.created_at = Date.now();
1849
+ }
1850
+
1851
+ /**
1852
+ * Closes a run step: stamps its terminal status + timestamp on the stored
1853
+ * `RunStep` and emits `ON_RUN_STEP_CLOSED`. First close wins — later calls
1854
+ * are no-ops — except a `restamp` close, which lets a `completed`
1855
+ * TOOL_CALLS step refresh `completed_at` when a late-registered parallel
1856
+ * tool call finishes after the step already closed (the eager-execution
1857
+ * race). `cancelled`/`failed` are immutable once stamped.
1858
+ */
1859
+ async closeRunStep(
1860
+ stepId: string,
1861
+ status: Exclude<t.RunStepStatus, 'in_progress'>,
1862
+ options?: t.RunStepCloseOptions
1863
+ ): Promise<boolean> {
1864
+ if (!stepId) {
1865
+ return false;
1866
+ }
1867
+ const runStep = this.getRunStep(stepId);
1868
+ if (!runStep) {
1869
+ return false;
1870
+ }
1871
+ if (runStep.status != null && runStep.status !== 'in_progress') {
1872
+ this.untrackRunStep(runStep);
1873
+ return false;
1874
+ }
1875
+
1876
+ const closedAt = options?.at ?? Date.now();
1877
+ runStep.status = status;
1878
+ if (status === 'completed') {
1879
+ runStep.completed_at = closedAt;
1880
+ } else if (status === 'cancelled') {
1881
+ runStep.cancelled_at = closedAt;
1882
+ } else {
1883
+ runStep.failed_at = closedAt;
1884
+ }
1885
+
1886
+ const closedEvent: t.RunStepClosedEvent = {
1887
+ id: stepId,
1888
+ index: runStep.index,
1889
+ type: runStep.type,
1890
+ status,
1891
+ closed_at: closedAt,
1892
+ };
1893
+ if (runStep.created_at != null) {
1894
+ closedEvent.created_at = runStep.created_at;
1895
+ }
1896
+ if (runStep.runId != null) {
1897
+ closedEvent.runId = runStep.runId;
1898
+ }
1899
+ if (runStep.agentId != null) {
1900
+ closedEvent.agentId = runStep.agentId;
1901
+ }
1902
+ if (runStep.groupId != null) {
1903
+ closedEvent.groupId = runStep.groupId;
1904
+ }
1905
+ if (runStep.stepIndex != null) {
1906
+ closedEvent.stepIndex = runStep.stepIndex;
1907
+ }
1908
+ this.untrackRunStep(runStep);
1909
+
1910
+ const handler = this.handlerRegistry?.getHandler(
1911
+ GraphEvents.ON_RUN_STEP_CLOSED
1912
+ );
1913
+ if (handler) {
1914
+ await handler.handle(
1915
+ GraphEvents.ON_RUN_STEP_CLOSED,
1916
+ closedEvent,
1917
+ options?.metadata,
1918
+ this
1919
+ );
1920
+ this.handlerDispatchedStepIds.add(stepId);
1921
+ }
1922
+ const unmarkHandlerDispatchedEvent = handler
1923
+ ? this.markHandlerDispatchedEvent(GraphEvents.ON_RUN_STEP_CLOSED, stepId)
1924
+ : undefined;
1925
+ try {
1926
+ if (this.config) {
1927
+ await safeDispatchCustomEvent(
1928
+ GraphEvents.ON_RUN_STEP_CLOSED,
1929
+ closedEvent,
1930
+ this.config
1931
+ );
1932
+ }
1933
+ } finally {
1934
+ unmarkHandlerDispatchedEvent?.();
1935
+ }
1936
+ return true;
1937
+ }
1938
+
1939
+ /**
1940
+ * Observes one `ON_RUN_STEP_COMPLETED` for a step and closes the step when
1941
+ * no registered tool calls remain pending. Steps without pending tracking
1942
+ * (summaries, cross-process resume) close on their first completion; the
1943
+ * terminal-status guard in `closeRunStep` absorbs duplicate echoes.
1944
+ */
1945
+ async recordStepCompletion(
1946
+ stepId: string,
1947
+ options?: t.RecordStepCompletionOptions
1948
+ ): Promise<void> {
1949
+ if (!stepId) {
1950
+ return;
1951
+ }
1952
+ const { toolCallId, metadata, at } = options ?? {};
1953
+ if (at != null) {
1954
+ const latest = this.latestCompletionByStep.get(stepId);
1955
+ if (latest == null || at > latest) {
1956
+ this.latestCompletionByStep.set(stepId, at);
1957
+ }
1958
+ }
1959
+ const closeAt = this.latestCompletionByStep.get(stepId) ?? at;
1960
+ const pending = this.pendingToolCallsByStep.get(stepId);
1961
+ if (pending == null) {
1962
+ await this.closeRunStep(stepId, 'completed', { metadata, at: closeAt });
1963
+ return;
1964
+ }
1965
+ if (toolCallId != null && toolCallId !== '') {
1966
+ pending.delete(toolCallId);
1967
+ }
1968
+ if (pending.size > 0) {
1969
+ return;
1970
+ }
1971
+ await this.closeRunStep(stepId, 'completed', { metadata, at: closeAt });
1972
+ }
1973
+
1974
+ /**
1975
+ * Closes the tracked open MESSAGE_CREATION step for the event's agent lane.
1976
+ * Fires on every model end, so the empty-map check short-circuits ahead of
1977
+ * resolving the lane key — a turn whose message step already closed through
1978
+ * successor-close does no work here.
1979
+ */
1980
+ async closeOpenMessageStep(
1981
+ metadata?: Record<string, unknown>,
1982
+ /** Model-end time captured before host handlers ran, so a slow usage sink
1983
+ * cannot inflate the step's measured duration. */
1984
+ at?: number
1985
+ ): Promise<void> {
1986
+ if (this.openMessageStepByAgent.size === 0) {
1987
+ return;
1988
+ }
1989
+ const agentKey = this.getStepAgentKey(metadata);
1990
+ const openStepId = this.openMessageStepByAgent.get(agentKey);
1991
+ if (openStepId == null) {
1992
+ return;
1993
+ }
1994
+ await this.closeRunStep(openStepId, 'completed', { metadata, at });
1995
+ }
1996
+
1997
+ /**
1998
+ * End-of-run sweep: closes every step that never reached a terminal
1999
+ * status. Dual-dispatches like any other close — the custom-event channel
2000
+ * is usually already torn down here and `safeDispatchCustomEvent` reports
2001
+ * that quietly, but callback-only subscribers still receive the terminal
2002
+ * signal whenever it is alive.
2003
+ */
2004
+ async closeUnfinishedRunSteps(
2005
+ status: Exclude<t.RunStepStatus, 'in_progress'>,
2006
+ at?: number
2007
+ ): Promise<void> {
2008
+ const closedAt = at ?? Date.now();
2009
+ for (const runStep of this.contentData) {
2010
+ if (runStep.status != null && runStep.status !== 'in_progress') {
2011
+ continue;
2012
+ }
2013
+ /**
2014
+ * Isolated per step: one host handler throwing must not strand the
2015
+ * remaining steps `in_progress` with no terminal event. The step is
2016
+ * stamped before dispatch either way, so a thrown handler still leaves
2017
+ * consistent state behind.
2018
+ */
2019
+ try {
2020
+ await this.closeRunStep(runStep.id, status, { at: closedAt });
2021
+ } catch (_e) {
2022
+ /** Delivery failure for one step must not halt the sweep */
2023
+ }
2024
+ }
2025
+ this.pendingToolCallsByStep.clear();
2026
+ this.latestCompletionByStep.clear();
2027
+ this.openMessageStepByAgent.clear();
2028
+ }
2029
+
1707
2030
  getAgentContext(metadata: Record<string, unknown> | undefined): AgentContext {
1708
2031
  if (!metadata) {
1709
2032
  throw new Error('No metadata provided to retrieve agent context');
@@ -4591,8 +4914,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4591
4914
  runStep.groupId = groupId;
4592
4915
  }
4593
4916
  }
4594
- this.contentData.push(runStep);
4595
- this.contentIndexMap.set(runStep.id, runStep.index);
4917
+ runStep.status ??= 'in_progress';
4918
+ await this.trackDispatchedRunStep(
4919
+ runStep,
4920
+ resolvedConfig?.metadata,
4921
+ false
4922
+ );
4596
4923
 
4597
4924
  const handler = this.handlerRegistry?.getHandler(
4598
4925
  GraphEvents.ON_RUN_STEP
@@ -4625,13 +4952,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4625
4952
  unmarkHandlerDispatchedEvent?.();
4626
4953
  }
4627
4954
  },
4955
+ closeRunStep: async (
4956
+ stepId: string,
4957
+ status: Exclude<t.RunStepStatus, 'in_progress'>,
4958
+ nodeConfig?: RunnableConfig
4959
+ ) => {
4960
+ await this.closeRunStep(stepId, status, {
4961
+ metadata: (nodeConfig ?? this.config)?.metadata,
4962
+ });
4963
+ },
4628
4964
  dispatchRunStepCompleted: async (
4629
4965
  stepId: string,
4630
4966
  result: t.StepCompleted,
4631
4967
  nodeConfig?: RunnableConfig
4632
4968
  ) => {
4633
4969
  const resolvedConfig = nodeConfig ?? this.config;
4634
- const runStep = this.contentData.find((s) => s.id === stepId);
4970
+ const completedAt = Date.now();
4971
+ const runStep = this.getRunStep(stepId);
4635
4972
  const handler = this.handlerRegistry?.getHandler(
4636
4973
  GraphEvents.ON_RUN_STEP_COMPLETED
4637
4974
  );
@@ -4643,12 +4980,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4643
4980
  ...result,
4644
4981
  id: stepId,
4645
4982
  index: runStep?.index ?? 0,
4983
+ completed_at: completedAt,
4646
4984
  },
4647
4985
  },
4648
4986
  resolvedConfig?.configurable,
4649
4987
  this
4650
4988
  );
4651
4989
  }
4990
+ await this.recordStepCompletion(stepId, {
4991
+ metadata: resolvedConfig?.metadata,
4992
+ at: completedAt,
4993
+ });
4652
4994
  },
4653
4995
  },
4654
4996
  generateStepId: (stepKey: string) => this.generateStepId(stepKey),
@@ -4757,12 +5099,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4757
5099
 
4758
5100
  const [stepId, stepIndex] = this.generateStepId(stepKey);
4759
5101
  if (stepDetails.type === StepTypes.TOOL_CALLS && stepDetails.tool_calls) {
5102
+ let pendingToolCalls: Set<string> | undefined;
4760
5103
  for (const tool_call of stepDetails.tool_calls) {
4761
5104
  const toolCallId = tool_call.id ?? '';
4762
5105
  if (!toolCallId || this.toolCallStepIds.has(toolCallId)) {
4763
5106
  continue;
4764
5107
  }
4765
5108
  this.toolCallStepIds.set(toolCallId, stepId);
5109
+ pendingToolCalls ??= this.getPendingToolCallSet(stepId);
5110
+ pendingToolCalls.add(toolCallId);
4766
5111
  }
4767
5112
  }
4768
5113
 
@@ -4773,6 +5118,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4773
5118
  index: this.contentData.length,
4774
5119
  stepDetails,
4775
5120
  usage: null,
5121
+ status: 'in_progress',
4776
5122
  };
4777
5123
 
4778
5124
  const runId = this.runId ?? '';
@@ -4780,10 +5126,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4780
5126
  runStep.runId = runId;
4781
5127
  }
4782
5128
 
4783
- if (metadata) {
5129
+ /**
5130
+ * `agentId`/`groupId` are multi-agent-only, and `getAgentContext` signals a
5131
+ * miss by throwing — so for a single-agent graph the lookup could only ever
5132
+ * build and discard an Error while producing the same undefined fields.
5133
+ * The constant-time check gates it out of the per-step dispatch path.
5134
+ */
5135
+ if (metadata && this.isMultiAgentGraph()) {
4784
5136
  try {
4785
5137
  const agentContext = this.getAgentContext(metadata);
4786
- if (this.isMultiAgentGraph() && agentContext.agentId) {
5138
+ if (agentContext.agentId) {
4787
5139
  runStep.agentId = agentContext.agentId;
4788
5140
  const groupId = this.resolveParallelGroupId(
4789
5141
  agentContext.agentId,
@@ -4798,8 +5150,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4798
5150
  }
4799
5151
  }
4800
5152
 
4801
- this.contentData.push(runStep);
4802
- this.contentIndexMap.set(stepId, runStep.index);
5153
+ await this.trackDispatchedRunStep(runStep, metadata);
4803
5154
 
4804
5155
  // Primary dispatch: handler registry (reliable, always works).
4805
5156
  // This mirrors how handleToolCallCompleted dispatches ON_RUN_STEP_COMPLETED
@@ -4877,6 +5228,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4877
5228
  return false;
4878
5229
  }
4879
5230
 
5231
+ const completedAt = Date.now();
4880
5232
  const tool_call: t.ProcessedToolCall = {
4881
5233
  id: data.id,
4882
5234
  name: name || '',
@@ -4904,11 +5256,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4904
5256
  index: runStep.index,
4905
5257
  type: 'tool_call',
4906
5258
  tool_call,
5259
+ completed_at: completedAt,
4907
5260
  } as t.ToolCompleteEvent,
4908
5261
  },
4909
5262
  metadata,
4910
5263
  graph
4911
5264
  );
5265
+ await graph.recordStepCompletion(stepId, {
5266
+ toolCallId: data.id,
5267
+ metadata,
5268
+ at: completedAt,
5269
+ });
4912
5270
  return true;
4913
5271
  }
4914
5272