@librechat/agents 3.7.7 → 3.7.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/agents/AgentContext.cjs +23 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +10 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +16 -5
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +75 -0
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/toolCache.cjs +1 -1
- package/dist/cjs/llm/fake.cjs +9 -5
- package/dist/cjs/llm/fake.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +1 -1
- package/dist/cjs/llm/openrouter/toolCache.cjs +1 -1
- package/dist/cjs/llm/prepareProviderRequest.cjs +2 -2
- package/dist/cjs/llm/providers.cjs +1 -1
- package/dist/cjs/llm/truncation.cjs +1 -0
- package/dist/cjs/main.cjs +3 -2
- package/dist/cjs/messages/format.cjs +298 -3
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +1 -1
- package/dist/cjs/run.cjs +25 -15
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +43 -9
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/summarization/semanticIndex.cjs +362 -0
- package/dist/cjs/summarization/semanticIndex.cjs.map +1 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +2 -1
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/esm/agents/AgentContext.mjs +23 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +10 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +16 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +75 -0
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/bedrock/toolCache.mjs +1 -1
- package/dist/esm/llm/fake.mjs +9 -5
- package/dist/esm/llm/fake.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +1 -1
- package/dist/esm/llm/openrouter/toolCache.mjs +1 -1
- package/dist/esm/llm/prepareProviderRequest.mjs +2 -2
- package/dist/esm/llm/providers.mjs +1 -1
- package/dist/esm/llm/truncation.mjs +1 -1
- package/dist/esm/main.mjs +4 -4
- package/dist/esm/messages/format.mjs +298 -3
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +1 -1
- package/dist/esm/run.mjs +25 -15
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +43 -9
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/summarization/semanticIndex.mjs +360 -0
- package/dist/esm/summarization/semanticIndex.mjs.map +1 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs +2 -1
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/types/agents/AgentContext.d.ts +27 -1
- package/dist/types/common/constants.d.ts +10 -0
- package/dist/types/graphs/Graph.d.ts +15 -0
- package/dist/types/llm/fake.d.ts +12 -2
- package/dist/types/messages/format.d.ts +10 -1
- package/dist/types/run.d.ts +17 -5
- package/dist/types/summarization/semanticIndex.d.ts +19 -0
- package/dist/types/types/graph.d.ts +9 -1
- package/dist/types/types/summarize.d.ts +38 -0
- package/package.json +2 -1
- package/src/agents/AgentContext.ts +61 -1
- package/src/common/constants.ts +11 -0
- package/src/graphs/Graph.ts +41 -8
- package/src/langfuseTraceShaping.ts +94 -0
- package/src/llm/fake.ts +35 -4
- package/src/messages/format.ts +652 -4
- package/src/run.ts +36 -10
- package/src/summarization/node.ts +90 -33
- package/src/summarization/semanticIndex.ts +653 -0
- package/src/tools/subagent/childGraphConfig.ts +3 -0
- package/src/types/graph.ts +9 -0
- package/src/types/summarize.ts +50 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AIMessage, ToolMessage, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
|
2
2
|
import type { MessageContentImageUrl } from '@langchain/core/messages';
|
|
3
3
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
4
|
-
import type { MessageContentComplex, TPayload, ProviderName } from '@/types';
|
|
4
|
+
import type { MessageContentComplex, TPayload, ProviderName, CompactionSemanticIndex } from '@/types';
|
|
5
5
|
import { Providers } from '@/common';
|
|
6
6
|
interface MediaMessageParams {
|
|
7
7
|
message: {
|
|
@@ -105,6 +105,13 @@ interface FormatAgentMessagesOptions {
|
|
|
105
105
|
* historical `skill` tool_calls are not reconstructed into a HumanMessage,
|
|
106
106
|
* so the same SKILL.md body is not injected twice in one request. */
|
|
107
107
|
skipSkillBodyNames?: Set<string>;
|
|
108
|
+
/** Derive bounded compaction guidance during the formatter's existing
|
|
109
|
+
* persisted-content analysis. Tool intents are accepted only for names
|
|
110
|
+
* the host identifies as semantic-label fields; business `intent`
|
|
111
|
+
* parameters must remain ordinary tool input. */
|
|
112
|
+
compactionSemanticIndex?: {
|
|
113
|
+
intentToolNames?: ReadonlySet<string>;
|
|
114
|
+
};
|
|
108
115
|
}
|
|
109
116
|
/**
|
|
110
117
|
* Groups content parts by agent and formats them with agent labels
|
|
@@ -148,6 +155,8 @@ export declare const formatAgentMessages: (payload: TPayload, indexTokenCountMap
|
|
|
148
155
|
/** When a positional summary boundary sliced content from a message, the token
|
|
149
156
|
* count was proportionally reduced. Returned so the caller can log it. */
|
|
150
157
|
boundaryTokenAdjustment?: SummaryTokenAdjustment;
|
|
158
|
+
/** Bounded semantic guidance derived during persisted-content analysis. */
|
|
159
|
+
compactionSemanticIndex?: CompactionSemanticIndex;
|
|
151
160
|
};
|
|
152
161
|
/**
|
|
153
162
|
* Adds a value at key 0 for system messages and shifts all key indices by one in an indexTokenCountMap.
|
package/dist/types/run.d.ts
CHANGED
|
@@ -134,6 +134,15 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
134
134
|
*/
|
|
135
135
|
getPreemptStats(): t.PreemptStats;
|
|
136
136
|
getToolCount(): number;
|
|
137
|
+
/**
|
|
138
|
+
* True when the run's last turn ended at `END` because the provider hit
|
|
139
|
+
* its output token ceiling while producing plain text/reasoning — no tool
|
|
140
|
+
* call, so `assertNotTruncatedToolCall` never sees it and the graph reads
|
|
141
|
+
* the turn as an ordinary completion. Hosts check this alongside
|
|
142
|
+
* `getPreemptStats()` / `getHaltReason()` to decide whether to persist the
|
|
143
|
+
* response as unfinished instead of a silently truncated "complete" one.
|
|
144
|
+
*/
|
|
145
|
+
getOutputTruncated(): boolean;
|
|
137
146
|
/**
|
|
138
147
|
* Creates a custom event callback handler that intercepts custom events
|
|
139
148
|
* and processes them through our handler registry instead of EventStreamCallbackHandler
|
|
@@ -175,13 +184,16 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
175
184
|
*/
|
|
176
185
|
getInterrupt<TPayload = t.HumanInterruptPayload>(): t.RunInterruptResult<TPayload> | undefined;
|
|
177
186
|
/**
|
|
178
|
-
* Returns the
|
|
179
|
-
*
|
|
187
|
+
* Returns why the run ended without a natural completion, or `undefined`
|
|
188
|
+
* when it completed normally. Reasons include hook- and prompt-driven
|
|
189
|
+
* halts, `preempt_incomplete` when a cooperative seal ended the turn
|
|
190
|
+
* without continuation content, and `output_truncated` when the provider
|
|
191
|
+
* stopped a plain-text/reasoning response at its output-token ceiling.
|
|
180
192
|
*
|
|
181
193
|
* Hosts inspect this after `processStream` returns to distinguish a
|
|
182
|
-
* natural completion
|
|
183
|
-
*
|
|
184
|
-
*
|
|
194
|
+
* natural completion from a terminal partial response. Independent from
|
|
195
|
+
* `getInterrupt()` — a halted run has no interrupt; an interrupted run has
|
|
196
|
+
* no halt reason.
|
|
185
197
|
*/
|
|
186
198
|
getHaltReason(): string | undefined;
|
|
187
199
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { CompactionSemanticIndex } from '@/types';
|
|
3
|
+
export { COMPACTION_SEMANTIC_INDEX_LIMITS } from '@/common';
|
|
4
|
+
/** Records producer-side omissions without retaining the discarded entries. */
|
|
5
|
+
export declare function setCompactionSemanticIndexProvidedEntryCount(index: CompactionSemanticIndex, providedEntryCount: number): void;
|
|
6
|
+
export type RenderedCompactionSemanticIndex = {
|
|
7
|
+
appendix: string;
|
|
8
|
+
providedEntryCount: number;
|
|
9
|
+
entryCount: number;
|
|
10
|
+
charCount: number;
|
|
11
|
+
omittedEntryCount: number;
|
|
12
|
+
};
|
|
13
|
+
/** Captures caller-owned data before graph execution can cross an await. */
|
|
14
|
+
export declare function snapshotCompactionSemanticIndex(index: CompactionSemanticIndex | undefined): CompactionSemanticIndex | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Produces a deterministic, bounded compaction appendix. Invalid, stale,
|
|
17
|
+
* pending, redacted, conflicting, and out-of-range entries fail closed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderCompactionSemanticIndex(index: CompactionSemanticIndex | undefined, messagesToRefine: BaseMessage[]): RenderedCompactionSemanticIndex;
|
|
@@ -4,7 +4,7 @@ import type { START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
|
|
|
4
4
|
import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
|
|
5
5
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
6
6
|
import type { GoogleAIToolType } from '@langchain/google-common';
|
|
7
|
-
import type { SummarizationNodeInput, SummarizeCompleteEvent, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
|
|
7
|
+
import type { SummarizationNodeInput, SummarizeCompleteEvent, CompactionSemanticIndex, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
|
|
8
8
|
import type { RunStep, RunStepDeltaEvent, RunStepResumeState, RunStepClosedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
|
|
9
9
|
import type { ToolMap, ToolSessionMap, ToolEndEvent, GenericTool, LCTool, ToolExecutionConfig, ToolExecuteBatchRequest } from '@/types/tools';
|
|
10
10
|
import type { TokenCounter, StreamLimits, StreamPreemption, TokenBudgetBreakdown } from '@/types/run';
|
|
@@ -693,6 +693,14 @@ interface AgentInputFields {
|
|
|
693
693
|
discoveredTools?: string[];
|
|
694
694
|
summarizationEnabled?: boolean;
|
|
695
695
|
summarizationConfig?: SummarizationConfig;
|
|
696
|
+
/**
|
|
697
|
+
* Optional host-supplied, user-visible guidance for compaction. The SDK
|
|
698
|
+
* validates, bounds, and scopes entries to the messages being compacted;
|
|
699
|
+
* raw conversation messages remain authoritative. Captured when the
|
|
700
|
+
* AgentContext is constructed; labels committed later in the same run are
|
|
701
|
+
* outside this construction-time interface.
|
|
702
|
+
*/
|
|
703
|
+
compactionSemanticIndex?: CompactionSemanticIndex;
|
|
696
704
|
/** Cross-run summary from a previous run, forwarded from formatAgentMessages.
|
|
697
705
|
* Injected into the dynamic system tail via AgentContext. */
|
|
698
706
|
initialSummary?: {
|
|
@@ -29,6 +29,39 @@ export type RetainRecentConfig = {
|
|
|
29
29
|
*/
|
|
30
30
|
tokens?: number;
|
|
31
31
|
};
|
|
32
|
+
export type CompactionSemanticIndexStatus = 'committed' | 'pending';
|
|
33
|
+
type CompactionSemanticIndexEntryBase = {
|
|
34
|
+
/** Persisted message that owns the indexed content. */
|
|
35
|
+
sourceMessageId: string;
|
|
36
|
+
/** Zero-based content-part index within the persisted source message. */
|
|
37
|
+
sourceContentIndex: number;
|
|
38
|
+
/** Monotonic host revision for this logical entry. */
|
|
39
|
+
revision: number;
|
|
40
|
+
/** Only committed entries may guide compaction. */
|
|
41
|
+
status: CompactionSemanticIndexStatus;
|
|
42
|
+
/** User-visible semantic guidance. Hidden reasoning must never be supplied. */
|
|
43
|
+
text: string;
|
|
44
|
+
/** Omits the entry entirely when host policy redacts its source. */
|
|
45
|
+
redacted?: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type CompactionToolSemanticIndexEntry = CompactionSemanticIndexEntryBase & {
|
|
48
|
+
type: 'tool_intent' | 'tool_outcome';
|
|
49
|
+
toolCallId: string;
|
|
50
|
+
};
|
|
51
|
+
export type CompactionActivitySemanticIndexEntry = CompactionSemanticIndexEntryBase & {
|
|
52
|
+
type: 'activity_phase';
|
|
53
|
+
};
|
|
54
|
+
export type CompactionReasoningSemanticIndexEntry = CompactionSemanticIndexEntryBase & {
|
|
55
|
+
type: 'reasoning_label';
|
|
56
|
+
/** Stable identity shared by every user-visible label revision. */
|
|
57
|
+
reasoningStepId: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Source-addressed navigation hints for the compaction model. Entries remain
|
|
61
|
+
* advisory: raw messages are always sent and remain authoritative.
|
|
62
|
+
*/
|
|
63
|
+
export type CompactionSemanticIndexEntry = CompactionToolSemanticIndexEntry | CompactionActivitySemanticIndexEntry | CompactionReasoningSemanticIndexEntry;
|
|
64
|
+
export type CompactionSemanticIndex = readonly CompactionSemanticIndexEntry[];
|
|
32
65
|
export type SummarizationConfig = {
|
|
33
66
|
provider?: ProviderName;
|
|
34
67
|
model?: string;
|
|
@@ -86,6 +119,10 @@ export interface SummarizeStartEvent {
|
|
|
86
119
|
messagesToRefineCount: number;
|
|
87
120
|
/** Which summarization cycle this is (1-based, increments each time summarization fires) */
|
|
88
121
|
summaryVersion: number;
|
|
122
|
+
/** Committed, source-valid semantic hints included in the request. */
|
|
123
|
+
semanticIndexEntryCount?: number;
|
|
124
|
+
/** Serialized semantic-index characters included in the request. */
|
|
125
|
+
semanticIndexCharCount?: number;
|
|
89
126
|
}
|
|
90
127
|
export interface SummarizeDeltaEvent {
|
|
91
128
|
id: string;
|
|
@@ -99,3 +136,4 @@ export interface SummarizeCompleteEvent {
|
|
|
99
136
|
summary?: SummaryContentBlock;
|
|
100
137
|
error?: string;
|
|
101
138
|
}
|
|
139
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.8",
|
|
4
4
|
"reova": {
|
|
5
5
|
"enabled": true,
|
|
6
6
|
"endpoint": "https://telemetry.reo.dev/data"
|
|
@@ -221,6 +221,7 @@
|
|
|
221
221
|
"bench:provider-derivation": "tsx ./src/scripts/bench-provider-derivation.ts",
|
|
222
222
|
"bench:provider-projection": "tsx ./src/scripts/bench-provider-request-projection.ts",
|
|
223
223
|
"bench:execution-world": "tsx ./src/scripts/bench-execution-world.ts",
|
|
224
|
+
"bench:compaction-semantic-index": "tsx ./src/scripts/bench-compaction-semantic-index.ts",
|
|
224
225
|
"probe:overflow": "tsx -r dotenv/config ./src/scripts/context-overflow-probe.ts",
|
|
225
226
|
"subagent": "tsx -r dotenv/config ./src/scripts/multi-agent-subagent.ts",
|
|
226
227
|
"subagent:events": "tsx -r dotenv/config ./src/scripts/subagent-event-driven-debug.ts",
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
Providers,
|
|
51
51
|
} from '@/common';
|
|
52
52
|
import { isTokenCounterCacheCompatible } from '@/llm/tokenCounterCacheCompatibility';
|
|
53
|
+
import { snapshotCompactionSemanticIndex } from '@/summarization/semanticIndex';
|
|
53
54
|
import { createExactTokenCountCache } from '@/llm/contextPressureMeter';
|
|
54
55
|
import { createSchemaOnlyTools } from '@/tools/schema';
|
|
55
56
|
import { apportionTokenCounts } from '@/utils/tokens';
|
|
@@ -74,6 +75,14 @@ type ProgrammaticToolInstructionTarget = {
|
|
|
74
75
|
executesDirectly: boolean;
|
|
75
76
|
};
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Consecutive summarization attempts that may return no usable summary before
|
|
80
|
+
* the run stops asking. Every such attempt spends a full model call over the
|
|
81
|
+
* whole history and leaves the message set exactly as it was, so the cap
|
|
82
|
+
* bounds spend as much as it bounds the compaction loop.
|
|
83
|
+
*/
|
|
84
|
+
const MAX_SUMMARIZATION_FAILURES = 3;
|
|
85
|
+
|
|
77
86
|
/**
|
|
78
87
|
* Encapsulates agent-specific state that can vary between agents in a multi-agent system
|
|
79
88
|
*/
|
|
@@ -108,6 +117,7 @@ export class AgentContext {
|
|
|
108
117
|
discoveredTools,
|
|
109
118
|
summarizationEnabled,
|
|
110
119
|
summarizationConfig,
|
|
120
|
+
compactionSemanticIndex,
|
|
111
121
|
initialSummary,
|
|
112
122
|
contextPruningConfig,
|
|
113
123
|
maxToolResultChars,
|
|
@@ -141,11 +151,18 @@ export class AgentContext {
|
|
|
141
151
|
discoveredTools,
|
|
142
152
|
summarizationEnabled,
|
|
143
153
|
summarizationConfig,
|
|
154
|
+
compactionSemanticIndex,
|
|
144
155
|
contextPruningConfig,
|
|
145
156
|
maxToolResultChars,
|
|
146
157
|
});
|
|
147
158
|
|
|
148
|
-
agentContext._sourceInputs =
|
|
159
|
+
agentContext._sourceInputs =
|
|
160
|
+
compactionSemanticIndex == null
|
|
161
|
+
? agentConfig
|
|
162
|
+
: {
|
|
163
|
+
...agentConfig,
|
|
164
|
+
compactionSemanticIndex: agentContext.compactionSemanticIndex,
|
|
165
|
+
};
|
|
149
166
|
agentContext.subagentConfigs = subagentConfigs;
|
|
150
167
|
agentContext.maxSubagentDepth = maxSubagentDepth;
|
|
151
168
|
/**
|
|
@@ -346,6 +363,8 @@ export class AgentContext {
|
|
|
346
363
|
summarizationEnabled?: boolean;
|
|
347
364
|
/** Summarization runtime settings used by graph pruning hooks */
|
|
348
365
|
summarizationConfig?: t.SummarizationConfig;
|
|
366
|
+
/** Host-supplied advisory guidance consumed only when compaction runs. */
|
|
367
|
+
compactionSemanticIndex?: t.CompactionSemanticIndex;
|
|
349
368
|
/** Current summary text produced by the summarize node, integrated into system message */
|
|
350
369
|
private summaryText?: string;
|
|
351
370
|
/** Token count of the current summary (tracked for token accounting) */
|
|
@@ -376,6 +395,13 @@ export class AgentContext {
|
|
|
376
395
|
* Summarization is allowed to fire again only when new messages appear.
|
|
377
396
|
*/
|
|
378
397
|
private _lastSummarizationMsgCount: number = 0;
|
|
398
|
+
/**
|
|
399
|
+
* Consecutive summarization attempts that produced no usable summary.
|
|
400
|
+
* An empty or failed summary leaves the message set exactly as it was, so
|
|
401
|
+
* the next prune cycle would ask again on identical state. Cleared by
|
|
402
|
+
* `setSummary` and by `reset()`.
|
|
403
|
+
*/
|
|
404
|
+
private _summarizationFailures: number = 0;
|
|
379
405
|
/**
|
|
380
406
|
* Forced compactions performed after a provider rejected a prompt as too
|
|
381
407
|
* large. Bounds the recovery loop so a model that keeps refusing cannot
|
|
@@ -430,6 +456,7 @@ export class AgentContext {
|
|
|
430
456
|
discoveredTools,
|
|
431
457
|
summarizationEnabled,
|
|
432
458
|
summarizationConfig,
|
|
459
|
+
compactionSemanticIndex,
|
|
433
460
|
contextPruningConfig,
|
|
434
461
|
maxToolResultChars,
|
|
435
462
|
}: {
|
|
@@ -456,6 +483,7 @@ export class AgentContext {
|
|
|
456
483
|
discoveredTools?: string[];
|
|
457
484
|
summarizationEnabled?: boolean;
|
|
458
485
|
summarizationConfig?: t.SummarizationConfig;
|
|
486
|
+
compactionSemanticIndex?: t.CompactionSemanticIndex;
|
|
459
487
|
contextPruningConfig?: t.ContextPruningConfig;
|
|
460
488
|
maxToolResultChars?: number;
|
|
461
489
|
}) {
|
|
@@ -495,6 +523,11 @@ export class AgentContext {
|
|
|
495
523
|
this.useLegacyContent = useLegacyContent ?? false;
|
|
496
524
|
this.summarizationEnabled = summarizationEnabled;
|
|
497
525
|
this.summarizationConfig = summarizationConfig;
|
|
526
|
+
if (compactionSemanticIndex != null) {
|
|
527
|
+
this.compactionSemanticIndex = snapshotCompactionSemanticIndex(
|
|
528
|
+
compactionSemanticIndex
|
|
529
|
+
);
|
|
530
|
+
}
|
|
498
531
|
this.contextPruningConfig = contextPruningConfig;
|
|
499
532
|
this.maxToolResultChars = maxToolResultChars;
|
|
500
533
|
|
|
@@ -1209,6 +1242,7 @@ export class AgentContext {
|
|
|
1209
1242
|
this.summaryTokenCount = this._durableSummaryTokenCount;
|
|
1210
1243
|
this.summaryPrecedesMessages = this.durableSummaryPrecedesMessages;
|
|
1211
1244
|
this._lastSummarizationMsgCount = 0;
|
|
1245
|
+
this._summarizationFailures = 0;
|
|
1212
1246
|
this.lastCallUsage = undefined;
|
|
1213
1247
|
this.totalTokensFresh = false;
|
|
1214
1248
|
this.restoreContextBudgetAfterOverflow();
|
|
@@ -1478,6 +1512,7 @@ export class AgentContext {
|
|
|
1478
1512
|
this._durableSummaryTokenCount = tokenCount;
|
|
1479
1513
|
this.durableSummaryPrecedesMessages = this.summaryPrecedesMessages;
|
|
1480
1514
|
this._summaryVersion += 1;
|
|
1515
|
+
this._summarizationFailures = 0;
|
|
1481
1516
|
this.systemRunnableStale = true;
|
|
1482
1517
|
this.pruneMessages = undefined;
|
|
1483
1518
|
}
|
|
@@ -1547,6 +1582,31 @@ export class AgentContext {
|
|
|
1547
1582
|
this._lastSummarizationMsgCount = msgCount;
|
|
1548
1583
|
}
|
|
1549
1584
|
|
|
1585
|
+
/**
|
|
1586
|
+
* Records a summarization attempt that produced no usable summary — an
|
|
1587
|
+
* empty model response, or a provider failure the run declined to paper
|
|
1588
|
+
* over with a metadata stub. Cleared by the next successful summary.
|
|
1589
|
+
*/
|
|
1590
|
+
recordSummarizationFailure(): void {
|
|
1591
|
+
this._summarizationFailures += 1;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
get summarizationFailures(): number {
|
|
1595
|
+
return this._summarizationFailures;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* True once consecutive no-progress attempts reach {@link MAX_SUMMARIZATION_FAILURES}.
|
|
1600
|
+
* A summarizer that has returned nothing this many times in a row will keep
|
|
1601
|
+
* returning nothing: each empty result leaves the history unchanged, so the
|
|
1602
|
+
* next prune cycle re-triggers on the same state and the run burns its
|
|
1603
|
+
* recursion budget on empty summary steps. Summarization stays off for the
|
|
1604
|
+
* remainder of the run; `reset()` restores it for the next one.
|
|
1605
|
+
*/
|
|
1606
|
+
get summarizationExhausted(): boolean {
|
|
1607
|
+
return this._summarizationFailures >= MAX_SUMMARIZATION_FAILURES;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1550
1610
|
get overflowRecoveryAttempts(): number {
|
|
1551
1611
|
return this._overflowRecoveryAttempts;
|
|
1552
1612
|
}
|
package/src/common/constants.ts
CHANGED
|
@@ -42,3 +42,14 @@ export const ACTIVITY_LABEL_RUN_NAME = 'StepLabel';
|
|
|
42
42
|
export const REASONING_LABEL_RUN_NAME = 'ReasoningLabel';
|
|
43
43
|
export const ACTIVITY_PHASE_RUN_NAME = 'MultiStepLabel';
|
|
44
44
|
export const ACTIVITY_PHASE_LABEL_RUN_NAME = 'MultiStepLabelGeneration';
|
|
45
|
+
|
|
46
|
+
/** Shared admission and rendering bounds for compaction navigation hints. */
|
|
47
|
+
export const COMPACTION_SEMANTIC_INDEX_LIMITS = Object.freeze({
|
|
48
|
+
maxInputEntries: 256,
|
|
49
|
+
maxEntries: 64,
|
|
50
|
+
maxEntryChars: 512,
|
|
51
|
+
maxTotalChars: 4_096,
|
|
52
|
+
maxInputTextChars: 4_096,
|
|
53
|
+
maxIdentityChars: 512,
|
|
54
|
+
maxSourceContentIndex: 4_095,
|
|
55
|
+
} as const);
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -153,10 +153,10 @@ import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
|
|
|
153
153
|
import { SUBAGENT_REPLAY_CONTROLLER } from '@/tools/subagent/SubagentReplay';
|
|
154
154
|
import { applyGraphRuntimeConfig } from '@/graphs/applyGraphRuntimeConfig';
|
|
155
155
|
import { createContextPressureMeter } from '@/llm/contextPressureMeter';
|
|
156
|
-
import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
|
|
157
156
|
import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
|
|
158
157
|
import { prepareProviderRequest } from '@/llm/prepareProviderRequest';
|
|
159
158
|
import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
|
|
159
|
+
import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
|
|
160
160
|
import { providerRequiresStrictAlternation } from '@/llm/providers';
|
|
161
161
|
import { buildSubagentToolParams } from '@/tools/SubagentTool';
|
|
162
162
|
import { initializeLangfuseTracing } from '@/instrumentation';
|
|
@@ -164,6 +164,7 @@ import { shouldTriggerSummarization } from '@/summarization';
|
|
|
164
164
|
import { isRunStepResumeState } from '@/tools/runStepResume';
|
|
165
165
|
import { resolveLocalToolsForBinding } from '@/tools/local';
|
|
166
166
|
import { createSummarizeNode } from '@/summarization/node';
|
|
167
|
+
import { getTruncationStopReason } from '@/llm/truncation';
|
|
167
168
|
import { messagesStateReducer } from '@/messages/reducer';
|
|
168
169
|
import { createSchemaOnlyTools } from '@/tools/schema';
|
|
169
170
|
import { AgentContext } from '@/agents/AgentContext';
|
|
@@ -1425,6 +1426,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1425
1426
|
* hosts persist it as unfinished rather than complete.
|
|
1426
1427
|
*/
|
|
1427
1428
|
preemptIncomplete = false;
|
|
1429
|
+
/**
|
|
1430
|
+
* True when `routeMessage` sent a turn to `END` because the last AI
|
|
1431
|
+
* message carries no tool call, AND the provider reports it stopped for
|
|
1432
|
+
* hitting the output token ceiling (`getTruncationStopReason`). Plain-text
|
|
1433
|
+
* and reasoning turns cut off this way carry no tool call for
|
|
1434
|
+
* `assertNotTruncatedToolCall` to catch, so `toolsCondition` reads them as
|
|
1435
|
+
* an ordinary finished turn otherwise — hosts read this flag to persist
|
|
1436
|
+
* the turn as unfinished instead of a silently truncated "complete" answer.
|
|
1437
|
+
*
|
|
1438
|
+
* Deliberately separate from `preemptIncomplete`/`preemptHaltReason`: this
|
|
1439
|
+
* has no interaction with the preempt/seal machinery (in particular the
|
|
1440
|
+
* `preemptHaltReason` check at each model node's entry), so setting it
|
|
1441
|
+
* cannot suppress an unrelated agent's turn in a multi-agent graph.
|
|
1442
|
+
*/
|
|
1443
|
+
outputTruncatedIncomplete = false;
|
|
1428
1444
|
/**
|
|
1429
1445
|
* `stopReason` from a `PreemptBoundary` hook that halted the turn.
|
|
1430
1446
|
*
|
|
@@ -1702,6 +1718,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1702
1718
|
this.preemptEmptyBoundaries = 0;
|
|
1703
1719
|
this.preemptIncomplete = false;
|
|
1704
1720
|
this.preemptHaltReason = undefined;
|
|
1721
|
+
this.outputTruncatedIncomplete = false;
|
|
1705
1722
|
}
|
|
1706
1723
|
|
|
1707
1724
|
/**
|
|
@@ -2801,9 +2818,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2801
2818
|
provider,
|
|
2802
2819
|
clientOptions,
|
|
2803
2820
|
tools,
|
|
2804
|
-
isDeferred: makeIsDeferred(
|
|
2805
|
-
agentContext.getEffectiveToolDefinitions()
|
|
2806
|
-
),
|
|
2821
|
+
isDeferred: makeIsDeferred(agentContext.getEffectiveToolDefinitions()),
|
|
2807
2822
|
});
|
|
2808
2823
|
}
|
|
2809
2824
|
|
|
@@ -3030,9 +3045,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3030
3045
|
messagesToRefine.length > 0;
|
|
3031
3046
|
|
|
3032
3047
|
if (hasPrunedMessages) {
|
|
3033
|
-
const shouldSkip =
|
|
3034
|
-
|
|
3035
|
-
|
|
3048
|
+
const shouldSkip =
|
|
3049
|
+
agentContext.summarizationExhausted ||
|
|
3050
|
+
agentContext.shouldSkipSummarization(messages.length);
|
|
3036
3051
|
const triggerResult =
|
|
3037
3052
|
!shouldSkip &&
|
|
3038
3053
|
shouldTriggerSummarization({
|
|
@@ -3089,6 +3104,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3089
3104
|
messageCount: messages.length,
|
|
3090
3105
|
messagesToRefineCount: messagesToRefine.length,
|
|
3091
3106
|
contextLength: context.length,
|
|
3107
|
+
summarizationFailures: agentContext.summarizationFailures,
|
|
3108
|
+
summarizationExhausted: agentContext.summarizationExhausted,
|
|
3092
3109
|
},
|
|
3093
3110
|
{ runId: this.runId, agentId }
|
|
3094
3111
|
);
|
|
@@ -4818,11 +4835,27 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
4818
4835
|
if (state.summarizationRequest != null) {
|
|
4819
4836
|
return summarizeNode;
|
|
4820
4837
|
}
|
|
4821
|
-
|
|
4838
|
+
const decision = toolsCondition(
|
|
4822
4839
|
state as t.BaseGraphState,
|
|
4823
4840
|
toolNode,
|
|
4824
4841
|
this.invokedToolIds
|
|
4825
4842
|
);
|
|
4843
|
+
/**
|
|
4844
|
+
* `toolsCondition` only looks at `tool_calls` — a plain-text/reasoning
|
|
4845
|
+
* turn cut off by the output token ceiling has none, so it reads as an
|
|
4846
|
+
* ordinary finished turn and routes here to END. Flag it so hosts can
|
|
4847
|
+
* tell a genuinely finished answer from one the model never got to
|
|
4848
|
+
* complete. See `outputTruncatedIncomplete` for why this stays clear
|
|
4849
|
+
* of the preempt/seal halt fields.
|
|
4850
|
+
*/
|
|
4851
|
+
if (decision === END) {
|
|
4852
|
+
const { messages } = state as t.BaseGraphState;
|
|
4853
|
+
const lastMessage = messages[messages.length - 1];
|
|
4854
|
+
if (getTruncationStopReason(lastMessage) != null) {
|
|
4855
|
+
this.outputTruncatedIncomplete = true;
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
return decision;
|
|
4826
4859
|
};
|
|
4827
4860
|
|
|
4828
4861
|
const StateAnnotation = Annotation.Root({
|
|
@@ -35,6 +35,11 @@ const DEPRECATED_TRACE_INPUT_ATTRIBUTE = 'langfuse.trace.input';
|
|
|
35
35
|
const DEPRECATED_TRACE_OUTPUT_ATTRIBUTE = 'langfuse.trace.output';
|
|
36
36
|
const OBSERVATION_METADATA_LANGGRAPH_NODE = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;
|
|
37
37
|
const OBSERVATION_METADATA_OPERATION = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.${LANGFUSE_OPERATION_METADATA_KEY}`;
|
|
38
|
+
const OBSERVATION_METADATA_COMPACTION_SEMANTIC_INDEX_ENTRIES = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.compaction_semantic_index_entries`;
|
|
39
|
+
const COMPACTION_SEMANTIC_INDEX_OPEN = '<compaction-semantic-index>';
|
|
40
|
+
const COMPACTION_SEMANTIC_INDEX_CLOSE = '</compaction-semantic-index>';
|
|
41
|
+
const REDACTED_COMPACTION_SEMANTIC_INDEX =
|
|
42
|
+
'<compaction-semantic-index redacted="true" />\n\n';
|
|
38
43
|
|
|
39
44
|
type MutableSpan = ReadableSpan & {
|
|
40
45
|
name: string;
|
|
@@ -66,6 +71,94 @@ function parseAttributeValue(value: unknown): unknown {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
type CompactionSemanticIndexRedaction = {
|
|
75
|
+
value: unknown;
|
|
76
|
+
redacted: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function redactCompactionSemanticIndexText(
|
|
80
|
+
value: string
|
|
81
|
+
): CompactionSemanticIndexRedaction {
|
|
82
|
+
if (!value.startsWith(COMPACTION_SEMANTIC_INDEX_OPEN)) {
|
|
83
|
+
return { value, redacted: false };
|
|
84
|
+
}
|
|
85
|
+
const start = 0;
|
|
86
|
+
const close = value.indexOf(COMPACTION_SEMANTIC_INDEX_CLOSE, start);
|
|
87
|
+
if (close < 0) {
|
|
88
|
+
return { value, redacted: false };
|
|
89
|
+
}
|
|
90
|
+
let end = close + COMPACTION_SEMANTIC_INDEX_CLOSE.length;
|
|
91
|
+
while (end < value.length && /\s/.test(value[end])) {
|
|
92
|
+
end++;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
value:
|
|
96
|
+
value.slice(0, start) +
|
|
97
|
+
REDACTED_COMPACTION_SEMANTIC_INDEX +
|
|
98
|
+
value.slice(end),
|
|
99
|
+
redacted: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function redactCompactionSemanticIndexValue(
|
|
104
|
+
value: unknown
|
|
105
|
+
): CompactionSemanticIndexRedaction {
|
|
106
|
+
if (typeof value === 'string') {
|
|
107
|
+
return redactCompactionSemanticIndexText(value);
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
111
|
+
const nested = redactCompactionSemanticIndexValue(value[index]);
|
|
112
|
+
if (nested.redacted) {
|
|
113
|
+
const result = [...value];
|
|
114
|
+
result[index] = nested.value;
|
|
115
|
+
return { value: result, redacted: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { value, redacted: false };
|
|
119
|
+
}
|
|
120
|
+
if (!isRecord(value)) {
|
|
121
|
+
return { value, redacted: false };
|
|
122
|
+
}
|
|
123
|
+
const entries = Object.entries(value);
|
|
124
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
125
|
+
const [key, nestedValue] = entries[index];
|
|
126
|
+
const nested = redactCompactionSemanticIndexValue(nestedValue);
|
|
127
|
+
if (nested.redacted) {
|
|
128
|
+
return {
|
|
129
|
+
value: { ...value, [key]: nested.value },
|
|
130
|
+
redacted: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { value, redacted: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function redactCompactionSemanticIndexInput(span: MutableSpan): void {
|
|
138
|
+
const entryCount =
|
|
139
|
+
span.attributes[
|
|
140
|
+
OBSERVATION_METADATA_COMPACTION_SEMANTIC_INDEX_ENTRIES
|
|
141
|
+
];
|
|
142
|
+
const numericEntryCount = Number(entryCount);
|
|
143
|
+
if (!Number.isFinite(numericEntryCount) || numericEntryCount <= 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
|
|
147
|
+
const input = span.attributes[inputKey];
|
|
148
|
+
if (input == null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const parsed = parseAttributeValue(input);
|
|
152
|
+
const redaction = redactCompactionSemanticIndexValue(parsed);
|
|
153
|
+
if (!redaction.redacted) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
span.attributes[inputKey] =
|
|
157
|
+
typeof redaction.value === 'string' && parsed === input
|
|
158
|
+
? redaction.value
|
|
159
|
+
: JSON.stringify(redaction.value);
|
|
160
|
+
}
|
|
161
|
+
|
|
69
162
|
function getMessageArray(
|
|
70
163
|
value: unknown
|
|
71
164
|
): Record<string, unknown>[] | undefined {
|
|
@@ -594,6 +687,7 @@ export function shapeLangfuseSpan(span: ReadableSpan): void {
|
|
|
594
687
|
const mutable = span as MutableSpan;
|
|
595
688
|
delete mutable.attributes[DEPRECATED_TRACE_INPUT_ATTRIBUTE];
|
|
596
689
|
delete mutable.attributes[DEPRECATED_TRACE_OUTPUT_ATTRIBUTE];
|
|
690
|
+
redactCompactionSemanticIndexInput(mutable);
|
|
597
691
|
const isGraphObservation = isGraphSpan(mutable);
|
|
598
692
|
if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {
|
|
599
693
|
shapeAgentNodeSpan(mutable);
|
package/src/llm/fake.ts
CHANGED
|
@@ -14,6 +14,15 @@ export class FakeChatModel extends FakeListChatModel {
|
|
|
14
14
|
private splitStrategy: SplitStrategy;
|
|
15
15
|
private toolCalls: ToolCall[] = [];
|
|
16
16
|
private addedToolCalls: boolean = false;
|
|
17
|
+
/**
|
|
18
|
+
* Attached to the last streamed chunk of every response, mirroring a real
|
|
19
|
+
* provider's terminal `finish_reason`/`stop_reason` on `generationInfo`.
|
|
20
|
+
* Lets truncation-path tests (`getTruncationStopReason`) drive a genuine
|
|
21
|
+
* `Run`/`StandardGraph` without a live provider. `undefined` (the
|
|
22
|
+
* default) reproduces the previous behavior exactly: no metadata on any
|
|
23
|
+
* chunk.
|
|
24
|
+
*/
|
|
25
|
+
private finalChunkGenerationInfo?: Record<string, unknown>;
|
|
17
26
|
|
|
18
27
|
constructor({
|
|
19
28
|
responses,
|
|
@@ -21,16 +30,19 @@ export class FakeChatModel extends FakeListChatModel {
|
|
|
21
30
|
emitCustomEvent,
|
|
22
31
|
splitStrategy = { type: 'regex', value: /(?<=\s+)|(?=\s+)/ },
|
|
23
32
|
toolCalls = [],
|
|
33
|
+
finalChunkGenerationInfo,
|
|
24
34
|
}: {
|
|
25
35
|
responses: string[];
|
|
26
36
|
sleep?: number;
|
|
27
37
|
emitCustomEvent?: boolean;
|
|
28
38
|
splitStrategy?: SplitStrategy;
|
|
29
39
|
toolCalls?: ToolCall[];
|
|
40
|
+
finalChunkGenerationInfo?: Record<string, unknown>;
|
|
30
41
|
}) {
|
|
31
42
|
super({ responses, sleep, emitCustomEvent });
|
|
32
43
|
this.splitStrategy = splitStrategy;
|
|
33
44
|
this.toolCalls = toolCalls;
|
|
45
|
+
this.finalChunkGenerationInfo = finalChunkGenerationInfo;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
private splitText(text: string): string[] {
|
|
@@ -47,14 +59,16 @@ export class FakeChatModel extends FakeListChatModel {
|
|
|
47
59
|
}
|
|
48
60
|
_createResponseChunk(
|
|
49
61
|
text: string,
|
|
50
|
-
tool_call_chunks?: ToolCallChunk[]
|
|
62
|
+
tool_call_chunks?: ToolCallChunk[],
|
|
63
|
+
responseMetadata?: Record<string, unknown>
|
|
51
64
|
): ChatGenerationChunk {
|
|
52
65
|
return new ChatGenerationChunk({
|
|
53
66
|
text,
|
|
54
|
-
generationInfo: {},
|
|
67
|
+
generationInfo: responseMetadata ?? {},
|
|
55
68
|
message: new AIMessageChunk({
|
|
56
69
|
content: text,
|
|
57
70
|
tool_call_chunks,
|
|
71
|
+
response_metadata: responseMetadata,
|
|
58
72
|
additional_kwargs: tool_call_chunks
|
|
59
73
|
? {
|
|
60
74
|
tool_calls: tool_call_chunks.map((toolCall) => ({
|
|
@@ -87,14 +101,31 @@ export class FakeChatModel extends FakeListChatModel {
|
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
const chunks = this.splitText(response);
|
|
90
|
-
for
|
|
104
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
105
|
+
const chunk = chunks[i];
|
|
91
106
|
await this._sleepIfRequested();
|
|
92
107
|
|
|
93
108
|
if (options.thrownErrorString != null && options.thrownErrorString) {
|
|
94
109
|
throw new Error(options.thrownErrorString);
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Only the true terminal chunk of the response gets the metadata: when
|
|
114
|
+
* this response also appends a trailing tool-call chunk below, THAT
|
|
115
|
+
* chunk is terminal instead (not currently exercised by any test —
|
|
116
|
+
* a caller wanting truncated-tool-call metadata should attach it
|
|
117
|
+
* there).
|
|
118
|
+
*/
|
|
119
|
+
const isTerminalChunk =
|
|
120
|
+
i === chunks.length - 1 && this.toolCalls.length === 0;
|
|
121
|
+
const responseChunk =
|
|
122
|
+
isTerminalChunk && this.finalChunkGenerationInfo != null
|
|
123
|
+
? this._createResponseChunk(
|
|
124
|
+
chunk,
|
|
125
|
+
undefined,
|
|
126
|
+
this.finalChunkGenerationInfo
|
|
127
|
+
)
|
|
128
|
+
: super._createResponseChunk(chunk);
|
|
98
129
|
yield responseChunk;
|
|
99
130
|
void runManager?.handleLLMNewToken(chunk);
|
|
100
131
|
}
|