@librechat/agents 3.2.68 → 3.3.1
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/agents/AgentContext.cjs +117 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +4 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +243 -49
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +103 -28
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +11 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
- package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +60 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +21 -7
- package/dist/cjs/messages/format.cjs +136 -4
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +16 -5
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +2 -0
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +101 -0
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
- package/dist/cjs/run.cjs +177 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +205 -49
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -7
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +69 -8
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/cjs/utils/errors.cjs +317 -52
- package/dist/cjs/utils/errors.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +118 -4
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +4 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +242 -48
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +103 -28
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +11 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
- package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +59 -4
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +10 -10
- package/dist/esm/messages/format.mjs +136 -5
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +14 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +2 -1
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +100 -0
- package/dist/esm/prompts/activityLabel.mjs.map +1 -0
- package/dist/esm/run.mjs +178 -4
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +205 -49
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +62 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/esm/utils/errors.mjs +317 -53
- package/dist/esm/utils/errors.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +62 -3
- package/dist/types/common/enum.d.ts +5 -1
- package/dist/types/graphs/Graph.d.ts +16 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
- package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
- package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
- package/dist/types/llm/invoke.d.ts +35 -6
- package/dist/types/messages/format.d.ts +22 -0
- package/dist/types/messages/prune.d.ts +10 -2
- package/dist/types/messages/recency.d.ts +1 -0
- package/dist/types/prompts/activityLabel.d.ts +31 -0
- package/dist/types/run.d.ts +16 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -1
- package/dist/types/types/activityLabel.d.ts +53 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/llm.d.ts +7 -4
- package/dist/types/types/stream.d.ts +7 -4
- package/dist/types/types/summarize.d.ts +22 -0
- package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
- package/dist/types/utils/errors.d.ts +65 -16
- package/dist/types/utils/redactSecrets.d.ts +3 -0
- package/package.json +7 -8
- package/src/agents/AgentContext.ts +188 -7
- package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
- package/src/common/enum.ts +4 -0
- package/src/graphs/Graph.ts +409 -58
- package/src/graphs/MultiAgentGraph.ts +184 -46
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
- package/src/langfuseToolOutputTracing.ts +4 -1
- package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
- package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
- package/src/llm/bedrock/index.ts +25 -12
- package/src/llm/contextOverflowRecovery.ts +292 -0
- package/src/llm/invoke.ts +119 -4
- package/src/messages/foldToollessToolBlocks.test.ts +438 -0
- package/src/messages/format.ts +233 -5
- package/src/messages/prune.ts +24 -11
- package/src/messages/recency.ts +3 -1
- package/src/prompts/activityLabel.ts +177 -0
- package/src/run.ts +322 -3
- package/src/scripts/context-overflow-probe.ts +997 -0
- package/src/specs/activity-label-prompt.test.ts +128 -0
- package/src/specs/activity-label-trace-seed.test.ts +47 -0
- package/src/specs/agent-handoffs.test.ts +903 -1
- package/src/specs/bedrock-toolless.live.test.ts +123 -0
- package/src/specs/context-overflow-recovery.live.test.ts +213 -0
- package/src/splitStream.test.ts +882 -0
- package/src/stream.ts +315 -51
- package/src/summarization/__tests__/aggregator.test.ts +83 -0
- package/src/summarization/__tests__/node.test.ts +139 -0
- package/src/summarization/node.ts +99 -14
- package/src/tools/BashExecutor.ts +4 -2
- package/src/tools/BashProgrammaticToolCalling.ts +4 -7
- package/src/tools/CodeExecutor.ts +119 -8
- package/src/tools/ProgrammaticToolCalling.ts +29 -27
- package/src/tools/ToolNode.ts +50 -8
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
- package/src/types/activityLabel.ts +55 -0
- package/src/types/index.ts +1 -0
- package/src/types/llm.ts +8 -1
- package/src/types/stream.ts +7 -4
- package/src/types/summarize.ts +22 -0
- package/src/utils/__tests__/errors.test.ts +270 -0
- package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
- package/src/utils/__tests__/redactSecrets.test.ts +56 -0
- package/src/utils/errors.ts +484 -66
- package/src/utils/redactSecrets.ts +61 -0
package/src/messages/format.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
SummaryContentBlock,
|
|
23
23
|
ThinkingContentText,
|
|
24
24
|
ToolCallContent,
|
|
25
|
+
ToolResultContent,
|
|
25
26
|
ToolCallPart,
|
|
26
27
|
TPayload,
|
|
27
28
|
TMessage,
|
|
@@ -400,7 +401,8 @@ function hasMeaningfulAssistantContent(part: MessageContentComplex): boolean {
|
|
|
400
401
|
part.type === ContentTypes.TOOL_CALL ||
|
|
401
402
|
part.type === ContentTypes.ERROR ||
|
|
402
403
|
part.type === ContentTypes.AGENT_UPDATE ||
|
|
403
|
-
part.type === ContentTypes.SUMMARY
|
|
404
|
+
part.type === ContentTypes.SUMMARY ||
|
|
405
|
+
part.type === ContentTypes.ACTIVITY_LABEL
|
|
404
406
|
) {
|
|
405
407
|
return false;
|
|
406
408
|
}
|
|
@@ -798,7 +800,8 @@ function formatAssistantMessage(
|
|
|
798
800
|
} else if (
|
|
799
801
|
part.type === ContentTypes.ERROR ||
|
|
800
802
|
part.type === ContentTypes.AGENT_UPDATE ||
|
|
801
|
-
part.type === ContentTypes.SUMMARY
|
|
803
|
+
part.type === ContentTypes.SUMMARY ||
|
|
804
|
+
part.type === ContentTypes.ACTIVITY_LABEL
|
|
802
805
|
) {
|
|
803
806
|
continue;
|
|
804
807
|
} else {
|
|
@@ -914,6 +917,14 @@ function labelAllAgentContent(
|
|
|
914
917
|
|
|
915
918
|
for (let i = 0; i < contentParts.length; i++) {
|
|
916
919
|
const part = contentParts[i];
|
|
920
|
+
/** UI-only progress headers are not agent content and must not disturb
|
|
921
|
+
* agent state: a label with no `agentIdMap` entry would otherwise read
|
|
922
|
+
* as an agent change and flush the buffer mid-agent, splitting one
|
|
923
|
+
* agent's contiguous content into two labeled blocks. Skipped before
|
|
924
|
+
* any state transition below (mirrors the transfer path). */
|
|
925
|
+
if (part.type === ContentTypes.ACTIVITY_LABEL) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
917
928
|
const agentId = agentIdMap[i];
|
|
918
929
|
|
|
919
930
|
// If agent changed, flush previous buffer
|
|
@@ -1037,6 +1048,14 @@ export const labelContentByAgent = (
|
|
|
1037
1048
|
|
|
1038
1049
|
for (let i = 0; i < contentParts.length; i++) {
|
|
1039
1050
|
const part = contentParts[i];
|
|
1051
|
+
/** UI-only progress headers are not agent content and must not disturb
|
|
1052
|
+
* agent state: a label with no `agentIdMap` entry would otherwise look
|
|
1053
|
+
* like an agent change, flushing the buffer and resetting an open
|
|
1054
|
+
* transfer capture so the transferred agent's following chunks lose
|
|
1055
|
+
* their frame. Skipped before any state transition below. */
|
|
1056
|
+
if (part.type === ContentTypes.ACTIVITY_LABEL) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1040
1059
|
const agentId = agentIdMap[i];
|
|
1041
1060
|
|
|
1042
1061
|
// Check if this is a transfer tool call
|
|
@@ -1806,6 +1825,65 @@ function appendMessageContent(
|
|
|
1806
1825
|
continue;
|
|
1807
1826
|
}
|
|
1808
1827
|
|
|
1828
|
+
// A `tool_call` content block appears either as the v1 standard shape
|
|
1829
|
+
// (`{ name, args }` at top level, which `@langchain/aws` maps to a Converse
|
|
1830
|
+
// toolUse) or this repo's `ToolCallContent` (`{ tool_call: { name, args,
|
|
1831
|
+
// output } }`, from `convertMessagesToContent` / persisted history). Handle
|
|
1832
|
+
// both, and emit any embedded output, so the name/args/result survive.
|
|
1833
|
+
if (block.type === 'tool_call') {
|
|
1834
|
+
hasToolUseBlock = true;
|
|
1835
|
+
const nested = (block as { tool_call?: ToolCallPart }).tool_call;
|
|
1836
|
+
const name = String(nested?.name ?? block.name ?? '');
|
|
1837
|
+
const rawArgs = nested?.args ?? block.args ?? {};
|
|
1838
|
+
const argsText =
|
|
1839
|
+
typeof rawArgs === 'string' ? rawArgs : JSON.stringify(rawArgs);
|
|
1840
|
+
textChunks.push(`${role}: [tool_use] ${name} ${argsText}`.trimEnd());
|
|
1841
|
+
const output = nested?.output;
|
|
1842
|
+
if (output != null && output !== '') {
|
|
1843
|
+
textChunks.push(`Tool: ${String(output)}`);
|
|
1844
|
+
}
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// A `tool_result` content block (e.g. an AIMessage(tool_call) followed by a
|
|
1849
|
+
// user message carrying the result). Preserve nested image blocks as-is
|
|
1850
|
+
// instead of JSON-stringifying them through the generic fallback.
|
|
1851
|
+
if (block.type === 'tool_result') {
|
|
1852
|
+
hasToolUseBlock = true;
|
|
1853
|
+
const inner = (block as { content?: ToolResultContent['content'] })
|
|
1854
|
+
.content;
|
|
1855
|
+
if (typeof inner === 'string') {
|
|
1856
|
+
if (inner) {
|
|
1857
|
+
textChunks.push(`${role}: [tool_result] ${inner}`);
|
|
1858
|
+
}
|
|
1859
|
+
} else if (Array.isArray(inner)) {
|
|
1860
|
+
for (const innerBlock of inner as Array<
|
|
1861
|
+
string | ExtendedMessageContent
|
|
1862
|
+
>) {
|
|
1863
|
+
if (typeof innerBlock === 'string') {
|
|
1864
|
+
if (innerBlock) {
|
|
1865
|
+
textChunks.push(`${role}: [tool_result] ${innerBlock}`);
|
|
1866
|
+
}
|
|
1867
|
+
} else if (IMAGE_BLOCK_TYPES.has(innerBlock.type ?? '')) {
|
|
1868
|
+
flushTextChunks(textChunks, parts);
|
|
1869
|
+
parts.push({ ...innerBlock } as MessageContentComplex);
|
|
1870
|
+
} else {
|
|
1871
|
+
const innerText = innerBlock.text ?? innerBlock.input;
|
|
1872
|
+
textChunks.push(
|
|
1873
|
+
`${role}: [tool_result] ${
|
|
1874
|
+
typeof innerText === 'string' && innerText
|
|
1875
|
+
? innerText
|
|
1876
|
+
: JSON.stringify(innerBlock)
|
|
1877
|
+
}`
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
} else if (inner != null) {
|
|
1882
|
+
textChunks.push(`${role}: [tool_result] ${JSON.stringify(inner)}`);
|
|
1883
|
+
}
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1809
1887
|
const text = block.text ?? block.input;
|
|
1810
1888
|
if (typeof text === 'string' && text) {
|
|
1811
1889
|
textChunks.push(`${role}: ${text}`);
|
|
@@ -1834,11 +1912,26 @@ function appendToolCalls(
|
|
|
1834
1912
|
return;
|
|
1835
1913
|
}
|
|
1836
1914
|
const aiMsg = msg as AIMessage;
|
|
1837
|
-
if (
|
|
1915
|
+
if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
|
|
1916
|
+
for (const tc of aiMsg.tool_calls) {
|
|
1917
|
+
textChunks.push(`AI: [tool_call] ${tc.name}(${JSON.stringify(tc.args)})`);
|
|
1918
|
+
}
|
|
1838
1919
|
return;
|
|
1839
1920
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1921
|
+
// Fall back to raw provider tool calls kept only in additional_kwargs.
|
|
1922
|
+
const rawToolCalls = aiMsg.additional_kwargs.tool_calls;
|
|
1923
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
for (const tc of rawToolCalls) {
|
|
1927
|
+
const fn = (tc as { function?: { name?: string; arguments?: string } })
|
|
1928
|
+
.function;
|
|
1929
|
+
if (fn == null) {
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
textChunks.push(
|
|
1933
|
+
`AI: [tool_call] ${String(fn.name ?? '')}(${String(fn.arguments ?? '')})`
|
|
1934
|
+
);
|
|
1842
1935
|
}
|
|
1843
1936
|
}
|
|
1844
1937
|
|
|
@@ -2024,6 +2117,141 @@ export function ensureThinkingBlockInMessages(
|
|
|
2024
2117
|
return result;
|
|
2025
2118
|
}
|
|
2026
2119
|
|
|
2120
|
+
/** Whether a message carries tool content a tool-less agent cannot legally
|
|
2121
|
+
* send. Covers every representation a provider converter will serialize back
|
|
2122
|
+
* into a request: a ToolMessage, parsed `AIMessage.tool_calls`, raw
|
|
2123
|
+
* `additional_kwargs.tool_calls` (OpenAI keeps calls here when the parsed
|
|
2124
|
+
* array is empty), and `tool_use` / `tool_call` / `tool_result` content
|
|
2125
|
+
* blocks (`@langchain/aws` and the Anthropic converter map these to Converse
|
|
2126
|
+
* `toolUse` / `toolResult`). Missing the parent AI message is not just a
|
|
2127
|
+
* passthrough: folding its ToolMessage alone would leave an orphan
|
|
2128
|
+
* `assistant(tool_calls) -> user(...)` sequence. */
|
|
2129
|
+
function messageHasToolContent(msg: BaseMessage): boolean {
|
|
2130
|
+
if (isToolMessage(msg)) {
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
const aiMsg = msg as AIMessage;
|
|
2134
|
+
if (aiMsg.tool_calls != null && aiMsg.tool_calls.length > 0) {
|
|
2135
|
+
return true;
|
|
2136
|
+
}
|
|
2137
|
+
const rawToolCalls = aiMsg.additional_kwargs.tool_calls;
|
|
2138
|
+
if (Array.isArray(rawToolCalls) && rawToolCalls.length > 0) {
|
|
2139
|
+
return true;
|
|
2140
|
+
}
|
|
2141
|
+
if (Array.isArray(msg.content)) {
|
|
2142
|
+
for (const block of msg.content as ExtendedMessageContent[]) {
|
|
2143
|
+
if (
|
|
2144
|
+
typeof block === 'object' &&
|
|
2145
|
+
(block.type === 'tool_use' ||
|
|
2146
|
+
block.type === 'tool_call' ||
|
|
2147
|
+
block.type === 'tool_result')
|
|
2148
|
+
) {
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return false;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/** Whether a message carries a tool RESULT: a ToolMessage, or a message whose
|
|
2157
|
+
* content includes a `tool_result` block (the shape when a call/result pair is
|
|
2158
|
+
* split as `AIMessage(tool_call)` + `HumanMessage(tool_result)`). Such a result
|
|
2159
|
+
* belongs with the preceding tool call, so it is absorbed into the same fold
|
|
2160
|
+
* and labelled as tool output. */
|
|
2161
|
+
function isToolResultMessage(msg: BaseMessage): boolean {
|
|
2162
|
+
if (isToolMessage(msg)) {
|
|
2163
|
+
return true;
|
|
2164
|
+
}
|
|
2165
|
+
if (Array.isArray(msg.content)) {
|
|
2166
|
+
return (msg.content as ExtendedMessageContent[]).some(
|
|
2167
|
+
(block) => typeof block === 'object' && block.type === 'tool_result'
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Folds tool_use / tool_result content into plain text for an agent that binds
|
|
2175
|
+
* no tools.
|
|
2176
|
+
*
|
|
2177
|
+
* In a multi-agent graph, a tool-less destination still inherits the prior
|
|
2178
|
+
* agent's conversation history, which can contain toolUse/toolResult blocks.
|
|
2179
|
+
* Because it binds no tools, the model is invoked with no tool schema — and
|
|
2180
|
+
* Bedrock's Converse API rejects any request that carries toolUse/toolResult
|
|
2181
|
+
* blocks without a top-level toolConfig ("The toolConfig field must be defined
|
|
2182
|
+
* when using toolUse and toolResult content blocks"). Adding a dummy toolConfig
|
|
2183
|
+
* is not an option: AWS requires at least one tool, and it would expose a
|
|
2184
|
+
* capability the destination was intentionally denied.
|
|
2185
|
+
*
|
|
2186
|
+
* Each tool-call turn plus its trailing tool results (ToolMessages or
|
|
2187
|
+
* `tool_result` content blocks) is collapsed into a single `[Previous tool
|
|
2188
|
+
* interaction]` HumanMessage that preserves the tool name, arguments and result
|
|
2189
|
+
* as text (image blocks are kept as-is). Runs in a single pass: non-tool
|
|
2190
|
+
* messages pass through, `result` is allocated lazily on the first fold, and the
|
|
2191
|
+
* original array is returned unchanged when it holds no tool content (the common
|
|
2192
|
+
* fresh-tool-less-agent case).
|
|
2193
|
+
*/
|
|
2194
|
+
export function foldToolBlocksForToollessAgent(
|
|
2195
|
+
messages: BaseMessage[],
|
|
2196
|
+
config?: RunnableConfig
|
|
2197
|
+
): BaseMessage[] {
|
|
2198
|
+
let result: BaseMessage[] | null = null;
|
|
2199
|
+
let foldedCount = 0;
|
|
2200
|
+
let i = 0;
|
|
2201
|
+
while (i < messages.length) {
|
|
2202
|
+
const msg = messages[i];
|
|
2203
|
+
if (!messageHasToolContent(msg)) {
|
|
2204
|
+
result?.push(msg);
|
|
2205
|
+
i++;
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/** First fold — copy the untouched prefix once, then append from here. */
|
|
2210
|
+
if (result === null) {
|
|
2211
|
+
result = messages.slice(0, i);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const parts: MessageContentComplex[] = [];
|
|
2215
|
+
const textChunks: string[] = ['[Previous tool interaction]'];
|
|
2216
|
+
appendMessageContent(
|
|
2217
|
+
msg,
|
|
2218
|
+
isToolResultMessage(msg) ? 'Tool' : 'AI',
|
|
2219
|
+
textChunks,
|
|
2220
|
+
parts
|
|
2221
|
+
);
|
|
2222
|
+
foldedCount++;
|
|
2223
|
+
|
|
2224
|
+
let j = i + 1;
|
|
2225
|
+
while (j < messages.length && isToolResultMessage(messages[j])) {
|
|
2226
|
+
appendMessageContent(messages[j], 'Tool', textChunks, parts);
|
|
2227
|
+
foldedCount++;
|
|
2228
|
+
j++;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
flushTextChunks(textChunks, parts);
|
|
2232
|
+
result.push(
|
|
2233
|
+
withMessageRole(
|
|
2234
|
+
new HumanMessage({ content: toLangChainContent(parts) }),
|
|
2235
|
+
'user'
|
|
2236
|
+
)
|
|
2237
|
+
);
|
|
2238
|
+
i = j;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (result === null) {
|
|
2242
|
+
return messages;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
emitAgentLog(
|
|
2246
|
+
config,
|
|
2247
|
+
'warn',
|
|
2248
|
+
'format',
|
|
2249
|
+
`foldToolBlocksForToollessAgent: folded ${foldedCount} tool message(s) into text for a tool-less agent`
|
|
2250
|
+
);
|
|
2251
|
+
|
|
2252
|
+
return result;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2027
2255
|
/**
|
|
2028
2256
|
* Walks backwards from `currentIndex` through the message array to check
|
|
2029
2257
|
* whether an earlier AI message in the same "chain" (no HumanMessage boundary)
|
package/src/messages/prune.ts
CHANGED
|
@@ -80,10 +80,18 @@ export function enforceOriginalContentCap(map: Map<number, string>): void {
|
|
|
80
80
|
|
|
81
81
|
/** Minimum cumulative calibration ratio — provider can't count fewer tokens
|
|
82
82
|
* than our raw estimate (within reason). Prevents divide-by-zero edge cases. */
|
|
83
|
-
const CALIBRATION_RATIO_MIN = 0.5;
|
|
83
|
+
export const CALIBRATION_RATIO_MIN = 0.5;
|
|
84
84
|
|
|
85
85
|
/** Maximum cumulative calibration ratio — sanity cap for the running ratio. */
|
|
86
|
-
const CALIBRATION_RATIO_MAX = 5;
|
|
86
|
+
export const CALIBRATION_RATIO_MAX = 5;
|
|
87
|
+
|
|
88
|
+
/** Keeps provider/local token calibration within the shared safe range. */
|
|
89
|
+
export function clampCalibrationRatio(ratio: number): number {
|
|
90
|
+
return Math.max(
|
|
91
|
+
CALIBRATION_RATIO_MIN,
|
|
92
|
+
Math.min(CALIBRATION_RATIO_MAX, ratio)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
87
95
|
|
|
88
96
|
export type PruneMessagesFactoryParams = {
|
|
89
97
|
provider?: Providers;
|
|
@@ -1002,8 +1010,8 @@ export function maskConsumedToolResults(params: {
|
|
|
1002
1010
|
/** When provided, original (pre-masking) content is stored here keyed by
|
|
1003
1011
|
* message index — only for entries that actually get truncated. */
|
|
1004
1012
|
originalContentStore?: Map<number, string>;
|
|
1005
|
-
/** Called after storing
|
|
1006
|
-
onContentStored?: (
|
|
1013
|
+
/** Called after storing a newly captured entry. */
|
|
1014
|
+
onContentStored?: (index: number, content: string) => void;
|
|
1007
1015
|
}): number {
|
|
1008
1016
|
const { messages, indexTokenCountMap, tokenCounter } = params;
|
|
1009
1017
|
let maskedCount = 0;
|
|
@@ -1079,7 +1087,7 @@ export function maskConsumedToolResults(params: {
|
|
|
1079
1087
|
if (params.originalContentStore && !params.originalContentStore.has(i)) {
|
|
1080
1088
|
params.originalContentStore.set(i, content);
|
|
1081
1089
|
if (params.onContentStored) {
|
|
1082
|
-
params.onContentStored(content
|
|
1090
|
+
params.onContentStored(i, content);
|
|
1083
1091
|
}
|
|
1084
1092
|
}
|
|
1085
1093
|
|
|
@@ -1311,6 +1319,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
1311
1319
|
remainingContextTokens?: number;
|
|
1312
1320
|
contextPressure?: number;
|
|
1313
1321
|
originalToolContent?: Map<number, string>;
|
|
1322
|
+
newOriginalToolContent?: Map<number, string>;
|
|
1314
1323
|
calibrationRatio?: number;
|
|
1315
1324
|
resolvedInstructionOverhead?: number;
|
|
1316
1325
|
/** Usable budget this call: maxTokens minus output reserve */
|
|
@@ -1318,6 +1327,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
1318
1327
|
/** Calibrated instruction overhead actually applied this call */
|
|
1319
1328
|
effectiveInstructionTokens?: number;
|
|
1320
1329
|
} {
|
|
1330
|
+
let newOriginalToolContent: Map<number, string> | undefined;
|
|
1321
1331
|
if (params.messages.length === 0) {
|
|
1322
1332
|
/** Post-compaction calls still invoke the model — report the same
|
|
1323
1333
|
* reserve-adjusted budget fields as the populated paths */
|
|
@@ -1479,10 +1489,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
1479
1489
|
cumulativeRawSent += rawSentThisTurn;
|
|
1480
1490
|
cumulativeProviderReported += providerMessageTokens;
|
|
1481
1491
|
const newRatio = cumulativeProviderReported / cumulativeRawSent;
|
|
1482
|
-
calibrationRatio =
|
|
1483
|
-
CALIBRATION_RATIO_MIN,
|
|
1484
|
-
Math.min(CALIBRATION_RATIO_MAX, newRatio)
|
|
1485
|
-
);
|
|
1492
|
+
calibrationRatio = clampCalibrationRatio(newRatio);
|
|
1486
1493
|
|
|
1487
1494
|
const calibratedOurTotal =
|
|
1488
1495
|
instructionOverhead + rawSentThisTurn * calibrationRatio;
|
|
@@ -1661,8 +1668,12 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
1661
1668
|
: undefined,
|
|
1662
1669
|
onContentStored:
|
|
1663
1670
|
factoryParams.summarizationEnabled === true
|
|
1664
|
-
? (
|
|
1665
|
-
originalToolContentSize +=
|
|
1671
|
+
? (index: number, content: string): void => {
|
|
1672
|
+
originalToolContentSize += content.length;
|
|
1673
|
+
if (newOriginalToolContent == null) {
|
|
1674
|
+
newOriginalToolContent = new Map();
|
|
1675
|
+
}
|
|
1676
|
+
newOriginalToolContent.set(index, content);
|
|
1666
1677
|
while (
|
|
1667
1678
|
originalToolContentSize > ORIGINAL_CONTENT_MAX_CHARS &&
|
|
1668
1679
|
originalToolContent.size > 0
|
|
@@ -1798,6 +1809,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
1798
1809
|
contextPressure,
|
|
1799
1810
|
originalToolContent:
|
|
1800
1811
|
originalToolContent.size > 0 ? originalToolContent : undefined,
|
|
1812
|
+
newOriginalToolContent,
|
|
1801
1813
|
calibrationRatio,
|
|
1802
1814
|
resolvedInstructionOverhead: bestInstructionOverhead,
|
|
1803
1815
|
contextBudget: pruningBudget,
|
|
@@ -2182,6 +2194,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
2182
2194
|
contextPressure,
|
|
2183
2195
|
originalToolContent:
|
|
2184
2196
|
originalToolContent.size > 0 ? originalToolContent : undefined,
|
|
2197
|
+
newOriginalToolContent,
|
|
2185
2198
|
calibrationRatio,
|
|
2186
2199
|
resolvedInstructionOverhead: bestInstructionOverhead,
|
|
2187
2200
|
contextBudget: pruningBudget,
|
package/src/messages/recency.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_RETAIN_RECENT_TURNS = 2;
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Configuration for splitting a message list into a head (to be summarized)
|
|
5
7
|
* and a tail (to be preserved verbatim).
|
|
@@ -68,7 +70,7 @@ export function splitAtRecencyBoundary(
|
|
|
68
70
|
messages: BaseMessage[],
|
|
69
71
|
options: RecencyWindowOptions = {}
|
|
70
72
|
): RecencySplit {
|
|
71
|
-
const turnsCap = options.turns ??
|
|
73
|
+
const turnsCap = options.turns ?? DEFAULT_RETAIN_RECENT_TURNS;
|
|
72
74
|
|
|
73
75
|
if (messages.length === 0 || turnsCap <= 0) {
|
|
74
76
|
return {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
|
|
2
|
+
import type { ActivityLabelToolEntry } from '@/types/activityLabel';
|
|
3
|
+
import { shouldRedactTool } from '@/langfuseToolOutputTracing';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default system prompt for fast-model activity labeling.
|
|
7
|
+
*
|
|
8
|
+
* Style synthesized from Claude Code's tool-use summary prompt (git-subject
|
|
9
|
+
* register, past tense, distinctive nouns) and claude.ai's observed group
|
|
10
|
+
* headers (5–9 words describing a mixed reasoning + tool block, e.g.
|
|
11
|
+
* "Synthesized version data and curated comparative framework").
|
|
12
|
+
*/
|
|
13
|
+
export const ACTIVITY_LABEL_PROMPT = `Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- 5 to 9 words, past-tense verb first
|
|
17
|
+
- Name the most distinctive subject (file, API, topic); drop articles and filler
|
|
18
|
+
- Describe outcomes, not mechanics; if something failed, say so plainly
|
|
19
|
+
- Output only the label — no quotes, no punctuation at the end, no preamble
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
- Searched Node.js release notes and changelogs
|
|
23
|
+
- Compared runtime versions across official sources
|
|
24
|
+
- Fixed failing auth middleware tests
|
|
25
|
+
- Read project config and dependency manifests
|
|
26
|
+
- Attempted database migration, hit permission errors`;
|
|
27
|
+
|
|
28
|
+
/** Truncates a serialized value for the label prompt. */
|
|
29
|
+
export function truncateForLabel(value: string, maxLength: number): string {
|
|
30
|
+
if (value.length <= maxLength) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
return value.slice(0, Math.max(0, maxLength - 1)) + '…';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ABORT_SERIALIZATION = Symbol('abort-label-serialization');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Serializes a tool value for the prompt WITHOUT materializing huge JSON:
|
|
40
|
+
* the output is clipped to a few hundred characters anyway, so a multi-
|
|
41
|
+
* megabyte tool result must not be stringified in full on the label path.
|
|
42
|
+
* Strings clip immediately; structured values serialize under a character
|
|
43
|
+
* budget and degrade to a shape summary once it is exhausted.
|
|
44
|
+
*/
|
|
45
|
+
function serializeForLabel(value: unknown, limit: number): string {
|
|
46
|
+
if (value == null) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
return value.length > limit ? value.slice(0, limit + 1) : value;
|
|
51
|
+
}
|
|
52
|
+
let budget = limit * 4;
|
|
53
|
+
try {
|
|
54
|
+
return (
|
|
55
|
+
JSON.stringify(value, (_key, nested: unknown) => {
|
|
56
|
+
if (budget <= 0) {
|
|
57
|
+
throw ABORT_SERIALIZATION;
|
|
58
|
+
}
|
|
59
|
+
if (typeof nested === 'string') {
|
|
60
|
+
const clipped =
|
|
61
|
+
nested.length > limit ? nested.slice(0, limit) : nested;
|
|
62
|
+
budget -= clipped.length;
|
|
63
|
+
return clipped;
|
|
64
|
+
}
|
|
65
|
+
budget -= 8;
|
|
66
|
+
return nested;
|
|
67
|
+
}) ?? ''
|
|
68
|
+
);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error === ABORT_SERIALIZATION) {
|
|
71
|
+
return Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';
|
|
72
|
+
}
|
|
73
|
+
return String(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const INPUT_CONTEXT_LIMIT = 200;
|
|
78
|
+
const MAX_THINKING_EXCERPTS = 4;
|
|
79
|
+
/** A label is 5-9 words; no batch needs more than this many entries to
|
|
80
|
+
* produce one, and the cap keeps a 200-call programmatic batch from
|
|
81
|
+
* building an enormous prompt out of per-field-bounded pieces. */
|
|
82
|
+
const MAX_PROMPT_ENTRIES = 12;
|
|
83
|
+
|
|
84
|
+
export type BuildActivityLabelPromptParams = {
|
|
85
|
+
entries: ActivityLabelToolEntry[];
|
|
86
|
+
charLimit: number;
|
|
87
|
+
thinkingExcerpts?: string[];
|
|
88
|
+
lastAssistantText?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Resolved tool-output tracing policy. The label prompt becomes Langfuse
|
|
91
|
+
* generation input, so outputs/errors excluded from tracing (global
|
|
92
|
+
* disable or `redactedToolNames`) must never appear in it — the same
|
|
93
|
+
* redaction the span processor applies to structured tool observations.
|
|
94
|
+
*/
|
|
95
|
+
redaction?: ResolvedLangfuseToolOutputTracingConfig;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds the user prompt for a fast-model activity label. Pure — exported
|
|
100
|
+
* for direct testing of redaction and truncation behavior.
|
|
101
|
+
*/
|
|
102
|
+
export function buildActivityLabelPrompt({
|
|
103
|
+
entries,
|
|
104
|
+
charLimit,
|
|
105
|
+
thinkingExcerpts,
|
|
106
|
+
lastAssistantText,
|
|
107
|
+
redaction,
|
|
108
|
+
}: BuildActivityLabelPromptParams): string {
|
|
109
|
+
const clip = truncateForLabel;
|
|
110
|
+
/** Reasoning and intent text can quote tool output verbatim — including
|
|
111
|
+
* output from EARLIER calls to a redacted tool that this batch does not
|
|
112
|
+
* contain — so any active policy (global disable or a configured
|
|
113
|
+
* redacted-name list) drops both wholesale. There is no reliable way to
|
|
114
|
+
* scrub a quoted fragment out of free-form model prose. */
|
|
115
|
+
const excerptsRedacted =
|
|
116
|
+
redaction != null &&
|
|
117
|
+
(redaction.enabled === false || redaction.redactedToolNames.size > 0);
|
|
118
|
+
const sections: string[] = [];
|
|
119
|
+
/** Intent text is free-form assistant prose that can quote a redacted
|
|
120
|
+
* tool result just as reasoning can, so it shares the excerpts' fate. */
|
|
121
|
+
if (
|
|
122
|
+
!excerptsRedacted &&
|
|
123
|
+
lastAssistantText != null &&
|
|
124
|
+
lastAssistantText.length > 0
|
|
125
|
+
) {
|
|
126
|
+
sections.push(
|
|
127
|
+
`Intent (assistant's last message): ${clip(lastAssistantText, INPUT_CONTEXT_LIMIT)}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
!excerptsRedacted &&
|
|
132
|
+
thinkingExcerpts != null &&
|
|
133
|
+
thinkingExcerpts.length > 0
|
|
134
|
+
) {
|
|
135
|
+
sections.push(
|
|
136
|
+
'Reasoning excerpts:\n' +
|
|
137
|
+
thinkingExcerpts
|
|
138
|
+
.slice(0, MAX_THINKING_EXCERPTS)
|
|
139
|
+
.map((excerpt) => `- ${clip(excerpt, charLimit)}`)
|
|
140
|
+
.join('\n')
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (entries.length > 0) {
|
|
144
|
+
const shown = entries.slice(0, MAX_PROMPT_ENTRIES);
|
|
145
|
+
const omitted = entries.length - shown.length;
|
|
146
|
+
sections.push(
|
|
147
|
+
'Tool calls:\n' +
|
|
148
|
+
shown
|
|
149
|
+
.map((entry) => {
|
|
150
|
+
const input = clip(
|
|
151
|
+
serializeForLabel(entry.toolInput, charLimit),
|
|
152
|
+
charLimit
|
|
153
|
+
);
|
|
154
|
+
const redacted =
|
|
155
|
+
redaction != null && shouldRedactTool(entry.toolName, redaction);
|
|
156
|
+
let outcome: string;
|
|
157
|
+
if (redacted) {
|
|
158
|
+
outcome = redaction.redactionText;
|
|
159
|
+
} else if (entry.status === 'error') {
|
|
160
|
+
outcome = `ERROR: ${clip(entry.error ?? 'unknown error', charLimit)}`;
|
|
161
|
+
} else {
|
|
162
|
+
outcome = clip(
|
|
163
|
+
serializeForLabel(entry.toolOutput, charLimit),
|
|
164
|
+
charLimit
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return `- ${entry.toolName}(${input}) → ${outcome}`;
|
|
168
|
+
})
|
|
169
|
+
.join('\n') +
|
|
170
|
+
(omitted > 0
|
|
171
|
+
? `\n- …and ${omitted} more tool ${omitted === 1 ? 'call' : 'calls'}`
|
|
172
|
+
: '')
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
sections.push('Label:');
|
|
176
|
+
return sections.join('\n\n');
|
|
177
|
+
}
|