@librechat/agents 3.9.3 → 3.9.4
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/README.md +32 -0
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +11 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +41 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
- package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
- package/dist/cjs/llm/invoke.cjs +10 -5
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +1 -1
- package/dist/cjs/llm/streamLimits.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/fading.cjs +14 -6
- package/dist/cjs/messages/fading.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +95 -36
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +2 -0
- package/dist/cjs/openai/index.cjs.map +1 -1
- package/dist/cjs/openai/toolProjection.cjs +196 -0
- package/dist/cjs/openai/toolProjection.cjs.map +1 -0
- package/dist/cjs/run.cjs +8 -1
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +9 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +2 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
- package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +11 -0
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +41 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
- package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
- package/dist/esm/llm/invoke.mjs +10 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +1 -1
- package/dist/esm/llm/streamLimits.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/fading.mjs +14 -7
- package/dist/esm/messages/fading.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +95 -37
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +2 -1
- package/dist/esm/openai/index.mjs.map +1 -1
- package/dist/esm/openai/toolProjection.mjs +196 -0
- package/dist/esm/openai/toolProjection.mjs.map +1 -0
- package/dist/esm/run.mjs +8 -1
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +9 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +2 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
- package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +4 -0
- package/dist/types/graphs/Graph.d.ts +3 -1
- package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
- package/dist/types/messages/fading.d.ts +14 -2
- package/dist/types/messages/prune.d.ts +7 -0
- package/dist/types/openai/arguments.d.ts +2 -0
- package/dist/types/openai/index.d.ts +2 -0
- package/dist/types/openai/toolProjection.d.ts +30 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +1 -1
- package/dist/types/types/graph.d.ts +5 -3
- package/dist/types/types/run.d.ts +5 -0
- package/dist/types/types/stream.d.ts +22 -1
- package/dist/types/types/tools.d.ts +2 -0
- package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
- package/package.json +1 -1
- package/src/common/enum.ts +5 -0
- package/src/events.ts +24 -1
- package/src/graphs/Graph.ts +95 -0
- package/src/graphs/acceptedModelResponse.ts +307 -0
- package/src/llm/invoke.ts +39 -14
- package/src/llm/streamLimits.ts +1 -1
- package/src/messages/fading.ts +30 -5
- package/src/messages/prune.ts +168 -55
- package/src/openai/arguments.ts +2 -0
- package/src/openai/index.ts +6 -0
- package/src/openai/toolProjection.ts +318 -0
- package/src/run.ts +21 -0
- package/src/session/AgentSession.ts +2 -2
- package/src/stream.ts +21 -1
- package/src/tools/ToolNode.ts +5 -0
- package/src/tools/subagent/SubagentReplay.ts +10 -9
- package/src/types/graph.ts +15 -9
- package/src/types/run.ts +5 -0
- package/src/types/stream.ts +26 -0
- package/src/types/tools.ts +2 -0
- package/src/utils/acceptedToolArguments.ts +204 -0
package/src/run.ts
CHANGED
|
@@ -468,6 +468,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
468
468
|
private langfuse?: t.LangfuseConfig;
|
|
469
469
|
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
470
470
|
private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
|
|
471
|
+
private clientDelegatedToolNames?: readonly string[];
|
|
471
472
|
private codeSessionToolNames?: string[];
|
|
472
473
|
private interruptingToolNames?: string[];
|
|
473
474
|
private toolExecution?: t.ToolExecutionConfig;
|
|
@@ -549,12 +550,19 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
549
550
|
}
|
|
550
551
|
}
|
|
551
552
|
|
|
553
|
+
if (
|
|
554
|
+
(config.clientDelegatedToolNames?.length ?? 0) > 0 &&
|
|
555
|
+
handlerRegistry.getHandler(GraphEvents.ON_MODEL_RESPONSE) == null
|
|
556
|
+
) {
|
|
557
|
+
throw new Error('Client tool delegation requires an accepted-result handler');
|
|
558
|
+
}
|
|
552
559
|
this.handlerRegistry = handlerRegistry;
|
|
553
560
|
this.hookRegistry = config.hooks;
|
|
554
561
|
this.humanInTheLoop = config.humanInTheLoop;
|
|
555
562
|
this.langfuse = config.langfuse;
|
|
556
563
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
557
564
|
this.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
565
|
+
this.clientDelegatedToolNames = config.clientDelegatedToolNames;
|
|
558
566
|
this.codeSessionToolNames = config.codeSessionToolNames;
|
|
559
567
|
this.interruptingToolNames = config.interruptingToolNames;
|
|
560
568
|
this.toolExecution = config.toolExecution;
|
|
@@ -573,6 +581,9 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
573
581
|
|
|
574
582
|
/** Handle different graph types */
|
|
575
583
|
if (config.graphConfig.type === 'multi-agent') {
|
|
584
|
+
if (this.clientDelegatedToolNames != null && this.clientDelegatedToolNames.length > 0) {
|
|
585
|
+
throw new Error('Client tool delegation requires a single-agent graph');
|
|
586
|
+
}
|
|
576
587
|
this.graphRunnable = this.createMultiAgentGraph(config.graphConfig);
|
|
577
588
|
if (this.Graph) {
|
|
578
589
|
this.Graph.handlerRegistry = handlerRegistry;
|
|
@@ -663,6 +674,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
663
674
|
preemption: this.preemption,
|
|
664
675
|
streamLimits: this.streamLimits,
|
|
665
676
|
toolExecution: this.toolExecution,
|
|
677
|
+
clientDelegatedToolNames: this.clientDelegatedToolNames,
|
|
666
678
|
},
|
|
667
679
|
});
|
|
668
680
|
/** Propagate compile options from graph config */
|
|
@@ -1058,6 +1070,11 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1058
1070
|
) {
|
|
1059
1071
|
return;
|
|
1060
1072
|
}
|
|
1073
|
+
// Accepted results are graph-owned, never inferred from provider/tool callbacks.
|
|
1074
|
+
if (
|
|
1075
|
+
eventName === GraphEvents.ON_MODEL_RESPONSE ||
|
|
1076
|
+
eventName === GraphEvents.ON_MODEL_TOOLS_CLAIMED
|
|
1077
|
+
) return;
|
|
1061
1078
|
const handler = this.handlerRegistry?.getHandler(eventName);
|
|
1062
1079
|
/**
|
|
1063
1080
|
* Tool completions arriving over the custom-event channel are the only
|
|
@@ -1554,6 +1571,10 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1554
1571
|
|
|
1555
1572
|
const modelEndAt =
|
|
1556
1573
|
eventName === GraphEvents.CHAT_MODEL_END ? Date.now() : undefined;
|
|
1574
|
+
if (
|
|
1575
|
+
eventName === GraphEvents.ON_MODEL_RESPONSE ||
|
|
1576
|
+
eventName === GraphEvents.ON_MODEL_TOOLS_CLAIMED
|
|
1577
|
+
) continue;
|
|
1557
1578
|
const handler = this.handlerRegistry?.getHandler(eventName);
|
|
1558
1579
|
if (handler) {
|
|
1559
1580
|
await handler.handle(eventName, data, metadata, this.Graph);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
deriveSessionMessages,
|
|
28
28
|
releaseSessionProjection,
|
|
29
29
|
} from './sessionProjection';
|
|
30
|
+
import { FADING_TIER_VERSION, isFadingTier } from '@/messages/fading';
|
|
30
31
|
import { createSummarizeNode } from '@/summarization/node';
|
|
31
32
|
import { resolveStreamLimits } from '@/llm/streamLimits';
|
|
32
33
|
import { JsonlSessionStore } from './JsonlSessionStore';
|
|
@@ -34,7 +35,6 @@ import { AgentContext } from '@/agents/AgentContext';
|
|
|
34
35
|
import { ContentTypes, GraphEvents } from '@/common';
|
|
35
36
|
import { createRunId, createSessionId } from './ids';
|
|
36
37
|
import { deriveMessages } from './deriveMessages';
|
|
37
|
-
import { isFadingTier } from '@/messages/fading';
|
|
38
38
|
import { createRunHandlers } from './handlers';
|
|
39
39
|
import { Run } from '@/run';
|
|
40
40
|
|
|
@@ -420,7 +420,7 @@ function mergeFadingTier(
|
|
|
420
420
|
masked !== current.masked ||
|
|
421
421
|
masked !== incoming.masked;
|
|
422
422
|
return {
|
|
423
|
-
v:
|
|
423
|
+
v: FADING_TIER_VERSION,
|
|
424
424
|
budgetTokens,
|
|
425
425
|
masked,
|
|
426
426
|
...(latched ? { latched: true } : {}),
|
package/src/stream.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
truncateToolResultContent,
|
|
57
57
|
} from '@/utils/truncation';
|
|
58
58
|
import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
|
|
59
|
+
import { snapshotValidatedModelChunk } from '@/graphs/acceptedModelResponse';
|
|
59
60
|
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
60
61
|
import { PreparedSubagentError } from '@/tools/preparedSubagents';
|
|
61
62
|
import { isReasoningContentBlock } from '@/messages/core';
|
|
@@ -746,6 +747,10 @@ function createEagerToolExecutionPlan(args: {
|
|
|
746
747
|
toolCall.id == null ||
|
|
747
748
|
toolCall.id === '' ||
|
|
748
749
|
toolCall.name === '' ||
|
|
750
|
+
// A serialized parsed call is not a prepared executable object;
|
|
751
|
+
// parsing is permitted only on sealed raw tool_call_chunks.
|
|
752
|
+
typeof toolCall.args === 'string' ||
|
|
753
|
+
graph.clientDelegatedToolNames?.has(toolCall.name) === true ||
|
|
749
754
|
(!skipExisting && graph.eagerEventToolExecutions.has(toolCall.id))
|
|
750
755
|
)
|
|
751
756
|
) {
|
|
@@ -794,6 +799,9 @@ function startEagerToolExecutions(args: {
|
|
|
794
799
|
skipExisting?: boolean;
|
|
795
800
|
}): void {
|
|
796
801
|
const { graph, metadata, agentContext, toolCalls, skipExisting } = args;
|
|
802
|
+
// A later call in the same model turn may be client-delegated. Do not
|
|
803
|
+
// pre-execute an earlier SDK call in a run that rejects mixed batches.
|
|
804
|
+
if ((graph.clientDelegatedToolNames?.size ?? 0) > 0) return;
|
|
797
805
|
const entries = createEagerToolExecutionPlan({
|
|
798
806
|
graph,
|
|
799
807
|
metadata,
|
|
@@ -1345,6 +1353,10 @@ function startPreparedSubagents(
|
|
|
1345
1353
|
metadata?: Record<string, unknown>
|
|
1346
1354
|
): void {
|
|
1347
1355
|
const attempt = resolveGenerationKey(metadata);
|
|
1356
|
+
if ((graph.clientDelegatedToolNames?.size ?? 0) > 0) return;
|
|
1357
|
+
// A parsed string call is not executable even if the same event carries
|
|
1358
|
+
// sealed raw fragments. Wait for a separately validated complete call.
|
|
1359
|
+
if (chunk.tool_calls?.some((call) => typeof call.args === 'string') === true) return;
|
|
1348
1360
|
if (
|
|
1349
1361
|
(graph as Partial<StandardGraph>).canPrestartSubagents?.(agentContext) !==
|
|
1350
1362
|
true ||
|
|
@@ -1389,6 +1401,9 @@ function startPreparedSubagents(
|
|
|
1389
1401
|
for (const call of calls) {
|
|
1390
1402
|
if (
|
|
1391
1403
|
call.name === Constants.SUBAGENT &&
|
|
1404
|
+
graph.clientDelegatedToolNames?.has(call.name) !== true &&
|
|
1405
|
+
typeof call.args === 'object' &&
|
|
1406
|
+
!Array.isArray(call.args) &&
|
|
1392
1407
|
!hasToolOutputReference(call.args)
|
|
1393
1408
|
) {
|
|
1394
1409
|
graph.prestartSubagent(call, attempt, agentContext);
|
|
@@ -1643,7 +1658,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1643
1658
|
return;
|
|
1644
1659
|
}
|
|
1645
1660
|
|
|
1646
|
-
|
|
1661
|
+
let chunk = data.chunk as Partial<AIMessageChunk>;
|
|
1647
1662
|
|
|
1648
1663
|
/** Attempts stamp their breaker epoch into event metadata; a mismatch
|
|
1649
1664
|
* marks a straggling chunk from a failed run that outlived
|
|
@@ -1681,6 +1696,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1681
1696
|
}
|
|
1682
1697
|
};
|
|
1683
1698
|
|
|
1699
|
+
// Callback delivery can beat the producer's iterator. Validate before
|
|
1700
|
+
// accounting, run steps, or eager dispatch reads raw tool descriptors.
|
|
1701
|
+
chunk = snapshotValidatedModelChunk(chunk as AIMessageChunk);
|
|
1702
|
+
|
|
1684
1703
|
/**
|
|
1685
1704
|
* Enforced before every content-specific early return below
|
|
1686
1705
|
* (server-tool results, deferred mixed reasoning, late OpenRouter
|
|
@@ -1885,6 +1904,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1885
1904
|
chunk.response_metadata as Record<string, unknown> | undefined
|
|
1886
1905
|
);
|
|
1887
1906
|
const canStreamEager =
|
|
1907
|
+
chunk.tool_calls?.some((call) => typeof call.args === 'string') !== true &&
|
|
1888
1908
|
(allowSequentialSeal || hasExplicitStreamedToolCallSeals(chunk)) &&
|
|
1889
1909
|
!hasPotentialDirectToolInStreamContext({ graph, agentContext }) &&
|
|
1890
1910
|
isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext });
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -1021,6 +1021,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1021
1021
|
preparedSubagents,
|
|
1022
1022
|
restoreRunStepResumeState,
|
|
1023
1023
|
createRunStepResumeState,
|
|
1024
|
+
onToolCallsClaimed,
|
|
1024
1025
|
}: t.ToolNodeConstructorParams) {
|
|
1025
1026
|
super({
|
|
1026
1027
|
name: name ?? TOOL_NODE_RUN_NAME,
|
|
@@ -1155,6 +1156,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1155
1156
|
}
|
|
1156
1157
|
const state = input as T & Pick<t.BaseGraphState, 'runStepState'>;
|
|
1157
1158
|
restoreRunStepResumeState?.(state.runStepState, config);
|
|
1159
|
+
// Freeze replay authority before observers can yield to sibling tasks.
|
|
1160
|
+
if (onToolCallsClaimed != null && assistantBatch?.message.id != null) {
|
|
1161
|
+
await onToolCallsClaimed(assistantBatch.message.id, config);
|
|
1162
|
+
}
|
|
1158
1163
|
let result: T;
|
|
1159
1164
|
try {
|
|
1160
1165
|
result = await this.run(input, config, referenceReplay);
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
RunStepResumeState,
|
|
9
9
|
ToolSessionContext,
|
|
10
10
|
} from '@/types';
|
|
11
|
-
import { isFadingTier } from '@/messages/fading';
|
|
11
|
+
import { isFadingTier, isLegacyFadingTier } from '@/messages/fading';
|
|
12
12
|
import {
|
|
13
13
|
attachRunStepResumeState,
|
|
14
14
|
getRunStepResumeState,
|
|
@@ -290,6 +290,11 @@ export function isToolOutputReferenceState(
|
|
|
290
290
|
);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/** A current tier, or a legacy one the run then drops so it re-derives. */
|
|
294
|
+
function isStoredFadingTier(value: unknown): boolean {
|
|
295
|
+
return isFadingTier(value) || isLegacyFadingTier(value);
|
|
296
|
+
}
|
|
297
|
+
|
|
293
298
|
function isGraphResumeState(value: unknown): value is SubagentGraphResumeState {
|
|
294
299
|
if (value == null || typeof value !== 'object') {
|
|
295
300
|
return false;
|
|
@@ -306,15 +311,14 @@ function isGraphResumeState(value: unknown): value is SubagentGraphResumeState {
|
|
|
306
311
|
!state.eagerToolUsage.every(isEagerToolUsageState) ||
|
|
307
312
|
!Array.isArray(state.eagerToolSuppressions) ||
|
|
308
313
|
!state.eagerToolSuppressions.every(isString) ||
|
|
309
|
-
(state.runStepState != null &&
|
|
310
|
-
!isRunStepResumeState(state.runStepState)) ||
|
|
314
|
+
(state.runStepState != null && !isRunStepResumeState(state.runStepState)) ||
|
|
311
315
|
(state.toolOutputReferences != null &&
|
|
312
316
|
!isToolOutputReferenceState(state.toolOutputReferences)) ||
|
|
313
|
-
(state.fadingTier != null && !
|
|
317
|
+
(state.fadingTier != null && !isStoredFadingTier(state.fadingTier)) ||
|
|
314
318
|
(state.fadingTiers != null &&
|
|
315
319
|
(typeof state.fadingTiers !== 'object' ||
|
|
316
320
|
Array.isArray(state.fadingTiers) ||
|
|
317
|
-
!Object.values(state.fadingTiers).every(
|
|
321
|
+
!Object.values(state.fadingTiers).every(isStoredFadingTier)))
|
|
318
322
|
) {
|
|
319
323
|
return false;
|
|
320
324
|
}
|
|
@@ -515,10 +519,7 @@ export function attachSubagentResumeManifest(
|
|
|
515
519
|
const runStepState = getRunStepResumeState(payload);
|
|
516
520
|
if (runStepState != null) {
|
|
517
521
|
return attachRunStepResumeState(
|
|
518
|
-
attachSubagentResumeManifest(
|
|
519
|
-
stripRunStepResumeState(payload),
|
|
520
|
-
manifest
|
|
521
|
-
),
|
|
522
|
+
attachSubagentResumeManifest(stripRunStepResumeState(payload), manifest),
|
|
522
523
|
runStepState
|
|
523
524
|
);
|
|
524
525
|
}
|
package/src/types/graph.ts
CHANGED
|
@@ -10,22 +10,24 @@ import type { START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
|
|
|
10
10
|
import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
|
|
11
11
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
12
12
|
import type { GoogleAIToolType } from '@langchain/google-common';
|
|
13
|
-
import type {
|
|
14
|
-
SummarizationNodeInput,
|
|
15
|
-
SummarizeCompleteEvent,
|
|
16
|
-
CompactionSemanticIndex,
|
|
17
|
-
SummarizationConfig,
|
|
18
|
-
SummarizeStartEvent,
|
|
19
|
-
SummarizeDeltaEvent,
|
|
20
|
-
} from '@/types/summarize';
|
|
21
13
|
import type {
|
|
22
14
|
RunStep,
|
|
15
|
+
ModelResponseEvent,
|
|
16
|
+
ModelToolsClaimedEvent,
|
|
23
17
|
RunStepDeltaEvent,
|
|
24
18
|
RunStepResumeState,
|
|
25
19
|
RunStepClosedEvent,
|
|
26
20
|
MessageDeltaEvent,
|
|
27
21
|
ReasoningDeltaEvent,
|
|
28
22
|
} from '@/types/stream';
|
|
23
|
+
import type {
|
|
24
|
+
SummarizationNodeInput,
|
|
25
|
+
SummarizeCompleteEvent,
|
|
26
|
+
CompactionSemanticIndex,
|
|
27
|
+
SummarizationConfig,
|
|
28
|
+
SummarizeStartEvent,
|
|
29
|
+
SummarizeDeltaEvent,
|
|
30
|
+
} from '@/types/summarize';
|
|
29
31
|
import type {
|
|
30
32
|
ToolMap,
|
|
31
33
|
ToolSessionMap,
|
|
@@ -183,7 +185,7 @@ export interface ContextUsageEvent {
|
|
|
183
185
|
* `RunConfig.fadingTiers[agentId]`.
|
|
184
186
|
*/
|
|
185
187
|
export interface FadingTier {
|
|
186
|
-
v:
|
|
188
|
+
v: 2;
|
|
187
189
|
/** Token budget the caps derive from, in raw token space. Never grows;
|
|
188
190
|
* clamped to the current context window when seeded. */
|
|
189
191
|
budgetTokens: number;
|
|
@@ -202,6 +204,8 @@ export interface EventHandler {
|
|
|
202
204
|
data:
|
|
203
205
|
| StreamEventData
|
|
204
206
|
| ModelEndData
|
|
207
|
+
| ModelResponseEvent
|
|
208
|
+
| ModelToolsClaimedEvent
|
|
205
209
|
| RunStep
|
|
206
210
|
| RunStepDeltaEvent
|
|
207
211
|
| RunStepClosedEvent
|
|
@@ -406,6 +410,8 @@ export type StandardGraphInput = {
|
|
|
406
410
|
agents: AgentInputs[];
|
|
407
411
|
/** Execution backend used to resolve the effective tool registry. */
|
|
408
412
|
toolExecution?: ToolExecutionConfig;
|
|
413
|
+
/** Trusted single-agent client delegation policy; mixed batches fail closed. */
|
|
414
|
+
clientDelegatedToolNames?: readonly string[];
|
|
409
415
|
langfuse?: LangfuseConfig;
|
|
410
416
|
tokenCounter?: TokenCounter;
|
|
411
417
|
indexTokenCountMap?: Record<string, number>;
|
package/src/types/run.ts
CHANGED
|
@@ -279,6 +279,11 @@ export type RunConfig = {
|
|
|
279
279
|
*/
|
|
280
280
|
langfuse?: g.LangfuseConfig;
|
|
281
281
|
customHandlers?: Record<string, g.EventHandler>;
|
|
282
|
+
/** Explicit client-owned tools for a single-agent graph. A batch mixing
|
|
283
|
+
* client and SDK/provider calls fails closed; omitted means SDK ownership.
|
|
284
|
+
* Hosts must register the corresponding model-facing tool schemas.
|
|
285
|
+
*/
|
|
286
|
+
clientDelegatedToolNames?: readonly string[];
|
|
282
287
|
/**
|
|
283
288
|
* Receives token usage for every model call made inside subagent child
|
|
284
289
|
* runs (including nested subagents). Child graphs execute via `invoke()`
|
package/src/types/stream.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/types/stream.ts
|
|
2
2
|
import type {
|
|
3
|
+
AIMessageChunk,
|
|
3
4
|
MessageContentImageUrl,
|
|
4
5
|
MessageContentText,
|
|
5
6
|
ToolMessage,
|
|
@@ -15,6 +16,24 @@ import type { SummarizeCompleteEvent } from '@/types/summarize';
|
|
|
15
16
|
import type { ToolEndEvent } from '@/types/tools';
|
|
16
17
|
import { StepTypes, ContentTypes, GraphEvents } from '@/common/enum';
|
|
17
18
|
|
|
19
|
+
/** One accepted model result, detached from execution state before host dispatch.
|
|
20
|
+
* Provider chunks, failed attempts and UI run-step events are not this contract. */
|
|
21
|
+
export interface ModelResponseEvent {
|
|
22
|
+
type: 'model_response';
|
|
23
|
+
/** Graph-generated acceptance ID, not a provider ID or run-step index. */
|
|
24
|
+
id: string;
|
|
25
|
+
agentId: string;
|
|
26
|
+
/** Graph-state message identity used to correlate ToolNode ownership. */
|
|
27
|
+
messageId?: string;
|
|
28
|
+
toolCalls: ReadonlyArray<ToolCall>;
|
|
29
|
+
/** Same index as toolCalls. Only a trusted graph decision of 'client'
|
|
30
|
+
* permits this call on the OpenAI client wire; absence fails closed. */
|
|
31
|
+
toolCallDispositions: ReadonlyArray<'sdk' | 'provider' | 'client'>;
|
|
32
|
+
invalidToolCalls: ReadonlyArray<
|
|
33
|
+
NonNullable<AIMessageChunk['invalid_tool_calls']>[number]
|
|
34
|
+
>;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
export type HandleLLMEnd = (
|
|
19
38
|
output: LLMResult,
|
|
20
39
|
runId: string,
|
|
@@ -542,3 +561,10 @@ export type ContentAggregatorResult = {
|
|
|
542
561
|
contentParts: Array<MessageContentComplex | undefined>;
|
|
543
562
|
aggregateContent: ContentAggregator;
|
|
544
563
|
};
|
|
564
|
+
|
|
565
|
+
/** Ownership, not successful completion. Interrupted/failed batches remain graph-owned. */
|
|
566
|
+
export interface ModelToolsClaimedEvent {
|
|
567
|
+
type: 'model_tools_claimed';
|
|
568
|
+
agentId: string;
|
|
569
|
+
messageId: string;
|
|
570
|
+
}
|
package/src/types/tools.ts
CHANGED
|
@@ -313,6 +313,8 @@ export type ToolNodeOptions = {
|
|
|
313
313
|
) => void;
|
|
314
314
|
/** SDK-owned checkpoint snapshot for open run-step lifecycle state. */
|
|
315
315
|
createRunStepResumeState?: () => RunStepResumeState;
|
|
316
|
+
/** Internal ownership bridge, awaited before ToolNode can execute a batch. */
|
|
317
|
+
onToolCallsClaimed?: (messageId: string, config: RunnableConfig) => Promise<void>;
|
|
316
318
|
};
|
|
317
319
|
|
|
318
320
|
export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { types } from 'node:util';
|
|
2
|
+
|
|
3
|
+
const MAX_ARGUMENT_DEPTH = 64;
|
|
4
|
+
const INVALID = 'Accepted tool call arguments are not JSON serializable';
|
|
5
|
+
const LIMIT = 'Tool projection buffer limit exceeded';
|
|
6
|
+
|
|
7
|
+
function invalid(): never {
|
|
8
|
+
throw new Error(INVALID);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** structuredClone and VM providers may return objects from another realm. */
|
|
12
|
+
function hasJSONPrototype(value: object, array: boolean): boolean {
|
|
13
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
14
|
+
if (proto === null) return true;
|
|
15
|
+
if (typeof proto !== 'object' || types.isProxy(proto)) return false;
|
|
16
|
+
const ctor: unknown = Object.getOwnPropertyDescriptor(
|
|
17
|
+
proto,
|
|
18
|
+
'constructor'
|
|
19
|
+
)?.value;
|
|
20
|
+
if (typeof ctor !== 'function' || types.isProxy(ctor)) return false;
|
|
21
|
+
return (
|
|
22
|
+
Object.getOwnPropertyDescriptor(ctor, 'prototype')?.value === proto &&
|
|
23
|
+
Function.prototype.toString.call(ctor) ===
|
|
24
|
+
Function.prototype.toString.call(array ? Array : Object)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Encode JSON data, not arbitrary JS values. No getters, proxies or toJSON hooks run.
|
|
29
|
+
* Repeated references expand like JSON (and count against the budget); cycles fail.
|
|
30
|
+
* Limits bound output and traversal, including deeply nested/alias-heavy input.
|
|
31
|
+
*/
|
|
32
|
+
export function serializeToolArguments(
|
|
33
|
+
value: unknown,
|
|
34
|
+
maxBytes: number
|
|
35
|
+
): string {
|
|
36
|
+
if (maxBytes < 2) throw new Error(LIMIT);
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
const ancestors = new Set<object>();
|
|
39
|
+
let remaining = maxBytes;
|
|
40
|
+
const append = (text: string): void => {
|
|
41
|
+
remaining -= Buffer.byteLength(text, 'utf8');
|
|
42
|
+
if (remaining < 0) throw new Error(LIMIT);
|
|
43
|
+
parts.push(text);
|
|
44
|
+
};
|
|
45
|
+
const string = (text: string): void => {
|
|
46
|
+
// Every UTF-16 unit needs at least one encoded byte. Check before allocating escapes.
|
|
47
|
+
if (text.length + 2 > remaining) throw new Error(LIMIT);
|
|
48
|
+
append(JSON.stringify(text));
|
|
49
|
+
};
|
|
50
|
+
const encode = (input: unknown, depth: number): void => {
|
|
51
|
+
if (depth > MAX_ARGUMENT_DEPTH) invalid();
|
|
52
|
+
if (input === null) {
|
|
53
|
+
append('null');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof input === 'string') {
|
|
57
|
+
string(input);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (typeof input === 'boolean') {
|
|
61
|
+
append(input ? 'true' : 'false');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof input === 'number') {
|
|
65
|
+
if (!Number.isFinite(input)) invalid();
|
|
66
|
+
append(Object.is(input, -0) ? '-0' : String(input));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof input !== 'object') invalid();
|
|
70
|
+
if (types.isProxy(input) || ancestors.has(input)) invalid();
|
|
71
|
+
const array = Array.isArray(input);
|
|
72
|
+
if (!hasJSONPrototype(input, array)) invalid();
|
|
73
|
+
const keys = Reflect.ownKeys(input);
|
|
74
|
+
// At least one byte per entry, plus punctuation. Bound descriptor work up front.
|
|
75
|
+
if (keys.length > remaining + (array ? 1 : 0)) throw new Error(LIMIT);
|
|
76
|
+
ancestors.add(input);
|
|
77
|
+
if (array) {
|
|
78
|
+
// Dense arrays only, with no symbols, hidden entries or extra named properties.
|
|
79
|
+
if (keys.length !== input.length + 1) invalid();
|
|
80
|
+
append('[');
|
|
81
|
+
for (let i = 0; i < input.length; i++) {
|
|
82
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, String(i));
|
|
83
|
+
if (
|
|
84
|
+
descriptor == null ||
|
|
85
|
+
descriptor.enumerable !== true ||
|
|
86
|
+
!('value' in descriptor)
|
|
87
|
+
)
|
|
88
|
+
invalid();
|
|
89
|
+
if (i !== 0) append(',');
|
|
90
|
+
encode(descriptor.value, depth + 1);
|
|
91
|
+
}
|
|
92
|
+
append(']');
|
|
93
|
+
} else {
|
|
94
|
+
append('{');
|
|
95
|
+
for (let i = 0; i < keys.length; i++) {
|
|
96
|
+
const key = keys[i];
|
|
97
|
+
if (typeof key !== 'string') invalid();
|
|
98
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
99
|
+
if (
|
|
100
|
+
descriptor == null ||
|
|
101
|
+
descriptor.enumerable !== true ||
|
|
102
|
+
!('value' in descriptor)
|
|
103
|
+
)
|
|
104
|
+
invalid();
|
|
105
|
+
if (i !== 0) append(',');
|
|
106
|
+
string(key);
|
|
107
|
+
append(':');
|
|
108
|
+
encode(descriptor.value, depth + 1);
|
|
109
|
+
}
|
|
110
|
+
append('}');
|
|
111
|
+
}
|
|
112
|
+
ancestors.delete(input);
|
|
113
|
+
};
|
|
114
|
+
if (value != null && typeof value === 'object' && types.isProxy(value))
|
|
115
|
+
invalid();
|
|
116
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
117
|
+
throw new Error('Accepted tool call arguments must be an object');
|
|
118
|
+
}
|
|
119
|
+
encode(value, 0);
|
|
120
|
+
return parts.join('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Detach JSON data without encoding it. Unlike projection, invocation has no
|
|
124
|
+
* formatting budget: preserve shared references and traverse iteratively so a
|
|
125
|
+
* deep or alias-heavy object cannot expand exponentially during validation.
|
|
126
|
+
*/
|
|
127
|
+
export function cloneToolArguments(value: unknown): Record<string, unknown> {
|
|
128
|
+
const copies = new Map<object, object>();
|
|
129
|
+
const ancestors = new Set<object>();
|
|
130
|
+
const stack: Array<{
|
|
131
|
+
input: object;
|
|
132
|
+
output: object;
|
|
133
|
+
keys: (string | symbol)[];
|
|
134
|
+
index: number;
|
|
135
|
+
}> = [];
|
|
136
|
+
const copy = (input: unknown): unknown => {
|
|
137
|
+
if (
|
|
138
|
+
input === null ||
|
|
139
|
+
typeof input === 'string' ||
|
|
140
|
+
typeof input === 'boolean'
|
|
141
|
+
)
|
|
142
|
+
return input;
|
|
143
|
+
if (typeof input === 'number') {
|
|
144
|
+
if (!Number.isFinite(input)) invalid();
|
|
145
|
+
return input;
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
typeof input !== 'object' ||
|
|
149
|
+
types.isProxy(input) ||
|
|
150
|
+
ancestors.has(input)
|
|
151
|
+
)
|
|
152
|
+
invalid();
|
|
153
|
+
const existing = copies.get(input);
|
|
154
|
+
if (existing != null) return existing;
|
|
155
|
+
const array = Array.isArray(input);
|
|
156
|
+
if (!hasJSONPrototype(input, array)) invalid();
|
|
157
|
+
const keys = Reflect.ownKeys(input);
|
|
158
|
+
if (array && keys.length !== input.length + 1) invalid();
|
|
159
|
+
const output: object = array ? [] : {};
|
|
160
|
+
copies.set(input, output);
|
|
161
|
+
ancestors.add(input);
|
|
162
|
+
stack.push({
|
|
163
|
+
input,
|
|
164
|
+
output,
|
|
165
|
+
keys: array ? keys.filter((key) => key !== 'length') : keys,
|
|
166
|
+
index: 0,
|
|
167
|
+
});
|
|
168
|
+
return output;
|
|
169
|
+
};
|
|
170
|
+
if (
|
|
171
|
+
value == null ||
|
|
172
|
+
typeof value !== 'object' ||
|
|
173
|
+
types.isProxy(value) ||
|
|
174
|
+
Array.isArray(value)
|
|
175
|
+
)
|
|
176
|
+
invalid();
|
|
177
|
+
const result = copy(value) as Record<string, unknown>;
|
|
178
|
+
while (stack.length > 0) {
|
|
179
|
+
const frame = stack[stack.length - 1];
|
|
180
|
+
if (frame.index === frame.keys.length) {
|
|
181
|
+
ancestors.delete(frame.input);
|
|
182
|
+
stack.pop();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const key = frame.keys[frame.index++];
|
|
186
|
+
if (typeof key !== 'string') invalid();
|
|
187
|
+
if (Array.isArray(frame.input) && key !== String(frame.index - 1))
|
|
188
|
+
invalid();
|
|
189
|
+
const descriptor = Object.getOwnPropertyDescriptor(frame.input, key);
|
|
190
|
+
if (
|
|
191
|
+
descriptor == null ||
|
|
192
|
+
!('value' in descriptor) ||
|
|
193
|
+
descriptor.enumerable !== true
|
|
194
|
+
)
|
|
195
|
+
invalid();
|
|
196
|
+
Object.defineProperty(frame.output, key, {
|
|
197
|
+
value: copy(descriptor.value),
|
|
198
|
+
enumerable: true,
|
|
199
|
+
writable: true,
|
|
200
|
+
configurable: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|