@librechat/agents 3.7.0 → 3.7.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 +11 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +56 -9
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +11 -1
- package/dist/cjs/messages/content.cjs +8 -5
- package/dist/cjs/messages/content.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +15 -5
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +2 -1
- package/dist/cjs/messages/projectionInvariant.cjs +74 -0
- package/dist/cjs/messages/projectionInvariant.cjs.map +1 -0
- package/dist/cjs/messages/provenance.cjs +30 -8
- package/dist/cjs/messages/provenance.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +202 -7
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/messages/toolResultTypes.cjs +1 -0
- package/dist/cjs/session/AgentSession.cjs +4 -23
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/deriveMessages.cjs +25 -0
- package/dist/cjs/session/deriveMessages.cjs.map +1 -0
- package/dist/cjs/session/index.cjs +1 -0
- package/dist/cjs/summarization/node.cjs +13 -6
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +65 -8
- package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +11 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +8 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +56 -9
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +6 -4
- package/dist/esm/messages/content.mjs +8 -6
- package/dist/esm/messages/content.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +16 -6
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +2 -1
- package/dist/esm/messages/projectionInvariant.mjs +72 -0
- package/dist/esm/messages/projectionInvariant.mjs.map +1 -0
- package/dist/esm/messages/provenance.mjs +30 -9
- package/dist/esm/messages/provenance.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +201 -8
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/messages/toolResultTypes.mjs +1 -1
- package/dist/esm/session/AgentSession.mjs +4 -23
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/deriveMessages.mjs +25 -0
- package/dist/esm/session/deriveMessages.mjs.map +1 -0
- package/dist/esm/session/index.mjs +1 -0
- package/dist/esm/summarization/node.mjs +14 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +65 -8
- package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +6 -1
- package/dist/types/messages/content.d.ts +4 -1
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/projectionInvariant.d.ts +25 -0
- package/dist/types/messages/provenance.d.ts +10 -0
- package/dist/types/messages/recency.d.ts +30 -18
- package/dist/types/session/deriveMessages.d.ts +11 -0
- package/dist/types/session/index.d.ts +2 -0
- package/dist/types/tools/subagent/InMemorySubagentTaskStore.d.ts +13 -1
- package/dist/types/types/graph.d.ts +1 -1
- package/dist/types/types/subagentTasks.d.ts +22 -0
- package/dist/types/types/summarize.d.ts +15 -13
- package/package.json +3 -1
- package/src/agents/AgentContext.ts +19 -5
- package/src/graphs/Graph.ts +9 -1
- package/src/llm/invoke.ts +102 -23
- package/src/messages/content.ts +20 -10
- package/src/messages/format.ts +29 -5
- package/src/messages/index.ts +1 -0
- package/src/messages/projectionInvariant.ts +134 -0
- package/src/messages/provenance.ts +60 -18
- package/src/messages/recency.ts +429 -27
- package/src/session/AgentSession.ts +4 -30
- package/src/session/deriveMessages.ts +37 -0
- package/src/session/index.ts +2 -0
- package/src/summarization/node.ts +37 -15
- package/src/tools/subagent/InMemorySubagentTaskStore.ts +139 -6
- package/src/types/graph.ts +1 -0
- package/src/types/subagentTasks.ts +28 -0
- package/src/types/summarize.ts +15 -13
|
@@ -91,6 +91,12 @@ interface LangChainMessage {
|
|
|
91
91
|
export declare const formatFromLangChain: (message: LangChainMessage) => Record<string, any>;
|
|
92
92
|
interface FormatAgentMessagesOptions {
|
|
93
93
|
provider?: ProviderName;
|
|
94
|
+
/** Emit flattenable text content as the joined string the legacy-content
|
|
95
|
+
* projection would produce, so the per-request `formatContentStrings` pass
|
|
96
|
+
* finds nothing to convert and every history message keeps its identity —
|
|
97
|
+
* which is what lets exact-count reuse skip re-tokenizing it. Set this if
|
|
98
|
+
* and only if the run's provider uses legacy string content. */
|
|
99
|
+
legacyContent?: boolean;
|
|
94
100
|
/** Reconstruct hidden `reasoning_content` from `THINK` parts onto prior
|
|
95
101
|
* tool-call messages. Explicit opt-in for OpenAI-compatible endpoints that
|
|
96
102
|
* replay reasoning across turns; defaults to on for DeepSeek thinking-mode. */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
export type ProviderMessageProjectionInvariantMode = 'off' | 'observe' | 'assert';
|
|
3
|
+
export type ProviderMessageProjectionIssueCode = 'absent_provenance' | 'invalid_provenance' | 'unsourced_non_synthetic_part';
|
|
4
|
+
export interface ProviderMessageProjectionInvariantIssue {
|
|
5
|
+
readonly code: ProviderMessageProjectionIssueCode;
|
|
6
|
+
readonly messageIndex: number;
|
|
7
|
+
readonly messageType: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProviderMessageProjectionInvariantReport {
|
|
10
|
+
readonly valid: boolean;
|
|
11
|
+
readonly messageCount: number;
|
|
12
|
+
readonly sourceBackedMessageCount: number;
|
|
13
|
+
readonly syntheticMessageCount: number;
|
|
14
|
+
readonly gapMessageCount: number;
|
|
15
|
+
readonly issues: readonly ProviderMessageProjectionInvariantIssue[];
|
|
16
|
+
}
|
|
17
|
+
/** Resolves the opt-in provider projection invariant without enabling it for
|
|
18
|
+
* unrecognized values. */
|
|
19
|
+
export declare function resolveProviderMessageProjectionInvariantMode(value?: string | undefined): ProviderMessageProjectionInvariantMode;
|
|
20
|
+
/** Inspects provider-bound lineage without reading message content or source ids. */
|
|
21
|
+
export declare function inspectProviderMessageProjection(messages: readonly BaseMessage[]): ProviderMessageProjectionInvariantReport;
|
|
22
|
+
export declare class ProviderMessageProjectionInvariantError extends Error {
|
|
23
|
+
readonly report: ProviderMessageProjectionInvariantReport;
|
|
24
|
+
constructor(report: ProviderMessageProjectionInvariantReport);
|
|
25
|
+
}
|
|
@@ -73,6 +73,16 @@ export declare function hasBijectiveProviderContentPartMapping(parts: readonly P
|
|
|
73
73
|
export declare function getProviderSourceMessageIds(message: BaseMessage): string[];
|
|
74
74
|
/** Replaces typed provenance and synchronizes its stable plural source ids. */
|
|
75
75
|
export declare function setProviderMessageProvenance(message: BaseMessage, parts: readonly ProviderMessageProvenancePart[]): void;
|
|
76
|
+
/**
|
|
77
|
+
* {@link setProviderMessageProvenance} for a message the caller itself just
|
|
78
|
+
* constructed from locally built plain objects. Such a message cannot carry
|
|
79
|
+
* proxies, accessors, or foreign aliases in `additional_kwargs`/`lc_kwargs`,
|
|
80
|
+
* so the publication skips the hardened descriptor walks while producing the
|
|
81
|
+
* same end state: fresh replacement objects on both slots, with the
|
|
82
|
+
* serialization mirror aliasing the live kwargs. Never call this with a
|
|
83
|
+
* message received across a seam — that is what the hardened variant is for.
|
|
84
|
+
*/
|
|
85
|
+
export declare function setFreshProviderMessageProvenance(message: BaseMessage, parts: readonly ProviderMessageProvenancePart[]): void;
|
|
76
86
|
/** Publishes the canonical fail-closed marker for malformed provenance. */
|
|
77
87
|
export declare function setInvalidProviderMessageProvenance(message: BaseMessage): void;
|
|
78
88
|
/** Marks a provider-visible runtime message as host-generated context. */
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
2
|
export declare const DEFAULT_RETAIN_RECENT_TURNS = 2;
|
|
3
|
+
export declare const DEFAULT_INTRA_TURN_RETAIN_RATIO = 0.16;
|
|
3
4
|
/**
|
|
4
5
|
* Configuration for splitting a message list into a head (to be summarized)
|
|
5
6
|
* and a tail (to be preserved verbatim).
|
|
6
7
|
*/
|
|
7
8
|
export interface RecencyWindowOptions {
|
|
8
9
|
/**
|
|
9
|
-
* Maximum number of recent user-led turns to keep in the tail.
|
|
10
|
-
* begins at a HumanMessage and includes every following
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Maximum number of recent user-led turns to keep in the tail. A "turn"
|
|
11
|
+
* begins at a user-authored HumanMessage and includes every following
|
|
12
|
+
* AIMessage and tool result up to the next user-authored HumanMessage.
|
|
13
|
+
* Provider-native HumanMessages containing only tool results remain in the
|
|
14
|
+
* current turn, so the boundary cannot split them from their calls.
|
|
14
15
|
*
|
|
15
|
-
* The most recent turn is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The most recent turn is preserved unless `intraTurnTokens` enables the
|
|
17
|
+
* pairing-balanced fallback for a tool-heavy history. A lone oversized user
|
|
18
|
+
* message is never eligible for that fallback.
|
|
18
19
|
*
|
|
19
20
|
* Defaults to `2`. A value of `0` disables the recency window (head =
|
|
20
21
|
* everything, tail = empty), restoring the pre-recency-window behavior.
|
|
@@ -32,29 +33,40 @@ export interface RecencyWindowOptions {
|
|
|
32
33
|
tokens?: number;
|
|
33
34
|
/** Token-counter used to evaluate the optional `tokens` cap. */
|
|
34
35
|
tokenCounter?: (m: BaseMessage) => number;
|
|
36
|
+
/**
|
|
37
|
+
* Minimum token budget to retain when the turn window would otherwise make
|
|
38
|
+
* the whole history indivisible. When set with `tokenCounter`, older closed
|
|
39
|
+
* tool-call/result units may be summarized from within the earliest retained
|
|
40
|
+
* turn. A lone user payload and open tool units remain indivisible.
|
|
41
|
+
*/
|
|
42
|
+
intraTurnTokens?: number;
|
|
35
43
|
}
|
|
36
44
|
export interface RecencySplit {
|
|
37
45
|
/** Older messages eligible for summarization. Empty when nothing to summarize. */
|
|
38
46
|
head: BaseMessage[];
|
|
39
|
-
/** Recent messages preserved verbatim
|
|
47
|
+
/** Recent messages preserved verbatim, beginning at a pairing-balanced boundary. */
|
|
40
48
|
tail: BaseMessage[];
|
|
41
49
|
/** Number of user-led turns retained in the tail (0 if no HumanMessage exists). */
|
|
42
50
|
tailTurnCount: number;
|
|
43
51
|
/** Index in the original `messages` array where the tail begins. */
|
|
44
52
|
tailStartIndex: number;
|
|
53
|
+
/** True when the pairing-balanced fallback selected the boundary. */
|
|
54
|
+
usedIntraTurnFallback: boolean;
|
|
45
55
|
}
|
|
56
|
+
export declare function resolveIntraTurnRetainTokens({ tokens, maxContextTokens, }: {
|
|
57
|
+
tokens?: number;
|
|
58
|
+
maxContextTokens?: number;
|
|
59
|
+
}): number | undefined;
|
|
46
60
|
/**
|
|
47
61
|
* Splits `messages` into a head (older, to summarize) and a tail (recent,
|
|
48
|
-
* to preserve verbatim)
|
|
49
|
-
* user-led turn is
|
|
50
|
-
*
|
|
62
|
+
* to preserve verbatim), preferring user-message boundaries. The most recent
|
|
63
|
+
* user-led turn is normally included in the tail; additional older turns are
|
|
64
|
+
* added subject to `turns` and `tokens` caps.
|
|
51
65
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* the "first turn destruction" failure mode where a single large
|
|
57
|
-
* user-pasted payload would otherwise be replaced by a generic summary.
|
|
66
|
+
* When that policy exposes no compactable head, `intraTurnTokens` may select
|
|
67
|
+
* a boundary after older closed tool-call/result units. This keeps runaway
|
|
68
|
+
* first-turn tool loops compactable without splitting parallel calls from
|
|
69
|
+
* their results. A user payload without a completed tool unit stays intact.
|
|
58
70
|
*
|
|
59
71
|
* When `messages` contains no HumanMessage (degenerate state — e.g. system
|
|
60
72
|
* + assistant messages from a programmatic preamble), everything is
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { SessionEntry } from './types';
|
|
3
|
+
export interface DerivedSessionMessages {
|
|
4
|
+
initialSummary?: {
|
|
5
|
+
text: string;
|
|
6
|
+
tokenCount: number;
|
|
7
|
+
};
|
|
8
|
+
messages: BaseMessage[];
|
|
9
|
+
}
|
|
10
|
+
/** Derives the active model context from one append-only session-log path. */
|
|
11
|
+
export declare function deriveMessages(entries: readonly SessionEntry[]): DerivedSessionMessages;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AgentSession, createAgentSession } from './AgentSession';
|
|
2
|
+
export { deriveMessages } from './deriveMessages';
|
|
2
3
|
export { JsonlSessionStore, SessionManager } from './JsonlSessionStore';
|
|
3
4
|
export { createRunHandlers } from './handlers';
|
|
4
5
|
export { serializeMessage, deserializeMessage, extractTextFromContent, } from './messageSerialization';
|
|
5
6
|
export type { AgentSessionConfig, AgentSessionCheckpointLookupOptions, AgentSessionCheckpointReference, AgentSessionCheckpointing, AgentSessionCheckpointingOptions, AgentSessionHandlersResult, AgentSessionInput, AgentSessionRunOptions, AgentSessionRunResult, AgentSessionStream, AgentSessionStreamEvent, AgentSessionUsage, CreateSessionFileOptions, JsonObject, JsonPrimitive, JsonValue, SerializedSessionMessage, SessionBranchOptions, SessionCompactOptions, SessionCheckpointEntry, SessionCompactionEntry, SessionEntry, SessionEntryBase, SessionEntryType, SessionForkOptions, SessionHeader, SessionLabelEntry, SessionListItem, SessionMessageEntry, SessionPosition, SessionRunEventEntry, SessionStateEntry, SessionSummaryEntry, SessionTreeNode, } from './types';
|
|
7
|
+
export type { DerivedSessionMessages } from './deriveMessages';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { SubagentTaskClaim, SubagentTaskControlCommand, SubagentTaskControlResult, SubagentTaskSnapshot, SubagentTaskStartRequest, SubagentTaskStartResult, SubagentTaskStore } from '@/types';
|
|
1
|
+
import type { SubagentTaskClaim, SubagentTaskControlCommand, SubagentTaskControlReceipt, SubagentTaskControlResult, SubagentTaskSnapshot, SubagentTaskStartRequest, SubagentTaskStartResult, SubagentTaskStore } from '@/types';
|
|
2
2
|
export interface InMemorySubagentTaskStoreOptions {
|
|
3
3
|
completedTtlMs?: number;
|
|
4
4
|
maxControlMessageChars?: number;
|
|
5
5
|
maxControlsPerTask?: number;
|
|
6
|
+
maxControlReceiptsPerTask?: number;
|
|
6
7
|
maxErrorChars?: number;
|
|
7
8
|
maxResultChars?: number;
|
|
8
9
|
maxRunningPerScope?: number;
|
|
@@ -25,6 +26,14 @@ export declare class InMemorySubagentTaskStore implements SubagentTaskStore {
|
|
|
25
26
|
private runningTasks;
|
|
26
27
|
private totalTasks;
|
|
27
28
|
constructor(options?: InMemorySubagentTaskStoreOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Payload-free transition seam for hosts that durably project authoritative
|
|
31
|
+
* receipts. Implementations must return synchronously, must not reproduce
|
|
32
|
+
* task-store transition rules, and cannot veto task-store state transitions:
|
|
33
|
+
* hook failures are deliberately isolated by the caller.
|
|
34
|
+
*/
|
|
35
|
+
protected onControlReceipt(_scopeId: string, _taskId: string, _receipt: SubagentTaskControlReceipt): void;
|
|
36
|
+
private emitControlReceipt;
|
|
28
37
|
start(request: SubagentTaskStartRequest): SubagentTaskStartResult;
|
|
29
38
|
get(scopeId: string, taskId: string): SubagentTaskSnapshot | undefined;
|
|
30
39
|
list(scopeId: string): SubagentTaskSnapshot[];
|
|
@@ -37,6 +46,9 @@ export declare class InMemorySubagentTaskStore implements SubagentTaskStore {
|
|
|
37
46
|
private sweepBucket;
|
|
38
47
|
private removeTask;
|
|
39
48
|
private dropEmptyBucket;
|
|
49
|
+
private makeRoomForControlReceipt;
|
|
50
|
+
private transitionControl;
|
|
51
|
+
private failPendingControls;
|
|
40
52
|
private scheduleExpiry;
|
|
41
53
|
private clearTaskExpiry;
|
|
42
54
|
private clearTaskTimeout;
|
|
@@ -452,7 +452,7 @@ export type ExecutableSubagentConfig = ResolvedSubagentConfig | LazySingleAgentS
|
|
|
452
452
|
/** Graph-aware config accepted by the executor. Graph configs stay eager. */
|
|
453
453
|
export type ExecutableSubagentConfigEntry = ExecutableSubagentConfig | GraphSubagentConfig;
|
|
454
454
|
/** Lifecycle phase carried on {@link SubagentUpdateEvent}. */
|
|
455
|
-
export type SubagentUpdatePhase = 'start' | 'run_step' | 'run_step_delta' | 'run_step_completed' | 'run_step_closed' | 'message_delta' | 'reasoning_delta' | 'stop' | 'error';
|
|
455
|
+
export type SubagentUpdatePhase = 'start' | 'run_step' | 'run_step_delta' | 'run_step_completed' | 'run_step_closed' | 'message_delta' | 'reasoning_delta' | 'control' | 'stop' | 'error';
|
|
456
456
|
export interface SubagentAncestryEntry {
|
|
457
457
|
readonly subagentRunId: string;
|
|
458
458
|
readonly subagentType: string;
|
|
@@ -5,6 +5,22 @@ import type { InjectedMessage } from './tools';
|
|
|
5
5
|
export type SubagentTaskStatus = 'running' | 'completed' | 'error' | 'cancelled';
|
|
6
6
|
/** Where a pending parent message may enter the child run. */
|
|
7
7
|
export type SubagentTaskBoundary = 'preempt' | 'tool' | 'turn';
|
|
8
|
+
/**
|
|
9
|
+
* Lifecycle of one parent-to-child message after the task store accepts it.
|
|
10
|
+
* Hosts may render a transient `submitted` state before this authoritative
|
|
11
|
+
* receipt exists; that transport state is intentionally not persisted here.
|
|
12
|
+
*/
|
|
13
|
+
export type SubagentTaskControlReceiptStatus = 'accepted' | 'applied' | 'rejected' | 'failed';
|
|
14
|
+
/** Bounded authoritative receipt for one steer, queue, or interrupt command. */
|
|
15
|
+
export interface SubagentTaskControlReceipt {
|
|
16
|
+
controlId: string;
|
|
17
|
+
action: 'steer' | 'queue' | 'interrupt';
|
|
18
|
+
status: SubagentTaskControlReceiptStatus;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
boundary?: SubagentTaskBoundary;
|
|
22
|
+
reason?: 'withdrawn' | 'task_completed' | 'task_cancelled' | 'task_failed';
|
|
23
|
+
}
|
|
8
24
|
/** Parent-to-child control operations accepted while a task is running. */
|
|
9
25
|
export type SubagentTaskControlCommand = {
|
|
10
26
|
action: 'steer' | 'queue' | 'interrupt';
|
|
@@ -39,6 +55,12 @@ export interface SubagentTaskSnapshot {
|
|
|
39
55
|
resultAvailable: boolean;
|
|
40
56
|
resultClaimed: boolean;
|
|
41
57
|
pendingControls: number;
|
|
58
|
+
/**
|
|
59
|
+
* Bounded receipts emitted by stores that support authoritative control
|
|
60
|
+
* tracking. Optional so legacy and custom stores remain compatible during
|
|
61
|
+
* rolling upgrades.
|
|
62
|
+
*/
|
|
63
|
+
controlReceipts?: SubagentTaskControlReceipt[];
|
|
42
64
|
progress?: SubagentTaskProgress;
|
|
43
65
|
error?: string;
|
|
44
66
|
}
|
|
@@ -5,25 +5,27 @@ export type SummarizationTrigger = {
|
|
|
5
5
|
value: number;
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
|
-
* Controls how
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Controls how much recent context is preserved verbatim during compaction.
|
|
9
|
+
* User-turn boundaries are preferred. Under context pressure, older closed
|
|
10
|
+
* tool units inside an otherwise indivisible turn may be summarized while a
|
|
11
|
+
* token-priced recent tail is retained. A lone user payload stays intact.
|
|
12
12
|
*/
|
|
13
13
|
export type RetainRecentConfig = {
|
|
14
14
|
/**
|
|
15
|
-
* Maximum number of recent user-led turns to keep in the tail.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* Maximum number of recent user-led turns to keep in the tail. A turn begins
|
|
16
|
+
* at a user-authored HumanMessage and includes every following AIMessage and
|
|
17
|
+
* tool result up to the next user-authored HumanMessage. Provider-native
|
|
18
|
+
* HumanMessages containing only tool results remain in the current turn.
|
|
19
|
+
* Set to `0` to disable the recency window (legacy behavior: summarize
|
|
20
|
+
* everything). Defaults to `2`.
|
|
21
21
|
*/
|
|
22
22
|
turns?: number;
|
|
23
23
|
/**
|
|
24
|
-
* Optional
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Optional retained-recent token budget. Older turns are added whole only
|
|
25
|
+
* while cumulative tokens stay below the cap. If a tool-heavy history has
|
|
26
|
+
* no compactable turn-level head, this is also the minimum recent tail kept
|
|
27
|
+
* behind a pairing-balanced intra-turn cut. When omitted, that fallback
|
|
28
|
+
* retains 16% of the configured context window.
|
|
27
29
|
*/
|
|
28
30
|
tokens?: number;
|
|
29
31
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.1",
|
|
4
4
|
"reova": {
|
|
5
5
|
"enabled": true,
|
|
6
6
|
"endpoint": "https://telemetry.reo.dev/data"
|
|
@@ -218,7 +218,9 @@
|
|
|
218
218
|
"tool_search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search.ts",
|
|
219
219
|
"bench:cache": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-prompt-cache.ts",
|
|
220
220
|
"bench:context-pressure": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-context-pressure-cache.ts",
|
|
221
|
+
"bench:compaction-range": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-compaction-range.ts",
|
|
221
222
|
"bench:provider-derivation": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-provider-derivation.ts",
|
|
223
|
+
"bench:provider-projection": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-provider-request-projection.ts",
|
|
222
224
|
"bench:execution-world": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-execution-world.ts",
|
|
223
225
|
"probe:overflow": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/context-overflow-probe.ts",
|
|
224
226
|
"subagent": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-subagent.ts",
|
|
@@ -357,6 +357,8 @@ export class AgentContext {
|
|
|
357
357
|
* - `'none'`: no summary present
|
|
358
358
|
*/
|
|
359
359
|
private _summaryLocation: 'system_prompt' | 'user_message' | 'none' = 'none';
|
|
360
|
+
/** Whether a mid-run summary must appear before every retained message. */
|
|
361
|
+
private summaryPrecedesMessages: boolean = false;
|
|
360
362
|
/**
|
|
361
363
|
* Durable summary that survives reset() calls. Set from initialSummary
|
|
362
364
|
* during fromConfig() and updated by setSummary() so that the latest
|
|
@@ -365,6 +367,7 @@ export class AgentContext {
|
|
|
365
367
|
*/
|
|
366
368
|
private _durableSummaryText?: string;
|
|
367
369
|
private _durableSummaryTokenCount: number = 0;
|
|
370
|
+
private durableSummaryPrecedesMessages: boolean = false;
|
|
368
371
|
/** Number of summarization cycles that have occurred for this agent context */
|
|
369
372
|
private _summaryVersion: number = 0;
|
|
370
373
|
/**
|
|
@@ -989,10 +992,10 @@ export class AgentContext {
|
|
|
989
992
|
return messages;
|
|
990
993
|
}
|
|
991
994
|
|
|
992
|
-
const tailIndex =
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
995
|
+
const tailIndex =
|
|
996
|
+
this._summaryLocation === 'user_message' && this.summaryPrecedesMessages
|
|
997
|
+
? 0
|
|
998
|
+
: this.getPromptCacheDynamicTailIndex(messages, promptCacheProvider);
|
|
996
999
|
const stablePrefix = messages.slice(0, tailIndex);
|
|
997
1000
|
const trailingMessages = messages.slice(tailIndex);
|
|
998
1001
|
const cacheablePrefix = this.addStablePromptCacheMarkers(
|
|
@@ -1204,6 +1207,7 @@ export class AgentContext {
|
|
|
1204
1207
|
|
|
1205
1208
|
this.summaryText = this._durableSummaryText;
|
|
1206
1209
|
this.summaryTokenCount = this._durableSummaryTokenCount;
|
|
1210
|
+
this.summaryPrecedesMessages = this.durableSummaryPrecedesMessages;
|
|
1207
1211
|
this._lastSummarizationMsgCount = 0;
|
|
1208
1212
|
this.lastCallUsage = undefined;
|
|
1209
1213
|
this.totalTokensFresh = false;
|
|
@@ -1461,12 +1465,18 @@ export class AgentContext {
|
|
|
1461
1465
|
}
|
|
1462
1466
|
}
|
|
1463
1467
|
|
|
1464
|
-
setSummary(
|
|
1468
|
+
setSummary(
|
|
1469
|
+
text: string,
|
|
1470
|
+
tokenCount: number,
|
|
1471
|
+
options?: { precedesMessages?: boolean }
|
|
1472
|
+
): void {
|
|
1465
1473
|
this.summaryText = text;
|
|
1466
1474
|
this.summaryTokenCount = tokenCount;
|
|
1467
1475
|
this._summaryLocation = 'user_message';
|
|
1476
|
+
this.summaryPrecedesMessages = options?.precedesMessages === true;
|
|
1468
1477
|
this._durableSummaryText = text;
|
|
1469
1478
|
this._durableSummaryTokenCount = tokenCount;
|
|
1479
|
+
this.durableSummaryPrecedesMessages = this.summaryPrecedesMessages;
|
|
1470
1480
|
this._summaryVersion += 1;
|
|
1471
1481
|
this.systemRunnableStale = true;
|
|
1472
1482
|
this.pruneMessages = undefined;
|
|
@@ -1477,8 +1487,10 @@ export class AgentContext {
|
|
|
1477
1487
|
this.summaryText = text;
|
|
1478
1488
|
this.summaryTokenCount = tokenCount;
|
|
1479
1489
|
this._summaryLocation = 'system_prompt';
|
|
1490
|
+
this.summaryPrecedesMessages = false;
|
|
1480
1491
|
this._durableSummaryText = text;
|
|
1481
1492
|
this._durableSummaryTokenCount = tokenCount;
|
|
1493
|
+
this.durableSummaryPrecedesMessages = false;
|
|
1482
1494
|
this._summaryVersion += 1;
|
|
1483
1495
|
this.systemRunnableStale = true;
|
|
1484
1496
|
}
|
|
@@ -1683,6 +1695,8 @@ export class AgentContext {
|
|
|
1683
1695
|
this.summaryTokenCount = 0;
|
|
1684
1696
|
this._durableSummaryText = undefined;
|
|
1685
1697
|
this._durableSummaryTokenCount = 0;
|
|
1698
|
+
this.summaryPrecedesMessages = false;
|
|
1699
|
+
this.durableSummaryPrecedesMessages = false;
|
|
1686
1700
|
this._summaryLocation = 'none';
|
|
1687
1701
|
this.systemRunnableStale = true;
|
|
1688
1702
|
}
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
makeIsDeferred,
|
|
69
69
|
partitionAndMarkAnthropicToolCache,
|
|
70
70
|
DEFAULT_RETAIN_RECENT_TURNS,
|
|
71
|
+
resolveIntraTurnRetainTokens,
|
|
71
72
|
splitAtRecencyBoundary,
|
|
72
73
|
convertInjectedMessages,
|
|
73
74
|
coalesceAdjacentUserTurns,
|
|
@@ -3936,6 +3937,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3936
3937
|
*/
|
|
3937
3938
|
const estimatedPromptTokens = getEstimatedPromptTokens(contextUsage);
|
|
3938
3939
|
|
|
3940
|
+
const recencyTokenCounter =
|
|
3941
|
+
agentContext.contextPressureTokenCounts?.count ??
|
|
3942
|
+
agentContext.tokenCounter;
|
|
3939
3943
|
const canSummarizeOverflow =
|
|
3940
3944
|
agentContext.summarizationEnabled === true &&
|
|
3941
3945
|
splitAtRecencyBoundary(messages, {
|
|
@@ -3943,7 +3947,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3943
3947
|
agentContext.summarizationConfig?.retainRecent?.turns ??
|
|
3944
3948
|
DEFAULT_RETAIN_RECENT_TURNS,
|
|
3945
3949
|
tokens: agentContext.summarizationConfig?.retainRecent?.tokens,
|
|
3946
|
-
tokenCounter:
|
|
3950
|
+
tokenCounter: recencyTokenCounter,
|
|
3951
|
+
intraTurnTokens: resolveIntraTurnRetainTokens({
|
|
3952
|
+
tokens: agentContext.summarizationConfig?.retainRecent?.tokens,
|
|
3953
|
+
maxContextTokens: agentContext.maxContextTokens,
|
|
3954
|
+
}),
|
|
3947
3955
|
}).head.length > 0;
|
|
3948
3956
|
|
|
3949
3957
|
const getLocalProviderOverflowMeasurement = (
|
package/src/llm/invoke.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { concat } from '@langchain/core/utils/stream';
|
|
2
2
|
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
3
4
|
import { getCallbackManagerForConfig } from '@langchain/core/runnables';
|
|
4
5
|
import {
|
|
5
6
|
CallbackManager,
|
|
@@ -40,7 +41,12 @@ import { assertNotTruncatedToolCall } from '@/llm/truncation';
|
|
|
40
41
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
41
42
|
import { getContextOverflowInfo } from '@/utils/errors';
|
|
42
43
|
import { appendCallbacks } from '@/utils/callbacks';
|
|
43
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
inspectProviderMessageProjection,
|
|
46
|
+
ProviderMessageProjectionInvariantError,
|
|
47
|
+
resolveProviderMessageProjectionInvariantMode,
|
|
48
|
+
modifyDeltaProperties,
|
|
49
|
+
} from '@/messages';
|
|
44
50
|
import { canSealPreempt } from '@/llm/preempt';
|
|
45
51
|
import { initializeModel } from '@/llm/init';
|
|
46
52
|
|
|
@@ -106,6 +112,79 @@ export type OnChunk = (
|
|
|
106
112
|
/** Unique per-model-attempt sequence; see the stamp in `attemptInvoke`. */
|
|
107
113
|
let streamLimitAttemptSeq = 0;
|
|
108
114
|
|
|
115
|
+
function createModelStartHandler({
|
|
116
|
+
config,
|
|
117
|
+
mode,
|
|
118
|
+
provider,
|
|
119
|
+
captureRunId,
|
|
120
|
+
}: {
|
|
121
|
+
config: RunnableConfig;
|
|
122
|
+
mode: ReturnType<typeof resolveProviderMessageProjectionInvariantMode>;
|
|
123
|
+
provider: t.ProviderName;
|
|
124
|
+
captureRunId?: (runId: string) => void;
|
|
125
|
+
}): BaseCallbackHandler {
|
|
126
|
+
let inspected = false;
|
|
127
|
+
const handler = BaseCallbackHandler.fromMethods({
|
|
128
|
+
handleChatModelStart: async (
|
|
129
|
+
_llm: Serialized,
|
|
130
|
+
messageBatches: BaseMessage[][],
|
|
131
|
+
runId: string
|
|
132
|
+
): Promise<void> => {
|
|
133
|
+
captureRunId?.(runId);
|
|
134
|
+
if (mode === 'off' || inspected) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
inspected = true;
|
|
138
|
+
const report = inspectProviderMessageProjection(messageBatches[0] ?? []);
|
|
139
|
+
if (report.valid) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (mode === 'assert') {
|
|
143
|
+
throw new ProviderMessageProjectionInvariantError(report);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const callbackManager = await getCallbackManagerForConfig(config);
|
|
147
|
+
await callbackManager?.handleCustomEvent?.(
|
|
148
|
+
GraphEvents.ON_AGENT_LOG,
|
|
149
|
+
{
|
|
150
|
+
level: 'warn',
|
|
151
|
+
scope: 'projection',
|
|
152
|
+
message: 'Provider message projection has provenance gaps',
|
|
153
|
+
data: { provider, report },
|
|
154
|
+
runId,
|
|
155
|
+
} satisfies t.AgentLogEvent,
|
|
156
|
+
runId
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
handler.name = 'provider-message-projection-invariant';
|
|
164
|
+
handler.raiseError = mode === 'assert';
|
|
165
|
+
handler.awaitHandlers = true;
|
|
166
|
+
return handler;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function withModelStartHandler({
|
|
170
|
+
config,
|
|
171
|
+
mode,
|
|
172
|
+
provider,
|
|
173
|
+
captureRunId,
|
|
174
|
+
}: {
|
|
175
|
+
config: RunnableConfig;
|
|
176
|
+
mode: ReturnType<typeof resolveProviderMessageProjectionInvariantMode>;
|
|
177
|
+
provider: t.ProviderName;
|
|
178
|
+
captureRunId?: (runId: string) => void;
|
|
179
|
+
}): RunnableConfig {
|
|
180
|
+
return {
|
|
181
|
+
...config,
|
|
182
|
+
callbacks: appendCallbacks(config.callbacks, [
|
|
183
|
+
createModelStartHandler({ config, mode, provider, captureRunId }),
|
|
184
|
+
]),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
109
188
|
function getManualToolStreamNormalizationProvider(
|
|
110
189
|
provider: t.ProviderName
|
|
111
190
|
): t.ProviderName {
|
|
@@ -632,6 +711,24 @@ async function attemptInvokeBody(
|
|
|
632
711
|
config: RunnableConfig
|
|
633
712
|
): Promise<Partial<t.BaseGraphState>> {
|
|
634
713
|
const { model, messages: messagesForProvider, provider } = request;
|
|
714
|
+
const projectionInvariantMode =
|
|
715
|
+
resolveProviderMessageProjectionInvariantMode();
|
|
716
|
+
let sealedRunId: string | undefined;
|
|
717
|
+
let invocationConfig = config;
|
|
718
|
+
const captureModelRunId =
|
|
719
|
+
model.stream != null && context?.preemption != null;
|
|
720
|
+
if (projectionInvariantMode !== 'off' || captureModelRunId) {
|
|
721
|
+
invocationConfig = withModelStartHandler({
|
|
722
|
+
config,
|
|
723
|
+
mode: projectionInvariantMode,
|
|
724
|
+
provider,
|
|
725
|
+
captureRunId: captureModelRunId
|
|
726
|
+
? (runId: string): void => {
|
|
727
|
+
sealedRunId ??= runId;
|
|
728
|
+
}
|
|
729
|
+
: undefined,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
635
732
|
|
|
636
733
|
/**
|
|
637
734
|
* Stamp the provider that is ACTUALLY serving this invocation onto the
|
|
@@ -647,28 +744,10 @@ async function attemptInvokeBody(
|
|
|
647
744
|
* Observed, not dictated. `handleChatModelStart` fires with the chat
|
|
648
745
|
* model's real run id before the first chunk, which is the only way to
|
|
649
746
|
* name the run a seal has to close — pinning `config.runId` does not
|
|
650
|
-
* survive the bound runnable.
|
|
651
|
-
*
|
|
747
|
+
* survive the bound runnable. The same handler owns the opt-in projection
|
|
748
|
+
* invariant so enabled diagnostics do not stack a second model callback.
|
|
652
749
|
*/
|
|
653
|
-
|
|
654
|
-
const streamConfig =
|
|
655
|
-
context?.preemption == null
|
|
656
|
-
? config
|
|
657
|
-
: {
|
|
658
|
-
...config,
|
|
659
|
-
callbacks: appendCallbacks(config.callbacks, [
|
|
660
|
-
{
|
|
661
|
-
handleChatModelStart: (
|
|
662
|
-
_llm: Serialized,
|
|
663
|
-
_messages: BaseMessage[][],
|
|
664
|
-
runId: string
|
|
665
|
-
): void => {
|
|
666
|
-
sealedRunId ??= runId;
|
|
667
|
-
},
|
|
668
|
-
},
|
|
669
|
-
]),
|
|
670
|
-
};
|
|
671
|
-
const stream = await model.stream(messagesForProvider, streamConfig);
|
|
750
|
+
const stream = await model.stream(messagesForProvider, invocationConfig);
|
|
672
751
|
let finalChunk: AIMessageChunk | undefined;
|
|
673
752
|
let preempted = false;
|
|
674
753
|
const registeredStreamHandler =
|
|
@@ -855,7 +934,7 @@ async function attemptInvokeBody(
|
|
|
855
934
|
return { messages: [finalChunk as AIMessageChunk] };
|
|
856
935
|
}
|
|
857
936
|
|
|
858
|
-
const finalMessage = await model.invoke(messagesForProvider,
|
|
937
|
+
const finalMessage = await model.invoke(messagesForProvider, invocationConfig);
|
|
859
938
|
if ((finalMessage.tool_calls?.length ?? 0) > 0) {
|
|
860
939
|
finalMessage.tool_calls = finalMessage.tool_calls?.filter(
|
|
861
940
|
(tool_call: ToolCall) => !!tool_call.name
|
package/src/messages/content.ts
CHANGED
|
@@ -22,6 +22,20 @@ export const isLegacyConvertible = (message: BaseMessage): boolean => {
|
|
|
22
22
|
return message.content.every((block) => block.type === ContentTypes.TEXT);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/** Joins the text of {@link isLegacyConvertible} content blocks into the exact
|
|
26
|
+
* string {@link formatContentStrings} has always produced for them. */
|
|
27
|
+
export const flattenLegacyContent = (
|
|
28
|
+
blocks: MessageContentComplex[]
|
|
29
|
+
): string => {
|
|
30
|
+
const content = blocks.reduce((acc, curr) => {
|
|
31
|
+
if (curr.type === ContentTypes.TEXT) {
|
|
32
|
+
return `${acc}${curr[ContentTypes.TEXT] ?? ''}\n`;
|
|
33
|
+
}
|
|
34
|
+
return acc;
|
|
35
|
+
}, '');
|
|
36
|
+
return content.trim();
|
|
37
|
+
};
|
|
38
|
+
|
|
25
39
|
/**
|
|
26
40
|
* Formats an array of messages for LangChain, making sure all content fields are strings
|
|
27
41
|
* @param {Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>} payload - The array of messages to format.
|
|
@@ -39,16 +53,12 @@ export const formatContentStrings = (
|
|
|
39
53
|
continue;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return acc;
|
|
49
|
-
}, '');
|
|
50
|
-
|
|
51
|
-
result.push(cloneMessage(message, content.trim()));
|
|
56
|
+
result.push(
|
|
57
|
+
cloneMessage(
|
|
58
|
+
message,
|
|
59
|
+
flattenLegacyContent(message.content as MessageContentComplex[])
|
|
60
|
+
)
|
|
61
|
+
);
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
return result;
|