@librechat/agents 3.4.4 → 3.4.6
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/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +268 -14
- package/dist/cjs/graphs/Graph.cjs.map +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/langfuseTraceShaping.cjs +9 -6
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -0
- package/dist/cjs/run.cjs +147 -14
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +93 -31
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +10 -0
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +17 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +1 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +4 -2
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -0
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +28 -0
- 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/handlers.cjs +3 -0
- package/dist/cjs/utils/handlers.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +268 -14
- package/dist/esm/graphs/Graph.mjs.map +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/langfuseTraceShaping.mjs +9 -6
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -1
- package/dist/esm/run.mjs +147 -14
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +93 -31
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +10 -0
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +17 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +1 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +4 -2
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -0
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +28 -0
- 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/handlers.mjs +3 -0
- package/dist/esm/utils/handlers.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +87 -0
- 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/langfuseTraceShaping.d.ts +4 -3
- package/dist/types/run.d.ts +9 -1
- package/dist/types/session/types.d.ts +1 -1
- package/dist/types/summarization/node.d.ts +6 -0
- package/dist/types/types/graph.d.ts +3 -3
- package/dist/types/types/hitl.d.ts +31 -2
- package/dist/types/types/stream.d.ts +57 -0
- package/dist/types/utils/handlers.d.ts +1 -0
- package/package.json +2 -1
- package/src/aggregator.test.ts +127 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +365 -7
- package/src/graphs/__tests__/Graph.closeRunStep.test.ts +385 -0
- package/src/hitl/askUserQuestions.ts +126 -0
- package/src/hitl/askUserQuestionsInterrupt.ts +115 -0
- package/src/hitl/index.ts +6 -0
- package/src/langfuseTraceShaping.ts +14 -7
- package/src/run.ts +219 -41
- package/src/session/AgentSession.ts +105 -13
- package/src/session/handlers.ts +14 -2
- package/src/session/types.ts +1 -0
- package/src/specs/activity-label-observability.live.test.ts +262 -0
- package/src/specs/activity-label-observability.test.ts +167 -0
- package/src/specs/ask-user-questions.live.test.ts +185 -0
- package/src/specs/ask-user-questions.test.ts +293 -0
- package/src/specs/langfuse-trace-shaping.test.ts +32 -0
- package/src/specs/run-step-timestamps.test.ts +414 -0
- package/src/stream.ts +29 -6
- package/src/summarization/node.ts +11 -0
- package/src/tools/ToolNode.ts +2 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +52 -10
- package/src/tools/__tests__/handlers.test.ts +2 -0
- package/src/tools/handlers.ts +1 -0
- package/src/tools/subagent/SubagentExecutor.ts +46 -4
- package/src/types/graph.ts +9 -6
- package/src/types/hitl.ts +36 -2
- package/src/types/stream.ts +65 -12
- package/src/utils/handlers.ts +13 -0
package/src/aggregator.test.ts
CHANGED
|
@@ -1085,3 +1085,130 @@ describe('ContentAggregator multi-entry deltas', () => {
|
|
|
1085
1085
|
});
|
|
1086
1086
|
});
|
|
1087
1087
|
});
|
|
1088
|
+
|
|
1089
|
+
describe('ContentAggregator citation deltas', () => {
|
|
1090
|
+
const citation = (url: string, cited_text: string) => ({
|
|
1091
|
+
type: 'web_search_result_location',
|
|
1092
|
+
url,
|
|
1093
|
+
title: 'Example',
|
|
1094
|
+
cited_text,
|
|
1095
|
+
encrypted_index: 'enc',
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Anthropic emits a search turn's citations as their own `citations_delta`,
|
|
1100
|
+
* which `_makeMessageChunkFromAnthropicEvent` normalizes into a `text` part
|
|
1101
|
+
* carrying `citations` and **no** `text` key.
|
|
1102
|
+
*/
|
|
1103
|
+
const citationDelta = (id: string, ...citations: unknown[]) => ({
|
|
1104
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
1105
|
+
data: {
|
|
1106
|
+
id,
|
|
1107
|
+
delta: { content: [{ type: ContentTypes.TEXT, citations }] },
|
|
1108
|
+
} as t.MessageDeltaEvent,
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const textDelta = (id: string, text: string) => ({
|
|
1112
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
1113
|
+
data: {
|
|
1114
|
+
id,
|
|
1115
|
+
delta: { content: [{ type: ContentTypes.TEXT, text }] },
|
|
1116
|
+
} as t.MessageDeltaEvent,
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it('keeps citations that arrive without accompanying text', () => {
|
|
1120
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
1121
|
+
aggregateContent({
|
|
1122
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
1123
|
+
data: createRunStep('step_citations'),
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
aggregateContent(textDelta('step_citations', 'Answer.'));
|
|
1127
|
+
aggregateContent(
|
|
1128
|
+
citationDelta('step_citations', citation('https://a.example', 'quoted a'))
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
expect(contentParts[0]).toEqual({
|
|
1132
|
+
type: ContentTypes.TEXT,
|
|
1133
|
+
text: 'Answer.',
|
|
1134
|
+
citations: [citation('https://a.example', 'quoted a')],
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it('accumulates citations across successive deltas', () => {
|
|
1139
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
1140
|
+
aggregateContent({
|
|
1141
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
1142
|
+
data: createRunStep('step_multi'),
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
aggregateContent(textDelta('step_multi', 'Part one. '));
|
|
1146
|
+
aggregateContent(
|
|
1147
|
+
citationDelta('step_multi', citation('https://a.example', 'quoted a'))
|
|
1148
|
+
);
|
|
1149
|
+
aggregateContent(textDelta('step_multi', 'Part two.'));
|
|
1150
|
+
aggregateContent(
|
|
1151
|
+
citationDelta('step_multi', citation('https://b.example', 'quoted b'))
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
expect(contentParts[0]).toEqual({
|
|
1155
|
+
type: ContentTypes.TEXT,
|
|
1156
|
+
text: 'Part one. Part two.',
|
|
1157
|
+
citations: [
|
|
1158
|
+
citation('https://a.example', 'quoted a'),
|
|
1159
|
+
citation('https://b.example', 'quoted b'),
|
|
1160
|
+
],
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it('leaves parts without citations untouched', () => {
|
|
1165
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
1166
|
+
aggregateContent({
|
|
1167
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
1168
|
+
data: createRunStep('step_plain'),
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
aggregateContent(textDelta('step_plain', 'Hello '));
|
|
1172
|
+
aggregateContent(textDelta('step_plain', 'world.'));
|
|
1173
|
+
|
|
1174
|
+
expect(contentParts[0]).toEqual({
|
|
1175
|
+
type: ContentTypes.TEXT,
|
|
1176
|
+
text: 'Hello world.',
|
|
1177
|
+
});
|
|
1178
|
+
expect(contentParts[0]).not.toHaveProperty('citations');
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
it('preserves tool_call_ids when a citations-only delta follows', () => {
|
|
1182
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
1183
|
+
aggregateContent({
|
|
1184
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
1185
|
+
data: createRunStep('step_tool_ids'),
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
aggregateContent({
|
|
1189
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
1190
|
+
data: {
|
|
1191
|
+
id: 'step_tool_ids',
|
|
1192
|
+
delta: {
|
|
1193
|
+
content: [
|
|
1194
|
+
{
|
|
1195
|
+
type: ContentTypes.TEXT,
|
|
1196
|
+
text: 'Answer.',
|
|
1197
|
+
tool_call_ids: ['call_1'],
|
|
1198
|
+
},
|
|
1199
|
+
],
|
|
1200
|
+
},
|
|
1201
|
+
} as t.MessageDeltaEvent,
|
|
1202
|
+
});
|
|
1203
|
+
aggregateContent(
|
|
1204
|
+
citationDelta('step_tool_ids', citation('https://a.example', 'quoted a'))
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
expect(contentParts[0]).toEqual({
|
|
1208
|
+
type: ContentTypes.TEXT,
|
|
1209
|
+
text: 'Answer.',
|
|
1210
|
+
tool_call_ids: ['call_1'],
|
|
1211
|
+
citations: [citation('https://a.example', 'quoted a')],
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
});
|
package/src/common/enum.ts
CHANGED
|
@@ -15,6 +15,8 @@ export enum GraphEvents {
|
|
|
15
15
|
ON_RUN_STEP_DELTA = 'on_run_step_delta',
|
|
16
16
|
/** [Custom] Completed event for run steps (tool calls) */
|
|
17
17
|
ON_RUN_STEP_COMPLETED = 'on_run_step_completed',
|
|
18
|
+
/** [Custom] Terminal signal for a run step: closed with status + timestamps */
|
|
19
|
+
ON_RUN_STEP_CLOSED = 'on_run_step_closed',
|
|
18
20
|
/** [Custom] Delta events for messages */
|
|
19
21
|
ON_MESSAGE_DELTA = 'on_message_delta',
|
|
20
22
|
/** [Custom] Reasoning Delta events for messages */
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
-
|
|
4595
|
-
this.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|
|