@librechat/agents 3.3.5 → 3.3.6

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 (127) hide show
  1. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  2. package/dist/cjs/llm/openai/index.cjs +51 -6
  3. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  4. package/dist/cjs/main.cjs +14 -0
  5. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  6. package/dist/cjs/stream.cjs +6 -1
  7. package/dist/cjs/stream.cjs.map +1 -1
  8. package/dist/cjs/tools/BashExecutor.cjs +3 -1
  9. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  10. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -0
  11. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/CodeExecutor.cjs +19 -16
  13. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +2 -0
  15. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  16. package/dist/cjs/tools/ReadFile.cjs +8 -4
  17. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  18. package/dist/cjs/tools/SkillTool.cjs +2 -0
  19. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  20. package/dist/cjs/tools/SubagentTool.cjs +3 -0
  21. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  22. package/dist/cjs/tools/ToolNode.cjs +88 -27
  23. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  24. package/dist/cjs/tools/ToolSearch.cjs +5 -1
  25. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  26. package/dist/cjs/tools/intentArg.cjs +230 -0
  27. package/dist/cjs/tools/intentArg.cjs.map +1 -0
  28. package/dist/cjs/tools/local/CompileCheckTool.cjs +3 -2
  29. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  30. package/dist/cjs/tools/local/LocalCodingTools.cjs +13 -12
  31. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  32. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +2 -2
  33. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  34. package/dist/cjs/tools/search/schema.cjs +2 -0
  35. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  36. package/dist/cjs/tools/search/tool.cjs +31 -1
  37. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  38. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  39. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  40. package/dist/cjs/tools/toolOutputReferences.cjs +41 -7
  41. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  42. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  43. package/dist/esm/llm/openai/index.mjs +51 -6
  44. package/dist/esm/llm/openai/index.mjs.map +1 -1
  45. package/dist/esm/main.mjs +4 -3
  46. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  47. package/dist/esm/stream.mjs +6 -1
  48. package/dist/esm/stream.mjs.map +1 -1
  49. package/dist/esm/tools/BashExecutor.mjs +3 -1
  50. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  51. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -0
  52. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  53. package/dist/esm/tools/CodeExecutor.mjs +19 -16
  54. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  55. package/dist/esm/tools/ProgrammaticToolCalling.mjs +2 -0
  56. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  57. package/dist/esm/tools/ReadFile.mjs +8 -4
  58. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  59. package/dist/esm/tools/SkillTool.mjs +2 -0
  60. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  61. package/dist/esm/tools/SubagentTool.mjs +3 -0
  62. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  63. package/dist/esm/tools/ToolNode.mjs +88 -28
  64. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  65. package/dist/esm/tools/ToolSearch.mjs +5 -1
  66. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  67. package/dist/esm/tools/intentArg.mjs +220 -0
  68. package/dist/esm/tools/intentArg.mjs.map +1 -0
  69. package/dist/esm/tools/local/CompileCheckTool.mjs +3 -2
  70. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  71. package/dist/esm/tools/local/LocalCodingTools.mjs +13 -12
  72. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  73. package/dist/esm/tools/local/LocalExecutionEngine.mjs +2 -2
  74. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  75. package/dist/esm/tools/search/schema.mjs +2 -0
  76. package/dist/esm/tools/search/schema.mjs.map +1 -1
  77. package/dist/esm/tools/search/tool.mjs +31 -2
  78. package/dist/esm/tools/search/tool.mjs.map +1 -1
  79. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  80. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  81. package/dist/esm/tools/toolOutputReferences.mjs +41 -7
  82. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  83. package/dist/types/index.d.ts +1 -0
  84. package/dist/types/llm/openai/index.d.ts +0 -11
  85. package/dist/types/tools/BashExecutor.d.ts +18 -0
  86. package/dist/types/tools/CodeExecutor.d.ts +18 -0
  87. package/dist/types/tools/ReadFile.d.ts +18 -0
  88. package/dist/types/tools/SkillTool.d.ts +18 -0
  89. package/dist/types/tools/SubagentTool.d.ts +9 -0
  90. package/dist/types/tools/ToolNode.d.ts +37 -5
  91. package/dist/types/tools/ToolSearch.d.ts +18 -0
  92. package/dist/types/tools/intentArg.d.ts +119 -0
  93. package/dist/types/tools/ptcTimeout.d.ts +2 -0
  94. package/dist/types/tools/search/schema.d.ts +18 -0
  95. package/dist/types/tools/search/tool.d.ts +18 -0
  96. package/dist/types/tools/toolOutputReferences.d.ts +12 -2
  97. package/dist/types/types/stream.d.ts +12 -0
  98. package/dist/types/types/tools.d.ts +27 -2
  99. package/package.json +1 -1
  100. package/src/index.ts +1 -0
  101. package/src/llm/openai/index.ts +74 -10
  102. package/src/llm/openai/llm.spec.ts +105 -0
  103. package/src/stream.ts +12 -1
  104. package/src/tools/BashExecutor.ts +6 -0
  105. package/src/tools/BashProgrammaticToolCalling.ts +2 -0
  106. package/src/tools/CodeExecutor.ts +6 -0
  107. package/src/tools/ProgrammaticToolCalling.ts +2 -0
  108. package/src/tools/ReadFile.ts +2 -0
  109. package/src/tools/SkillTool.ts +2 -0
  110. package/src/tools/SubagentTool.ts +3 -0
  111. package/src/tools/ToolNode.ts +190 -43
  112. package/src/tools/ToolSearch.ts +3 -0
  113. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +174 -0
  114. package/src/tools/__tests__/intentArg.test.ts +281 -0
  115. package/src/tools/__tests__/intentCoverage.test.ts +139 -0
  116. package/src/tools/__tests__/toolOutputReferences.test.ts +83 -0
  117. package/src/tools/intentArg.ts +323 -0
  118. package/src/tools/local/CompileCheckTool.ts +4 -3
  119. package/src/tools/local/LocalCodingTools.ts +14 -13
  120. package/src/tools/ptcTimeout.ts +2 -0
  121. package/src/tools/search/outcome.test.ts +90 -0
  122. package/src/tools/search/schema.ts +3 -0
  123. package/src/tools/search/tool.ts +47 -1
  124. package/src/tools/subagent/SubagentExecutor.ts +2 -1
  125. package/src/tools/toolOutputReferences.ts +77 -9
  126. package/src/types/stream.ts +12 -0
  127. package/src/types/tools.ts +24 -2
@@ -36,6 +36,15 @@ export declare const newsSchema: {
36
36
  export declare const WebSearchToolSchema: {
37
37
  readonly type: "object";
38
38
  readonly properties: {
39
+ readonly intent: {
40
+ readonly type: "string" | "number" | "integer" | "float" | "boolean" | "array" | "object";
41
+ readonly enum?: string[];
42
+ readonly items?: import("../..").JsonSchemaType;
43
+ readonly properties?: Record<string, import("../..").JsonSchemaType>;
44
+ readonly required?: string[];
45
+ readonly description?: string;
46
+ readonly additionalProperties?: boolean | import("../..").JsonSchemaType;
47
+ };
39
48
  readonly query: {
40
49
  readonly type: "string";
41
50
  readonly description: string;
@@ -72,6 +81,15 @@ export declare const WebSearchToolDefinition: {
72
81
  readonly schema: {
73
82
  readonly type: "object";
74
83
  readonly properties: {
84
+ readonly intent: {
85
+ readonly type: "string" | "number" | "integer" | "float" | "boolean" | "array" | "object";
86
+ readonly enum?: string[];
87
+ readonly items?: import("../..").JsonSchemaType;
88
+ readonly properties?: Record<string, import("../..").JsonSchemaType>;
89
+ readonly required?: string[];
90
+ readonly description?: string;
91
+ readonly additionalProperties?: boolean | import("../..").JsonSchemaType;
92
+ };
75
93
  readonly query: {
76
94
  readonly type: "string";
77
95
  readonly description: string;
@@ -2,6 +2,24 @@ import { DynamicStructuredTool } from '@langchain/core/tools';
2
2
  import type * as t from './types';
3
3
  import { DATE_RANGE } from './schema';
4
4
  import { createSearchAPI } from './search';
5
+ /**
6
+ * Settled label for a `web_search` call's intent (see `intentArg.ts`).
7
+ *
8
+ * Counts the result kinds `formatResultsForLLM` actually renders —
9
+ * `references` only tracks links embedded in extracted highlights, so it
10
+ * undercounts ordinary results and can overcount when one highlight embeds
11
+ * several links.
12
+ *
13
+ * A caught provider or processing failure is reported through `data.error`
14
+ * while the tool still returns NORMALLY, so that case must author its own
15
+ * label: the `ToolMessage` carries success status, and a bare intent would
16
+ * otherwise settle mechanically from "Searching…" to "Searched…" and present
17
+ * a failed search as a successful one.
18
+ *
19
+ * Returns undefined for a genuine zero-result search, leaving the host's
20
+ * mechanical past-tense transform to label it.
21
+ */
22
+ export declare function resolveSearchOutcome(data: t.SearchResultData, query: string): string | undefined;
5
23
  /**
6
24
  * Executes parallel searches and merges the results,
7
25
  * deduplicating top stories by link
@@ -69,8 +69,18 @@ export type ResolveResult<T> = {
69
69
  * point in time, ignoring any subsequent registrations.
70
70
  */
71
71
  export interface ToolOutputResolveView {
72
- resolve<T>(args: T): ResolveResult<T>;
72
+ resolve<T>(args: T, options?: ResolveOptions): ResolveResult<T>;
73
73
  }
74
+ /**
75
+ * Per-call resolution options. `substituteIntentKey` opts the top-level
76
+ * `intent` key back INTO placeholder substitution: the exemption protects
77
+ * the injected display label, but a tool whose own schema declares a
78
+ * business parameter named `intent` (the injectors skip such tools) still
79
+ * needs references piped into it like any other argument.
80
+ */
81
+ export type ResolveOptions = {
82
+ substituteIntentKey?: boolean;
83
+ };
74
84
  /**
75
85
  * Pre-resolved arg map keyed by `toolCallId`. Used by the mixed
76
86
  * direct+event dispatch path to feed event calls' resolved args
@@ -166,7 +176,7 @@ export declare class ToolOutputReferenceRegistry {
166
176
  * the serialized args, the original input is returned without
167
177
  * walking the tree.
168
178
  */
169
- resolve<T>(runId: string | undefined, args: T): ResolveResult<T>;
179
+ resolve<T>(runId: string | undefined, args: T, options?: ResolveOptions): ResolveResult<T>;
170
180
  /**
171
181
  * Captures a frozen snapshot of `runId`'s current entries and
172
182
  * returns a view that resolves placeholders against *only* that
@@ -98,6 +98,13 @@ export type ProcessedToolCall = {
98
98
  id: string;
99
99
  output: string;
100
100
  progress: number;
101
+ /**
102
+ * Settled label for the call, resolved from the tool-supplied
103
+ * `outcome`/`outcome_patch` result fields against the model-authored
104
+ * `intent` arg. Only present when the tool authored one — hosts apply
105
+ * the mechanical intent transform themselves when absent.
106
+ */
107
+ outcome?: string;
101
108
  };
102
109
  export type ProcessedContent = {
103
110
  type: ContentType;
@@ -265,6 +272,11 @@ export type ToolCallPart = {
265
272
  id?: string;
266
273
  /** If provided, the output of the tool call */
267
274
  output?: ToolResultContent['content'];
275
+ /**
276
+ * Tool-authored settled label for the call (see `ProcessedToolCall.outcome`),
277
+ * preserved through aggregation so it survives persistence/reload.
278
+ */
279
+ outcome?: string;
268
280
  /** Auth URL */
269
281
  auth?: string;
270
282
  /** Expiration time */
@@ -224,8 +224,16 @@ export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
224
224
  export type ToolEndEvent = {
225
225
  /** The Step Id of the Tool Call */
226
226
  id: string;
227
- /** The Completed Tool Call */
228
- tool_call: ToolCall;
227
+ /**
228
+ * The Completed Tool Call. Carries the tool-authored `outcome` label when
229
+ * present (see `ProcessedToolCall.outcome`) so `ON_RUN_STEP_COMPLETED`
230
+ * consumers can read it without an unsafe cast.
231
+ */
232
+ tool_call: ToolCall & {
233
+ output?: string;
234
+ progress?: number;
235
+ outcome?: string;
236
+ };
229
237
  /** The content index of the tool call */
230
238
  index: number;
231
239
  type?: 'tool_call';
@@ -486,6 +494,16 @@ export type InjectedMessage = {
486
494
  /** Only set when source is 'skill', for compaction preservation */
487
495
  skillName?: string;
488
496
  };
497
+ /**
498
+ * In-place edit of a call's model-authored `intent` label: the first
499
+ * occurrence of `from` in the intent is replaced with `to` (case-sensitive).
500
+ * Lets a tool settle the label while preserving the model's own phrasing,
501
+ * e.g. `{ from: 'Searching', to: 'Searched' }`.
502
+ */
503
+ export type OutcomePatch = {
504
+ from: string;
505
+ to: string;
506
+ };
489
507
  /** Result for a single tool call in event-driven execution */
490
508
  export type ToolExecuteResult = {
491
509
  /** Matches ToolCallRequest.id */
@@ -498,6 +516,13 @@ export type ToolExecuteResult = {
498
516
  status: 'success' | 'error';
499
517
  /** Error message if status is 'error' */
500
518
  errorMessage?: string;
519
+ /**
520
+ * Settled human-readable label for this call, replacing the model-authored
521
+ * `intent` arg in the UI. Full replacement; wins over `outcome_patch`.
522
+ */
523
+ outcome?: string;
524
+ /** In-place edit of the model-authored `intent` label (see {@link OutcomePatch}). */
525
+ outcome_patch?: OutcomePatch;
501
526
  /**
502
527
  * Messages to inject into graph state after the ToolMessage for this call.
503
528
  * Placed after tool results to respect provider message ordering (tool_call -> tool_result adjacency).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.3.5",
3
+ "version": "3.3.6",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from './tools/ReadFile';
26
26
  export * from './tools/skillCatalog';
27
27
  export * from './tools/ToolSearch';
28
28
  export * from './tools/ToolNode';
29
+ export * from './tools/intentArg';
29
30
  export * from './tools/schema';
30
31
  export * from './tools/handlers';
31
32
  export * from './tools/local';
@@ -51,6 +51,7 @@ import {
51
51
  projectOpenAIResponsesToolMessageContent,
52
52
  projectToolStreamContentForProvider,
53
53
  } from '@/messages/core';
54
+ import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
54
55
  import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
55
56
  import { dropRepeatedScalarMetadata } from './streamMetadata';
56
57
 
@@ -1003,6 +1004,65 @@ function createAbortHandler(controller: AbortController): () => void {
1003
1004
  * @param {Object} [fields] Additional fields to add to the OpenAI tool.
1004
1005
  * @returns {ToolDefinition} The inputted tool in OpenAI tool format.
1005
1006
  */
1007
+ /**
1008
+ * OpenAI strict function schemas require every property to appear in
1009
+ * `required`. The optional `intent` label (see `tools/intentArg.ts`) is
1010
+ * deliberately NOT required — the same schema is callable from programmatic
1011
+ * tool calling — so a tool auto-marked `strict: true` (the non-streaming
1012
+ * `json_schema` structured-output path) would be rejected as invalid before
1013
+ * execution. That path never streams a live label anyway, so the
1014
+ * marker-identified property is dropped there; every other path keeps it.
1015
+ */
1016
+ function stripIntentFromStrictTools<T extends object>(params: T): T {
1017
+ const record = params as { tools?: unknown[] };
1018
+ const tools = record.tools;
1019
+ if (!Array.isArray(tools) || tools.length === 0) {
1020
+ return params;
1021
+ }
1022
+ const nextTools = tools.map((tool) => {
1023
+ const candidate = tool as {
1024
+ strict?: boolean;
1025
+ parameters?: { properties?: Record<string, unknown>; required?: unknown };
1026
+ function?: {
1027
+ strict?: boolean;
1028
+ parameters?: {
1029
+ properties?: Record<string, unknown>;
1030
+ required?: unknown;
1031
+ };
1032
+ };
1033
+ };
1034
+ /** Chat-completions tools nest under `function`; responses-API tools are flat. */
1035
+ const holder = candidate.function ?? candidate;
1036
+ if (holder.strict !== true) {
1037
+ return tool;
1038
+ }
1039
+ const parameters = holder.parameters;
1040
+ const properties = parameters?.properties;
1041
+ if (properties == null || !isIntentLabelProperty(properties[INTENT_ARG])) {
1042
+ return tool;
1043
+ }
1044
+ const required = Array.isArray(parameters?.required)
1045
+ ? (parameters.required as unknown[])
1046
+ : [];
1047
+ if (required.includes(INTENT_ARG)) {
1048
+ return tool;
1049
+ }
1050
+ const { [INTENT_ARG]: _omit, ...restProps } = properties;
1051
+ const nextParams = { ...parameters, properties: restProps };
1052
+ if (candidate.function != null) {
1053
+ return {
1054
+ ...candidate,
1055
+ function: { ...candidate.function, parameters: nextParams },
1056
+ };
1057
+ }
1058
+ return { ...candidate, parameters: nextParams };
1059
+ });
1060
+ if (nextTools.every((tool, index) => tool === tools[index])) {
1061
+ return params;
1062
+ }
1063
+ return { ...params, tools: nextTools } as T;
1064
+ }
1065
+
1006
1066
  export function _convertToOpenAITool(
1007
1067
  tool: BindToolsInput,
1008
1068
  fields?: {
@@ -1184,10 +1244,12 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
1184
1244
  options?: this['ParsedCallOptions'],
1185
1245
  extra?: { streaming?: boolean }
1186
1246
  ): ReturnType<OriginalChatOpenAICompletions['invocationParams']> {
1187
- return applyManagedRequestParams(super.invocationParams(options, extra), {
1188
- promptCacheExplicit: this.promptCacheExplicit,
1189
- safetyIdentifier: this.safetyIdentifier,
1190
- });
1247
+ return stripIntentFromStrictTools(
1248
+ applyManagedRequestParams(super.invocationParams(options, extra), {
1249
+ promptCacheExplicit: this.promptCacheExplicit,
1250
+ safetyIdentifier: this.safetyIdentifier,
1251
+ })
1252
+ );
1191
1253
  }
1192
1254
 
1193
1255
  protected _getReasoningParams(
@@ -1640,7 +1702,7 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
1640
1702
  ]),
1641
1703
  ];
1642
1704
  }
1643
- return params;
1705
+ return stripIntentFromStrictTools(params);
1644
1706
  }
1645
1707
 
1646
1708
  async completionWithRetry(
@@ -1741,10 +1803,12 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
1741
1803
  options?: this['ParsedCallOptions'],
1742
1804
  extra?: { streaming?: boolean }
1743
1805
  ): ReturnType<OriginalAzureChatOpenAICompletions['invocationParams']> {
1744
- return applyManagedRequestParams(super.invocationParams(options, extra), {
1745
- promptCacheExplicit: this.promptCacheExplicit,
1746
- safetyIdentifier: this.safetyIdentifier,
1747
- });
1806
+ return stripIntentFromStrictTools(
1807
+ applyManagedRequestParams(super.invocationParams(options, extra), {
1808
+ promptCacheExplicit: this.promptCacheExplicit,
1809
+ safetyIdentifier: this.safetyIdentifier,
1810
+ })
1811
+ );
1748
1812
  }
1749
1813
 
1750
1814
  protected _getReasoningParams(
@@ -1888,7 +1952,7 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
1888
1952
  ]),
1889
1953
  ];
1890
1954
  }
1891
- return params;
1955
+ return stripIntentFromStrictTools(params);
1892
1956
  }
1893
1957
 
1894
1958
  async completionWithRetry(
@@ -1206,6 +1206,111 @@ describe('ChatOpenAICompletions strict tools for structured output', () => {
1206
1206
  toolStrict({ response_format: { type: 'json_object' } })
1207
1207
  ).toBeUndefined();
1208
1208
  });
1209
+
1210
+ describe('optional intent labels under strict mode', () => {
1211
+ const intentTool = {
1212
+ type: 'function' as const,
1213
+ function: {
1214
+ name: 'search_mcp_docs',
1215
+ description: 'Search docs',
1216
+ parameters: {
1217
+ type: 'object',
1218
+ properties: {
1219
+ intent: {
1220
+ type: 'string',
1221
+ description:
1222
+ 'ALWAYS write this field FIRST, before any other argument. One short sentence…',
1223
+ },
1224
+ query: { type: 'string' },
1225
+ },
1226
+ required: ['query'],
1227
+ },
1228
+ },
1229
+ };
1230
+
1231
+ function toolProperties(options: Record<string, unknown>): string[] {
1232
+ const model = new ChatOpenAI({ model: 'gpt-4', apiKey: 'test-key' });
1233
+ const completions = completionsOf<InvocationParamsDelegate>(model);
1234
+ const params = completions.invocationParams({
1235
+ tools: [intentTool],
1236
+ ...options,
1237
+ }) as unknown as {
1238
+ tools?: { function: { parameters: { properties: object } } }[];
1239
+ };
1240
+ return Object.keys(params.tools?.[0]?.function.parameters.properties ?? {});
1241
+ }
1242
+
1243
+ it('drops the optional label when strict is auto-enabled (invalid otherwise)', () => {
1244
+ expect(toolProperties({ response_format: jsonSchemaResponseFormat })).toEqual(['query']);
1245
+ });
1246
+
1247
+ it('keeps the label on non-strict requests', () => {
1248
+ expect(toolProperties({})).toEqual(['intent', 'query']);
1249
+ });
1250
+
1251
+ /**
1252
+ * The Responses API flattens tools to `{type, name, parameters, strict}`
1253
+ * and does NOT infer strict from a `json_schema` response_format the way
1254
+ * Completions does — an explicit `strict` (per-call option or on the tool)
1255
+ * is what turns it on there.
1256
+ */
1257
+ function responsesToolProperties(
1258
+ options: Record<string, unknown>
1259
+ ): string[] {
1260
+ const model = new ChatOpenAI({ model: 'gpt-4', apiKey: 'test-key' });
1261
+ const responses = (
1262
+ model as unknown as {
1263
+ responses: {
1264
+ invocationParams: (o: Record<string, unknown>) => {
1265
+ tools?: { parameters?: { properties?: object } }[];
1266
+ };
1267
+ };
1268
+ }
1269
+ ).responses;
1270
+ const params = responses.invocationParams({
1271
+ tools: [intentTool],
1272
+ ...options,
1273
+ });
1274
+ return Object.keys(params.tools?.[0]?.parameters?.properties ?? {});
1275
+ }
1276
+
1277
+ it('drops the label on the RESPONSES delegate under strict too', () => {
1278
+ expect(responsesToolProperties({ strict: true })).toEqual(['query']);
1279
+ });
1280
+
1281
+ it('keeps the label on a non-strict RESPONSES request', () => {
1282
+ expect(responsesToolProperties({})).toEqual(['intent', 'query']);
1283
+ });
1284
+
1285
+ it('spares a business `intent` param even under strict', () => {
1286
+ const businessTool = {
1287
+ type: 'function' as const,
1288
+ function: {
1289
+ name: 'create_record',
1290
+ parameters: {
1291
+ type: 'object',
1292
+ properties: {
1293
+ intent: { type: 'string', description: 'CRM intent category' },
1294
+ title: { type: 'string' },
1295
+ },
1296
+ required: ['intent', 'title'],
1297
+ },
1298
+ },
1299
+ };
1300
+ const model = new ChatOpenAI({ model: 'gpt-4', apiKey: 'test-key' });
1301
+ const completions = completionsOf<InvocationParamsDelegate>(model);
1302
+ const params = completions.invocationParams({
1303
+ tools: [businessTool],
1304
+ response_format: jsonSchemaResponseFormat,
1305
+ }) as unknown as {
1306
+ tools?: { function: { parameters: { properties: object } } }[];
1307
+ };
1308
+ expect(Object.keys(params.tools?.[0]?.function.parameters.properties ?? {})).toEqual([
1309
+ 'intent',
1310
+ 'title',
1311
+ ]);
1312
+ });
1313
+ });
1209
1314
  });
1210
1315
 
1211
1316
  describe('ChatOpenAI._streamChatModelEvents (native, fork)', () => {
package/src/stream.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  calculateMaxToolResultChars,
40
40
  truncateToolResultContent,
41
41
  } from '@/utils/truncation';
42
+ import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
42
43
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
43
44
  import { safeDispatchCustomEvent } from '@/utils/events';
44
45
  import { isGoogleLike } from '@/utils/llm';
@@ -859,6 +860,11 @@ async function dispatchEagerToolCompletions(args: {
859
860
  maxToolResultChars
860
861
  ).content;
861
862
  }
863
+ const outcome = resolveToolOutcome(
864
+ record.request.args,
865
+ outcomeFieldsFromResult(result),
866
+ { isError: result.status === 'error' }
867
+ );
862
868
 
863
869
  try {
864
870
  const dispatched = await safeDispatchCustomEvent(
@@ -878,6 +884,7 @@ async function dispatchEagerToolCompletions(args: {
878
884
  id: result.toolCallId,
879
885
  output,
880
886
  progress: 1,
887
+ ...(outcome != null && { outcome }),
881
888
  } as t.ProcessedToolCall,
882
889
  },
883
890
  },
@@ -2160,7 +2167,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2160
2167
  toolCallContentIndexMap.delete(existingToolCallId);
2161
2168
  }
2162
2169
 
2163
- const newToolCall: ToolCall & t.PartMetadata = {
2170
+ const newToolCall: ToolCall & t.PartMetadata & { outcome?: string } = {
2164
2171
  id,
2165
2172
  name,
2166
2173
  args,
@@ -2180,6 +2187,10 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2180
2187
  if (finalUpdate) {
2181
2188
  newToolCall.progress = 1;
2182
2189
  newToolCall.output = contentPart.tool_call.output;
2190
+ const outcome = (contentPart.tool_call as t.ToolCallPart).outcome;
2191
+ if (outcome != null) {
2192
+ newToolCall.outcome = outcome;
2193
+ }
2183
2194
  }
2184
2195
 
2185
2196
  contentParts[index] = {
@@ -16,6 +16,7 @@ import {
16
16
  normalizeCodeApiRequestError,
17
17
  resolveCodeApiAuthHeaders,
18
18
  } from './CodeExecutor';
19
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
19
20
  import { Constants } from '@/common';
20
21
 
21
22
  config();
@@ -26,6 +27,7 @@ const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
26
27
  export const BashExecutionToolSchema = {
27
28
  type: 'object',
28
29
  properties: {
30
+ intent: { ...INTENT_PROPERTY },
29
31
  command: {
30
32
  type: 'string',
31
33
  description: `The bash command or script to execute.
@@ -187,16 +189,20 @@ function createBashExecutionTool(
187
189
  /* Drop any model-supplied `runtime_session_hint` from the raw args: the
188
190
  * hint must only come from ToolNode's injected `_runtime_session_hint`
189
191
  * (below), never from the tool call itself. */
192
+ /* `intent` is a UI display label — never part of the wire body. */
190
193
  const {
191
194
  command,
195
+ intent: _ignoredIntent,
192
196
  runtime_session_hint: _ignoredModelHint,
193
197
  ...rest
194
198
  } = rawInput as {
195
199
  command: string;
200
+ intent?: unknown;
196
201
  runtime_session_hint?: unknown;
197
202
  args?: string[];
198
203
  };
199
204
  void _ignoredModelHint;
205
+ void _ignoredIntent;
200
206
  const { session_id, _injected_files, _runtime_session_hint } =
201
207
  (config.toolCall ?? {}) as {
202
208
  session_id?: string;
@@ -21,6 +21,7 @@ import {
21
21
  executeTools,
22
22
  formatCompletedResponse,
23
23
  } from './ProgrammaticToolCalling';
24
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
24
25
  import { Constants } from '@/common';
25
26
 
26
27
  config();
@@ -115,6 +116,7 @@ export function createBashProgrammaticToolCallingSchema(
115
116
  return {
116
117
  type: 'object',
117
118
  properties: {
119
+ intent: { ...INTENT_PROPERTY },
118
120
  code: {
119
121
  type: 'string',
120
122
  minLength: 1,
@@ -5,6 +5,7 @@ import { getEnvironmentVariable } from '@langchain/core/utils/env';
5
5
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
6
6
  import type * as t from '@/types';
7
7
  import { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
8
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
8
9
  import { EnvVar, Constants } from '@/common';
9
10
 
10
11
  export {
@@ -75,6 +76,7 @@ const SUPPORTED_LANGUAGES = [
75
76
  export const CodeExecutionToolSchema = {
76
77
  type: 'object',
77
78
  properties: {
79
+ intent: { ...INTENT_PROPERTY },
78
80
  lang: {
79
81
  type: 'string',
80
82
  enum: SUPPORTED_LANGUAGES,
@@ -346,18 +348,22 @@ function createCodeExecutionTool(
346
348
  * injected `_runtime_session_hint` (below). Spreading `...rest` into
347
349
  * postData would otherwise let a tool call opt itself into / pick a
348
350
  * stateful runtime even when statefulSessions is off. */
351
+ /* `intent` is a UI display label — never part of the wire body. */
349
352
  const {
350
353
  lang,
351
354
  code,
355
+ intent: _ignoredIntent,
352
356
  runtime_session_hint: _ignoredModelHint,
353
357
  ...rest
354
358
  } = rawInput as {
355
359
  lang: SupportedLanguage;
356
360
  code: string;
361
+ intent?: unknown;
357
362
  runtime_session_hint?: unknown;
358
363
  args?: string[];
359
364
  };
360
365
  void _ignoredModelHint;
366
+ void _ignoredIntent;
361
367
  /**
362
368
  * Extract session context from config.toolCall (injected by ToolNode).
363
369
  * - session_id: associates with the previous run.
@@ -24,6 +24,7 @@ import {
24
24
  createCodeApiRunTimeoutSchema,
25
25
  resolveCodeApiRunTimeoutMs,
26
26
  } from './ptcTimeout';
27
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
27
28
  import { Constants } from '@/common';
28
29
 
29
30
  config();
@@ -89,6 +90,7 @@ export function createProgrammaticToolCallingSchema(
89
90
  return {
90
91
  type: 'object',
91
92
  properties: {
93
+ intent: { ...INTENT_PROPERTY },
92
94
  code: {
93
95
  type: 'string',
94
96
  minLength: 1,
@@ -1,4 +1,5 @@
1
1
  // src/tools/ReadFile.ts
2
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
2
3
  import { Constants } from '@/common';
3
4
 
4
5
  export const ReadFileToolName = Constants.READ_FILE;
@@ -22,6 +23,7 @@ CONSTRAINTS:
22
23
  export const ReadFileToolSchema = {
23
24
  type: 'object',
24
25
  properties: {
26
+ intent: { ...INTENT_PROPERTY },
25
27
  path: {
26
28
  type: 'string',
27
29
  description:
@@ -1,4 +1,5 @@
1
1
  // src/tools/SkillTool.ts
2
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
2
3
  import { Constants } from '@/common';
3
4
 
4
5
  export const SkillToolName = Constants.SKILL_TOOL;
@@ -26,6 +27,7 @@ CONSTRAINTS:
26
27
  export const SkillToolSchema = {
27
28
  type: 'object',
28
29
  properties: {
30
+ intent: { ...INTENT_PROPERTY },
29
31
  skillName: {
30
32
  type: 'string',
31
33
  description:
@@ -1,5 +1,6 @@
1
1
  import type { JsonSchemaType, LCTool } from '@/types/tools';
2
2
  import type { SubagentConfig } from '@/types';
3
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
3
4
  import { Constants } from '@/common';
4
5
 
5
6
  export const SubagentToolName = Constants.SUBAGENT;
@@ -29,6 +30,7 @@ const SUBAGENT_TYPE_PROP_DESCRIPTION =
29
30
  export const SubagentToolSchema = {
30
31
  type: 'object',
31
32
  properties: {
33
+ intent: { ...INTENT_PROPERTY },
32
34
  description: {
33
35
  type: 'string',
34
36
  description: DESCRIPTION_PROP_DESCRIPTION,
@@ -67,6 +69,7 @@ export function buildSubagentToolParams(configs: SubagentConfig[]): {
67
69
  schema: {
68
70
  type: 'object',
69
71
  properties: {
72
+ intent: { ...INTENT_PROPERTY },
70
73
  description: {
71
74
  type: 'string',
72
75
  description: DESCRIPTION_PROP_DESCRIPTION,