@librechat/agents 3.7.10 → 3.7.12
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 +11 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -3
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +6 -2
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/format.cjs +6 -34
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +5 -1
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +22 -6
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs +10 -0
- package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +43 -6
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +12 -2
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +5 -3
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +11 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +36 -3
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +6 -2
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/format.mjs +6 -34
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +5 -1
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +23 -7
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticCallerPolicy.mjs +10 -1
- package/dist/esm/tools/ProgrammaticCallerPolicy.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +42 -7
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +12 -2
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +5 -3
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/langfuse.d.ts +5 -1
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/tools/ProgrammaticCallerPolicy.d.ts +12 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +35 -0
- package/dist/types/tools/ToolNode.d.ts +4 -1
- package/dist/types/types/tools.d.ts +5 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +10 -0
- package/src/graphs/MultiAgentGraph.ts +71 -2
- package/src/langfuse.ts +12 -0
- package/src/messages/format.ts +23 -70
- package/src/run.ts +4 -0
- package/src/tools/BashProgrammaticToolCalling.ts +57 -19
- package/src/tools/ProgrammaticCallerPolicy.ts +31 -0
- package/src/tools/ProgrammaticToolCalling.ts +132 -19
- package/src/tools/ToolNode.ts +19 -0
- package/src/tools/local/LocalProgrammaticToolCalling.ts +19 -8
- package/src/types/tools.ts +5 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -2620,6 +2620,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2620
2620
|
// run_id); `executingAgentId` always identifies the owning agent.
|
|
2621
2621
|
agentId: this.subagentScope ? agentContext?.agentId : undefined,
|
|
2622
2622
|
executingAgentId: agentContext?.agentId,
|
|
2623
|
+
executingAgentName: agentContext?.name,
|
|
2624
|
+
rootAgentId: this.defaultAgentId,
|
|
2625
|
+
rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
|
|
2623
2626
|
toolCallStepIds: this.toolCallStepIds,
|
|
2624
2627
|
toolRegistry: agentContext?.toolRegistry,
|
|
2625
2628
|
getDiscoveredToolNames: (): readonly string[] =>
|
|
@@ -2697,6 +2700,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2697
2700
|
// hooks can attribute the batch even at the top level.
|
|
2698
2701
|
agentId: this.subagentScope ? agentContext?.agentId : undefined,
|
|
2699
2702
|
executingAgentId: agentContext?.agentId,
|
|
2703
|
+
executingAgentName: agentContext?.name,
|
|
2704
|
+
rootAgentId: this.defaultAgentId,
|
|
2705
|
+
rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
|
|
2700
2706
|
toolCallStepIds: this.toolCallStepIds,
|
|
2701
2707
|
errorHandler: (data, metadata): Promise<boolean> =>
|
|
2702
2708
|
StandardGraph.handleToolCallErrorStatic(this, data, metadata),
|
|
@@ -3815,6 +3821,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3815
3821
|
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
3816
3822
|
agentId,
|
|
3817
3823
|
agentName: agentContext.name,
|
|
3824
|
+
rootAgentId: this.defaultAgentId,
|
|
3825
|
+
rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
|
|
3826
|
+
activeAgentId: agentId,
|
|
3827
|
+
activeAgentName: agentContext.name,
|
|
3818
3828
|
});
|
|
3819
3829
|
let langfuseHandler: CallbackEntry | undefined;
|
|
3820
3830
|
let invokeConfig = {
|
|
@@ -101,6 +101,14 @@ function isTransferToolName(name: unknown): boolean {
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function graphToolName(tool: unknown): string | undefined {
|
|
105
|
+
if (tool == null || typeof tool !== 'object' || !('name' in tool)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const { name } = tool;
|
|
109
|
+
return typeof name === 'string' ? name : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
/**
|
|
105
113
|
* Drop transfer `tool_use` content blocks from an AI message's array content.
|
|
106
114
|
* Companion to the reception's tool-call filtering: array-content providers
|
|
@@ -287,6 +295,21 @@ function withHandoffGroupMetadata(
|
|
|
287
295
|
};
|
|
288
296
|
}
|
|
289
297
|
|
|
298
|
+
function withActiveAgentMetadata(
|
|
299
|
+
config: LangGraphRunnableConfig | undefined,
|
|
300
|
+
agentId: string,
|
|
301
|
+
agentName: string | undefined
|
|
302
|
+
): LangGraphRunnableConfig {
|
|
303
|
+
return {
|
|
304
|
+
...config,
|
|
305
|
+
metadata: {
|
|
306
|
+
...config?.metadata,
|
|
307
|
+
activeAgentId: agentId,
|
|
308
|
+
...(agentName == null ? {} : { activeAgentName: agentName }),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
290
313
|
/**
|
|
291
314
|
* MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
|
|
292
315
|
* with handoffs, fan-in/fan-out, and other composable patterns.
|
|
@@ -594,6 +617,24 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
594
617
|
private createHandoffTools(): void {
|
|
595
618
|
// Group handoff edges by source agent(s)
|
|
596
619
|
const handoffsByAgent = new Map<string, t.GraphEdge[]>();
|
|
620
|
+
const tokenAccountingRefresh = new Set<string>();
|
|
621
|
+
|
|
622
|
+
/** Transfer tool names are SDK-reserved. Remove externally supplied or
|
|
623
|
+
* stale transfer tools before deriving the source agent's allowed set
|
|
624
|
+
* from the graph edges below. */
|
|
625
|
+
for (const agentContext of this.agentContexts.values()) {
|
|
626
|
+
const originalTools = agentContext.graphTools;
|
|
627
|
+
const retainedTools = agentContext.graphTools?.filter(
|
|
628
|
+
(graphTool) => !isTransferToolName(graphToolName(graphTool))
|
|
629
|
+
);
|
|
630
|
+
if (retainedTools?.length !== originalTools?.length) {
|
|
631
|
+
tokenAccountingRefresh.add(agentContext.agentId);
|
|
632
|
+
}
|
|
633
|
+
agentContext.graphTools =
|
|
634
|
+
retainedTools != null && retainedTools.length > 0
|
|
635
|
+
? retainedTools
|
|
636
|
+
: undefined;
|
|
637
|
+
}
|
|
597
638
|
|
|
598
639
|
// Only process handoff edges for tool creation
|
|
599
640
|
for (const edge of this.handoffEdges) {
|
|
@@ -643,6 +684,29 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
643
684
|
for (const handoffTool of handoffTools) {
|
|
644
685
|
agentContext.graphTools.push(handoffTool);
|
|
645
686
|
}
|
|
687
|
+
if (handoffTools.length > 0) {
|
|
688
|
+
tokenAccountingRefresh.add(agentId);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
for (const agentId of tokenAccountingRefresh) {
|
|
693
|
+
const agentContext = this.agentContexts.get(agentId);
|
|
694
|
+
if (agentContext?.tokenCounter == null) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const { tokenCounter, baseIndexTokenCountMap } = agentContext;
|
|
698
|
+
agentContext.tokenCalculationPromise = agentContext
|
|
699
|
+
.calculateInstructionTokens(tokenCounter)
|
|
700
|
+
.then(() => {
|
|
701
|
+
agentContext.updateTokenMapWithInstructions(baseIndexTokenCountMap);
|
|
702
|
+
})
|
|
703
|
+
.catch((err) => {
|
|
704
|
+
// eslint-disable-next-line no-console
|
|
705
|
+
console.error(
|
|
706
|
+
'Error recalculating instruction tokens after handoff tool updates:',
|
|
707
|
+
err
|
|
708
|
+
);
|
|
709
|
+
});
|
|
646
710
|
}
|
|
647
711
|
}
|
|
648
712
|
|
|
@@ -1239,10 +1303,16 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1239
1303
|
): Promise<t.MultiAgentGraphState | Command> => {
|
|
1240
1304
|
let result: t.MultiAgentGraphState;
|
|
1241
1305
|
let inputMessages = state.messages;
|
|
1242
|
-
const
|
|
1306
|
+
const agentContext = this.agentContexts.get(agentId);
|
|
1307
|
+
const recursionLimitedConfig =
|
|
1243
1308
|
this.memberRecursionLimit == null
|
|
1244
1309
|
? config
|
|
1245
1310
|
: { ...config, recursionLimit: this.memberRecursionLimit };
|
|
1311
|
+
const memberConfig = withActiveAgentMetadata(
|
|
1312
|
+
recursionLimitedConfig,
|
|
1313
|
+
agentId,
|
|
1314
|
+
agentContext?.name
|
|
1315
|
+
);
|
|
1246
1316
|
|
|
1247
1317
|
/**
|
|
1248
1318
|
* Check if this agent is receiving a handoff.
|
|
@@ -1254,7 +1324,6 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1254
1324
|
state.messages,
|
|
1255
1325
|
agentId
|
|
1256
1326
|
);
|
|
1257
|
-
const agentContext = this.agentContexts.get(agentId);
|
|
1258
1327
|
|
|
1259
1328
|
if (
|
|
1260
1329
|
handoffContext?.sourceAgentName != null &&
|
package/src/langfuse.ts
CHANGED
|
@@ -783,17 +783,29 @@ export function createLangfuseTraceMetadata({
|
|
|
783
783
|
parentMessageId,
|
|
784
784
|
agentId,
|
|
785
785
|
agentName,
|
|
786
|
+
rootAgentId,
|
|
787
|
+
rootAgentName,
|
|
788
|
+
activeAgentId,
|
|
789
|
+
activeAgentName,
|
|
786
790
|
}: {
|
|
787
791
|
messageId?: unknown;
|
|
788
792
|
parentMessageId?: unknown;
|
|
789
793
|
agentId?: unknown;
|
|
790
794
|
agentName?: unknown;
|
|
795
|
+
rootAgentId?: unknown;
|
|
796
|
+
rootAgentName?: unknown;
|
|
797
|
+
activeAgentId?: unknown;
|
|
798
|
+
activeAgentName?: unknown;
|
|
791
799
|
}): LangfuseTraceMetadata {
|
|
792
800
|
return createTraceMetadata({
|
|
793
801
|
messageId,
|
|
794
802
|
parentMessageId,
|
|
795
803
|
agentId,
|
|
796
804
|
agentName,
|
|
805
|
+
rootAgentId,
|
|
806
|
+
rootAgentName,
|
|
807
|
+
activeAgentId,
|
|
808
|
+
activeAgentName,
|
|
797
809
|
});
|
|
798
810
|
}
|
|
799
811
|
|
package/src/messages/format.ts
CHANGED
|
@@ -2771,12 +2771,22 @@ function extractSkillName(args: unknown): string | undefined {
|
|
|
2771
2771
|
return typeof name === 'string' && name !== '' ? name : undefined;
|
|
2772
2772
|
}
|
|
2773
2773
|
|
|
2774
|
+
function isSdkManagedToolName(name: unknown): name is string {
|
|
2775
|
+
return (
|
|
2776
|
+
typeof name === 'string' &&
|
|
2777
|
+
(name === Constants.SUBAGENT ||
|
|
2778
|
+
name === 'conditional_transfer' ||
|
|
2779
|
+
name.startsWith(Constants.LC_TRANSFER_TO_))
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2774
2783
|
/**
|
|
2775
2784
|
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
|
|
2776
2785
|
*
|
|
2777
2786
|
* @param payload - The array of messages to format.
|
|
2778
2787
|
* @param indexTokenCountMap - Optional map of message indices to token counts.
|
|
2779
|
-
* @param tools - Optional set of tool names
|
|
2788
|
+
* @param tools - Optional set of host-managed tool names allowed in the request. SDK-managed tool
|
|
2789
|
+
* names are preserved independently.
|
|
2780
2790
|
* @param skills - Optional map of skill name to body for reconstructing skill HumanMessages.
|
|
2781
2791
|
* @param options - Optional formatting options (provider, skipSkillBodyNames).
|
|
2782
2792
|
* @returns - Object containing formatted messages and updated indexTokenCountMap if provided.
|
|
@@ -2957,8 +2967,8 @@ export const formatAgentMessages = (
|
|
|
2957
2967
|
|
|
2958
2968
|
/**
|
|
2959
2969
|
* If tools set is provided, process tool_calls:
|
|
2960
|
-
* - Keep valid tool_calls (tools in the set or dynamically discovered)
|
|
2961
|
-
* -
|
|
2970
|
+
* - Keep valid tool_calls (tools in the set, SDK-managed, or dynamically discovered)
|
|
2971
|
+
* - Omit invalid tool_calls without exposing their output as assistant-authored text
|
|
2962
2972
|
* - Dynamically expand the set when tool_search results are encountered
|
|
2963
2973
|
*/
|
|
2964
2974
|
let processedMessage = message;
|
|
@@ -2977,8 +2987,6 @@ export const formatAgentMessages = (
|
|
|
2977
2987
|
const filteredContent: typeof content = [];
|
|
2978
2988
|
const filteredSourceContentPartIndices: SourceContentPartIndices[] = [];
|
|
2979
2989
|
const invalidToolCallIds = new Set<string>();
|
|
2980
|
-
const invalidToolStrings: string[] = [];
|
|
2981
|
-
const invalidToolSourceContentPartIndices: number[] = [];
|
|
2982
2990
|
|
|
2983
2991
|
for (let partIndex = 0; partIndex < content.length; partIndex++) {
|
|
2984
2992
|
const part = content[partIndex] as
|
|
@@ -3029,7 +3037,10 @@ export const formatAgentMessages = (
|
|
|
3029
3037
|
}
|
|
3030
3038
|
}
|
|
3031
3039
|
|
|
3032
|
-
if (
|
|
3040
|
+
if (
|
|
3041
|
+
discoveredTools.has(toolName) ||
|
|
3042
|
+
isSdkManagedToolName(toolName)
|
|
3043
|
+
) {
|
|
3033
3044
|
filteredContent.push(part);
|
|
3034
3045
|
filteredSourceContentPartIndices.push(partSourceContentIndices);
|
|
3035
3046
|
if (
|
|
@@ -3042,20 +3053,11 @@ export const formatAgentMessages = (
|
|
|
3042
3053
|
(pendingSkillNames ??= new Set()).add(skillName);
|
|
3043
3054
|
}
|
|
3044
3055
|
}
|
|
3045
|
-
} else
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
) {
|
|
3051
|
-
invalidToolCallIds.add(part.tool_call.id);
|
|
3052
|
-
}
|
|
3053
|
-
const output = part.tool_call.output ?? '';
|
|
3054
|
-
invalidToolStrings.push(`Tool: ${toolName}, ${output}`);
|
|
3055
|
-
appendSourceContentPartIndices(
|
|
3056
|
-
invalidToolSourceContentPartIndices,
|
|
3057
|
-
partSourceContentIndices
|
|
3058
|
-
);
|
|
3056
|
+
} else if (
|
|
3057
|
+
typeof part.tool_call.id === 'string' &&
|
|
3058
|
+
part.tool_call.id !== ''
|
|
3059
|
+
) {
|
|
3060
|
+
invalidToolCallIds.add(part.tool_call.id);
|
|
3059
3061
|
}
|
|
3060
3062
|
}
|
|
3061
3063
|
|
|
@@ -3076,57 +3078,8 @@ export const formatAgentMessages = (
|
|
|
3076
3078
|
}
|
|
3077
3079
|
}
|
|
3078
3080
|
|
|
3079
|
-
/** Append invalid tool strings to the content for context preservation */
|
|
3080
|
-
if (invalidToolStrings.length > 0) {
|
|
3081
|
-
/** Find the last text part or create one */
|
|
3082
|
-
let lastTextPartIndex = -1;
|
|
3083
|
-
for (let j = filteredContent.length - 1; j >= 0; j--) {
|
|
3084
|
-
if (filteredContent[j].type === ContentTypes.TEXT) {
|
|
3085
|
-
lastTextPartIndex = j;
|
|
3086
|
-
break;
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
const invalidToolText = invalidToolStrings.join('\n');
|
|
3091
|
-
if (lastTextPartIndex >= 0) {
|
|
3092
|
-
const lastTextPart = filteredContent[lastTextPartIndex] as {
|
|
3093
|
-
type: string;
|
|
3094
|
-
[ContentTypes.TEXT]?: string;
|
|
3095
|
-
text?: string;
|
|
3096
|
-
};
|
|
3097
|
-
const existingText =
|
|
3098
|
-
lastTextPart[ContentTypes.TEXT] ?? lastTextPart.text ?? '';
|
|
3099
|
-
filteredContent[lastTextPartIndex] = {
|
|
3100
|
-
...lastTextPart,
|
|
3101
|
-
[ContentTypes.TEXT]: existingText
|
|
3102
|
-
? `${existingText}\n${invalidToolText}`
|
|
3103
|
-
: invalidToolText,
|
|
3104
|
-
};
|
|
3105
|
-
const lastTextSourceContentPartIndices =
|
|
3106
|
-
filteredSourceContentPartIndices[lastTextPartIndex];
|
|
3107
|
-
filteredSourceContentPartIndices[lastTextPartIndex] = [
|
|
3108
|
-
...(typeof lastTextSourceContentPartIndices === 'number'
|
|
3109
|
-
? [lastTextSourceContentPartIndices]
|
|
3110
|
-
: lastTextSourceContentPartIndices),
|
|
3111
|
-
...invalidToolSourceContentPartIndices,
|
|
3112
|
-
];
|
|
3113
|
-
} else {
|
|
3114
|
-
/** No text part exists, create one */
|
|
3115
|
-
filteredContent.push({
|
|
3116
|
-
type: ContentTypes.TEXT,
|
|
3117
|
-
[ContentTypes.TEXT]: invalidToolText,
|
|
3118
|
-
});
|
|
3119
|
-
filteredSourceContentPartIndices.push(
|
|
3120
|
-
invalidToolSourceContentPartIndices
|
|
3121
|
-
);
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
3081
|
/** Use filtered content if we made any changes */
|
|
3126
|
-
if (
|
|
3127
|
-
filteredContent.length !== content.length ||
|
|
3128
|
-
invalidToolStrings.length > 0
|
|
3129
|
-
) {
|
|
3082
|
+
if (filteredContent.length !== content.length) {
|
|
3130
3083
|
processedMessage = { ...message, content: filteredContent };
|
|
3131
3084
|
processedSourceContentPartIndices = filteredSourceContentPartIndices;
|
|
3132
3085
|
}
|
package/src/run.ts
CHANGED
|
@@ -1311,6 +1311,10 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1311
1311
|
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
1312
1312
|
agentId: graph.defaultAgentId,
|
|
1313
1313
|
agentName: primaryContext?.name,
|
|
1314
|
+
rootAgentId: graph.defaultAgentId,
|
|
1315
|
+
rootAgentName: primaryContext?.name,
|
|
1316
|
+
activeAgentId: graph.defaultAgentId,
|
|
1317
|
+
activeAgentName: primaryContext?.name,
|
|
1314
1318
|
});
|
|
1315
1319
|
const traceName = config.runName ?? getLangfuseTraceName(traceMetadata);
|
|
1316
1320
|
const streamLangfuseConfig = this.getStreamLangfuseConfig(graph);
|
|
@@ -14,11 +14,7 @@ import {
|
|
|
14
14
|
selectRuntimeSessionHint,
|
|
15
15
|
} from './CodeExecutor';
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
executeTools,
|
|
19
|
-
formatCompletedResponse,
|
|
20
|
-
} from './ProgrammaticToolCalling';
|
|
21
|
-
import {
|
|
17
|
+
assertUnambiguousIdentifiers,
|
|
22
18
|
projectProgrammaticToolMap,
|
|
23
19
|
resolveProgrammaticToolDefinitions,
|
|
24
20
|
selectProgrammaticTools,
|
|
@@ -29,6 +25,12 @@ import {
|
|
|
29
25
|
createCodeApiRunTimeoutSchema,
|
|
30
26
|
resolveCodeApiRunTimeoutMs,
|
|
31
27
|
} from './ptcTimeout';
|
|
28
|
+
import {
|
|
29
|
+
makeRequest,
|
|
30
|
+
executeTools,
|
|
31
|
+
runPlainExecution,
|
|
32
|
+
formatCompletedResponse,
|
|
33
|
+
} from './ProgrammaticToolCalling';
|
|
32
34
|
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
33
35
|
import { Constants } from '@/common';
|
|
34
36
|
|
|
@@ -117,7 +119,7 @@ ${CORE_RULES}`;
|
|
|
117
119
|
|
|
118
120
|
const TOOL_MANIFEST_DESCRIPTION =
|
|
119
121
|
'Exact registered tool names used by the code. Required when direct-only tools are configured; ' +
|
|
120
|
-
'validated before execution starts.';
|
|
122
|
+
'validated before execution starts. Pass [] when the code calls no tools at all.';
|
|
121
123
|
|
|
122
124
|
// ============================================================================
|
|
123
125
|
// Schema
|
|
@@ -361,22 +363,41 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
361
363
|
programmaticToolName,
|
|
362
364
|
});
|
|
363
365
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
366
|
+
/* These guard the replay path only. A call that selected no tools never
|
|
367
|
+
* replays, and event-driven ToolNode configurations legitimately inject
|
|
368
|
+
* an empty toolMap/toolDefs. Injected context is still required to
|
|
369
|
+
* conclude that — with nothing injected at all, an empty selection means
|
|
370
|
+
* the host wired the tool up wrong. */
|
|
371
|
+
const needsNoTools =
|
|
372
|
+
effectiveTools.length === 0 &&
|
|
373
|
+
(params.tool_manifest?.length === 0 ||
|
|
374
|
+
toolDefs != null ||
|
|
375
|
+
disallowedToolDefs != null);
|
|
376
|
+
|
|
377
|
+
if (!needsNoTools) {
|
|
378
|
+
if (toolMap == null || toolMap.size === 0) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
'No toolMap provided. ' +
|
|
381
|
+
'ToolNode should inject this from AgentContext when invoked through the graph.'
|
|
382
|
+
);
|
|
383
|
+
}
|
|
370
384
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
385
|
+
if (toolDefs == null || toolDefs.length === 0) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
'No tool definitions provided. ' +
|
|
388
|
+
'Either pass tools in the input or ensure ToolNode injects toolDefs.'
|
|
389
|
+
);
|
|
390
|
+
}
|
|
376
391
|
}
|
|
377
392
|
|
|
393
|
+
assertUnambiguousIdentifiers(
|
|
394
|
+
effectiveTools,
|
|
395
|
+
normalizeToBashIdentifier,
|
|
396
|
+
programmaticToolName
|
|
397
|
+
);
|
|
398
|
+
|
|
378
399
|
const effectiveToolMap = projectProgrammaticToolMap(
|
|
379
|
-
toolMap,
|
|
400
|
+
toolMap ?? new Map(),
|
|
380
401
|
effectiveTools
|
|
381
402
|
);
|
|
382
403
|
|
|
@@ -391,7 +412,7 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
391
412
|
// eslint-disable-next-line no-console
|
|
392
413
|
console.log(
|
|
393
414
|
`[BashPTC Debug] Sending ${effectiveTools.length} tools to API ` +
|
|
394
|
-
`(selected from ${toolDefs
|
|
415
|
+
`(selected from ${toolDefs?.length ?? 0})`
|
|
395
416
|
);
|
|
396
417
|
}
|
|
397
418
|
|
|
@@ -423,6 +444,23 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
423
444
|
? selectedRuntimeSessionHint
|
|
424
445
|
: undefined;
|
|
425
446
|
|
|
447
|
+
/* Raw `code`, not `preparedCode`: the `$!` guard exists for the
|
|
448
|
+
* programmatic replay wrapper, which plain `/exec` never applies. */
|
|
449
|
+
if (needsNoTools) {
|
|
450
|
+
return await runPlainExecution({
|
|
451
|
+
baseUrl,
|
|
452
|
+
lang: 'bash',
|
|
453
|
+
code,
|
|
454
|
+
timeout,
|
|
455
|
+
sessionId: session_id,
|
|
456
|
+
files,
|
|
457
|
+
runtimeSessionHint,
|
|
458
|
+
proxy,
|
|
459
|
+
authHeaders: initParams.authHeaders,
|
|
460
|
+
executionProfile: initParams.executionProfile,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
426
464
|
let response = await makeRequest(
|
|
427
465
|
EXEC_ENDPOINT,
|
|
428
466
|
{
|
|
@@ -43,6 +43,37 @@ export function assertDisallowedToolUsage(
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Rejects a stub set holding two tools that collapse to one runtime identifier.
|
|
48
|
+
*
|
|
49
|
+
* Stub generation binds by identifier, so such tools are indistinguishable once
|
|
50
|
+
* emitted — the generated program defines both and the later one wins. Failing
|
|
51
|
+
* beats dispatching by declaration order.
|
|
52
|
+
*
|
|
53
|
+
* Called on the set each backend actually emits, not on the selection: a
|
|
54
|
+
* definition dropped before codegen never binds an identifier, so a collision
|
|
55
|
+
* among such definitions is not a conflict.
|
|
56
|
+
*/
|
|
57
|
+
export function assertUnambiguousIdentifiers(
|
|
58
|
+
selectedToolDefs: readonly t.LCTool[],
|
|
59
|
+
normalizeIdentifier: (name: string) => string,
|
|
60
|
+
programmaticToolName: string
|
|
61
|
+
): void {
|
|
62
|
+
const seen = new Map<string, string>();
|
|
63
|
+
for (const toolDef of selectedToolDefs) {
|
|
64
|
+
const identifier = normalizeIdentifier(toolDef.name);
|
|
65
|
+
const prior = seen.get(identifier);
|
|
66
|
+
if (prior != null && prior !== toolDef.name) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Tools "${prior}" and "${toolDef.name}" both become "${identifier}" in ` +
|
|
69
|
+
`the sandbox, so "${programmaticToolName}" cannot tell which one the ` +
|
|
70
|
+
'code means. Call them directly, or rename one.'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
seen.set(identifier, toolDef.name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
export function selectProgrammaticTools(args: {
|
|
47
78
|
requestedToolNames?: string[];
|
|
48
79
|
allowedToolDefs?: t.LCTool[];
|
|
@@ -22,16 +22,17 @@ import {
|
|
|
22
22
|
selectRuntimeSessionHint,
|
|
23
23
|
} from './CodeExecutor';
|
|
24
24
|
import {
|
|
25
|
-
|
|
26
|
-
createCodeApiRunTimeoutSchema,
|
|
27
|
-
resolveCodeApiRunTimeoutMs,
|
|
28
|
-
} from './ptcTimeout';
|
|
29
|
-
import {
|
|
25
|
+
assertUnambiguousIdentifiers,
|
|
30
26
|
projectProgrammaticToolMap,
|
|
31
27
|
resolveProgrammaticToolDefinitions,
|
|
32
28
|
selectProgrammaticTools,
|
|
33
29
|
type ProgrammaticInvocationParams,
|
|
34
30
|
} from './ProgrammaticCallerPolicy';
|
|
31
|
+
import {
|
|
32
|
+
clampCodeApiRunTimeoutMs,
|
|
33
|
+
createCodeApiRunTimeoutSchema,
|
|
34
|
+
resolveCodeApiRunTimeoutMs,
|
|
35
|
+
} from './ptcTimeout';
|
|
35
36
|
import { resolveFetchProxyAgent } from '@/utils/proxy';
|
|
36
37
|
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
37
38
|
import { Constants } from '@/common';
|
|
@@ -95,7 +96,7 @@ ${CORE_RULES}`;
|
|
|
95
96
|
|
|
96
97
|
const TOOL_MANIFEST_DESCRIPTION =
|
|
97
98
|
'Exact registered tool names used by the code. Required when direct-only tools are configured; ' +
|
|
98
|
-
'validated before execution starts.';
|
|
99
|
+
'validated before execution starts. Pass [] when the code calls no tools at all.';
|
|
99
100
|
|
|
100
101
|
export function createProgrammaticToolCallingSchema(
|
|
101
102
|
maxRunTimeoutMs = DEFAULT_RUN_TIMEOUT_MS
|
|
@@ -874,6 +875,84 @@ export function formatCompletedResponse(
|
|
|
874
875
|
];
|
|
875
876
|
}
|
|
876
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Mirrors the Code API's programmatic Python wrapper (`wrapUserCodeInAsync`).
|
|
880
|
+
*
|
|
881
|
+
* The programmatic contract promises top-level `await` works; plain `/exec`
|
|
882
|
+
* applies no wrapper, so forwarding source unchanged would turn valid
|
|
883
|
+
* programmatic input into a SyntaxError. The wrapper is self-contained, so a
|
|
884
|
+
* later divergence in the Code API's own copy cannot break this path.
|
|
885
|
+
*/
|
|
886
|
+
export function wrapPythonForPlainExecution(userCode: string): string {
|
|
887
|
+
const indented = userCode
|
|
888
|
+
.split('\n')
|
|
889
|
+
.map((line) => (line.trim() === '' ? '' : ` ${line}`))
|
|
890
|
+
.join('\n');
|
|
891
|
+
|
|
892
|
+
return (
|
|
893
|
+
'async def __user_main__():\n' +
|
|
894
|
+
' """Auto-generated wrapper for user code to support top-level await"""\n' +
|
|
895
|
+
`${indented}\n` +
|
|
896
|
+
'\n' +
|
|
897
|
+
'if __name__ == "__main__":\n' +
|
|
898
|
+
' import asyncio\n' +
|
|
899
|
+
' asyncio.run(__user_main__())\n'
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Runs code that selected no tools through plain `/exec`.
|
|
905
|
+
*
|
|
906
|
+
* `/exec/programmatic` rejects an empty tool manifest outright, and the sandbox
|
|
907
|
+
* has nothing to inject for such a call anyway — no stubs, no replay round
|
|
908
|
+
* trips. Routing it here makes a tool-free programmatic call behave exactly like
|
|
909
|
+
* the equivalent `execute_code`, against every deployed Code API, instead of
|
|
910
|
+
* failing with a rejection the model cannot act on.
|
|
911
|
+
*/
|
|
912
|
+
export async function runPlainExecution(args: {
|
|
913
|
+
baseUrl: string;
|
|
914
|
+
lang: 'bash' | 'py';
|
|
915
|
+
code: string;
|
|
916
|
+
/**
|
|
917
|
+
* Forwarded so the model-supplied cap applies as soon as the Code API honors
|
|
918
|
+
* it on `/exec`, which does not read `timeout` today. Session affinity and
|
|
919
|
+
* input files are honored there, so those ride the same body.
|
|
920
|
+
*/
|
|
921
|
+
timeout?: number;
|
|
922
|
+
sessionId?: string;
|
|
923
|
+
files?: t.CodeEnvFile[];
|
|
924
|
+
runtimeSessionHint?: string;
|
|
925
|
+
proxy?: string;
|
|
926
|
+
authHeaders?: t.CodeApiAuthHeaders;
|
|
927
|
+
executionProfile?: t.CodeApiExecutionProfile;
|
|
928
|
+
}): Promise<[string, t.ProgrammaticExecutionArtifact]> {
|
|
929
|
+
const response = await makeRequest(
|
|
930
|
+
buildCodeApiEndpoint(args.baseUrl, 'exec'),
|
|
931
|
+
{
|
|
932
|
+
lang: args.lang,
|
|
933
|
+
code:
|
|
934
|
+
args.lang === 'py'
|
|
935
|
+
? wrapPythonForPlainExecution(args.code)
|
|
936
|
+
: args.code,
|
|
937
|
+
...(args.timeout != null ? { timeout: args.timeout } : {}),
|
|
938
|
+
...(args.sessionId != null && args.sessionId !== ''
|
|
939
|
+
? { session_id: args.sessionId }
|
|
940
|
+
: {}),
|
|
941
|
+
...(args.files != null && args.files.length > 0
|
|
942
|
+
? { files: args.files }
|
|
943
|
+
: {}),
|
|
944
|
+
...(args.runtimeSessionHint != null
|
|
945
|
+
? { runtime_session_hint: args.runtimeSessionHint }
|
|
946
|
+
: {}),
|
|
947
|
+
},
|
|
948
|
+
args.proxy,
|
|
949
|
+
args.authHeaders,
|
|
950
|
+
args.executionProfile
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
return formatCompletedResponse(response, args.code);
|
|
954
|
+
}
|
|
955
|
+
|
|
877
956
|
// ============================================================================
|
|
878
957
|
// Tool Factory
|
|
879
958
|
// ============================================================================
|
|
@@ -943,22 +1022,41 @@ export function createProgrammaticToolCallingTool(
|
|
|
943
1022
|
programmaticToolName,
|
|
944
1023
|
});
|
|
945
1024
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1025
|
+
/* These guard the replay path only. A call that selected no tools never
|
|
1026
|
+
* replays, and event-driven ToolNode configurations legitimately inject
|
|
1027
|
+
* an empty toolMap/toolDefs. Injected context is still required to
|
|
1028
|
+
* conclude that — with nothing injected at all, an empty selection means
|
|
1029
|
+
* the host wired the tool up wrong. */
|
|
1030
|
+
const needsNoTools =
|
|
1031
|
+
effectiveTools.length === 0 &&
|
|
1032
|
+
(params.tool_manifest?.length === 0 ||
|
|
1033
|
+
toolDefs != null ||
|
|
1034
|
+
disallowedToolDefs != null);
|
|
1035
|
+
|
|
1036
|
+
if (!needsNoTools) {
|
|
1037
|
+
if (toolMap == null || toolMap.size === 0) {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
'No toolMap provided. ' +
|
|
1040
|
+
'ToolNode should inject this from AgentContext when invoked through the graph.'
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
952
1043
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1044
|
+
if (toolDefs == null || toolDefs.length === 0) {
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
'No tool definitions provided. ' +
|
|
1047
|
+
'Either pass tools in the input or ensure ToolNode injects toolDefs.'
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
958
1050
|
}
|
|
959
1051
|
|
|
1052
|
+
assertUnambiguousIdentifiers(
|
|
1053
|
+
effectiveTools,
|
|
1054
|
+
normalizeToPythonIdentifier,
|
|
1055
|
+
programmaticToolName
|
|
1056
|
+
);
|
|
1057
|
+
|
|
960
1058
|
const effectiveToolMap = projectProgrammaticToolMap(
|
|
961
|
-
toolMap,
|
|
1059
|
+
toolMap ?? new Map(),
|
|
962
1060
|
effectiveTools
|
|
963
1061
|
);
|
|
964
1062
|
|
|
@@ -973,7 +1071,7 @@ export function createProgrammaticToolCallingTool(
|
|
|
973
1071
|
// eslint-disable-next-line no-console
|
|
974
1072
|
console.log(
|
|
975
1073
|
`[PTC Debug] Sending ${effectiveTools.length} tools to API ` +
|
|
976
|
-
`(selected from ${toolDefs
|
|
1074
|
+
`(selected from ${toolDefs?.length ?? 0})`
|
|
977
1075
|
);
|
|
978
1076
|
}
|
|
979
1077
|
|
|
@@ -1008,6 +1106,21 @@ export function createProgrammaticToolCallingTool(
|
|
|
1008
1106
|
? selectedRuntimeSessionHint
|
|
1009
1107
|
: undefined;
|
|
1010
1108
|
|
|
1109
|
+
if (needsNoTools) {
|
|
1110
|
+
return await runPlainExecution({
|
|
1111
|
+
baseUrl,
|
|
1112
|
+
lang: 'py',
|
|
1113
|
+
code,
|
|
1114
|
+
timeout,
|
|
1115
|
+
sessionId: session_id,
|
|
1116
|
+
files,
|
|
1117
|
+
runtimeSessionHint,
|
|
1118
|
+
proxy,
|
|
1119
|
+
authHeaders: initParams.authHeaders,
|
|
1120
|
+
executionProfile: initParams.executionProfile,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1011
1124
|
let response = await makeRequest(
|
|
1012
1125
|
EXEC_ENDPOINT,
|
|
1013
1126
|
{
|