@librechat/agents 3.7.11 → 3.7.13

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.
Files changed (36) hide show
  1. package/dist/cjs/main.cjs +2 -0
  2. package/dist/cjs/messages/format.cjs +6 -34
  3. package/dist/cjs/messages/format.cjs.map +1 -1
  4. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +22 -6
  5. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  6. package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs +10 -0
  7. package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs.map +1 -1
  8. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +43 -6
  9. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  10. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +5 -3
  11. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/search/keenable-search.cjs +9 -6
  13. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
  14. package/dist/esm/main.mjs +2 -2
  15. package/dist/esm/messages/format.mjs +6 -34
  16. package/dist/esm/messages/format.mjs.map +1 -1
  17. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +23 -7
  18. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  19. package/dist/esm/tools/ProgrammaticCallerPolicy.mjs +10 -1
  20. package/dist/esm/tools/ProgrammaticCallerPolicy.mjs.map +1 -1
  21. package/dist/esm/tools/ProgrammaticToolCalling.mjs +42 -7
  22. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  23. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +5 -3
  24. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  25. package/dist/esm/tools/search/keenable-search.mjs +9 -6
  26. package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
  27. package/dist/types/messages/format.d.ts +2 -1
  28. package/dist/types/tools/ProgrammaticCallerPolicy.d.ts +12 -0
  29. package/dist/types/tools/ProgrammaticToolCalling.d.ts +35 -0
  30. package/package.json +1 -1
  31. package/src/messages/format.ts +23 -70
  32. package/src/tools/BashProgrammaticToolCalling.ts +57 -19
  33. package/src/tools/ProgrammaticCallerPolicy.ts +31 -0
  34. package/src/tools/ProgrammaticToolCalling.ts +132 -19
  35. package/src/tools/local/LocalProgrammaticToolCalling.ts +19 -8
  36. package/src/tools/search/keenable-search.ts +1 -1
@@ -22,16 +22,17 @@ import {
22
22
  selectRuntimeSessionHint,
23
23
  } from './CodeExecutor';
24
24
  import {
25
- clampCodeApiRunTimeoutMs,
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
- if (toolMap == null || toolMap.size === 0) {
947
- throw new Error(
948
- 'No toolMap provided. ' +
949
- 'ToolNode should inject this from AgentContext when invoked through the graph.'
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
- if (toolDefs == null || toolDefs.length === 0) {
954
- throw new Error(
955
- 'No tool definitions provided. ' +
956
- 'Either pass tools in the input or ensure ToolNode injects toolDefs.'
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.length})`
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
  {
@@ -589,17 +589,28 @@ async function runLocalProgrammaticTool(args: {
589
589
  programmaticToolName: runnerName,
590
590
  });
591
591
 
592
- if (toolMap == null || toolMap.size === 0) {
593
- throw new Error('No toolMap provided for local programmatic execution.');
594
- }
595
- if (toolDefs == null || toolDefs.length === 0) {
596
- throw new Error(
597
- 'No tool definitions provided for local programmatic execution.'
598
- );
592
+ /* The local program builder supports zero stubs, so a call that selected no
593
+ * tools needs neither a toolMap nor toolDefs. Injected context is still
594
+ * required to conclude that. */
595
+ const needsNoTools =
596
+ selectedTools.length === 0 &&
597
+ (args.params.tool_manifest?.length === 0 ||
598
+ toolDefs != null ||
599
+ disallowedToolDefs != null);
600
+
601
+ if (!needsNoTools) {
602
+ if (toolMap == null || toolMap.size === 0) {
603
+ throw new Error('No toolMap provided for local programmatic execution.');
604
+ }
605
+ if (toolDefs == null || toolDefs.length === 0) {
606
+ throw new Error(
607
+ 'No tool definitions provided for local programmatic execution.'
608
+ );
609
+ }
599
610
  }
600
611
 
601
612
  const { effectiveTools, effectiveMap } = createEffectiveToolMap(
602
- toolMap,
613
+ toolMap ?? new Map(),
603
614
  selectedTools
604
615
  );
605
616
  const bridge = await createToolBridge(effectiveMap, hookContext);
@@ -105,7 +105,7 @@ export const createKeenableAPI = (
105
105
  date: result.published_at,
106
106
  }));
107
107
 
108
- return { success: true, data: { organic } };
108
+ return { success: true, data: { organic, topStories: [] } };
109
109
  } catch (error) {
110
110
  const errorMessage =
111
111
  error instanceof Error ? error.message : String(error);