@librechat/agents 3.7.2 → 3.7.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.
@@ -1,5 +1,10 @@
1
1
  import type * as t from '@/types';
2
2
  export declare function coerceRecordArgs(args: unknown): Record<string, unknown> | undefined;
3
+ /**
4
+ * Applies only lossless, schema-directed repairs to model-generated arguments.
5
+ * The host remains responsible for full schema validation and all business rules.
6
+ */
7
+ export declare function coerceArgsForSchema(args: Record<string, unknown>, schema: t.JsonSchemaType | undefined): Record<string, unknown>;
3
8
  export declare function stableStringify(value: unknown): string;
4
9
  export declare function recordArgsEqual(left: Record<string, unknown>, right: Record<string, unknown>): boolean;
5
10
  export declare function normalizeError(error: unknown): Error;
@@ -28,4 +33,5 @@ export declare function buildToolExecutionRequestPlan(args: {
28
33
  usageCount: Map<string, number>;
29
34
  invalidArgsBehavior?: 'abort' | 'error-result';
30
35
  recordTurn?: (toolName: string, turn: number, callId: string) => void;
36
+ getToolSchema?: (toolName: string) => t.JsonSchemaType | undefined;
31
37
  }): ToolExecutionRequestPlan | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.7.2",
3
+ "version": "3.7.4",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
package/src/stream.ts CHANGED
@@ -736,6 +736,12 @@ function createEagerToolExecutionPlan(args: {
736
736
  if (candidateToolCalls.length === 0) {
737
737
  return [];
738
738
  }
739
+ const toolSchemas = new Map(
740
+ agentContext?.toolDefinitions?.map(({ name, parameters }) => [
741
+ name,
742
+ parameters,
743
+ ])
744
+ );
739
745
 
740
746
  // Eager execution must preserve ToolNode batch semantics exactly for every
741
747
  // unstarted call. If any candidate cannot be planned, fall back for that
@@ -770,6 +776,7 @@ function createEagerToolExecutionPlan(args: {
770
776
  ),
771
777
  })),
772
778
  usageCount: graph.getEagerEventToolUsageCount(agentContext?.agentId),
779
+ getToolSchema: (toolName) => toolSchemas.get(toolName),
773
780
  });
774
781
  if (plan == null) {
775
782
  return undefined;
@@ -40,6 +40,7 @@ config();
40
40
 
41
41
  const DEFAULT_MAX_ROUND_TRIPS = 20;
42
42
  const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();
43
+ const BASH_LAST_BACKGROUND_PID_GUARD = ': &\nwait "$!"';
43
44
 
44
45
  /** Bash reserved words that get `_tool` suffix when used as function names */
45
46
  const BASH_RESERVED = new Set([
@@ -171,6 +172,14 @@ export const BashProgrammaticToolCallingDefinition = {
171
172
  schema: BashProgrammaticToolCallingSchema,
172
173
  } as const;
173
174
 
175
+ function prepareBashProgrammaticCode(code: string): string {
176
+ /* The Code API's generated Bash wrapper reads `$!` after user code. A user
177
+ * `set -u` makes that expansion fail when no background process has run.
178
+ * Seed and reap a no-op job before user code so strict mode remains active
179
+ * for the payload while the wrapper can safely read its special parameter. */
180
+ return `${BASH_LAST_BACKGROUND_PID_GUARD}\n${code}`;
181
+ }
182
+
174
183
  function maybeParseJsonResultString(result: unknown): unknown {
175
184
  if (typeof result !== 'string') {
176
185
  return result;
@@ -320,6 +329,7 @@ export function createBashProgrammaticToolCallingTool(
320
329
  async (rawParams, config) => {
321
330
  const params = rawParams as ProgrammaticInvocationParams;
322
331
  const { code } = params;
332
+ const preparedCode = prepareBashProgrammaticCode(code);
323
333
  const timeout = clampCodeApiRunTimeoutMs(params.timeout, maxRunTimeoutMs);
324
334
 
325
335
  const toolCall = (config.toolCall ?? {}) as ToolCall &
@@ -417,7 +427,7 @@ export function createBashProgrammaticToolCallingTool(
417
427
  EXEC_ENDPOINT,
418
428
  {
419
429
  lang: 'bash',
420
- code,
430
+ code: preparedCode,
421
431
  tools: effectiveTools,
422
432
  session_id,
423
433
  timeout,
@@ -71,6 +71,8 @@ import {
71
71
  } from '@/tools/intentArg';
72
72
  import {
73
73
  buildToolExecutionRequestPlan,
74
+ coerceArgsForSchema,
75
+ coerceRecordArgs,
74
76
  resolveRuntimeSessionHint,
75
77
  recordArgsEqual,
76
78
  } from '@/tools/eagerEventExecution';
@@ -1129,6 +1131,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1129
1131
  );
1130
1132
  }
1131
1133
 
1134
+ private getToolParameterSchema(
1135
+ toolName: string
1136
+ ): t.JsonSchemaType | undefined {
1137
+ return (
1138
+ this.toolDefinitions?.get(toolName)?.parameters ??
1139
+ this.toolRegistry?.get(toolName)?.parameters
1140
+ );
1141
+ }
1142
+
1143
+ private coerceEventToolArgs(
1144
+ toolName: string,
1145
+ args: unknown
1146
+ ): Record<string, unknown> {
1147
+ const recordArgs = coerceRecordArgs(args);
1148
+ return coerceArgsForSchema(
1149
+ recordArgs ?? (args as Record<string, unknown>),
1150
+ this.getToolParameterSchema(toolName)
1151
+ );
1152
+ }
1153
+
1132
1154
  /** Serializes the live caller projection for event-driven hosts. */
1133
1155
  private getCallerCapabilityProjectionSnapshot(): t.CallerCapabilityProjectionSnapshot {
1134
1156
  return createCallerCapabilityProjectionSnapshot(
@@ -2758,7 +2780,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2758
2780
  return {
2759
2781
  call,
2760
2782
  stepId: this.toolCallStepIds?.get(call.id!) ?? '',
2761
- args: resolvedArgs,
2783
+ args: this.coerceEventToolArgs(call.name, resolvedArgs),
2762
2784
  batchIndex: batchIndices?.[i],
2763
2785
  };
2764
2786
  });
@@ -3002,7 +3024,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3002
3024
  entry.call.name
3003
3025
  ),
3004
3026
  });
3005
- entry.args = resolved as Record<string, unknown>;
3027
+ entry.args = this.coerceEventToolArgs(
3028
+ entry.call.name,
3029
+ resolved as Record<string, unknown>
3030
+ );
3006
3031
  if (entry.call.id != null) {
3007
3032
  if (unresolved.length > 0) {
3008
3033
  unresolvedByCallId.set(entry.call.id, unresolved);
@@ -3012,7 +3037,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3012
3037
  }
3013
3038
  return;
3014
3039
  }
3015
- entry.args = nextArgs;
3040
+ entry.args = this.coerceEventToolArgs(entry.call.name, nextArgs);
3016
3041
  };
3017
3042
 
3018
3043
  const askEntries: Array<{
@@ -3369,6 +3394,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3369
3394
  }),
3370
3395
  usageCount: this.toolUsageCount,
3371
3396
  invalidArgsBehavior: 'error-result',
3397
+ getToolSchema: (toolName) => this.getToolParameterSchema(toolName),
3372
3398
  recordTurn: (toolName, reservedTurn, callId) => {
3373
3399
  this.recordEventToolPlanningTurn(
3374
3400
  toolName,
@@ -19,6 +19,89 @@ export function coerceRecordArgs(
19
19
  return args as Record<string, unknown>;
20
20
  }
21
21
 
22
+ const INTEGER_STRING = /^-?(?:0|[1-9]\d*)$/;
23
+ const NUMBER_STRING = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
24
+
25
+ function coerceValueForSchema(
26
+ value: unknown,
27
+ schema: t.JsonSchemaType | undefined
28
+ ): unknown {
29
+ if (schema == null) {
30
+ return value;
31
+ }
32
+
33
+ if (schema.type === 'integer' && typeof value === 'string') {
34
+ if (!INTEGER_STRING.test(value)) {
35
+ return value;
36
+ }
37
+ const parsed = Number(value);
38
+ return Number.isSafeInteger(parsed) ? parsed : value;
39
+ }
40
+
41
+ if (
42
+ (schema.type === 'number' || schema.type === 'float') &&
43
+ typeof value === 'string'
44
+ ) {
45
+ if (!NUMBER_STRING.test(value)) {
46
+ return value;
47
+ }
48
+ const parsed = Number(value);
49
+ return Number.isFinite(parsed) && String(parsed) === value
50
+ ? parsed
51
+ : value;
52
+ }
53
+
54
+ if (schema.type === 'boolean' && typeof value === 'string') {
55
+ if (value === 'true') {
56
+ return true;
57
+ }
58
+ if (value === 'false') {
59
+ return false;
60
+ }
61
+ return value;
62
+ }
63
+
64
+ if (schema.type === 'array' && Array.isArray(value)) {
65
+ return value.map((item) => coerceValueForSchema(item, schema.items));
66
+ }
67
+
68
+ if (
69
+ schema.type !== 'object' ||
70
+ value == null ||
71
+ typeof value !== 'object' ||
72
+ Array.isArray(value)
73
+ ) {
74
+ return value;
75
+ }
76
+
77
+ const record = value as Record<string, unknown>;
78
+ const additionalProperties =
79
+ typeof schema.additionalProperties === 'object'
80
+ ? schema.additionalProperties
81
+ : undefined;
82
+
83
+ return Object.fromEntries(
84
+ Object.entries(record).map(([key, entry]) => [
85
+ key,
86
+ coerceValueForSchema(
87
+ entry,
88
+ schema.properties?.[key] ?? additionalProperties
89
+ ),
90
+ ])
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Applies only lossless, schema-directed repairs to model-generated arguments.
96
+ * The host remains responsible for full schema validation and all business rules.
97
+ */
98
+ export function coerceArgsForSchema(
99
+ args: Record<string, unknown>,
100
+ schema: t.JsonSchemaType | undefined
101
+ ): Record<string, unknown> {
102
+ return coerceValueForSchema(args, schema) as Record<string, unknown>;
103
+ }
104
+
22
105
  export function stableStringify(value: unknown): string {
23
106
  if (Array.isArray(value)) {
24
107
  return `[${value.map((item) => stableStringify(item)).join(',')}]`;
@@ -87,6 +170,7 @@ export function buildToolExecutionRequestPlan(args: {
87
170
  usageCount: Map<string, number>;
88
171
  invalidArgsBehavior?: 'abort' | 'error-result';
89
172
  recordTurn?: (toolName: string, turn: number, callId: string) => void;
173
+ getToolSchema?: (toolName: string) => t.JsonSchemaType | undefined;
90
174
  }): ToolExecutionRequestPlan | undefined {
91
175
  const invalidArgsBehavior = args.invalidArgsBehavior ?? 'abort';
92
176
  const prepared: Array<{
@@ -103,8 +187,8 @@ export function buildToolExecutionRequestPlan(args: {
103
187
  if (toolCall.id == null || toolCall.id === '' || toolCall.name === '') {
104
188
  return undefined;
105
189
  }
106
- const coercedArgs = coerceRecordArgs(toolCall.args);
107
- if (coercedArgs == null) {
190
+ const recordArgs = coerceRecordArgs(toolCall.args);
191
+ if (recordArgs == null) {
108
192
  if (invalidArgsBehavior === 'abort') {
109
193
  return undefined;
110
194
  }
@@ -120,6 +204,10 @@ export function buildToolExecutionRequestPlan(args: {
120
204
  });
121
205
  continue;
122
206
  }
207
+ const coercedArgs = coerceArgsForSchema(
208
+ recordArgs,
209
+ args.getToolSchema?.(toolCall.name)
210
+ );
123
211
  prepared.push({
124
212
  id: toolCall.id,
125
213
  name: toolCall.name,