@librechat/agents 3.3.5 → 3.3.7

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 +16 -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 +256 -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 +244 -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 +181 -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 +18 -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 +357 -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 +357 -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 +18 -0
  127. package/src/types/tools.ts +24 -2
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @fileoverview Tool intent labels.
3
+ *
4
+ * Lets a tool declare, as the FIRST property of its input schema, an `intent`
5
+ * string: one model-authored sentence stating what that specific call is about
6
+ * to do ("Searching for OAuth handling in the callback router"). Because the
7
+ * property is first, it is the first key providers stream in the tool-call
8
+ * args, so a host UI can render it as the call's live status label before the
9
+ * rest of the args exist. When the call settles, {@link applyOutcome} edits
10
+ * the sentence in place into its outcome form — a tool-supplied replacement
11
+ * (`outcome`) or a tool-supplied span edit (`outcome_patch`). Absent either,
12
+ * the label is left exactly as the model wrote it: completion is a UI state
13
+ * (the shimmer stopping, the icon settling), not a tense change.
14
+ *
15
+ * The arg is always optional (never listed in `required`): the same schemas
16
+ * are callable from programmatic tool calling, where no UI renders a label
17
+ * and forcing generated code to fabricate one would be pure cost. Tool bodies
18
+ * must call {@link stripIntent} before using their args so no tool receives a
19
+ * parameter it did not declare.
20
+ */
21
+ import type { JsonSchemaType, OutcomePatch } from '@/types';
22
+ /** Argument carrying the model-authored label for a tool call. */
23
+ export declare const INTENT_ARG = "intent";
24
+ /**
25
+ * Opening words of {@link INTENT_DESCRIPTION}, and the discriminator that
26
+ * tells the injected LABEL apart from a tool's own business parameter that
27
+ * merely shares the name `intent`.
28
+ *
29
+ * Exported because host applications reimplement the same strip/sanitize
30
+ * passes and would otherwise duplicate this as a string literal: if the two
31
+ * copies drift, the host silently stops recognizing SDK-native labels and
32
+ * fails OPEN (labels stay in schemas, opt-outs stop working) with no error.
33
+ * Any edit to the description must preserve this prefix verbatim.
34
+ */
35
+ export declare const INTENT_LABEL_MARKER = "ALWAYS write this field FIRST";
36
+ /**
37
+ * Model-facing instruction for the injected `intent` property.
38
+ *
39
+ * Deliberately terse — it is repeated on every opted-in tool schema, on every
40
+ * request, so each sentence is paid for many times over. What remains is
41
+ * load-bearing: first-position placement (the entire streaming mechanism),
42
+ * the one-sentence present-progressive form, who reads it, and the sibling
43
+ * rule, without which models emit identical labels for parallel calls to one
44
+ * tool and defeat the feature's headline case.
45
+ */
46
+ export declare const INTENT_DESCRIPTION: string;
47
+ /**
48
+ * Canonical (frozen) shape of the injected property. Always embed a COPY
49
+ * (`{ ...INTENT_PROPERTY }`): LangChain's JSON-schema validator stamps a
50
+ * `__absolute_uri__` marker onto every subschema it dereferences, which
51
+ * throws on a frozen object — and a single shared instance would be stamped
52
+ * with one schema's URI while embedded in many.
53
+ */
54
+ export declare const INTENT_PROPERTY: JsonSchemaType;
55
+ /**
56
+ * Discriminates the intent LABEL property from a tool's own business
57
+ * parameter that merely shares the name: the label contract always opens
58
+ * with the same instruction. Removal/sanitize passes must never strip a
59
+ * parameter the tool actually needs.
60
+ */
61
+ export declare function isIntentLabelProperty(property: unknown): boolean;
62
+ /**
63
+ * Schema shape accepted by {@link withoutIntent}.
64
+ *
65
+ * `required` is widened to `readonly string[]` because the SDK's own native
66
+ * schemas are declared `as const` — their `required` is a readonly tuple, and
67
+ * a mutable `string[]` parameter would reject the very schemas this helper
68
+ * exists for (TS2345), forcing embedders to cast to use the advertised API.
69
+ */
70
+ export type IntentStrippableSchema = Omit<JsonSchemaType, 'required'> & {
71
+ required?: readonly string[];
72
+ };
73
+ /**
74
+ * Returns a copy of `parameters` without the injected intent LABEL — the
75
+ * opt-out for consumers that render no status label and should not pay for
76
+ * the property.
77
+ *
78
+ * The SDK's native schemas carry the label unconditionally, so without this
79
+ * an embedder has no lever at all: `withIntent` is applied at module scope.
80
+ * Marker-guarded, so a tool's own business parameter named `intent` is never
81
+ * removed. Returns the input unchanged when there is nothing to strip.
82
+ *
83
+ * `required` is pruned alongside the property: a schema that lists `intent`
84
+ * as required (strict-mode normalization does exactly that, since OpenAI
85
+ * strict function schemas require every property to appear in `required`)
86
+ * would otherwise be left naming a property it no longer declares, which is
87
+ * invalid JSON Schema and gets rejected by the provider instead of quietly
88
+ * opting out.
89
+ */
90
+ export declare function withoutIntent(parameters?: IntentStrippableSchema): JsonSchemaType | undefined;
91
+ /**
92
+ * Returns a copy of the parameters schema with `intent` prepended as the
93
+ * FIRST property (object key order is insertion order and every provider
94
+ * serializer preserves it — first key in the schema means first key in the
95
+ * streamed input). Never mutates the input; no-op when the schema already
96
+ * declares `intent`. The property is not added to `required`.
97
+ */
98
+ export declare function withIntent(parameters?: JsonSchemaType): JsonSchemaType;
99
+ /**
100
+ * Reads the model-authored intent from tool-call args (handles stringified
101
+ * args). Returns undefined when absent, empty, or not a string.
102
+ */
103
+ export declare function readIntent(args: unknown): string | undefined;
104
+ /**
105
+ * Returns the args without the `intent` key so downstream consumers that did
106
+ * not declare it never receive it. Parses stringified JSON object args;
107
+ * returns the value unchanged when the key is absent.
108
+ */
109
+ export declare function stripIntent(args: unknown): unknown;
110
+ /**
111
+ * Resolves the settled label for a call from its model-authored `intent` and
112
+ * the tool's result fields, in precedence order:
113
+ *
114
+ * 1. `outcome` — full replacement authored by the tool.
115
+ * 2. `outcome_patch` — first occurrence of `from` in the intent replaced
116
+ * with `to` (case-sensitive); no-op when `from` is absent or empty.
117
+ * 3. Otherwise the intent is returned UNCHANGED.
118
+ *
119
+ * There is deliberately no mechanical present-progressive→past-tense rewrite.
120
+ * Such a transform can only be a closed list of English verbs, which makes it
121
+ * wrong in three ways at once: it never fires for the non-English labels this
122
+ * feature expects (the model answers in the user's language), it fires for
123
+ * some sibling calls and not others inside one group — "Searched…" beside
124
+ * "Recording…" — and it quietly enumerates a vocabulary in a feature whose
125
+ * premise is that the sentence is free-form. Completion is conveyed by UI
126
+ * state (the shimmer stopping, the icon settling), which is language-neutral
127
+ * and always consistent; a tool that wants past tense says so explicitly via
128
+ * `outcome` or `outcome_patch`.
129
+ *
130
+ * Returns undefined when there is neither an intent nor an outcome, so
131
+ * callers fall back to their default label. Pure and dependency-free — host
132
+ * UIs needing identical logic can import or mirror it.
133
+ */
134
+ export declare function applyOutcome(intent: string | undefined, result?: {
135
+ outcome?: string;
136
+ outcome_patch?: OutcomePatch;
137
+ }): string | undefined;
138
+ /**
139
+ * Resolves the settled label to emit on a completion event: only when the
140
+ * tool actually authored `outcome`/`outcome_patch` fields. Returns undefined
141
+ * otherwise, so the wire never carries a label the host already has — a bare
142
+ * intent needs no settled form, because it is displayed unchanged and the UI
143
+ * conveys completion through its own state. Hosts must NOT rewrite it (see
144
+ * {@link applyOutcome} for why a tense transform is deliberately absent). The
145
+ * result is collapsed to a bounded single line before emission.
146
+ *
147
+ * For failed calls (`isError`), only tool-AUTHORED text may label the call:
148
+ * an explicit `outcome`, or a patch whose `from` actually matches the intent.
149
+ * An unmatched patch resolves to undefined rather than silently reusing the
150
+ * in-flight intent, so a failure is never labelled as though it succeeded.
151
+ */
152
+ export declare function resolveToolOutcome(args: unknown, fields?: {
153
+ outcome?: string;
154
+ outcome_patch?: OutcomePatch;
155
+ } | null, options?: {
156
+ isError?: boolean;
157
+ }): string | undefined;
158
+ /**
159
+ * Reads the outcome fields off a tool-execution result: the typed
160
+ * `outcome`/`outcome_patch` fields when present, else the artifact channel
161
+ * (see {@link readOutcomeFields}) — so a `content_and_artifact` tool authors
162
+ * its label the same way on the direct and event-driven paths.
163
+ */
164
+ export declare function outcomeFieldsFromResult(result: {
165
+ outcome?: string;
166
+ outcome_patch?: OutcomePatch;
167
+ artifact?: unknown;
168
+ }): {
169
+ outcome?: string;
170
+ outcome_patch?: OutcomePatch;
171
+ } | undefined;
172
+ /**
173
+ * Extracts validated `outcome`/`outcome_patch` fields from an arbitrary
174
+ * value — the artifact channel through which an in-process
175
+ * `content_and_artifact` tool authors its settled label. Returns undefined
176
+ * when neither field is usable.
177
+ */
178
+ export declare function readOutcomeFields(source: unknown): {
179
+ outcome?: string;
180
+ outcome_patch?: OutcomePatch;
181
+ } | undefined;
@@ -1,3 +1,4 @@
1
+ import type { JsonSchemaType } from '@/types';
1
2
  export declare const DEFAULT_CODE_API_RUN_TIMEOUT_MS = 15000;
2
3
  export declare const MIN_CODE_API_RUN_TIMEOUT_MS = 1000;
3
4
  export declare const MAX_CODE_API_RUN_TIMEOUT_SCHEMA_MS = 300000;
@@ -11,6 +12,7 @@ type TimeoutSchema = {
11
12
  export type ProgrammaticToolCallingJsonSchema = {
12
13
  type: 'object';
13
14
  properties: {
15
+ intent: JsonSchemaType;
14
16
  code: {
15
17
  type: 'string';
16
18
  minLength: number;
@@ -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, so without an authored
16
+ * outcome the in-flight intent ("Searching…") would stand as the settled
17
+ * label and present a failed search as an ordinary one.
18
+ *
19
+ * Returns undefined for a genuine zero-result search, leaving the
20
+ * model-authored intent to stand unchanged as the label.
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,19 @@ 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. Present ONLY when the tool authored one.
105
+ *
106
+ * When absent, display the `intent` arg unchanged — do NOT rewrite its
107
+ * tense. A gerund→past-tense rewrite can only be a closed list of English
108
+ * verbs, so it never fires for the non-English labels this feature expects
109
+ * and fires for some sibling calls but not others within one group.
110
+ * Completion belongs to UI state (the shimmer stopping, the icon settling),
111
+ * which is language-neutral and always consistent.
112
+ */
113
+ outcome?: string;
101
114
  };
102
115
  export type ProcessedContent = {
103
116
  type: ContentType;
@@ -265,6 +278,11 @@ export type ToolCallPart = {
265
278
  id?: string;
266
279
  /** If provided, the output of the tool call */
267
280
  output?: ToolResultContent['content'];
281
+ /**
282
+ * Tool-authored settled label for the call (see `ProcessedToolCall.outcome`),
283
+ * preserved through aggregation so it survives persistence/reload.
284
+ */
285
+ outcome?: string;
268
286
  /** Auth URL */
269
287
  auth?: string;
270
288
  /** 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.7",
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] = {