@librechat/agents 3.9.3 → 3.9.5
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/README.md +32 -0
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +11 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +41 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
- package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
- package/dist/cjs/llm/invoke.cjs +10 -5
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +1 -1
- package/dist/cjs/llm/streamLimits.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/fading.cjs +14 -6
- package/dist/cjs/messages/fading.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +95 -36
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +2 -0
- package/dist/cjs/openai/index.cjs.map +1 -1
- package/dist/cjs/openai/toolProjection.cjs +196 -0
- package/dist/cjs/openai/toolProjection.cjs.map +1 -0
- package/dist/cjs/run.cjs +8 -1
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +9 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ArtifactTruncation.cjs +45 -0
- package/dist/cjs/tools/ArtifactTruncation.cjs.map +1 -0
- package/dist/cjs/tools/BashExecutor.cjs +6 -1
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +6 -1
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +2 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
- package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +11 -0
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +41 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
- package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
- package/dist/esm/llm/invoke.mjs +10 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +1 -1
- package/dist/esm/llm/streamLimits.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/fading.mjs +14 -7
- package/dist/esm/messages/fading.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +95 -37
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +2 -1
- package/dist/esm/openai/index.mjs.map +1 -1
- package/dist/esm/openai/toolProjection.mjs +196 -0
- package/dist/esm/openai/toolProjection.mjs.map +1 -0
- package/dist/esm/run.mjs +8 -1
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +9 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ArtifactTruncation.mjs +44 -0
- package/dist/esm/tools/ArtifactTruncation.mjs.map +1 -0
- package/dist/esm/tools/BashExecutor.mjs +6 -1
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +6 -1
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +2 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
- package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +4 -0
- package/dist/types/graphs/Graph.d.ts +3 -1
- package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
- package/dist/types/messages/fading.d.ts +14 -2
- package/dist/types/messages/prune.d.ts +7 -0
- package/dist/types/openai/arguments.d.ts +2 -0
- package/dist/types/openai/index.d.ts +2 -0
- package/dist/types/openai/toolProjection.d.ts +30 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ArtifactTruncation.d.ts +3 -0
- package/dist/types/tools/ToolNode.d.ts +1 -1
- package/dist/types/types/graph.d.ts +5 -3
- package/dist/types/types/run.d.ts +5 -0
- package/dist/types/types/stream.d.ts +22 -1
- package/dist/types/types/tools.d.ts +14 -0
- package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
- package/package.json +1 -1
- package/src/common/enum.ts +5 -0
- package/src/events.ts +24 -1
- package/src/graphs/Graph.ts +95 -0
- package/src/graphs/acceptedModelResponse.ts +307 -0
- package/src/llm/invoke.ts +39 -14
- package/src/llm/streamLimits.ts +1 -1
- package/src/messages/fading.ts +30 -5
- package/src/messages/prune.ts +168 -55
- package/src/openai/arguments.ts +2 -0
- package/src/openai/index.ts +6 -0
- package/src/openai/toolProjection.ts +318 -0
- package/src/run.ts +21 -0
- package/src/session/AgentSession.ts +2 -2
- package/src/stream.ts +21 -1
- package/src/tools/ArtifactTruncation.ts +85 -0
- package/src/tools/BashExecutor.ts +22 -5
- package/src/tools/CodeExecutor.ts +18 -1
- package/src/tools/ProgrammaticToolCalling.ts +17 -3
- package/src/tools/ToolNode.ts +5 -0
- package/src/tools/subagent/SubagentReplay.ts +10 -9
- package/src/types/graph.ts +15 -9
- package/src/types/run.ts +5 -0
- package/src/types/stream.ts +26 -0
- package/src/types/tools.ts +21 -0
- package/src/utils/acceptedToolArguments.ts +204 -0
package/src/messages/fading.ts
CHANGED
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
calculateMaxToolResultChars,
|
|
5
5
|
} from '@/utils/truncation';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Version 2: exchange width counts only the current turn. Version 1 tiers may
|
|
9
|
+
* have latched on history whose steps storage had merged into one assistant
|
|
10
|
+
* message, so they are discarded and re-derived once.
|
|
11
|
+
*/
|
|
12
|
+
export const FADING_TIER_VERSION = 2;
|
|
8
13
|
|
|
9
14
|
/** Context pressure at which observation masking activates. */
|
|
10
15
|
export const PRESSURE_THRESHOLD_MASKING = 0.8;
|
|
@@ -34,7 +39,7 @@ export type FadingSignals = {
|
|
|
34
39
|
/** (pruningBudget − instruction tokens) ÷ calibrationRatio, in raw token space. */
|
|
35
40
|
effectiveRawTokens: number;
|
|
36
41
|
summarizationEnabled: boolean;
|
|
37
|
-
/** Largest number of parallel calls
|
|
42
|
+
/** Largest number of parallel calls in one assistant exchange of the current turn. */
|
|
38
43
|
toolExchangeWidth?: number;
|
|
39
44
|
/** Recovery paths force at least this rung on the current window's ladder. */
|
|
40
45
|
minRung?: number;
|
|
@@ -59,7 +64,10 @@ export function createFadingTier(window: number): FadingTier {
|
|
|
59
64
|
return { v: FADING_TIER_VERSION, budgetTokens: window, masked: false };
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
/** Tier version before exchange width became turn-scoped. */
|
|
68
|
+
const LEGACY_FADING_TIER_VERSION = 1;
|
|
69
|
+
|
|
70
|
+
function isFadingTierOfVersion(value: unknown, version: number): boolean {
|
|
63
71
|
if (typeof value !== 'object' || value === null) {
|
|
64
72
|
return false;
|
|
65
73
|
}
|
|
@@ -67,7 +75,7 @@ export function isFadingTier(value: unknown): value is FadingTier {
|
|
|
67
75
|
Record<keyof FadingTier, unknown>
|
|
68
76
|
>;
|
|
69
77
|
return (
|
|
70
|
-
v ===
|
|
78
|
+
v === version &&
|
|
71
79
|
typeof budgetTokens === 'number' &&
|
|
72
80
|
Number.isFinite(budgetTokens) &&
|
|
73
81
|
budgetTokens > 0 &&
|
|
@@ -76,6 +84,20 @@ export function isFadingTier(value: unknown): value is FadingTier {
|
|
|
76
84
|
);
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
export function isFadingTier(value: unknown): value is FadingTier {
|
|
88
|
+
return isFadingTierOfVersion(value, FADING_TIER_VERSION);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A well-formed tier persisted before version 2. Stored snapshots that carry
|
|
93
|
+
* one (resume manifests, session files) stay valid; every consumer then drops
|
|
94
|
+
* it through `isFadingTier`, so the tier re-derives once instead of the
|
|
95
|
+
* snapshot being rejected as corrupt.
|
|
96
|
+
*/
|
|
97
|
+
export function isLegacyFadingTier(value: unknown): boolean {
|
|
98
|
+
return isFadingTierOfVersion(value, LEGACY_FADING_TIER_VERSION);
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
/** Deepest rung for a window: the point where the budget reaches its floor. */
|
|
80
102
|
export function maxFadingRung(window: number): number {
|
|
81
103
|
const floor = floorBudgetTokens(window);
|
|
@@ -199,7 +221,10 @@ export function fadingRungForExchangeChars(
|
|
|
199
221
|
maxToolResultChars == null
|
|
200
222
|
? windowResultChars
|
|
201
223
|
: Math.min(windowResultChars, maxToolResultChars);
|
|
202
|
-
if (
|
|
224
|
+
if (
|
|
225
|
+
resultChars + calculateMaxToolCallInputChars(budgetTokens) <=
|
|
226
|
+
targetChars
|
|
227
|
+
) {
|
|
203
228
|
return rung;
|
|
204
229
|
}
|
|
205
230
|
}
|
package/src/messages/prune.ts
CHANGED
|
@@ -1556,7 +1556,28 @@ function cloneAIMessageWithProjectedStreamContent(
|
|
|
1556
1556
|
) as AIMessage | AIMessageChunk;
|
|
1557
1557
|
}
|
|
1558
1558
|
|
|
1559
|
-
|
|
1559
|
+
/** Marker that led the preview in the legacy `{_truncated, _originalChars}` envelope. */
|
|
1560
|
+
const LEGACY_TOOL_INPUT_TRUNCATION_MARKER = '… [truncated]\n';
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Leads every shortened tool-call input. Fading only shortens calls already in
|
|
1564
|
+
* history, which ran with their full input; a model that reads a shortened copy
|
|
1565
|
+
* as a call that was cut off re-issues it, and a side-effecting tool (sending an
|
|
1566
|
+
* email) then runs again. The note says what actually happened.
|
|
1567
|
+
*/
|
|
1568
|
+
export const TOOL_INPUT_ELISION_NOTE =
|
|
1569
|
+
'Completed call; input shortened to save context.';
|
|
1570
|
+
|
|
1571
|
+
function toolInputElision(
|
|
1572
|
+
originalChars: number,
|
|
1573
|
+
inputPrefix: string
|
|
1574
|
+
): { _note: string; _originalChars: number; _inputPrefix: string } {
|
|
1575
|
+
return {
|
|
1576
|
+
_note: TOOL_INPUT_ELISION_NOTE,
|
|
1577
|
+
_originalChars: originalChars,
|
|
1578
|
+
_inputPrefix: inputPrefix,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1560
1581
|
|
|
1561
1582
|
function createBoundedTruncationValue(
|
|
1562
1583
|
preview: string,
|
|
@@ -1564,14 +1585,22 @@ function createBoundedTruncationValue(
|
|
|
1564
1585
|
maxChars: number
|
|
1565
1586
|
): unknown {
|
|
1566
1587
|
const normalizedMaxChars = normalizeToolInputLimit(maxChars);
|
|
1567
|
-
const canonicalPrefix = preview.startsWith(
|
|
1568
|
-
|
|
1588
|
+
const canonicalPrefix = preview.startsWith(
|
|
1589
|
+
LEGACY_TOOL_INPUT_TRUNCATION_MARKER
|
|
1590
|
+
)
|
|
1591
|
+
? preview.slice(LEGACY_TOOL_INPUT_TRUNCATION_MARKER.length)
|
|
1569
1592
|
: preview;
|
|
1570
|
-
const emptyEnvelope =
|
|
1571
|
-
_truncated: TOOL_INPUT_TRUNCATION_MARKER,
|
|
1572
|
-
_originalChars: originalChars,
|
|
1573
|
-
};
|
|
1593
|
+
const emptyEnvelope = toolInputElision(originalChars, '');
|
|
1574
1594
|
if (JSON.stringify(emptyEnvelope).length > normalizedMaxChars) {
|
|
1595
|
+
/** The deepest fading rung caps inputs near 100 chars, below the full
|
|
1596
|
+
* envelope for large inputs, so the note and size survive on their own. */
|
|
1597
|
+
const sizedNote = {
|
|
1598
|
+
_note: TOOL_INPUT_ELISION_NOTE,
|
|
1599
|
+
_originalChars: originalChars,
|
|
1600
|
+
};
|
|
1601
|
+
if (JSON.stringify(sizedNote).length <= normalizedMaxChars) {
|
|
1602
|
+
return sizedNote;
|
|
1603
|
+
}
|
|
1575
1604
|
/**
|
|
1576
1605
|
* Even the empty envelope overflows the cap, so no preview survives —
|
|
1577
1606
|
* but the result must still be a JSON OBJECT, never `null`. This value
|
|
@@ -1590,49 +1619,84 @@ function createBoundedTruncationValue(
|
|
|
1590
1619
|
let high = Math.min(canonicalPrefix.length, normalizedMaxChars);
|
|
1591
1620
|
while (low < high) {
|
|
1592
1621
|
const next = Math.ceil((low + high) / 2);
|
|
1593
|
-
const candidate =
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
_originalChars: originalChars,
|
|
1598
|
-
};
|
|
1622
|
+
const candidate = toolInputElision(
|
|
1623
|
+
originalChars,
|
|
1624
|
+
sliceWithoutSplittingSurrogates(canonicalPrefix, 0, next)
|
|
1625
|
+
);
|
|
1599
1626
|
if (JSON.stringify(candidate).length <= normalizedMaxChars) {
|
|
1600
1627
|
low = next;
|
|
1601
1628
|
} else {
|
|
1602
1629
|
high = next - 1;
|
|
1603
1630
|
}
|
|
1604
1631
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1632
|
+
// The prefix stays a pure canonical prefix, so another, slightly smaller cap
|
|
1633
|
+
// can be derived from it without nesting the envelope.
|
|
1634
|
+
return toolInputElision(
|
|
1635
|
+
originalChars,
|
|
1636
|
+
sliceWithoutSplittingSurrogates(canonicalPrefix, 0, low)
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const ELISION_ENVELOPE_KEYS = ['_note', '_originalChars', '_inputPrefix'];
|
|
1641
|
+
const SIZED_NOTE_KEYS = ['_note', '_originalChars'];
|
|
1642
|
+
const LEGACY_ENVELOPE_KEYS = ['_truncated', '_originalChars'];
|
|
1643
|
+
|
|
1644
|
+
function hasExactKeys(keys: string[], expected: string[]): boolean {
|
|
1645
|
+
return (
|
|
1646
|
+
keys.length === expected.length &&
|
|
1647
|
+
expected.every((key) => keys.includes(key))
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function hasElisionNote(input: object): boolean {
|
|
1652
|
+
const note = readPropertyWithoutAccessors(input, '_note');
|
|
1653
|
+
return note.own && !note.accessor && note.value === TOOL_INPUT_ELISION_NOTE;
|
|
1613
1654
|
}
|
|
1614
1655
|
|
|
1656
|
+
/** Reads a shortened-input envelope, including the legacy `{_truncated,
|
|
1657
|
+
* _originalChars}` shape persisted before the elision note existed. */
|
|
1615
1658
|
function readBoundedTruncationValue(
|
|
1616
1659
|
input: unknown
|
|
1617
|
-
): { preview: string; originalChars: number } | undefined {
|
|
1660
|
+
): { preview: string; originalChars: number; legacy: boolean } | undefined {
|
|
1618
1661
|
if (input == null || typeof input !== 'object' || isProxy(input)) {
|
|
1619
1662
|
return undefined;
|
|
1620
1663
|
}
|
|
1664
|
+
let preview: PropertyRead;
|
|
1665
|
+
let legacy = false;
|
|
1621
1666
|
try {
|
|
1622
1667
|
const prototype = Object.getPrototypeOf(input);
|
|
1623
1668
|
const keys = Object.keys(input);
|
|
1624
|
-
if (
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
!
|
|
1629
|
-
|
|
1669
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1670
|
+
return undefined;
|
|
1671
|
+
}
|
|
1672
|
+
if (hasExactKeys(keys, ELISION_ENVELOPE_KEYS)) {
|
|
1673
|
+
if (!hasElisionNote(input)) {
|
|
1674
|
+
return undefined;
|
|
1675
|
+
}
|
|
1676
|
+
preview = readPropertyWithoutAccessors(input, '_inputPrefix');
|
|
1677
|
+
} else if (hasExactKeys(keys, SIZED_NOTE_KEYS)) {
|
|
1678
|
+
/** The fallback that keeps only the note and size at the tightest caps. */
|
|
1679
|
+
if (!hasElisionNote(input)) {
|
|
1680
|
+
return undefined;
|
|
1681
|
+
}
|
|
1682
|
+
preview = { found: true, own: true, accessor: false, value: '' };
|
|
1683
|
+
} else if (hasExactKeys(keys, LEGACY_ENVELOPE_KEYS)) {
|
|
1684
|
+
preview = readPropertyWithoutAccessors(input, '_truncated');
|
|
1685
|
+
/** Every legacy envelope led with the marker; without it, a genuine input
|
|
1686
|
+
* that merely shares the two field names is left alone. */
|
|
1687
|
+
if (
|
|
1688
|
+
typeof preview.value !== 'string' ||
|
|
1689
|
+
!preview.value.startsWith(LEGACY_TOOL_INPUT_TRUNCATION_MARKER)
|
|
1690
|
+
) {
|
|
1691
|
+
return undefined;
|
|
1692
|
+
}
|
|
1693
|
+
legacy = true;
|
|
1694
|
+
} else {
|
|
1630
1695
|
return undefined;
|
|
1631
1696
|
}
|
|
1632
1697
|
} catch {
|
|
1633
1698
|
return undefined;
|
|
1634
1699
|
}
|
|
1635
|
-
const preview = readPropertyWithoutAccessors(input, '_truncated');
|
|
1636
1700
|
const originalChars = readPropertyWithoutAccessors(input, '_originalChars');
|
|
1637
1701
|
return preview.own &&
|
|
1638
1702
|
!preview.accessor &&
|
|
@@ -1642,7 +1706,11 @@ function readBoundedTruncationValue(
|
|
|
1642
1706
|
typeof originalChars.value === 'number' &&
|
|
1643
1707
|
Number.isFinite(originalChars.value) &&
|
|
1644
1708
|
originalChars.value >= 0
|
|
1645
|
-
? {
|
|
1709
|
+
? {
|
|
1710
|
+
preview: preview.value,
|
|
1711
|
+
originalChars: originalChars.value,
|
|
1712
|
+
legacy,
|
|
1713
|
+
}
|
|
1646
1714
|
: undefined;
|
|
1647
1715
|
}
|
|
1648
1716
|
|
|
@@ -1657,7 +1725,9 @@ function projectToolInputWithinLimit(
|
|
|
1657
1725
|
input,
|
|
1658
1726
|
normalizedMaxChars
|
|
1659
1727
|
);
|
|
1660
|
-
|
|
1728
|
+
/** A legacy envelope converts even when it fits: its `_truncated` wording
|
|
1729
|
+
* is what led models to re-issue completed calls. */
|
|
1730
|
+
if (!serializedLength.truncated && !priorTruncation.legacy) {
|
|
1661
1731
|
return { value: input, changed: false };
|
|
1662
1732
|
}
|
|
1663
1733
|
return {
|
|
@@ -1801,17 +1871,17 @@ function projectSerializedArguments(
|
|
|
1801
1871
|
maxChars: number
|
|
1802
1872
|
): { value: string; changed: boolean } {
|
|
1803
1873
|
const normalizedMaxChars = normalizeToolInputLimit(maxChars);
|
|
1804
|
-
if (typeof value === 'string' && value.length <= normalizedMaxChars) {
|
|
1805
|
-
return { value, changed: false };
|
|
1806
|
-
}
|
|
1807
1874
|
if (
|
|
1808
1875
|
typeof value === 'string' &&
|
|
1809
|
-
value.includes('"
|
|
1810
|
-
value.includes('"
|
|
1876
|
+
value.includes('"_originalChars"') &&
|
|
1877
|
+
(value.includes('"_inputPrefix"') || value.includes('"_truncated"'))
|
|
1811
1878
|
) {
|
|
1812
1879
|
try {
|
|
1813
1880
|
const priorTruncation = readBoundedTruncationValue(JSON.parse(value));
|
|
1814
1881
|
if (priorTruncation != null) {
|
|
1882
|
+
if (!priorTruncation.legacy && value.length <= normalizedMaxChars) {
|
|
1883
|
+
return { value, changed: false };
|
|
1884
|
+
}
|
|
1815
1885
|
return {
|
|
1816
1886
|
value: JSON.stringify(
|
|
1817
1887
|
createBoundedTruncationValue(
|
|
@@ -1827,26 +1897,34 @@ function projectSerializedArguments(
|
|
|
1827
1897
|
// Fall through to the accessor-safe serializer for malformed JSON.
|
|
1828
1898
|
}
|
|
1829
1899
|
}
|
|
1900
|
+
if (typeof value === 'string' && value.length <= normalizedMaxChars) {
|
|
1901
|
+
return { value, changed: false };
|
|
1902
|
+
}
|
|
1830
1903
|
return {
|
|
1831
1904
|
value: serializeToolCallInput(value, normalizedMaxChars),
|
|
1832
1905
|
changed: true,
|
|
1833
1906
|
};
|
|
1834
1907
|
}
|
|
1835
1908
|
|
|
1836
|
-
const TRUNCATED_STRING_INPUT_PATTERN =
|
|
1909
|
+
const TRUNCATED_STRING_INPUT_PATTERN =
|
|
1910
|
+
/\n… \[(?:truncated|shortened; call completed): (\d+) chars\]$/u;
|
|
1911
|
+
const LEGACY_TRUNCATED_STRING_INPUT_PATTERN = /\n… \[truncated: \d+ chars\]$/u;
|
|
1837
1912
|
|
|
1838
1913
|
function projectStringInputWithinLimit(
|
|
1839
1914
|
value: string,
|
|
1840
1915
|
maxChars: number
|
|
1841
1916
|
): { value: string; changed: boolean } {
|
|
1842
1917
|
const normalizedMaxChars = normalizeToolInputLimit(maxChars);
|
|
1843
|
-
if (
|
|
1918
|
+
if (
|
|
1919
|
+
value.length <= normalizedMaxChars &&
|
|
1920
|
+
!LEGACY_TRUNCATED_STRING_INPUT_PATTERN.test(value)
|
|
1921
|
+
) {
|
|
1844
1922
|
return { value, changed: false };
|
|
1845
1923
|
}
|
|
1846
1924
|
const match = TRUNCATED_STRING_INPUT_PATTERN.exec(value);
|
|
1847
1925
|
const originalChars = match == null ? value.length : Number(match[1]);
|
|
1848
1926
|
const prefix = match == null ? value : value.slice(0, match.index);
|
|
1849
|
-
const marker = `\n… [
|
|
1927
|
+
const marker = `\n… [shortened; call completed: ${originalChars} chars]`;
|
|
1850
1928
|
return {
|
|
1851
1929
|
value:
|
|
1852
1930
|
marker.length >= normalizedMaxChars
|
|
@@ -1938,6 +2016,19 @@ function projectRawOpenAIToolCalls(
|
|
|
1938
2016
|
};
|
|
1939
2017
|
}
|
|
1940
2018
|
|
|
2019
|
+
/** A serialized legacy `{_truncated, _originalChars}` envelope, which is rewritten
|
|
2020
|
+
* even when it fits because its wording led models to re-issue completed calls. */
|
|
2021
|
+
function isLegacyEnvelopeString(value: string): boolean {
|
|
2022
|
+
if (!value.includes('"_truncated"') || !value.includes('"_originalChars"')) {
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
try {
|
|
2026
|
+
return readBoundedTruncationValue(JSON.parse(value))?.legacy === true;
|
|
2027
|
+
} catch {
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1941
2032
|
function projectLegacyFunctionCall(
|
|
1942
2033
|
property: PropertyRead,
|
|
1943
2034
|
maxChars: number
|
|
@@ -1984,6 +2075,7 @@ function projectLegacyFunctionCall(
|
|
|
1984
2075
|
enumerableKeys.includes('arguments') &&
|
|
1985
2076
|
typeof argsProperty.value === 'string' &&
|
|
1986
2077
|
argsProperty.value.length <= normalizeToolInputLimit(maxChars) &&
|
|
2078
|
+
!isLegacyEnvelopeString(argsProperty.value) &&
|
|
1987
2079
|
!hasUnsafeStructuredSerialization(property.value)
|
|
1988
2080
|
) {
|
|
1989
2081
|
return { value: property.value, changed: false };
|
|
@@ -2064,16 +2156,9 @@ function projectResponsesOutput(
|
|
|
2064
2156
|
? ACCESSOR_INPUT_PLACEHOLDER
|
|
2065
2157
|
: canonicalProperty.value;
|
|
2066
2158
|
}
|
|
2067
|
-
if (
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
source.length <= normalizeToolInputLimit(maxChars)
|
|
2071
|
-
) {
|
|
2072
|
-
projectedInput = {
|
|
2073
|
-
value: source,
|
|
2074
|
-
changed: !inputProperty.own || inputProperty.value !== source,
|
|
2075
|
-
};
|
|
2076
|
-
} else if (type === 'custom_tool_call') {
|
|
2159
|
+
if (type === 'custom_tool_call') {
|
|
2160
|
+
/** The string projection keeps fitting input as-is, except a legacy
|
|
2161
|
+
* `[truncated: N chars]` marker, which it rewrites. */
|
|
2077
2162
|
const value =
|
|
2078
2163
|
typeof source === 'string'
|
|
2079
2164
|
? projectStringInputWithinLimit(source, maxChars).value
|
|
@@ -2389,6 +2474,29 @@ function applyToolCallInputCaps(params: {
|
|
|
2389
2474
|
return truncatedCount;
|
|
2390
2475
|
}
|
|
2391
2476
|
|
|
2477
|
+
/**
|
|
2478
|
+
* Whether a message opens a user turn: a human message or a role-based `user`
|
|
2479
|
+
* chat message. The SDK stamps every `HumanMessage` it synthesizes with a
|
|
2480
|
+
* `source` (hook context, steers, routing and handoff cues, skills) and often
|
|
2481
|
+
* `injected`, `isMeta` or `role: 'system'`; those belong to the turn they follow.
|
|
2482
|
+
*/
|
|
2483
|
+
function startsUserTurn(message: BaseMessage): boolean {
|
|
2484
|
+
const type = message.getType();
|
|
2485
|
+
if (type === 'generic') {
|
|
2486
|
+
return (message as { role?: unknown }).role === 'user';
|
|
2487
|
+
}
|
|
2488
|
+
if (type !== 'human') {
|
|
2489
|
+
return false;
|
|
2490
|
+
}
|
|
2491
|
+
const kwargs = message.additional_kwargs;
|
|
2492
|
+
return (
|
|
2493
|
+
kwargs.source === undefined &&
|
|
2494
|
+
kwargs.injected !== true &&
|
|
2495
|
+
kwargs.isMeta !== true &&
|
|
2496
|
+
kwargs.role !== 'system'
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2392
2500
|
export function preFlightTruncateToolCallInputs(params: {
|
|
2393
2501
|
messages: BaseMessage[];
|
|
2394
2502
|
maxContextTokens: number;
|
|
@@ -2466,7 +2574,8 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
2466
2574
|
factoryParams.fadingTier
|
|
2467
2575
|
);
|
|
2468
2576
|
let restoredTierPending = isFadingTier(factoryParams.fadingTier);
|
|
2469
|
-
/** Widest exchange
|
|
2577
|
+
/** Widest exchange of the current turn (since the last human message);
|
|
2578
|
+
* updated only from the appended suffix. */
|
|
2470
2579
|
let maxToolExchangeWidth = 1;
|
|
2471
2580
|
let toolExchangeWidthThrough = 0;
|
|
2472
2581
|
let toolExchangeWidthSources: BaseMessage[] = [];
|
|
@@ -2533,11 +2642,15 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
2533
2642
|
originalToolContentSize = 0;
|
|
2534
2643
|
}
|
|
2535
2644
|
for (let i = toolExchangeWidthThrough; i < canonicalMessages.length; i++) {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2645
|
+
const message = canonicalMessages[i];
|
|
2646
|
+
/** Earlier turns come back from storage with each turn's steps merged into
|
|
2647
|
+
* one assistant message, so their call counts are an artifact of that
|
|
2648
|
+
* reconstruction, not fan-out; only the current turn's are real. The tier
|
|
2649
|
+
* itself stays latched, so a narrower width never loosens it. */
|
|
2650
|
+
maxToolExchangeWidth = startsUserTurn(message)
|
|
2651
|
+
? 1
|
|
2652
|
+
: Math.max(maxToolExchangeWidth, getToolCallIds(message).size);
|
|
2653
|
+
toolExchangeWidthSources[i] = message;
|
|
2541
2654
|
}
|
|
2542
2655
|
toolExchangeWidthThrough = canonicalMessages.length;
|
|
2543
2656
|
let newOriginalToolContent: Map<number, string> | undefined;
|
package/src/openai/index.ts
CHANGED
|
@@ -402,3 +402,9 @@ export async function sendOpenAIFinalChunk(
|
|
|
402
402
|
);
|
|
403
403
|
await writeOpenAISSE(config.writer, '[DONE]');
|
|
404
404
|
}
|
|
405
|
+
|
|
406
|
+
export { createOpenAIToolCallStream } from './toolProjection';
|
|
407
|
+
export type {
|
|
408
|
+
OpenAIToolCallStream,
|
|
409
|
+
OpenAIToolCallStreamConfig,
|
|
410
|
+
} from './toolProjection';
|