@librechat/agents 3.9.6 → 3.9.8
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 +1 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +4 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +3 -3
- package/dist/cjs/langfuse.cjs +49 -5
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
- package/dist/cjs/llm/invoke.cjs +1 -1
- package/dist/cjs/run.cjs +39 -15
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs +46 -11
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +2 -2
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +5 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +74 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/types/hitl.cjs +4 -0
- package/dist/cjs/types/hitl.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +20 -2
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +4 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +3 -3
- package/dist/esm/langfuse.mjs +50 -7
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
- package/dist/esm/llm/invoke.mjs +1 -1
- package/dist/esm/run.mjs +40 -16
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +11 -7
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs +50 -14
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +2 -2
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +5 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +75 -7
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/types/hitl.mjs +4 -1
- package/dist/esm/types/hitl.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +20 -3
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +2 -2
- package/dist/types/langfuse.d.ts +19 -0
- package/dist/types/tools/ToolNode.d.ts +1 -0
- package/dist/types/tools/handlers.d.ts +2 -1
- package/dist/types/types/graph.d.ts +3 -3
- package/dist/types/types/hitl.d.ts +11 -0
- package/dist/types/types/stream.d.ts +24 -0
- package/dist/types/types/tools.d.ts +4 -0
- package/dist/types/utils/events.d.ts +4 -0
- package/package.json +1 -1
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +5 -2
- package/src/langfuse.ts +100 -3
- package/src/run.ts +41 -23
- package/src/stream.ts +12 -3
- package/src/tools/ToolNode.ts +108 -17
- package/src/tools/handlers.ts +4 -1
- package/src/tools/local/LocalProgrammaticToolCalling.ts +16 -4
- package/src/tools/subagent/SubagentExecutor.ts +129 -10
- package/src/types/graph.ts +3 -0
- package/src/types/hitl.ts +20 -0
- package/src/types/stream.ts +21 -0
- package/src/types/tools.ts +4 -0
- package/src/utils/events.ts +21 -2
package/src/langfuse.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { tool } from '@langchain/core/tools';
|
|
2
2
|
import { CallbackHandler } from '@langfuse/langchain';
|
|
3
3
|
import { LangfuseOtelContextKeys } from '@langfuse/core';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AIMessage,
|
|
6
|
+
HumanMessage,
|
|
7
|
+
AIMessageChunk,
|
|
8
|
+
} from '@langchain/core/messages';
|
|
5
9
|
import { isGraphInterrupt, isParentCommand } from '@langchain/langgraph';
|
|
6
10
|
import { context as otelContext, trace as otelTrace } from '@opentelemetry/api';
|
|
7
11
|
import {
|
|
@@ -67,6 +71,13 @@ const GRAPH_INTERRUPT_TOOL_OUTPUT = JSON.stringify(
|
|
|
67
71
|
|
|
68
72
|
export type LangfuseTraceMetadata = Record<string, string>;
|
|
69
73
|
export type LangfuseTraceAttributes = Record<string, string | number | boolean>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What a label call's Langfuse generation records in place of the prompt the
|
|
77
|
+
* model received and the text it returned, when the model saw evidence the
|
|
78
|
+
* tool-output redaction policy keeps out of traces.
|
|
79
|
+
*/
|
|
80
|
+
export type LangfuseGenerationMask = { input: string; output: string };
|
|
70
81
|
type LangfuseMetadata = NonNullable<t.LangfuseConfig['metadata']>;
|
|
71
82
|
type LangfuseConfigTraceAttributes = NonNullable<
|
|
72
83
|
t.LangfuseConfig['librechatTraceAttributes']
|
|
@@ -310,6 +321,7 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
310
321
|
private readonly identity: HandlerIdentity;
|
|
311
322
|
private readonly toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
|
|
312
323
|
private readonly trackedRunIds = new Set<string>();
|
|
324
|
+
private generationMask?: LangfuseGenerationMask;
|
|
313
325
|
private deferredRootStarted = false;
|
|
314
326
|
private deferredRootOutcome:
|
|
315
327
|
| {
|
|
@@ -607,12 +619,56 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
607
619
|
): ReturnType<CallbackHandler['handleChainEnd']> {
|
|
608
620
|
const [output, runId, parentRunId] = args;
|
|
609
621
|
if (runId === this.deferredRootRunId && parentRunId == null) {
|
|
610
|
-
this.deferredRootOutcome = {
|
|
622
|
+
this.deferredRootOutcome = {
|
|
623
|
+
type: 'end',
|
|
624
|
+
output: this.maskOutputs(output),
|
|
625
|
+
};
|
|
611
626
|
return Promise.resolve();
|
|
612
627
|
}
|
|
628
|
+
args[0] = this.maskOutputs(output);
|
|
613
629
|
return super.handleChainEnd(...args);
|
|
614
630
|
}
|
|
615
631
|
|
|
632
|
+
/** Keeps usage and response metadata so cost tracking survives the mask. */
|
|
633
|
+
private maskGenerations(output: LLMResult): LLMResult {
|
|
634
|
+
const mask = this.generationMask;
|
|
635
|
+
if (mask == null) {
|
|
636
|
+
return output;
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
...output,
|
|
640
|
+
generations: output.generations.map((generations) =>
|
|
641
|
+
generations.map((generation) => {
|
|
642
|
+
if (!('message' in generation) || generation.message == null) {
|
|
643
|
+
return { ...generation, text: mask.output };
|
|
644
|
+
}
|
|
645
|
+
const message = generation.message as AIMessage;
|
|
646
|
+
return {
|
|
647
|
+
...generation,
|
|
648
|
+
text: mask.output,
|
|
649
|
+
message: new AIMessage({
|
|
650
|
+
content: mask.output,
|
|
651
|
+
usage_metadata: message.usage_metadata,
|
|
652
|
+
response_metadata: message.response_metadata,
|
|
653
|
+
}),
|
|
654
|
+
};
|
|
655
|
+
})
|
|
656
|
+
),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private maskOutputs(
|
|
661
|
+
output: Parameters<CallbackHandler['handleChainEnd']>[0]
|
|
662
|
+
): Parameters<CallbackHandler['handleChainEnd']>[0] {
|
|
663
|
+
const mask = this.generationMask;
|
|
664
|
+
return mask == null ? output : { output: mask.output };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Records `mask` for every model call and chain result this handler traces. */
|
|
668
|
+
maskGeneration(mask: LangfuseGenerationMask): void {
|
|
669
|
+
this.generationMask = mask;
|
|
670
|
+
}
|
|
671
|
+
|
|
616
672
|
async finishDeferredRoot(): Promise<void> {
|
|
617
673
|
const runId = this.deferredRootRunId;
|
|
618
674
|
const outcome = this.deferredRootOutcome;
|
|
@@ -650,6 +706,13 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
650
706
|
override handleChatModelStart(
|
|
651
707
|
...args: Parameters<CallbackHandler['handleChatModelStart']>
|
|
652
708
|
): ReturnType<CallbackHandler['handleChatModelStart']> {
|
|
709
|
+
const mask = this.generationMask;
|
|
710
|
+
if (mask != null) {
|
|
711
|
+
args[1] = args[1].map((messages) => [
|
|
712
|
+
...messages.filter((message) => message.getType() === 'system'),
|
|
713
|
+
new HumanMessage(mask.input),
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
653
716
|
return this.withRuntimeContext(
|
|
654
717
|
() => super.handleChatModelStart(...args),
|
|
655
718
|
this.startsDetachedRun(args[2], args[3]),
|
|
@@ -660,6 +723,10 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
660
723
|
override handleLLMStart(
|
|
661
724
|
...args: Parameters<CallbackHandler['handleLLMStart']>
|
|
662
725
|
): ReturnType<CallbackHandler['handleLLMStart']> {
|
|
726
|
+
const mask = this.generationMask;
|
|
727
|
+
if (mask != null) {
|
|
728
|
+
args[1] = args[1].map(() => mask.input);
|
|
729
|
+
}
|
|
663
730
|
return this.withRuntimeContext(
|
|
664
731
|
() => super.handleLLMStart(...args),
|
|
665
732
|
this.startsDetachedRun(args[2], args[3]),
|
|
@@ -673,7 +740,7 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
673
740
|
parentRunId?: string
|
|
674
741
|
): Promise<void> {
|
|
675
742
|
return super.handleLLMEnd(
|
|
676
|
-
normalizeBedrockUsageForLangfuse(output),
|
|
743
|
+
this.maskGenerations(normalizeBedrockUsageForLangfuse(output)),
|
|
677
744
|
runId,
|
|
678
745
|
parentRunId
|
|
679
746
|
);
|
|
@@ -937,6 +1004,36 @@ export function createLegacyLangfuseHandler(
|
|
|
937
1004
|
return new ScopedLangfuseCallbackHandler(params);
|
|
938
1005
|
}
|
|
939
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Label calls send the model every piece of evidence. When the redacted copy
|
|
1009
|
+
* differs, the trace records that copy and hides the reply, which may restate
|
|
1010
|
+
* what was redacted. A no-op for any callback other than a label handler.
|
|
1011
|
+
*/
|
|
1012
|
+
export function maskRedactedLabelGeneration(
|
|
1013
|
+
handler: unknown,
|
|
1014
|
+
{
|
|
1015
|
+
modelPrompt,
|
|
1016
|
+
tracedPrompt,
|
|
1017
|
+
redaction,
|
|
1018
|
+
}: {
|
|
1019
|
+
modelPrompt: string;
|
|
1020
|
+
tracedPrompt: string;
|
|
1021
|
+
redaction?: ResolvedLangfuseToolOutputTracingConfig;
|
|
1022
|
+
}
|
|
1023
|
+
): void {
|
|
1024
|
+
if (
|
|
1025
|
+
!(handler instanceof ScopedLangfuseCallbackHandler) ||
|
|
1026
|
+
redaction == null ||
|
|
1027
|
+
tracedPrompt === modelPrompt
|
|
1028
|
+
) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
handler.maskGeneration({
|
|
1032
|
+
input: tracedPrompt === '' ? redaction.redactionText : tracedPrompt,
|
|
1033
|
+
output: redaction.redactionText,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
940
1037
|
export function createLangfuseHandler({
|
|
941
1038
|
langfuse,
|
|
942
1039
|
userId,
|
package/src/run.ts
CHANGED
|
@@ -37,21 +37,22 @@ import {
|
|
|
37
37
|
DEFAULT_MAX_STOP_CONTINUATIONS,
|
|
38
38
|
DEFAULT_RECURSION_LIMIT,
|
|
39
39
|
} from '@/common';
|
|
40
|
-
import {
|
|
41
|
-
ACTIVITY_PHASE_LABEL_PROMPT,
|
|
42
|
-
ACTIVITY_LABEL_PROMPT,
|
|
43
|
-
buildActivityLabelPrompt,
|
|
44
|
-
buildActivityPhaseLabelPrompt,
|
|
45
|
-
normalizeActivityPhaseLabel,
|
|
46
|
-
} from '@/prompts/activityLabel';
|
|
47
40
|
import {
|
|
48
41
|
createLangfuseTraceMetadata,
|
|
49
42
|
createLangfuseHandler,
|
|
43
|
+
maskRedactedLabelGeneration,
|
|
50
44
|
disposeLangfuseHandler,
|
|
51
45
|
getLangfuseTraceName,
|
|
52
46
|
isLangfuseCallbackHandler,
|
|
53
47
|
withLangfuseAttributes,
|
|
54
48
|
} from '@/langfuse';
|
|
49
|
+
import {
|
|
50
|
+
ACTIVITY_PHASE_LABEL_PROMPT,
|
|
51
|
+
ACTIVITY_LABEL_PROMPT,
|
|
52
|
+
buildActivityLabelPrompt,
|
|
53
|
+
buildActivityPhaseLabelPrompt,
|
|
54
|
+
normalizeActivityPhaseLabel,
|
|
55
|
+
} from '@/prompts/activityLabel';
|
|
55
56
|
import {
|
|
56
57
|
requireValidSubagentResumeManifest,
|
|
57
58
|
SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
|
|
@@ -203,6 +204,7 @@ const CUSTOM_GRAPH_EVENTS = new Set<string>([
|
|
|
203
204
|
GraphEvents.ON_RUN_STEP_DELTA,
|
|
204
205
|
GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
205
206
|
GraphEvents.ON_RUN_STEP_CLOSED,
|
|
207
|
+
GraphEvents.ON_TOOL_CALLS_DISPATCHED,
|
|
206
208
|
GraphEvents.ON_MESSAGE_DELTA,
|
|
207
209
|
GraphEvents.ON_REASONING_DELTA,
|
|
208
210
|
GraphEvents.ON_TOOL_EXECUTE,
|
|
@@ -1094,6 +1096,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1094
1096
|
| t.RunStep
|
|
1095
1097
|
| t.RunStepDeltaEvent
|
|
1096
1098
|
| t.RunStepClosedEvent
|
|
1099
|
+
| t.ToolCallsDispatchedEvent
|
|
1097
1100
|
| t.MessageDeltaEvent
|
|
1098
1101
|
| t.ReasoningDeltaEvent
|
|
1099
1102
|
| { result: t.ToolEndEvent },
|
|
@@ -2663,22 +2666,21 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
2663
2666
|
};
|
|
2664
2667
|
}
|
|
2665
2668
|
}
|
|
2666
|
-
|
|
2667
|
-
* a reasoning-only block has nothing describable left — skip the model
|
|
2668
|
-
* call rather than paying for a label built from the prompt alone. */
|
|
2669
|
-
const freeFormSuppressed =
|
|
2670
|
-
redaction != null &&
|
|
2671
|
-
(redaction.enabled === false || redaction.redactedToolNames.size > 0);
|
|
2672
|
-
if (entries.length === 0 && freeFormSuppressed) {
|
|
2673
|
-
return {};
|
|
2674
|
-
}
|
|
2675
|
-
const userPrompt = buildActivityLabelPrompt({
|
|
2669
|
+
const labelEvidence = {
|
|
2676
2670
|
entries,
|
|
2677
2671
|
charLimit,
|
|
2678
2672
|
thinkingExcerpts,
|
|
2679
2673
|
lastAssistantText:
|
|
2680
2674
|
lastAssistantPhase === 'final_answer' ? undefined : lastAssistantText,
|
|
2681
2675
|
previousLabels,
|
|
2676
|
+
};
|
|
2677
|
+
const userPrompt = buildActivityLabelPrompt(labelEvidence);
|
|
2678
|
+
maskRedactedLabelGeneration(labelLangfuseHandler, {
|
|
2679
|
+
modelPrompt: userPrompt,
|
|
2680
|
+
tracedPrompt:
|
|
2681
|
+
redaction == null
|
|
2682
|
+
? userPrompt
|
|
2683
|
+
: buildActivityLabelPrompt({ ...labelEvidence, redaction }),
|
|
2682
2684
|
redaction,
|
|
2683
2685
|
});
|
|
2684
2686
|
|
|
@@ -2983,17 +2985,25 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
2983
2985
|
reasoningContext?.langfuse
|
|
2984
2986
|
)
|
|
2985
2987
|
: undefined;
|
|
2986
|
-
const
|
|
2988
|
+
const reasoningEvidence = {
|
|
2987
2989
|
visibleReasoning,
|
|
2988
2990
|
status,
|
|
2989
2991
|
charLimit,
|
|
2990
2992
|
previousLabel,
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
+
};
|
|
2994
|
+
const userPrompt = buildReasoningLabelPrompt(reasoningEvidence);
|
|
2993
2995
|
if (userPrompt === '') {
|
|
2994
2996
|
await disposeLangfuseHandler(reasoningLangfuseHandler);
|
|
2995
2997
|
return {};
|
|
2996
2998
|
}
|
|
2999
|
+
maskRedactedLabelGeneration(reasoningLangfuseHandler, {
|
|
3000
|
+
modelPrompt: userPrompt,
|
|
3001
|
+
tracedPrompt:
|
|
3002
|
+
redaction == null
|
|
3003
|
+
? userPrompt
|
|
3004
|
+
: buildReasoningLabelPrompt({ ...reasoningEvidence, redaction }),
|
|
3005
|
+
redaction,
|
|
3006
|
+
});
|
|
2997
3007
|
|
|
2998
3008
|
const model = initializeModel({
|
|
2999
3009
|
provider,
|
|
@@ -3180,13 +3190,13 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
3180
3190
|
};
|
|
3181
3191
|
}
|
|
3182
3192
|
|
|
3183
|
-
const
|
|
3193
|
+
const phaseEvidence = {
|
|
3184
3194
|
activities,
|
|
3185
3195
|
totalActivityCount,
|
|
3186
3196
|
charLimit,
|
|
3187
3197
|
assistantContext,
|
|
3188
|
-
|
|
3189
|
-
|
|
3198
|
+
};
|
|
3199
|
+
const userPrompt = buildActivityPhaseLabelPrompt(phaseEvidence);
|
|
3190
3200
|
if (userPrompt === '') {
|
|
3191
3201
|
return {};
|
|
3192
3202
|
}
|
|
@@ -3305,6 +3315,14 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
3305
3315
|
traceName: phaseParentSpanContext == null ? phaseTraceName : undefined,
|
|
3306
3316
|
});
|
|
3307
3317
|
}
|
|
3318
|
+
maskRedactedLabelGeneration(phaseLangfuseHandler, {
|
|
3319
|
+
modelPrompt: userPrompt,
|
|
3320
|
+
tracedPrompt:
|
|
3321
|
+
redaction == null
|
|
3322
|
+
? userPrompt
|
|
3323
|
+
: buildActivityPhaseLabelPrompt({ ...phaseEvidence, redaction }),
|
|
3324
|
+
redaction,
|
|
3325
|
+
});
|
|
3308
3326
|
if (phaseLangfuseHandler != null) {
|
|
3309
3327
|
phaseChainOptions.callbacks = appendCallbacks(
|
|
3310
3328
|
phaseChainOptions.callbacks,
|
package/src/stream.ts
CHANGED
|
@@ -55,13 +55,13 @@ import {
|
|
|
55
55
|
calculateMaxToolResultChars,
|
|
56
56
|
truncateToolResultContent,
|
|
57
57
|
} from '@/utils/truncation';
|
|
58
|
+
import { createToolCallsDispatchedEvent, safeDispatchCustomEvent } from '@/utils/events';
|
|
58
59
|
import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
|
|
59
|
-
import { formatToolErrorContent } from '@/tools/toolErrorContent';
|
|
60
60
|
import { snapshotValidatedModelChunk } from '@/graphs/acceptedModelResponse';
|
|
61
61
|
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
62
|
+
import { formatToolErrorContent } from '@/tools/toolErrorContent';
|
|
62
63
|
import { PreparedSubagentError } from '@/tools/preparedSubagents';
|
|
63
64
|
import { isReasoningContentBlock } from '@/messages/core';
|
|
64
|
-
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
65
65
|
import { composeAbortSignals } from '@/utils/misc';
|
|
66
66
|
import { isGoogleLike } from '@/utils/llm';
|
|
67
67
|
import { getMessageId } from '@/messages';
|
|
@@ -871,6 +871,13 @@ function startEagerToolExecutions(args: {
|
|
|
871
871
|
reject,
|
|
872
872
|
};
|
|
873
873
|
|
|
874
|
+
if (graph.config != null) {
|
|
875
|
+
void safeDispatchCustomEvent(
|
|
876
|
+
GraphEvents.ON_TOOL_CALLS_DISPATCHED,
|
|
877
|
+
createToolCallsDispatchedEvent(graph.config, batchRequest.toolCalls),
|
|
878
|
+
graph.config
|
|
879
|
+
);
|
|
880
|
+
}
|
|
874
881
|
void safeDispatchCustomEvent(
|
|
875
882
|
GraphEvents.ON_TOOL_EXECUTE,
|
|
876
883
|
batchRequest,
|
|
@@ -980,7 +987,7 @@ async function dispatchEagerToolCompletions(args: {
|
|
|
980
987
|
progress: 1,
|
|
981
988
|
...(outcome != null && { outcome }),
|
|
982
989
|
} as t.ProcessedToolCall,
|
|
983
|
-
completed_at: Date.now(),
|
|
990
|
+
completed_at: result.received_at ?? Date.now(),
|
|
984
991
|
},
|
|
985
992
|
},
|
|
986
993
|
graph.config
|
|
@@ -1701,6 +1708,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1701
1708
|
// Callback delivery can beat the producer's iterator. Validate before
|
|
1702
1709
|
// accounting, run steps, or eager dispatch reads raw tool descriptors.
|
|
1703
1710
|
chunk = snapshotValidatedModelChunk(chunk as AIMessageChunk);
|
|
1711
|
+
const observedAt = Date.now();
|
|
1704
1712
|
|
|
1705
1713
|
/**
|
|
1706
1714
|
* Enforced before every content-specific early return below
|
|
@@ -1923,6 +1931,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1923
1931
|
stepKey,
|
|
1924
1932
|
toolCallChunks: chunk.tool_call_chunks,
|
|
1925
1933
|
metadata,
|
|
1934
|
+
observedAt,
|
|
1926
1935
|
});
|
|
1927
1936
|
if (canStreamEager) {
|
|
1928
1937
|
if (runScopeInvalidated()) {
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -129,10 +129,11 @@ import {
|
|
|
129
129
|
resolveLocalExecutionTools,
|
|
130
130
|
} from '@/tools/local';
|
|
131
131
|
import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
132
|
-
import { formatToolErrorContent } from '@/tools/toolErrorContent';
|
|
133
132
|
import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
|
|
133
|
+
import { formatToolErrorContent } from '@/tools/toolErrorContent';
|
|
134
134
|
import { PreparedSubagentError } from '@/tools/preparedSubagents';
|
|
135
135
|
import { attachRunStepResumeState } from '@/tools/runStepResume';
|
|
136
|
+
import { isBackgroundDenyMode } from '@/types/hitl';
|
|
136
137
|
|
|
137
138
|
function stripToolApprovalReviewConfig(
|
|
138
139
|
configurable: Record<string, unknown> | undefined
|
|
@@ -169,7 +170,7 @@ function stripHostExecutionConfig(
|
|
|
169
170
|
}
|
|
170
171
|
import { convertInjectedMessages } from '@/messages/injected';
|
|
171
172
|
import { stampSyntheticProviderMessage } from '@/messages/provenance';
|
|
172
|
-
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
173
|
+
import { createToolCallsDispatchedEvent, safeDispatchCustomEvent } from '@/utils/events';
|
|
173
174
|
import { RunnableCallable, composeAbortSignals } from '@/utils';
|
|
174
175
|
import {
|
|
175
176
|
executeHooks,
|
|
@@ -1671,6 +1672,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1671
1672
|
};
|
|
1672
1673
|
this.throwIfBreakerTripped(config);
|
|
1673
1674
|
config.signal?.throwIfAborted();
|
|
1675
|
+
if (call.id != null && call.id !== '') {
|
|
1676
|
+
void safeDispatchCustomEvent(
|
|
1677
|
+
GraphEvents.ON_TOOL_CALLS_DISPATCHED,
|
|
1678
|
+
createToolCallsDispatchedEvent(config, [
|
|
1679
|
+
{ id: call.id, name: call.name, stepId: this.toolCallStepIds?.get(call.id) },
|
|
1680
|
+
]),
|
|
1681
|
+
config
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1674
1684
|
return tool.invoke(invokeParams, runtime);
|
|
1675
1685
|
}
|
|
1676
1686
|
|
|
@@ -1949,6 +1959,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1949
1959
|
// they dispatch — including HITL gates on `write_file` / `edit_file`.
|
|
1950
1960
|
hookContext: {
|
|
1951
1961
|
registry: this.hookRegistry,
|
|
1962
|
+
failClosedOnHookError:
|
|
1963
|
+
isBackgroundDenyMode(this.humanInTheLoop),
|
|
1952
1964
|
runId: (config.configurable?.run_id as string | undefined) ?? '',
|
|
1953
1965
|
threadId: config.configurable?.thread_id as string | undefined,
|
|
1954
1966
|
agentId: this.agentId,
|
|
@@ -2541,11 +2553,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2541
2553
|
onceReplayKey: approvalReplayKey,
|
|
2542
2554
|
onceReplaySessionId: approvalReplaySessionId,
|
|
2543
2555
|
}).catch((): AggregatedHookResult | undefined =>
|
|
2544
|
-
approvalReviewEvidence == null
|
|
2556
|
+
approvalReviewEvidence == null &&
|
|
2557
|
+
!isBackgroundDenyMode(this.humanInTheLoop)
|
|
2545
2558
|
? undefined
|
|
2546
2559
|
: {
|
|
2547
2560
|
decision: 'deny',
|
|
2548
|
-
reason:
|
|
2561
|
+
reason:
|
|
2562
|
+
isBackgroundDenyMode(this.humanInTheLoop)
|
|
2563
|
+
? 'Approval policy could not be evaluated'
|
|
2564
|
+
: 'Approval policy could not be evaluated on resume',
|
|
2565
|
+
...(isBackgroundDenyMode(this.humanInTheLoop)
|
|
2566
|
+
? { hasHookFailures: true }
|
|
2567
|
+
: {}),
|
|
2549
2568
|
additionalContexts: [],
|
|
2550
2569
|
injectedMessages: [],
|
|
2551
2570
|
errors: [],
|
|
@@ -2582,6 +2601,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2582
2601
|
};
|
|
2583
2602
|
}
|
|
2584
2603
|
|
|
2604
|
+
if (
|
|
2605
|
+
preResult.hasHookFailures === true &&
|
|
2606
|
+
isBackgroundDenyMode(this.humanInTheLoop)
|
|
2607
|
+
) {
|
|
2608
|
+
return persistOutput(
|
|
2609
|
+
this.blockDirectCall({
|
|
2610
|
+
call,
|
|
2611
|
+
resolvedArgs,
|
|
2612
|
+
reason: this.backgroundApprovalReason(
|
|
2613
|
+
call.name,
|
|
2614
|
+
'Approval policy could not be evaluated'
|
|
2615
|
+
),
|
|
2616
|
+
hookRegistry,
|
|
2617
|
+
runId,
|
|
2618
|
+
threadId,
|
|
2619
|
+
}),
|
|
2620
|
+
effectiveCall.args as Record<string, unknown>
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2585
2624
|
if (preResult.decision === 'deny') {
|
|
2586
2625
|
return persistOutput(
|
|
2587
2626
|
this.blockDirectCall({
|
|
@@ -2621,12 +2660,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2621
2660
|
|
|
2622
2661
|
if (preResult.decision === 'ask' || reviewedApproval != null) {
|
|
2623
2662
|
if (this.humanInTheLoop?.enabled !== true) {
|
|
2624
|
-
// Fail
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2663
|
+
// Fail closed when there is no foreground approval channel.
|
|
2664
|
+
const reason =
|
|
2665
|
+
isBackgroundDenyMode(this.humanInTheLoop)
|
|
2666
|
+
? this.backgroundApprovalReason(call.name, preResult.reason)
|
|
2667
|
+
: this.resolveAskDecisionForDirectTool(
|
|
2668
|
+
preResult.reason,
|
|
2669
|
+
call.name
|
|
2670
|
+
);
|
|
2630
2671
|
return persistOutput(
|
|
2631
2672
|
this.blockDirectCall({
|
|
2632
2673
|
call,
|
|
@@ -3008,6 +3049,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3008
3049
|
* LangGraph `interrupt()` instead — see `runDirectToolWithLifecycleHooks`.
|
|
3009
3050
|
*/
|
|
3010
3051
|
private askDirectWarningEmitted = false;
|
|
3052
|
+
private backgroundApprovalReason(toolName: string, reason?: string): string {
|
|
3053
|
+
return (
|
|
3054
|
+
`Approval required for "${toolName}"; unavailable in a background subagent. ` +
|
|
3055
|
+
`Ask the parent to run it in the foreground.${reason == null ? '' : ` Reason: ${reason}`}`
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3011
3059
|
private resolveAskDecisionForDirectTool(
|
|
3012
3060
|
reason: string | undefined,
|
|
3013
3061
|
toolName: string
|
|
@@ -3626,7 +3674,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3626
3674
|
matchQuery: entry.call.name,
|
|
3627
3675
|
onceReplayKey: approvalReplayKey,
|
|
3628
3676
|
onceReplaySessionId: approvalReplaySessionId,
|
|
3629
|
-
}).catch((): AggregatedHookResult =>
|
|
3677
|
+
}).catch((): AggregatedHookResult =>
|
|
3678
|
+
isBackgroundDenyMode(this.humanInTheLoop)
|
|
3679
|
+
? { ...HOOK_FALLBACK, hasHookFailures: true }
|
|
3680
|
+
: HOOK_FALLBACK
|
|
3681
|
+
);
|
|
3630
3682
|
})
|
|
3631
3683
|
);
|
|
3632
3684
|
|
|
@@ -3793,6 +3845,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3793
3845
|
batchAdditionalContexts.push(ctx);
|
|
3794
3846
|
}
|
|
3795
3847
|
|
|
3848
|
+
if (
|
|
3849
|
+
hookResult.hasHookFailures === true &&
|
|
3850
|
+
isBackgroundDenyMode(this.humanInTheLoop)
|
|
3851
|
+
) {
|
|
3852
|
+
blockEntry(
|
|
3853
|
+
entry,
|
|
3854
|
+
this.backgroundApprovalReason(
|
|
3855
|
+
entry.call.name,
|
|
3856
|
+
'Approval policy could not be evaluated'
|
|
3857
|
+
)
|
|
3858
|
+
);
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3796
3862
|
if (hookResult.decision === 'deny') {
|
|
3797
3863
|
blockEntry(entry, hookResult.reason ?? 'Blocked by hook');
|
|
3798
3864
|
continue;
|
|
@@ -3810,7 +3876,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3810
3876
|
* JSDoc for the full rationale and the migration plan.
|
|
3811
3877
|
*/
|
|
3812
3878
|
if (this.humanInTheLoop?.enabled !== true) {
|
|
3813
|
-
blockEntry(
|
|
3879
|
+
blockEntry(
|
|
3880
|
+
entry,
|
|
3881
|
+
isBackgroundDenyMode(this.humanInTheLoop)
|
|
3882
|
+
? this.backgroundApprovalReason(
|
|
3883
|
+
entry.call.name,
|
|
3884
|
+
hookResult.reason
|
|
3885
|
+
)
|
|
3886
|
+
: (hookResult.reason ?? 'Blocked by hook')
|
|
3887
|
+
);
|
|
3814
3888
|
continue;
|
|
3815
3889
|
}
|
|
3816
3890
|
/**
|
|
@@ -4269,7 +4343,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4269
4343
|
const dispatchRequestById = new Map(
|
|
4270
4344
|
dispatchRequests.map((request) => [request.id, request])
|
|
4271
4345
|
);
|
|
4346
|
+
const receivedAtById = new Map<string, number>();
|
|
4272
4347
|
const onResult = (result: t.ToolExecuteResult): void => {
|
|
4348
|
+
const receivedAt = Date.now();
|
|
4273
4349
|
/** Runtime-honest widening: hosts may omit the id despite the
|
|
4274
4350
|
* field type. */
|
|
4275
4351
|
const resultToolCallId = result.toolCallId as string | undefined;
|
|
@@ -4284,8 +4360,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4284
4360
|
return;
|
|
4285
4361
|
}
|
|
4286
4362
|
earlyCompletionDispatchedIds.add(result.toolCallId);
|
|
4363
|
+
receivedAtById.set(result.toolCallId, receivedAt);
|
|
4287
4364
|
earlyCompletionDispatches.push(
|
|
4288
|
-
this.dispatchEarlyToolCompletion(result, request, config).then(
|
|
4365
|
+
this.dispatchEarlyToolCompletion({ ...result, received_at: receivedAt }, request, config).then(
|
|
4289
4366
|
(dispatched) => {
|
|
4290
4367
|
if (!dispatched) {
|
|
4291
4368
|
earlyCompletionDispatchedIds.delete(result.toolCallId);
|
|
@@ -4337,6 +4414,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4337
4414
|
| undefined,
|
|
4338
4415
|
signal: config.signal,
|
|
4339
4416
|
resolve: (results): void => {
|
|
4417
|
+
const receivedAt = Date.now();
|
|
4418
|
+
for (const result of results) {
|
|
4419
|
+
if (dispatchRequestById.has(result.toolCallId)) {
|
|
4420
|
+
receivedAtById.set(result.toolCallId, result.received_at ?? receivedAt);
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4340
4423
|
resultSettled = true;
|
|
4341
4424
|
settledResults = results;
|
|
4342
4425
|
maybeResolve();
|
|
@@ -4345,6 +4428,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4345
4428
|
...(canEmitEarlyCompletions && { onResult }),
|
|
4346
4429
|
};
|
|
4347
4430
|
|
|
4431
|
+
void safeDispatchCustomEvent(
|
|
4432
|
+
GraphEvents.ON_TOOL_CALLS_DISPATCHED,
|
|
4433
|
+
createToolCallsDispatchedEvent(config, dispatchRequests),
|
|
4434
|
+
config
|
|
4435
|
+
);
|
|
4348
4436
|
void safeDispatchCustomEvent(
|
|
4349
4437
|
GraphEvents.ON_TOOL_EXECUTE,
|
|
4350
4438
|
batchRequest,
|
|
@@ -4630,7 +4718,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4630
4718
|
request?.turn,
|
|
4631
4719
|
resolveToolOutcome(request?.args, outcomeFieldsFromResult(result), {
|
|
4632
4720
|
isError: result.status === 'error',
|
|
4633
|
-
})
|
|
4721
|
+
}),
|
|
4722
|
+
receivedAtById.get(result.toolCallId) ?? result.received_at
|
|
4634
4723
|
);
|
|
4635
4724
|
}
|
|
4636
4725
|
|
|
@@ -4932,7 +5021,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4932
5021
|
output: string,
|
|
4933
5022
|
config: RunnableConfig,
|
|
4934
5023
|
turn?: number,
|
|
4935
|
-
outcome?: string
|
|
5024
|
+
outcome?: string,
|
|
5025
|
+
receivedAt?: number
|
|
4936
5026
|
): Promise<boolean> {
|
|
4937
5027
|
const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
|
|
4938
5028
|
if (!stepId) {
|
|
@@ -4959,7 +5049,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4959
5049
|
progress: 1,
|
|
4960
5050
|
...(outcome != null && { outcome }),
|
|
4961
5051
|
} as t.ProcessedToolCall,
|
|
4962
|
-
completed_at: Date.now(),
|
|
5052
|
+
completed_at: receivedAt ?? Date.now(),
|
|
4963
5053
|
},
|
|
4964
5054
|
},
|
|
4965
5055
|
config
|
|
@@ -4999,7 +5089,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4999
5089
|
request.turn,
|
|
5000
5090
|
resolveToolOutcome(request.args, outcomeFieldsFromResult(result), {
|
|
5001
5091
|
isError: result.status === 'error',
|
|
5002
|
-
})
|
|
5092
|
+
}),
|
|
5093
|
+
result.received_at
|
|
5003
5094
|
);
|
|
5004
5095
|
}
|
|
5005
5096
|
|
package/src/tools/handlers.ts
CHANGED
|
@@ -26,11 +26,13 @@ export async function handleToolCallChunks({
|
|
|
26
26
|
stepKey,
|
|
27
27
|
toolCallChunks,
|
|
28
28
|
metadata,
|
|
29
|
+
observedAt = Date.now(),
|
|
29
30
|
}: {
|
|
30
31
|
graph: StandardGraph | MultiAgentGraph;
|
|
31
32
|
stepKey: string;
|
|
32
33
|
toolCallChunks: ToolCallChunk[];
|
|
33
34
|
metadata?: Record<string, unknown>;
|
|
35
|
+
observedAt?: number;
|
|
34
36
|
}): Promise<void> {
|
|
35
37
|
let prevStepId: string;
|
|
36
38
|
let prevRunStep: t.RunStep | undefined;
|
|
@@ -124,7 +126,8 @@ export async function handleToolCallChunks({
|
|
|
124
126
|
type: StepTypes.TOOL_CALLS,
|
|
125
127
|
tool_calls: toolCallChunks,
|
|
126
128
|
},
|
|
127
|
-
metadata
|
|
129
|
+
metadata,
|
|
130
|
+
observedAt
|
|
128
131
|
);
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -225,6 +225,15 @@ export async function applyPreToolUseHooksForBridge(
|
|
|
225
225
|
sessionId: hookContext.runId,
|
|
226
226
|
matchQuery: toolName,
|
|
227
227
|
}).catch(() => undefined);
|
|
228
|
+
if (
|
|
229
|
+
hookContext.failClosedOnHookError === true &&
|
|
230
|
+
(result == null || result.hasHookFailures === true)
|
|
231
|
+
) {
|
|
232
|
+
return {
|
|
233
|
+
input: toolInput,
|
|
234
|
+
denyReason: `Approval policy could not be evaluated for "${toolName}" in a background subagent. Ask the parent to run it in the foreground.`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
228
237
|
if (result == null) {
|
|
229
238
|
return { input: toolInput };
|
|
230
239
|
}
|
|
@@ -236,10 +245,13 @@ export async function applyPreToolUseHooksForBridge(
|
|
|
236
245
|
return {
|
|
237
246
|
input: nextInput,
|
|
238
247
|
denyReason:
|
|
239
|
-
result.
|
|
240
|
-
|
|
241
|
-
? `
|
|
242
|
-
:
|
|
248
|
+
result.decision === 'ask' &&
|
|
249
|
+
hookContext.failClosedOnHookError === true
|
|
250
|
+
? `Approval required for "${toolName}"; unavailable in a background subagent. Ask the parent to run it in the foreground.${result.reason == null ? '' : ` Reason: ${result.reason}`}`
|
|
251
|
+
: (result.reason ??
|
|
252
|
+
(result.decision === 'ask'
|
|
253
|
+
? `Tool "${toolName}" requires human approval; bridge cannot raise an interrupt — denying.`
|
|
254
|
+
: `Tool "${toolName}" denied by PreToolUse hook.`)),
|
|
243
255
|
};
|
|
244
256
|
}
|
|
245
257
|
return { input: nextInput };
|