@librechat/agents 3.3.6 → 3.3.8
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/MultiAgentGraph.cjs +21 -4
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/format.cjs +124 -15
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/injected.cjs +10 -1
- package/dist/cjs/messages/injected.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +29 -1
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/intentArg.cjs +78 -52
- package/dist/cjs/tools/intentArg.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +5 -5
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +21 -4
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/format.mjs +124 -15
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/injected.mjs +10 -1
- package/dist/esm/messages/injected.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +29 -1
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/intentArg.mjs +77 -53
- package/dist/esm/tools/intentArg.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +5 -5
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/messages/format.d.ts +9 -8
- package/dist/types/prompts/activityLabel.d.ts +8 -1
- package/dist/types/run.d.ts +1 -1
- package/dist/types/tools/intentArg.d.ts +74 -12
- package/dist/types/tools/search/tool.d.ts +5 -5
- package/dist/types/types/activityLabel.d.ts +8 -0
- package/dist/types/types/stream.d.ts +27 -2
- package/package.json +1 -1
- package/src/graphs/MultiAgentGraph.ts +18 -4
- package/src/messages/format.ts +222 -50
- package/src/messages/formatAgentMessages.test.ts +308 -6
- package/src/messages/injected.test.ts +18 -1
- package/src/messages/injected.ts +8 -1
- package/src/prompts/activityLabel.ts +48 -0
- package/src/run.ts +10 -1
- package/src/specs/activity-label-prompt.test.ts +93 -0
- package/src/summarization/__tests__/node.test.ts +188 -0
- package/src/summarization/node.ts +67 -0
- package/src/tools/__tests__/intentArg.test.ts +101 -25
- package/src/tools/intentArg.ts +102 -68
- package/src/tools/search/outcome.test.ts +1 -1
- package/src/tools/search/tool.ts +5 -5
- package/src/types/activityLabel.ts +8 -0
- package/src/types/stream.ts +28 -2
package/src/tools/intentArg.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* args, so a host UI can render it as the call's live status label before the
|
|
9
9
|
* rest of the args exist. When the call settles, {@link applyOutcome} edits
|
|
10
10
|
* the sentence in place into its outcome form — a tool-supplied replacement
|
|
11
|
-
* (`outcome`)
|
|
12
|
-
*
|
|
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.
|
|
13
14
|
*
|
|
14
15
|
* The arg is always optional (never listed in `required`): the same schemas
|
|
15
16
|
* are callable from programmatic tool calling, where no UI renders a label
|
|
@@ -23,15 +24,34 @@ import type { JsonSchemaType, OutcomePatch } from '@/types';
|
|
|
23
24
|
/** Argument carrying the model-authored label for a tool call. */
|
|
24
25
|
export const INTENT_ARG = 'intent';
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Opening words of {@link INTENT_DESCRIPTION}, and the discriminator that
|
|
29
|
+
* tells the injected LABEL apart from a tool's own business parameter that
|
|
30
|
+
* merely shares the name `intent`.
|
|
31
|
+
*
|
|
32
|
+
* Exported because host applications reimplement the same strip/sanitize
|
|
33
|
+
* passes and would otherwise duplicate this as a string literal: if the two
|
|
34
|
+
* copies drift, the host silently stops recognizing SDK-native labels and
|
|
35
|
+
* fails OPEN (labels stay in schemas, opt-outs stop working) with no error.
|
|
36
|
+
* Any edit to the description must preserve this prefix verbatim.
|
|
37
|
+
*/
|
|
38
|
+
export const INTENT_LABEL_MARKER = 'ALWAYS write this field FIRST';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Model-facing instruction for the injected `intent` property.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately terse — it is repeated on every opted-in tool schema, on every
|
|
44
|
+
* request, so each sentence is paid for many times over. What remains is
|
|
45
|
+
* load-bearing: first-position placement (the entire streaming mechanism),
|
|
46
|
+
* the one-sentence present-progressive form, who reads it, and the sibling
|
|
47
|
+
* rule, without which models emit identical labels for parallel calls to one
|
|
48
|
+
* tool and defeat the feature's headline case.
|
|
49
|
+
*/
|
|
27
50
|
export const INTENT_DESCRIPTION =
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'reading a progress line. Do not restate the tool name. Do not exceed one sentence. ' +
|
|
33
|
-
'When you make several calls to the same tool in one turn, each intent must ' +
|
|
34
|
-
'distinguish that call from its siblings.';
|
|
51
|
+
`${INTENT_LABEL_MARKER}, before any other argument. One present-progressive ` +
|
|
52
|
+
'sentence saying what THIS call is about to do: "Searching for OAuth handling ' +
|
|
53
|
+
'in the callback router". Shown to the user as this call\'s live status. ' +
|
|
54
|
+
'Never name the tool. Sibling calls to one tool must differ.';
|
|
35
55
|
|
|
36
56
|
/**
|
|
37
57
|
* Canonical (frozen) shape of the injected property. Always embed a COPY
|
|
@@ -59,10 +79,60 @@ export function isIntentLabelProperty(property: unknown): boolean {
|
|
|
59
79
|
return (
|
|
60
80
|
record.type === 'string' &&
|
|
61
81
|
typeof record.description === 'string' &&
|
|
62
|
-
record.description.startsWith(
|
|
82
|
+
record.description.startsWith(INTENT_LABEL_MARKER)
|
|
63
83
|
);
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Schema shape accepted by {@link withoutIntent}.
|
|
88
|
+
*
|
|
89
|
+
* `required` is widened to `readonly string[]` because the SDK's own native
|
|
90
|
+
* schemas are declared `as const` — their `required` is a readonly tuple, and
|
|
91
|
+
* a mutable `string[]` parameter would reject the very schemas this helper
|
|
92
|
+
* exists for (TS2345), forcing embedders to cast to use the advertised API.
|
|
93
|
+
*/
|
|
94
|
+
export type IntentStrippableSchema = Omit<JsonSchemaType, 'required'> & {
|
|
95
|
+
required?: readonly string[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns a copy of `parameters` without the injected intent LABEL — the
|
|
100
|
+
* opt-out for consumers that render no status label and should not pay for
|
|
101
|
+
* the property.
|
|
102
|
+
*
|
|
103
|
+
* The SDK's native schemas carry the label unconditionally, so without this
|
|
104
|
+
* an embedder has no lever at all: `withIntent` is applied at module scope.
|
|
105
|
+
* Marker-guarded, so a tool's own business parameter named `intent` is never
|
|
106
|
+
* removed. Returns the input unchanged when there is nothing to strip.
|
|
107
|
+
*
|
|
108
|
+
* `required` is pruned alongside the property: a schema that lists `intent`
|
|
109
|
+
* as required (strict-mode normalization does exactly that, since OpenAI
|
|
110
|
+
* strict function schemas require every property to appear in `required`)
|
|
111
|
+
* would otherwise be left naming a property it no longer declares, which is
|
|
112
|
+
* invalid JSON Schema and gets rejected by the provider instead of quietly
|
|
113
|
+
* opting out.
|
|
114
|
+
*/
|
|
115
|
+
export function withoutIntent(parameters?: IntentStrippableSchema): JsonSchemaType | undefined {
|
|
116
|
+
const props = parameters?.properties;
|
|
117
|
+
if (parameters == null || props == null || !isIntentLabelProperty(props[INTENT_ARG])) {
|
|
118
|
+
return parameters as JsonSchemaType | undefined;
|
|
119
|
+
}
|
|
120
|
+
const { [INTENT_ARG]: _omit, ...rest } = props;
|
|
121
|
+
const next: JsonSchemaType = {
|
|
122
|
+
...(parameters as JsonSchemaType),
|
|
123
|
+
properties: rest,
|
|
124
|
+
};
|
|
125
|
+
if (parameters.required != null) {
|
|
126
|
+
const required = parameters.required.filter((key) => key !== INTENT_ARG);
|
|
127
|
+
if (required.length > 0) {
|
|
128
|
+
next.required = required;
|
|
129
|
+
} else {
|
|
130
|
+
delete next.required;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
|
|
66
136
|
/**
|
|
67
137
|
* Returns a copy of the parameters schema with `intent` prepended as the
|
|
68
138
|
* FIRST property (object key order is insertion order and every provider
|
|
@@ -130,53 +200,6 @@ export function stripIntent(args: unknown): unknown {
|
|
|
130
200
|
return rest;
|
|
131
201
|
}
|
|
132
202
|
|
|
133
|
-
/**
|
|
134
|
-
* Leading-verb map for the mechanical outcome transform, keyed by the
|
|
135
|
-
* lowercased first word of the intent. Deliberately small: an unknown leading
|
|
136
|
-
* word leaves the intent unchanged rather than mangling it.
|
|
137
|
-
*/
|
|
138
|
-
const OUTCOME_VERB_MAP: ReadonlyMap<string, string> = new Map([
|
|
139
|
-
['searching', 'Searched'],
|
|
140
|
-
['reading', 'Read'],
|
|
141
|
-
['writing', 'Wrote'],
|
|
142
|
-
['editing', 'Edited'],
|
|
143
|
-
['running', 'Ran'],
|
|
144
|
-
['creating', 'Created'],
|
|
145
|
-
['checking', 'Checked'],
|
|
146
|
-
['fetching', 'Fetched'],
|
|
147
|
-
['listing', 'Listed'],
|
|
148
|
-
['looking', 'Looked'],
|
|
149
|
-
['building', 'Built'],
|
|
150
|
-
['deleting', 'Deleted'],
|
|
151
|
-
['updating', 'Updated'],
|
|
152
|
-
['adding', 'Added'],
|
|
153
|
-
['removing', 'Removed'],
|
|
154
|
-
['verifying', 'Verified'],
|
|
155
|
-
['analyzing', 'Analyzed'],
|
|
156
|
-
['generating', 'Generated'],
|
|
157
|
-
['delegating', 'Delegated'],
|
|
158
|
-
['spawning', 'Spawned'],
|
|
159
|
-
['compiling', 'Compiled'],
|
|
160
|
-
['grepping', 'Grepped'],
|
|
161
|
-
]);
|
|
162
|
-
|
|
163
|
-
function matchLeadingCase(replacement: string, original: string): string {
|
|
164
|
-
if (original.charAt(0) === original.charAt(0).toLowerCase()) {
|
|
165
|
-
return replacement.charAt(0).toLowerCase() + replacement.slice(1);
|
|
166
|
-
}
|
|
167
|
-
return replacement;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function transformLeadingVerb(intent: string): string {
|
|
171
|
-
const spaceIdx = intent.search(/\s/);
|
|
172
|
-
const leading = spaceIdx === -1 ? intent : intent.slice(0, spaceIdx);
|
|
173
|
-
const mapped = OUTCOME_VERB_MAP.get(leading.toLowerCase());
|
|
174
|
-
if (mapped == null) {
|
|
175
|
-
return intent;
|
|
176
|
-
}
|
|
177
|
-
return matchLeadingCase(mapped, leading) + intent.slice(leading.length);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
203
|
/**
|
|
181
204
|
* Resolves the settled label for a call from its model-authored `intent` and
|
|
182
205
|
* the tool's result fields, in precedence order:
|
|
@@ -184,8 +207,18 @@ function transformLeadingVerb(intent: string): string {
|
|
|
184
207
|
* 1. `outcome` — full replacement authored by the tool.
|
|
185
208
|
* 2. `outcome_patch` — first occurrence of `from` in the intent replaced
|
|
186
209
|
* with `to` (case-sensitive); no-op when `from` is absent or empty.
|
|
187
|
-
* 3.
|
|
188
|
-
*
|
|
210
|
+
* 3. Otherwise the intent is returned UNCHANGED.
|
|
211
|
+
*
|
|
212
|
+
* There is deliberately no mechanical present-progressive→past-tense rewrite.
|
|
213
|
+
* Such a transform can only be a closed list of English verbs, which makes it
|
|
214
|
+
* wrong in three ways at once: it never fires for the non-English labels this
|
|
215
|
+
* feature expects (the model answers in the user's language), it fires for
|
|
216
|
+
* some sibling calls and not others inside one group — "Searched…" beside
|
|
217
|
+
* "Recording…" — and it quietly enumerates a vocabulary in a feature whose
|
|
218
|
+
* premise is that the sentence is free-form. Completion is conveyed by UI
|
|
219
|
+
* state (the shimmer stopping, the icon settling), which is language-neutral
|
|
220
|
+
* and always consistent; a tool that wants past tense says so explicitly via
|
|
221
|
+
* `outcome` or `outcome_patch`.
|
|
189
222
|
*
|
|
190
223
|
* Returns undefined when there is neither an intent nor an outcome, so
|
|
191
224
|
* callers fall back to their default label. Pure and dependency-free — host
|
|
@@ -209,7 +242,7 @@ export function applyOutcome(
|
|
|
209
242
|
* text (e.g. labels derived from shell syntax). */
|
|
210
243
|
return intent.replace(patch.from, () => patch.to);
|
|
211
244
|
}
|
|
212
|
-
return
|
|
245
|
+
return intent;
|
|
213
246
|
}
|
|
214
247
|
|
|
215
248
|
/**
|
|
@@ -236,15 +269,16 @@ function boundOutcomeLabel(label: string | undefined): string | undefined {
|
|
|
236
269
|
/**
|
|
237
270
|
* Resolves the settled label to emit on a completion event: only when the
|
|
238
271
|
* tool actually authored `outcome`/`outcome_patch` fields. Returns undefined
|
|
239
|
-
* otherwise
|
|
240
|
-
*
|
|
241
|
-
*
|
|
272
|
+
* otherwise, so the wire never carries a label the host already has — a bare
|
|
273
|
+
* intent needs no settled form, because it is displayed unchanged and the UI
|
|
274
|
+
* conveys completion through its own state. Hosts must NOT rewrite it (see
|
|
275
|
+
* {@link applyOutcome} for why a tense transform is deliberately absent). The
|
|
276
|
+
* result is collapsed to a bounded single line before emission.
|
|
242
277
|
*
|
|
243
278
|
* For failed calls (`isError`), only tool-AUTHORED text may label the call:
|
|
244
|
-
* an explicit `outcome`, or a patch whose `from` actually matches the
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* render a success-looking label for an error.
|
|
279
|
+
* an explicit `outcome`, or a patch whose `from` actually matches the intent.
|
|
280
|
+
* An unmatched patch resolves to undefined rather than silently reusing the
|
|
281
|
+
* in-flight intent, so a failure is never labelled as though it succeeded.
|
|
248
282
|
*/
|
|
249
283
|
export function resolveToolOutcome(
|
|
250
284
|
args: unknown,
|
|
@@ -84,7 +84,7 @@ describe('resolveSearchOutcome', () => {
|
|
|
84
84
|
).toBe('Found 2 results for "oauth"');
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
it('leaves a genuine zero-result search
|
|
87
|
+
it('leaves a genuine zero-result search unlabeled, so the intent stands', () => {
|
|
88
88
|
expect(resolveSearchOutcome(data({ organic: [] }), 'oauth')).toBeUndefined();
|
|
89
89
|
});
|
|
90
90
|
});
|
package/src/tools/search/tool.ts
CHANGED
|
@@ -35,12 +35,12 @@ import { Constants } from '@/common';
|
|
|
35
35
|
*
|
|
36
36
|
* A caught provider or processing failure is reported through `data.error`
|
|
37
37
|
* while the tool still returns NORMALLY, so that case must author its own
|
|
38
|
-
* label: the `ToolMessage` carries success status,
|
|
39
|
-
*
|
|
40
|
-
* a failed search as
|
|
38
|
+
* label: the `ToolMessage` carries success status, so without an authored
|
|
39
|
+
* outcome the in-flight intent ("Searching…") would stand as the settled
|
|
40
|
+
* label and present a failed search as an ordinary one.
|
|
41
41
|
*
|
|
42
|
-
* Returns undefined for a genuine zero-result search, leaving the
|
|
43
|
-
*
|
|
42
|
+
* Returns undefined for a genuine zero-result search, leaving the
|
|
43
|
+
* model-authored intent to stand unchanged as the label.
|
|
44
44
|
*/
|
|
45
45
|
export function resolveSearchOutcome(
|
|
46
46
|
data: t.SearchResultData,
|
|
@@ -38,6 +38,14 @@ export type RunActivityLabelOptions = {
|
|
|
38
38
|
thinkingExcerpts?: string[];
|
|
39
39
|
/** Assistant's last text before the block (~200 chars), as intent context. */
|
|
40
40
|
lastAssistantText?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Headers already committed for earlier batches in this run (run order,
|
|
43
|
+
* most recent last). Continuity context: the prompt shows them so the new
|
|
44
|
+
* header extends the run's story instead of restating a line already on
|
|
45
|
+
* screen. Hosts should pass only COMMITTED labels — a pending slot's text
|
|
46
|
+
* is empty and a dropped fill never surfaced to the user.
|
|
47
|
+
*/
|
|
48
|
+
previousLabels?: string[];
|
|
41
49
|
/** Override for the default label system prompt. */
|
|
42
50
|
prompt?: string;
|
|
43
51
|
/** Per-entry serialization cap for the prompt. Default 600. */
|
package/src/types/stream.ts
CHANGED
|
@@ -145,8 +145,14 @@ export type ProcessedToolCall = {
|
|
|
145
145
|
/**
|
|
146
146
|
* Settled label for the call, resolved from the tool-supplied
|
|
147
147
|
* `outcome`/`outcome_patch` result fields against the model-authored
|
|
148
|
-
* `intent` arg.
|
|
149
|
-
*
|
|
148
|
+
* `intent` arg. Present ONLY when the tool authored one.
|
|
149
|
+
*
|
|
150
|
+
* When absent, display the `intent` arg unchanged — do NOT rewrite its
|
|
151
|
+
* tense. A gerund→past-tense rewrite can only be a closed list of English
|
|
152
|
+
* verbs, so it never fires for the non-English labels this feature expects
|
|
153
|
+
* and fires for some sibling calls but not others within one group.
|
|
154
|
+
* Completion belongs to UI state (the shimmer stopping, the icon settling),
|
|
155
|
+
* which is language-neutral and always consistent.
|
|
150
156
|
*/
|
|
151
157
|
outcome?: string;
|
|
152
158
|
};
|
|
@@ -295,10 +301,30 @@ export type SummaryBoundary = {
|
|
|
295
301
|
contentIndex: number;
|
|
296
302
|
};
|
|
297
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Semantic extent of a summary: the first source message compaction retained
|
|
306
|
+
* verbatim, meaning everything before it is covered. Distinct from `boundary`,
|
|
307
|
+
* which records where the block was emitted — a retained recency tail sits
|
|
308
|
+
* *before* the block's own position, so position alone cannot say what the
|
|
309
|
+
* summary replaced.
|
|
310
|
+
*
|
|
311
|
+
* Anchored to the retained side rather than the covered side so that a source
|
|
312
|
+
* message expanding into several messages (a steer splits an assistant entry
|
|
313
|
+
* into pre-steer, steer, and post-steer entries sharing one ID) stays whole:
|
|
314
|
+
* such a message is the retained anchor and survives intact.
|
|
315
|
+
*/
|
|
316
|
+
export type SummaryCoverage = {
|
|
317
|
+
retainedFromMessageId: string;
|
|
318
|
+
};
|
|
319
|
+
|
|
298
320
|
export type SummaryContentBlock = {
|
|
299
321
|
type: ContentTypes.SUMMARY;
|
|
300
322
|
content?: MessageContentComplex[];
|
|
323
|
+
/** Injection budget: provider output-token space when usage was reported, plus
|
|
324
|
+
* the wrapper added at injection time. Not comparable with per-message counts
|
|
325
|
+
* such as `indexTokenCountMap`, which are in the consumer's own tokenizer. */
|
|
301
326
|
tokenCount?: number;
|
|
327
|
+
coverage?: SummaryCoverage;
|
|
302
328
|
boundary?: SummaryBoundary;
|
|
303
329
|
summaryVersion?: number;
|
|
304
330
|
model?: string;
|