@librechat/agents 3.6.1 → 3.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +3 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +36 -0
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/messages/core.cjs +3 -0
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -5
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +2 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +3 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +37 -1
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/core.mjs +3 -1
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -5
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +2 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +5 -4
- package/dist/types/messages/core.d.ts +2 -0
- package/package.json +4 -4
- package/src/graphs/Graph.ts +42 -22
- package/src/llm/openai/index.ts +96 -0
- package/src/messages/core.ts +5 -0
- package/src/run.ts +16 -5
- package/src/tools/subagent/SubagentExecutor.ts +17 -3
package/src/graphs/Graph.ts
CHANGED
|
@@ -161,6 +161,7 @@ import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
|
|
|
161
161
|
import { buildSubagentToolParams } from '@/tools/SubagentTool';
|
|
162
162
|
import { initializeLangfuseTracing } from '@/instrumentation';
|
|
163
163
|
import { shouldTriggerSummarization } from '@/summarization';
|
|
164
|
+
import { isRunStepResumeState } from '@/tools/runStepResume';
|
|
164
165
|
import { resolveLocalToolsForBinding } from '@/tools/local';
|
|
165
166
|
import { createSummarizeNode } from '@/summarization/node';
|
|
166
167
|
import { messagesStateReducer } from '@/messages/reducer';
|
|
@@ -168,7 +169,6 @@ import { createSchemaOnlyTools } from '@/tools/schema';
|
|
|
168
169
|
import { AgentContext } from '@/agents/AgentContext';
|
|
169
170
|
import { createFakeStreamingLLM } from '@/llm/fake';
|
|
170
171
|
import { handleToolCalls } from '@/tools/handlers';
|
|
171
|
-
import { isRunStepResumeState } from '@/tools/runStepResume';
|
|
172
172
|
import { isThinkingEnabled } from '@/llm/request';
|
|
173
173
|
import { resolveMaxSeals } from '@/llm/preempt';
|
|
174
174
|
import { initializeModel } from '@/llm/init';
|
|
@@ -1800,9 +1800,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1800
1800
|
version: 1,
|
|
1801
1801
|
revision: this.runStepStateRevision,
|
|
1802
1802
|
nextIndex: this.nextContentIndex,
|
|
1803
|
-
toolCallSteps: [...this.toolCallStepIds].map(
|
|
1804
|
-
|
|
1805
|
-
|
|
1803
|
+
toolCallSteps: [...this.toolCallStepIds].map(([toolCallId, stepId]) => ({
|
|
1804
|
+
toolCallId,
|
|
1805
|
+
stepId,
|
|
1806
|
+
})),
|
|
1806
1807
|
steps,
|
|
1807
1808
|
};
|
|
1808
1809
|
}
|
|
@@ -1836,10 +1837,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1836
1837
|
this.pendingToolCallsByStep.set(runStep.id, pending);
|
|
1837
1838
|
}
|
|
1838
1839
|
if (entry.latestCompletionAt != null) {
|
|
1839
|
-
this.latestCompletionByStep.set(
|
|
1840
|
-
runStep.id,
|
|
1841
|
-
entry.latestCompletionAt
|
|
1842
|
-
);
|
|
1840
|
+
this.latestCompletionByStep.set(runStep.id, entry.latestCompletionAt);
|
|
1843
1841
|
}
|
|
1844
1842
|
if (entry.openMessageStep) {
|
|
1845
1843
|
this.openMessageStepByAgent.set(runStep.agentId ?? '', runStep.id);
|
|
@@ -1964,10 +1962,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1964
1962
|
/**
|
|
1965
1963
|
* Closes a run step: stamps its terminal status + timestamp on the stored
|
|
1966
1964
|
* `RunStep` and emits `ON_RUN_STEP_CLOSED`. First close wins — later calls
|
|
1967
|
-
* are no-ops
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1965
|
+
* are no-ops with no exceptions; every terminal status is immutable once
|
|
1966
|
+
* stamped. (A `restamp` option once let a completed TOOL_CALLS step
|
|
1967
|
+
* refresh `completed_at` for the eager-execution race, but that race is
|
|
1968
|
+
* unreachable — each step registers its calls before any completion can
|
|
1969
|
+
* reference it — and the mechanism was removed.)
|
|
1971
1970
|
*/
|
|
1972
1971
|
async closeRunStep(
|
|
1973
1972
|
stepId: string,
|
|
@@ -2025,12 +2024,36 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2025
2024
|
GraphEvents.ON_RUN_STEP_CLOSED
|
|
2026
2025
|
);
|
|
2027
2026
|
if (handler) {
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2027
|
+
/**
|
|
2028
|
+
* Isolated, unlike the other dual-dispatch sites, because this one
|
|
2029
|
+
* reports state that is already committed: the step was stamped
|
|
2030
|
+
* terminal and untracked above, and nothing a failed delivery can do
|
|
2031
|
+
* will undo that. Propagating instead costs two things.
|
|
2032
|
+
*
|
|
2033
|
+
* First, it fails an entire run over an observational event —
|
|
2034
|
+
* `closeOpenMessageStep` awaits this inside the stream loop on every
|
|
2035
|
+
* CHAT_MODEL_END, where a rejection sets `streamThrew` and fires the
|
|
2036
|
+
* StopFailure hooks for a response that was fully delivered.
|
|
2037
|
+
*
|
|
2038
|
+
* Second, and worse, it skips the secondary custom-event dispatch
|
|
2039
|
+
* below. That channel exists precisely as the fallback for when the
|
|
2040
|
+
* primary path does not deliver, so letting the primary's failure
|
|
2041
|
+
* suppress it removes the redundancy exactly when it is needed.
|
|
2042
|
+
*
|
|
2043
|
+
* `closeUnfinishedRunSteps` and `dispatchRunStep` already wrap their
|
|
2044
|
+
* own calls for the same reason; this closes the gap inside, which
|
|
2045
|
+
* those wrappers cannot reach.
|
|
2046
|
+
*/
|
|
2047
|
+
try {
|
|
2048
|
+
await handler.handle(
|
|
2049
|
+
GraphEvents.ON_RUN_STEP_CLOSED,
|
|
2050
|
+
closedEvent,
|
|
2051
|
+
options?.metadata,
|
|
2052
|
+
this
|
|
2053
|
+
);
|
|
2054
|
+
} catch (_e) {
|
|
2055
|
+
/** Host delivery failure must not fail the run or block the echo */
|
|
2056
|
+
}
|
|
2034
2057
|
this.handlerDispatchedStepIds.add(stepId);
|
|
2035
2058
|
}
|
|
2036
2059
|
const unmarkHandlerDispatchedEvent = handler
|
|
@@ -4990,10 +5013,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
4990
5013
|
.addNode(agentNode, (state, config) =>
|
|
4991
5014
|
invokeWithRunStepState(state, config, () => callModel(state, config))
|
|
4992
5015
|
)
|
|
4993
|
-
.addNode(
|
|
4994
|
-
toolNode,
|
|
4995
|
-
callTools
|
|
4996
|
-
)
|
|
5016
|
+
.addNode(toolNode, callTools)
|
|
4997
5017
|
.addNode(
|
|
4998
5018
|
summarizeNode,
|
|
4999
5019
|
createSummarizeNode({
|
package/src/llm/openai/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import type { SeenScalarMetadata } from './streamMetadata';
|
|
|
43
43
|
import type { HeaderValue, HeadersLike } from './types';
|
|
44
44
|
import type { PromptCacheTtl } from '@/messages/cache';
|
|
45
45
|
import {
|
|
46
|
+
OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY,
|
|
46
47
|
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
|
|
47
48
|
projectOpenAIResponsesToolMessageContent,
|
|
48
49
|
projectToolStreamContentForProvider,
|
|
@@ -606,6 +607,85 @@ function isResponsesReplayOutputItem(item: unknown): boolean {
|
|
|
606
607
|
);
|
|
607
608
|
}
|
|
608
609
|
|
|
610
|
+
type ResponsesReasoningSlot = {
|
|
611
|
+
encrypted_content?: string;
|
|
612
|
+
id?: string;
|
|
613
|
+
status?: string;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
function getResponsesReasoningSlot(
|
|
617
|
+
reasoning: unknown
|
|
618
|
+
): ResponsesReasoningSlot | undefined {
|
|
619
|
+
return typeof reasoning === 'object' && reasoning != null
|
|
620
|
+
? (reasoning as ResponsesReasoningSlot)
|
|
621
|
+
: undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function isSealedReasoningSlot(
|
|
625
|
+
slot: ResponsesReasoningSlot | undefined
|
|
626
|
+
): slot is ResponsesReasoningSlot & { encrypted_content: string } {
|
|
627
|
+
return (
|
|
628
|
+
typeof slot?.encrypted_content === 'string' &&
|
|
629
|
+
slot.encrypted_content.length > 0
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function resolveActiveReasoningItemId(
|
|
634
|
+
incoming: ResponsesReasoningSlot | undefined,
|
|
635
|
+
carried: unknown
|
|
636
|
+
): string | undefined {
|
|
637
|
+
if (typeof incoming?.id === 'string' && incoming.id.length > 0) {
|
|
638
|
+
return incoming.id;
|
|
639
|
+
}
|
|
640
|
+
return typeof carried === 'string' ? carried : undefined;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* A single `additional_kwargs.reasoning` slot has to stand in for a turn that
|
|
645
|
+
* can emit many reasoning items, and the chunk merge folds it field by field:
|
|
646
|
+
* `encrypted_content` and `status` are strings, so they concatenate, while
|
|
647
|
+
* `id` takes whichever item arrived last. An interrupted turn replays from
|
|
648
|
+
* that slot, handing the provider one item id welded to every item's
|
|
649
|
+
* ciphertext — rejected as "Encrypted content could not be decrypted or
|
|
650
|
+
* parsed". Keep the slot describing whichever item most recently sealed, so
|
|
651
|
+
* the id, its ciphertext, and its status always come from the same item.
|
|
652
|
+
*
|
|
653
|
+
* `activeItemId` tracks the item currently streaming, which is the one a
|
|
654
|
+
* terminal `encrypted_content` belongs to: the slot's own id has already been
|
|
655
|
+
* pinned back to the last sealed item by an earlier merge.
|
|
656
|
+
*/
|
|
657
|
+
function resealReasoningItemBoundary(
|
|
658
|
+
combined: AIMessageChunk,
|
|
659
|
+
accumulated: ResponsesReasoningSlot | undefined,
|
|
660
|
+
incoming: ResponsesReasoningSlot | undefined,
|
|
661
|
+
activeItemId: string | undefined
|
|
662
|
+
): void {
|
|
663
|
+
const merged = getResponsesReasoningSlot(
|
|
664
|
+
combined.additional_kwargs.reasoning
|
|
665
|
+
);
|
|
666
|
+
if (merged == null || incoming == null) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (isSealedReasoningSlot(incoming)) {
|
|
670
|
+
combined.additional_kwargs.reasoning = {
|
|
671
|
+
...merged,
|
|
672
|
+
id: activeItemId ?? merged.id,
|
|
673
|
+
encrypted_content: incoming.encrypted_content,
|
|
674
|
+
status: incoming.status,
|
|
675
|
+
};
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (!isSealedReasoningSlot(accumulated)) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
combined.additional_kwargs.reasoning = {
|
|
682
|
+
...merged,
|
|
683
|
+
id: accumulated.id,
|
|
684
|
+
encrypted_content: accumulated.encrypted_content,
|
|
685
|
+
status: accumulated.status,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
609
689
|
/**
|
|
610
690
|
* LangChain's Responses converter places the authoritative terminal output in
|
|
611
691
|
* response_metadata.output. Its chunk merge has no way to delete provisional
|
|
@@ -619,10 +699,26 @@ class ResponsesReplayAIMessageChunk extends AIMessageChunk {
|
|
|
619
699
|
}
|
|
620
700
|
|
|
621
701
|
override concat(chunk: AIMessageChunk): this {
|
|
702
|
+
const accumulated = getResponsesReasoningSlot(
|
|
703
|
+
this.additional_kwargs.reasoning
|
|
704
|
+
);
|
|
705
|
+
const incoming = getResponsesReasoningSlot(
|
|
706
|
+
chunk.additional_kwargs.reasoning
|
|
707
|
+
);
|
|
708
|
+
const activeItemId = resolveActiveReasoningItemId(
|
|
709
|
+
incoming,
|
|
710
|
+
this.additional_kwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY]
|
|
711
|
+
);
|
|
622
712
|
const combined = super.concat(chunk);
|
|
713
|
+
resealReasoningItemBoundary(combined, accumulated, incoming, activeItemId);
|
|
714
|
+
if (activeItemId != null) {
|
|
715
|
+
combined.additional_kwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY] =
|
|
716
|
+
activeItemId;
|
|
717
|
+
}
|
|
623
718
|
if (!Array.isArray(chunk.response_metadata.output)) {
|
|
624
719
|
return combined;
|
|
625
720
|
}
|
|
721
|
+
delete combined.additional_kwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY];
|
|
626
722
|
delete combined.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
|
|
627
723
|
const toolOutputs = combined.additional_kwargs.tool_outputs;
|
|
628
724
|
if (!Array.isArray(toolOutputs)) {
|
package/src/messages/core.ts
CHANGED
|
@@ -671,6 +671,10 @@ type ResponsesReplayProjection = 'fallback' | 'native';
|
|
|
671
671
|
export const OPENAI_RESPONSES_REPLAY_POSITIONS_KEY =
|
|
672
672
|
'__openai_responses_replay_positions__';
|
|
673
673
|
|
|
674
|
+
/** Reasoning item currently streaming, so a terminal ciphertext seals against its own id. */
|
|
675
|
+
export const OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY =
|
|
676
|
+
'__openai_responses_active_reasoning_id__';
|
|
677
|
+
|
|
674
678
|
export type ResponsesReplayPosition = {
|
|
675
679
|
contentIndex?: number;
|
|
676
680
|
itemId: string;
|
|
@@ -1813,6 +1817,7 @@ function projectPreemptedOpenAIResponsesMessage(
|
|
|
1813
1817
|
}
|
|
1814
1818
|
delete additionalKwargs.tool_outputs;
|
|
1815
1819
|
delete additionalKwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
|
|
1820
|
+
delete additionalKwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY];
|
|
1816
1821
|
delete additionalKwargs.__openai_function_call_ids__;
|
|
1817
1822
|
delete additionalKwargs.__openai_custom_tool_call_ids__;
|
|
1818
1823
|
|
package/src/run.ts
CHANGED
|
@@ -826,12 +826,23 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
826
826
|
* The producer stamped `completed_at` before dispatch. Carrying it
|
|
827
827
|
* through keeps the recorded duration the tool's, not the host
|
|
828
828
|
* handler's — this runs after an arbitrarily slow handler resolves.
|
|
829
|
+
*
|
|
830
|
+
* Isolated because this is a `finally`: a throw here would
|
|
831
|
+
* REPLACE an in-flight error from the handler above, so the
|
|
832
|
+
* host's original failure would be reported as whatever went
|
|
833
|
+
* wrong during closure instead. Closing is best-effort at this
|
|
834
|
+
* point either way — the end-of-run sweep still stamps any step
|
|
835
|
+
* this misses.
|
|
829
836
|
*/
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
837
|
+
try {
|
|
838
|
+
await this.Graph.recordStepCompletion(completion.stepId, {
|
|
839
|
+
toolCallId: completion.toolCallId,
|
|
840
|
+
metadata,
|
|
841
|
+
at: completion.completedAt,
|
|
842
|
+
});
|
|
843
|
+
} catch (_e) {
|
|
844
|
+
/** Never mask the handler's error with a closure failure */
|
|
845
|
+
}
|
|
835
846
|
}
|
|
836
847
|
}
|
|
837
848
|
}
|
|
@@ -87,7 +87,6 @@ import type {
|
|
|
87
87
|
import type { GraphFactory } from '@/graphs/graphFactory';
|
|
88
88
|
import type { StandardGraph } from '@/graphs/Graph';
|
|
89
89
|
import type { HandlerRegistry } from '@/events';
|
|
90
|
-
import { stripRunStepResumeState } from '@/tools/runStepResume';
|
|
91
90
|
import {
|
|
92
91
|
getSubagentApprovalExecutionScope,
|
|
93
92
|
SubagentDefinitionBindingError,
|
|
@@ -125,6 +124,7 @@ import {
|
|
|
125
124
|
createChildGraphPlan,
|
|
126
125
|
isGraphSubagentConfig,
|
|
127
126
|
} from './childGraphConfig';
|
|
127
|
+
import { stripRunStepResumeState } from '@/tools/runStepResume';
|
|
128
128
|
import { seedAgentInitialSessions } from '@/utils/toolSessions';
|
|
129
129
|
import { stableStringify } from '@/tools/eagerEventExecution';
|
|
130
130
|
import { composeAbortSignals } from '@/utils/misc';
|
|
@@ -2450,6 +2450,18 @@ export class SubagentExecutor {
|
|
|
2450
2450
|
} catch (error) {
|
|
2451
2451
|
/** Stamped at failure, not after the error-envelope work below. */
|
|
2452
2452
|
const childTerminalAt = Date.now();
|
|
2453
|
+
/**
|
|
2454
|
+
* Captured at catch entry, BEFORE the self-abort below flips it. The
|
|
2455
|
+
* closure sweep distinguishes "stopped on purpose" from "died of this
|
|
2456
|
+
* error" by whether the child was already aborted when the error
|
|
2457
|
+
* arrived — reading the signal after `childBreaker.abort(error)` would
|
|
2458
|
+
* relabel the child's own stream-limit failure as an intentional stop
|
|
2459
|
+
* (`cancelled`), while the parent stamps `failed` for the same
|
|
2460
|
+
* incident. A trip that arrived from a parallel sibling has already
|
|
2461
|
+
* aborted the composed signal by this point, so it still reads as
|
|
2462
|
+
* `cancelled` here.
|
|
2463
|
+
*/
|
|
2464
|
+
const abortedBeforeError = childSignal.aborted;
|
|
2453
2465
|
if (isGraphInterrupt(error)) {
|
|
2454
2466
|
const activeChildRun = execution.activeRun;
|
|
2455
2467
|
if (activeChildRun != null) {
|
|
@@ -2488,11 +2500,13 @@ export class SubagentExecutor {
|
|
|
2488
2500
|
/**
|
|
2489
2501
|
* `cancelled` vs `failed` mirrors `Run.resolveSweepStatus`: an aborted
|
|
2490
2502
|
* child was stopped on purpose (caller abort, or a breaker trip from a
|
|
2491
|
-
* parallel sibling), anything else died of an unexpected error.
|
|
2503
|
+
* parallel sibling), anything else died of an unexpected error. Uses
|
|
2504
|
+
* the pre-error snapshot, not the live signal — the self-abort above
|
|
2505
|
+
* has already tripped it for the child's own limit error.
|
|
2492
2506
|
*/
|
|
2493
2507
|
await this.closeChildRunSteps(
|
|
2494
2508
|
childGraph,
|
|
2495
|
-
|
|
2509
|
+
abortedBeforeError ? 'cancelled' : 'failed',
|
|
2496
2510
|
childTerminalAt
|
|
2497
2511
|
);
|
|
2498
2512
|
if (forwarding) {
|