@librechat/agents 3.1.67-dev.4 → 3.1.68
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 +3 -23
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +0 -16
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +0 -91
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -53
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +12 -74
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +0 -111
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/index.cjs +41 -0
- package/dist/cjs/summarization/index.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +121 -63
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +140 -304
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +3 -23
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -15
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +0 -91
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -13
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +4 -66
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +0 -111
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/index.mjs +41 -1
- package/dist/esm/summarization/index.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +121 -63
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +142 -306
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +0 -6
- package/dist/types/common/enum.d.ts +1 -10
- package/dist/types/graphs/Graph.d.ts +0 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +12 -0
- package/dist/types/index.d.ts +0 -8
- package/dist/types/messages/format.d.ts +1 -2
- package/dist/types/run.d.ts +0 -1
- package/dist/types/summarization/index.d.ts +2 -0
- package/dist/types/summarization/node.d.ts +0 -2
- package/dist/types/tools/ToolNode.d.ts +2 -24
- package/dist/types/types/graph.d.ts +2 -61
- package/dist/types/types/index.d.ts +0 -1
- package/dist/types/types/run.d.ts +0 -20
- package/dist/types/types/tools.d.ts +1 -38
- package/package.json +1 -5
- package/src/agents/AgentContext.ts +2 -26
- package/src/common/enum.ts +0 -15
- package/src/graphs/Graph.ts +0 -113
- package/src/graphs/MultiAgentGraph.ts +39 -0
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -0
- package/src/index.ts +0 -10
- package/src/messages/format.ts +4 -74
- package/src/run.ts +0 -126
- package/src/summarization/__tests__/node.test.ts +42 -0
- package/src/summarization/__tests__/trigger.test.ts +100 -1
- package/src/summarization/index.ts +47 -0
- package/src/summarization/node.ts +149 -77
- package/src/tools/ToolNode.ts +169 -391
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/types/graph.ts +1 -80
- package/src/types/index.ts +0 -1
- package/src/types/run.ts +0 -20
- package/src/types/tools.ts +1 -41
- package/dist/cjs/hooks/HookRegistry.cjs +0 -162
- package/dist/cjs/hooks/HookRegistry.cjs.map +0 -1
- package/dist/cjs/hooks/executeHooks.cjs +0 -276
- package/dist/cjs/hooks/executeHooks.cjs.map +0 -1
- package/dist/cjs/hooks/matchers.cjs +0 -256
- package/dist/cjs/hooks/matchers.cjs.map +0 -1
- package/dist/cjs/hooks/types.cjs +0 -27
- package/dist/cjs/hooks/types.cjs.map +0 -1
- package/dist/cjs/tools/BashExecutor.cjs +0 -175
- package/dist/cjs/tools/BashExecutor.cjs.map +0 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -296
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +0 -1
- package/dist/cjs/tools/ReadFile.cjs +0 -43
- package/dist/cjs/tools/ReadFile.cjs.map +0 -1
- package/dist/cjs/tools/SkillTool.cjs +0 -50
- package/dist/cjs/tools/SkillTool.cjs.map +0 -1
- package/dist/cjs/tools/SubagentTool.cjs +0 -92
- package/dist/cjs/tools/SubagentTool.cjs.map +0 -1
- package/dist/cjs/tools/skillCatalog.cjs +0 -84
- package/dist/cjs/tools/skillCatalog.cjs.map +0 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +0 -511
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +0 -1
- package/dist/esm/hooks/HookRegistry.mjs +0 -160
- package/dist/esm/hooks/HookRegistry.mjs.map +0 -1
- package/dist/esm/hooks/executeHooks.mjs +0 -273
- package/dist/esm/hooks/executeHooks.mjs.map +0 -1
- package/dist/esm/hooks/matchers.mjs +0 -251
- package/dist/esm/hooks/matchers.mjs.map +0 -1
- package/dist/esm/hooks/types.mjs +0 -25
- package/dist/esm/hooks/types.mjs.map +0 -1
- package/dist/esm/tools/BashExecutor.mjs +0 -169
- package/dist/esm/tools/BashExecutor.mjs.map +0 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -287
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +0 -1
- package/dist/esm/tools/ReadFile.mjs +0 -38
- package/dist/esm/tools/ReadFile.mjs.map +0 -1
- package/dist/esm/tools/SkillTool.mjs +0 -45
- package/dist/esm/tools/SkillTool.mjs.map +0 -1
- package/dist/esm/tools/SubagentTool.mjs +0 -85
- package/dist/esm/tools/SubagentTool.mjs.map +0 -1
- package/dist/esm/tools/skillCatalog.mjs +0 -82
- package/dist/esm/tools/skillCatalog.mjs.map +0 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +0 -505
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +0 -1
- package/dist/types/hooks/HookRegistry.d.ts +0 -56
- package/dist/types/hooks/executeHooks.d.ts +0 -79
- package/dist/types/hooks/index.d.ts +0 -6
- package/dist/types/hooks/matchers.d.ts +0 -95
- package/dist/types/hooks/types.d.ts +0 -320
- package/dist/types/tools/BashExecutor.d.ts +0 -45
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +0 -72
- package/dist/types/tools/ReadFile.d.ts +0 -28
- package/dist/types/tools/SkillTool.d.ts +0 -40
- package/dist/types/tools/SubagentTool.d.ts +0 -36
- package/dist/types/tools/skillCatalog.d.ts +0 -19
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +0 -137
- package/dist/types/tools/subagent/index.d.ts +0 -2
- package/dist/types/types/skill.d.ts +0 -9
- package/src/hooks/HookRegistry.ts +0 -208
- package/src/hooks/__tests__/HookRegistry.test.ts +0 -190
- package/src/hooks/__tests__/compactHooks.test.ts +0 -214
- package/src/hooks/__tests__/executeHooks.test.ts +0 -1013
- package/src/hooks/__tests__/integration.test.ts +0 -337
- package/src/hooks/__tests__/matchers.test.ts +0 -238
- package/src/hooks/__tests__/toolHooks.test.ts +0 -669
- package/src/hooks/executeHooks.ts +0 -375
- package/src/hooks/index.ts +0 -57
- package/src/hooks/matchers.ts +0 -280
- package/src/hooks/types.ts +0 -404
- package/src/messages/formatAgentMessages.skills.test.ts +0 -334
- package/src/scripts/multi-agent-subagent.ts +0 -246
- package/src/scripts/subagent-event-driven-debug.ts +0 -190
- package/src/scripts/subagent-tools-debug.ts +0 -160
- package/src/specs/subagent.test.ts +0 -305
- package/src/tools/BashExecutor.ts +0 -205
- package/src/tools/BashProgrammaticToolCalling.ts +0 -397
- package/src/tools/ReadFile.ts +0 -39
- package/src/tools/SkillTool.ts +0 -46
- package/src/tools/SubagentTool.ts +0 -100
- package/src/tools/__tests__/ReadFile.test.ts +0 -44
- package/src/tools/__tests__/SkillTool.test.ts +0 -442
- package/src/tools/__tests__/SubagentExecutor.test.ts +0 -1148
- package/src/tools/__tests__/SubagentTool.test.ts +0 -149
- package/src/tools/__tests__/skillCatalog.test.ts +0 -161
- package/src/tools/__tests__/subagentHooks.test.ts +0 -215
- package/src/tools/skillCatalog.ts +0 -126
- package/src/tools/subagent/SubagentExecutor.ts +0 -676
- package/src/tools/subagent/index.ts +0 -13
- package/src/types/skill.ts +0 -11
package/src/run.ts
CHANGED
|
@@ -22,10 +22,8 @@ import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
|
|
|
22
22
|
import { StandardGraph } from '@/graphs/Graph';
|
|
23
23
|
import { initializeModel } from '@/llm/init';
|
|
24
24
|
import { HandlerRegistry } from '@/events';
|
|
25
|
-
import { executeHooks } from '@/hooks';
|
|
26
25
|
import { isOpenAILike } from '@/utils/llm';
|
|
27
26
|
import { isPresent } from '@/utils/misc';
|
|
28
|
-
import type { HookRegistry } from '@/hooks';
|
|
29
27
|
|
|
30
28
|
export const defaultOmitOptions = new Set([
|
|
31
29
|
'stream',
|
|
@@ -44,7 +42,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
44
42
|
id: string;
|
|
45
43
|
private tokenCounter?: t.TokenCounter;
|
|
46
44
|
private handlerRegistry?: HandlerRegistry;
|
|
47
|
-
private hookRegistry?: HookRegistry;
|
|
48
45
|
private indexTokenCountMap?: Record<string, number>;
|
|
49
46
|
calibrationRatio: number = 1;
|
|
50
47
|
graphRunnable?: t.CompiledStateWorkflow;
|
|
@@ -77,7 +74,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
this.handlerRegistry = handlerRegistry;
|
|
80
|
-
this.hookRegistry = config.hooks;
|
|
81
77
|
|
|
82
78
|
if (!config.graphConfig) {
|
|
83
79
|
throw new Error('Graph config not provided');
|
|
@@ -99,12 +95,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
99
95
|
}
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
if (config.initialSessions && this.Graph) {
|
|
103
|
-
for (const [key, value] of config.initialSessions) {
|
|
104
|
-
this.Graph.sessions.set(key, value);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
98
|
this.returnContent = config.returnContent ?? false;
|
|
109
99
|
this.skipCleanup = config.skipCleanup ?? false;
|
|
110
100
|
}
|
|
@@ -153,7 +143,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
153
143
|
});
|
|
154
144
|
/** Propagate compile options from graph config */
|
|
155
145
|
standardGraph.compileOptions = config.compileOptions;
|
|
156
|
-
standardGraph.hookRegistry = this.hookRegistry;
|
|
157
146
|
this.Graph = standardGraph;
|
|
158
147
|
return standardGraph.createWorkflow();
|
|
159
148
|
}
|
|
@@ -176,7 +165,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
176
165
|
multiAgentGraph.compileOptions = compileOptions;
|
|
177
166
|
}
|
|
178
167
|
|
|
179
|
-
multiAgentGraph.hookRegistry = this.hookRegistry;
|
|
180
168
|
this.Graph = multiAgentGraph;
|
|
181
169
|
return multiAgentGraph.createWorkflow();
|
|
182
170
|
}
|
|
@@ -344,47 +332,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
344
332
|
run_id: this.id,
|
|
345
333
|
});
|
|
346
334
|
|
|
347
|
-
const threadId = config.configurable.thread_id as string | undefined;
|
|
348
|
-
|
|
349
|
-
if (this.hookRegistry != null) {
|
|
350
|
-
await executeHooks({
|
|
351
|
-
registry: this.hookRegistry,
|
|
352
|
-
input: {
|
|
353
|
-
hook_event_name: 'RunStart',
|
|
354
|
-
runId: this.id,
|
|
355
|
-
threadId,
|
|
356
|
-
agentId: this.Graph.defaultAgentId,
|
|
357
|
-
messages: inputs.messages,
|
|
358
|
-
},
|
|
359
|
-
sessionId: this.id,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const lastHuman = findLastMessageOfType(inputs.messages, 'human');
|
|
363
|
-
if (lastHuman != null) {
|
|
364
|
-
const promptResult = await executeHooks({
|
|
365
|
-
registry: this.hookRegistry,
|
|
366
|
-
input: {
|
|
367
|
-
hook_event_name: 'UserPromptSubmit',
|
|
368
|
-
runId: this.id,
|
|
369
|
-
threadId,
|
|
370
|
-
agentId: this.Graph.defaultAgentId,
|
|
371
|
-
prompt: extractPromptText(lastHuman),
|
|
372
|
-
// attachments: not yet wired — Phase 2 will extract
|
|
373
|
-
// non-text content blocks (images, files) from messages
|
|
374
|
-
},
|
|
375
|
-
sessionId: this.id,
|
|
376
|
-
});
|
|
377
|
-
if (
|
|
378
|
-
promptResult.decision === 'deny' ||
|
|
379
|
-
promptResult.decision === 'ask'
|
|
380
|
-
) {
|
|
381
|
-
this.hookRegistry.clearSession(this.id);
|
|
382
|
-
config.callbacks = undefined;
|
|
383
|
-
return undefined;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
335
|
const stream = this.graphRunnable.streamEvents(inputs, config, {
|
|
389
336
|
raiseError: true,
|
|
390
337
|
/**
|
|
@@ -414,45 +361,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
414
361
|
await handler.handle(eventName, data, metadata, this.Graph);
|
|
415
362
|
}
|
|
416
363
|
}
|
|
417
|
-
|
|
418
|
-
if (this.hookRegistry?.hasHookFor('Stop', this.id) === true) {
|
|
419
|
-
await executeHooks({
|
|
420
|
-
registry: this.hookRegistry,
|
|
421
|
-
input: {
|
|
422
|
-
hook_event_name: 'Stop',
|
|
423
|
-
runId: this.id,
|
|
424
|
-
threadId,
|
|
425
|
-
agentId: this.Graph.defaultAgentId,
|
|
426
|
-
messages: this.Graph.getRunMessages() ?? inputs.messages,
|
|
427
|
-
stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
|
|
428
|
-
},
|
|
429
|
-
sessionId: this.id,
|
|
430
|
-
}).catch(() => {
|
|
431
|
-
/* Stop hook errors must not masquerade as stream failures */
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
} catch (err) {
|
|
435
|
-
if (this.hookRegistry?.hasHookFor('StopFailure', this.id) === true) {
|
|
436
|
-
const runMessages = this.Graph.getRunMessages() ?? [];
|
|
437
|
-
await executeHooks({
|
|
438
|
-
registry: this.hookRegistry,
|
|
439
|
-
input: {
|
|
440
|
-
hook_event_name: 'StopFailure',
|
|
441
|
-
runId: this.id,
|
|
442
|
-
threadId,
|
|
443
|
-
agentId: this.Graph.defaultAgentId,
|
|
444
|
-
error: err instanceof Error ? err.message : String(err),
|
|
445
|
-
lastAssistantMessage: findLastMessageOfType(runMessages, 'ai'),
|
|
446
|
-
},
|
|
447
|
-
sessionId: this.id,
|
|
448
|
-
}).catch(() => {
|
|
449
|
-
/* swallow hook errors — the original error must propagate */
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
throw err;
|
|
453
364
|
} finally {
|
|
454
|
-
this.hookRegistry?.clearSession(this.id);
|
|
455
|
-
|
|
456
365
|
/**
|
|
457
366
|
* Break the reference chain that keeps heavy data alive via
|
|
458
367
|
* LangGraph's internal `__pregel_scratchpad.currentTaskInput` →
|
|
@@ -648,38 +557,3 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
648
557
|
}
|
|
649
558
|
}
|
|
650
559
|
}
|
|
651
|
-
|
|
652
|
-
function findLastMessageOfType(
|
|
653
|
-
messages: BaseMessage[],
|
|
654
|
-
type: string
|
|
655
|
-
): BaseMessage | undefined {
|
|
656
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
657
|
-
if (messages[i].getType() === type) {
|
|
658
|
-
return messages[i];
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
return undefined;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function extractPromptText(message: BaseMessage): string {
|
|
665
|
-
const content = message.content;
|
|
666
|
-
if (typeof content === 'string') {
|
|
667
|
-
return content;
|
|
668
|
-
}
|
|
669
|
-
if (!Array.isArray(content)) {
|
|
670
|
-
return String(content);
|
|
671
|
-
}
|
|
672
|
-
const parts: string[] = [];
|
|
673
|
-
for (const block of content) {
|
|
674
|
-
if (
|
|
675
|
-
typeof block === 'object' &&
|
|
676
|
-
'type' in block &&
|
|
677
|
-
block.type === 'text' &&
|
|
678
|
-
'text' in block &&
|
|
679
|
-
typeof block.text === 'string'
|
|
680
|
-
) {
|
|
681
|
-
parts.push(block.text);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return parts.join('\n');
|
|
685
|
-
}
|
|
@@ -343,6 +343,48 @@ describe('createSummarizeNode', () => {
|
|
|
343
343
|
).toBeUndefined();
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
it('catches model initialization errors and falls back to metadata stub', async () => {
|
|
347
|
+
captureEvents();
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Simulate the "Unsupported LLM provider" case — e.g. when a caller
|
|
351
|
+
* forwards an unrecognized provider name (custom-endpoint label) that
|
|
352
|
+
* getChatModelClass cannot resolve. Prior to the defense-in-depth fix,
|
|
353
|
+
* this error was thrown outside the try/catch in executeSummarizationWithFallback
|
|
354
|
+
* and bubbled up silently. Now it is caught and the metadata stub is used.
|
|
355
|
+
*/
|
|
356
|
+
jest.spyOn(providers, 'getChatModelClass').mockImplementation(() => {
|
|
357
|
+
throw new Error('Unsupported LLM provider: Ollama');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const setSummary = jest.fn();
|
|
361
|
+
const agentContext = createAgentContext({ setSummary } as never);
|
|
362
|
+
const graph = mockGraph();
|
|
363
|
+
const node = createSummarizeNode({
|
|
364
|
+
agentContext,
|
|
365
|
+
graph,
|
|
366
|
+
generateStepId,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await expect(
|
|
370
|
+
node(
|
|
371
|
+
{
|
|
372
|
+
messages: [new HumanMessage('Test message')],
|
|
373
|
+
summarizationRequest: {
|
|
374
|
+
remainingContextTokens: 1000,
|
|
375
|
+
agentId: 'agent_0',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{} as RunnableConfig
|
|
379
|
+
)
|
|
380
|
+
).resolves.not.toThrow();
|
|
381
|
+
|
|
382
|
+
expect(setSummary).toHaveBeenCalledWith(
|
|
383
|
+
expect.stringContaining('[Metadata summary:'),
|
|
384
|
+
expect.any(Number)
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
346
388
|
it('falls back to metadata stub when primary LLM call fails', async () => {
|
|
347
389
|
captureEvents();
|
|
348
390
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
shouldTriggerSummarization,
|
|
3
|
+
_resetUnrecognizedTriggerWarnings,
|
|
4
|
+
} from '@/summarization';
|
|
2
5
|
|
|
3
6
|
describe('shouldTriggerSummarization', () => {
|
|
4
7
|
it('uses pre-prune pressure for token_ratio triggers when messages were pruned', () => {
|
|
@@ -47,4 +50,100 @@ describe('shouldTriggerSummarization', () => {
|
|
|
47
50
|
|
|
48
51
|
expect(result).toBe(false);
|
|
49
52
|
});
|
|
53
|
+
|
|
54
|
+
describe('unrecognized trigger type', () => {
|
|
55
|
+
let warnSpy: jest.SpyInstance;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
_resetUnrecognizedTriggerWarnings();
|
|
59
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
warnSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not fire and warns once per unrecognized type', () => {
|
|
67
|
+
const baseParams = {
|
|
68
|
+
maxContextTokens: 2500,
|
|
69
|
+
prePruneContextTokens: 2400,
|
|
70
|
+
remainingContextTokens: 100,
|
|
71
|
+
messagesToRefineCount: 4,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Cast via `unknown` because the type union guards against this at compile
|
|
75
|
+
// time; we are intentionally exercising the runtime fallback.
|
|
76
|
+
const result1 = shouldTriggerSummarization({
|
|
77
|
+
...baseParams,
|
|
78
|
+
trigger: { type: 'token_count', value: 8000 } as unknown as {
|
|
79
|
+
type: 'token_ratio';
|
|
80
|
+
value: number;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result1).toBe(false);
|
|
85
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(warnSpy.mock.calls[0][0]).toContain('token_count');
|
|
87
|
+
expect(warnSpy.mock.calls[0][0]).toContain('token_ratio');
|
|
88
|
+
expect(warnSpy.mock.calls[0][0]).toContain('remaining_tokens');
|
|
89
|
+
expect(warnSpy.mock.calls[0][0]).toContain('messages_to_refine');
|
|
90
|
+
|
|
91
|
+
// Same unrecognized type a second time: no duplicate warning.
|
|
92
|
+
shouldTriggerSummarization({
|
|
93
|
+
...baseParams,
|
|
94
|
+
trigger: { type: 'token_count', value: 8000 } as unknown as {
|
|
95
|
+
type: 'token_ratio';
|
|
96
|
+
value: number;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
100
|
+
|
|
101
|
+
// Different unrecognized type: warns again, once.
|
|
102
|
+
shouldTriggerSummarization({
|
|
103
|
+
...baseParams,
|
|
104
|
+
trigger: { type: 'nonsense', value: 1 } as unknown as {
|
|
105
|
+
type: 'token_ratio';
|
|
106
|
+
value: number;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(warnSpy.mock.calls[1][0]).toContain('nonsense');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not grow memory unboundedly under a flood of unique types', () => {
|
|
114
|
+
const baseParams = {
|
|
115
|
+
maxContextTokens: 2500,
|
|
116
|
+
prePruneContextTokens: 2400,
|
|
117
|
+
remainingContextTokens: 100,
|
|
118
|
+
messagesToRefineCount: 4,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < 500; i++) {
|
|
122
|
+
shouldTriggerSummarization({
|
|
123
|
+
...baseParams,
|
|
124
|
+
trigger: { type: `bogus-${i}`, value: 1 } as unknown as {
|
|
125
|
+
type: 'token_ratio';
|
|
126
|
+
value: number;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Still logged each new type (up to the cap) — we never silently dropped
|
|
132
|
+
// warnings; we just evicted oldest entries from the dedup set.
|
|
133
|
+
expect(warnSpy).toHaveBeenCalledTimes(500);
|
|
134
|
+
|
|
135
|
+
// Re-warns for a recently-seen type that should still be in the cache
|
|
136
|
+
// (last one just inserted). No duplicate warning means the dedup set
|
|
137
|
+
// still functions; the size cap did not break the dedup contract.
|
|
138
|
+
const beforeRecent = warnSpy.mock.calls.length;
|
|
139
|
+
shouldTriggerSummarization({
|
|
140
|
+
...baseParams,
|
|
141
|
+
trigger: { type: 'bogus-499', value: 1 } as unknown as {
|
|
142
|
+
type: 'token_ratio';
|
|
143
|
+
value: number;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
expect(warnSpy).toHaveBeenCalledTimes(beforeRecent);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
50
149
|
});
|
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
import type { SummarizationTrigger } from '@/types';
|
|
2
2
|
|
|
3
|
+
const VALID_TRIGGER_TYPES = [
|
|
4
|
+
'token_ratio',
|
|
5
|
+
'remaining_tokens',
|
|
6
|
+
'messages_to_refine',
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Upper bound on the dedup set for unrecognized trigger types. Bounds memory in
|
|
11
|
+
* case a caller threads dynamic/user-provided strings through `trigger.type`.
|
|
12
|
+
* Well above the handful of legit misconfigurations a process would ever see.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_WARNED_TRIGGER_TYPES = 32;
|
|
15
|
+
|
|
16
|
+
const warnedUnrecognizedTriggerTypes = new Set<string>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Warn (once per process, per unrecognized type) when the configured trigger
|
|
20
|
+
* type is something the runtime does not evaluate. Without this, a misconfigured
|
|
21
|
+
* `trigger.type` silently disables summarization with no visible signal.
|
|
22
|
+
*
|
|
23
|
+
* The dedup set is size-capped; on overflow we evict the oldest entry (Set
|
|
24
|
+
* preserves insertion order) so we keep bounded memory and still warn on
|
|
25
|
+
* recently-seen types.
|
|
26
|
+
*/
|
|
27
|
+
function warnUnrecognizedTriggerType(type: string): void {
|
|
28
|
+
if (warnedUnrecognizedTriggerTypes.has(type)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (warnedUnrecognizedTriggerTypes.size >= MAX_WARNED_TRIGGER_TYPES) {
|
|
32
|
+
const oldest = warnedUnrecognizedTriggerTypes.values().next().value;
|
|
33
|
+
if (oldest !== undefined) {
|
|
34
|
+
warnedUnrecognizedTriggerTypes.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
warnedUnrecognizedTriggerTypes.add(type);
|
|
38
|
+
console.warn(
|
|
39
|
+
`[shouldTriggerSummarization] Unrecognized trigger.type: "${type}". ` +
|
|
40
|
+
`Summarization will not fire. Valid values: ${VALID_TRIGGER_TYPES.join(', ')}.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** For tests only. Resets the dedup set so warnings can be observed again. */
|
|
45
|
+
export function _resetUnrecognizedTriggerWarnings(): void {
|
|
46
|
+
warnedUnrecognizedTriggerTypes.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
3
49
|
/**
|
|
4
50
|
* Determines whether summarization should be triggered based on the configured trigger
|
|
5
51
|
* and current context state.
|
|
@@ -98,5 +144,6 @@ export function shouldTriggerSummarization(params: {
|
|
|
98
144
|
}
|
|
99
145
|
|
|
100
146
|
// Unrecognized trigger type: cannot evaluate, do not fire.
|
|
147
|
+
warnUnrecognizedTriggerType(trigger.type);
|
|
101
148
|
return false;
|
|
102
149
|
}
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
8
8
|
import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
|
|
9
9
|
import type { AgentContext } from '@/agents/AgentContext';
|
|
10
|
-
import type { HookRegistry } from '@/hooks';
|
|
11
10
|
import type { OnChunk } from '@/llm/invoke';
|
|
12
11
|
import type * as t from '@/types';
|
|
13
12
|
import { ContentTypes, GraphEvents, StepTypes, Providers } from '@/common';
|
|
@@ -18,7 +17,6 @@ import { getMaxOutputTokensKey } from '@/llm/request';
|
|
|
18
17
|
import { addCacheControl } from '@/messages/cache';
|
|
19
18
|
import { initializeModel } from '@/llm/init';
|
|
20
19
|
import { getChunkContent } from '@/stream';
|
|
21
|
-
import { executeHooks } from '@/hooks';
|
|
22
20
|
|
|
23
21
|
const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
|
|
24
22
|
|
|
@@ -366,6 +364,122 @@ type LogFn = (
|
|
|
366
364
|
data?: Record<string, unknown>
|
|
367
365
|
) => void;
|
|
368
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Extracts an HTTP status code from a thrown LLM-provider error. Returns
|
|
369
|
+
* `undefined` for non-object values (including `null` or `undefined`, both
|
|
370
|
+
* valid `throw` targets in JS) so callers never dereference a nullish
|
|
371
|
+
* value.
|
|
372
|
+
*/
|
|
373
|
+
function extractHttpStatus(err: unknown): number | undefined {
|
|
374
|
+
if (err == null || typeof err !== 'object') {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const errRecord = err as Record<string, unknown>;
|
|
378
|
+
const direct = errRecord.status;
|
|
379
|
+
if (typeof direct === 'number') {
|
|
380
|
+
return direct;
|
|
381
|
+
}
|
|
382
|
+
const statusCode = errRecord.statusCode;
|
|
383
|
+
if (typeof statusCode === 'number') {
|
|
384
|
+
return statusCode;
|
|
385
|
+
}
|
|
386
|
+
const response = errRecord.response;
|
|
387
|
+
if (response != null && typeof response === 'object') {
|
|
388
|
+
const nested = (response as Record<string, unknown>).status;
|
|
389
|
+
if (typeof nested === 'number') {
|
|
390
|
+
return nested;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Formats a provider-level error for logging. Returns both a human-readable
|
|
398
|
+
* suffix (safe to include in the message string so it survives any host-side
|
|
399
|
+
* formatter) and a structured metadata bag for rich log backends.
|
|
400
|
+
*/
|
|
401
|
+
function describeProviderError(
|
|
402
|
+
err: unknown,
|
|
403
|
+
provider: string,
|
|
404
|
+
modelName?: string
|
|
405
|
+
): { suffix: string; data: Record<string, unknown> } {
|
|
406
|
+
const providerLabel = `${provider}/${modelName ?? '(no-model)'}`;
|
|
407
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
408
|
+
|
|
409
|
+
const data: Record<string, unknown> = {
|
|
410
|
+
provider,
|
|
411
|
+
model: modelName,
|
|
412
|
+
};
|
|
413
|
+
if (err instanceof Error) {
|
|
414
|
+
data.errorName = err.name;
|
|
415
|
+
data.errorStack = err.stack;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const status = extractHttpStatus(err);
|
|
419
|
+
const statusSuffix = status != null ? ` (HTTP ${status})` : '';
|
|
420
|
+
if (status != null) {
|
|
421
|
+
data.status = status;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
suffix: `[${providerLabel}]${statusSuffix}: ${errMsg}`,
|
|
426
|
+
data,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Formats an exhausted-fallback error. `tryFallbackProviders` throws the
|
|
432
|
+
* last fallback provider's error, which may be from any of the configured
|
|
433
|
+
* fallbacks — not the primary — so we label the log with the list of
|
|
434
|
+
* fallback providers attempted rather than mis-attributing to the primary.
|
|
435
|
+
*
|
|
436
|
+
* Entries in `fallbacks` are normally strongly typed, but we defend against
|
|
437
|
+
* malformed runtime config (null/undefined entries, missing `provider`
|
|
438
|
+
* field) so a recoverable summarization failure is never promoted to an
|
|
439
|
+
* uncaught exception from inside the logging path.
|
|
440
|
+
*/
|
|
441
|
+
function describeFallbackError(
|
|
442
|
+
err: unknown,
|
|
443
|
+
fallbacks: unknown
|
|
444
|
+
): { suffix: string; data: Record<string, unknown> } {
|
|
445
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
446
|
+
const list: ReadonlyArray<unknown> = Array.isArray(fallbacks)
|
|
447
|
+
? fallbacks
|
|
448
|
+
: [];
|
|
449
|
+
const providerNames = list
|
|
450
|
+
.map((f) => {
|
|
451
|
+
if (f == null || typeof f !== 'object') {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
const raw = (f as { provider?: unknown }).provider;
|
|
455
|
+
return raw != null ? String(raw) : undefined;
|
|
456
|
+
})
|
|
457
|
+
.filter((p): p is string => typeof p === 'string');
|
|
458
|
+
const label =
|
|
459
|
+
providerNames.length > 0
|
|
460
|
+
? `fallbacks=[${providerNames.join(',')}]`
|
|
461
|
+
: 'no-fallbacks';
|
|
462
|
+
|
|
463
|
+
const data: Record<string, unknown> = {
|
|
464
|
+
fallbackProviders: providerNames,
|
|
465
|
+
fallbackCount: list.length,
|
|
466
|
+
};
|
|
467
|
+
if (err instanceof Error) {
|
|
468
|
+
data.errorName = err.name;
|
|
469
|
+
data.errorStack = err.stack;
|
|
470
|
+
}
|
|
471
|
+
const status = extractHttpStatus(err);
|
|
472
|
+
const statusSuffix = status != null ? ` (HTTP ${status})` : '';
|
|
473
|
+
if (status != null) {
|
|
474
|
+
data.status = status;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
suffix: `[${label}]${statusSuffix}: ${errMsg}`,
|
|
479
|
+
data,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
369
483
|
/**
|
|
370
484
|
* Runs the summarization LLM call with primary + fallback providers,
|
|
371
485
|
* falling back to a metadata stub when all calls fail.
|
|
@@ -389,18 +503,23 @@ async function executeSummarizationWithFallback(params: {
|
|
|
389
503
|
log,
|
|
390
504
|
} = params;
|
|
391
505
|
|
|
392
|
-
const summarizationModel = initializeModel({
|
|
393
|
-
provider: clientConfig.provider as Providers,
|
|
394
|
-
clientOptions: clientConfig.clientOptions as t.ClientOptions,
|
|
395
|
-
tools: agentContext.getToolsForBinding(),
|
|
396
|
-
}) as t.ChatModel;
|
|
397
|
-
|
|
398
506
|
const priorSummaryText = agentContext.getSummaryText()?.trim() ?? '';
|
|
399
507
|
|
|
400
508
|
let summaryText = '';
|
|
401
509
|
let summaryUsage: Partial<UsageMetadata> | undefined;
|
|
402
510
|
|
|
403
511
|
try {
|
|
512
|
+
/**
|
|
513
|
+
* Initialize inside the try so that a misconfigured provider
|
|
514
|
+
* (e.g. an unrecognized summarization.provider) surfaces through the
|
|
515
|
+
* `log('error', ...)` path below rather than bubbling up silently.
|
|
516
|
+
*/
|
|
517
|
+
const summarizationModel = initializeModel({
|
|
518
|
+
provider: clientConfig.provider as Providers,
|
|
519
|
+
clientOptions: clientConfig.clientOptions as t.ClientOptions,
|
|
520
|
+
tools: agentContext.getToolsForBinding(),
|
|
521
|
+
}) as t.ChatModel;
|
|
522
|
+
|
|
404
523
|
const result = await summarizeWithCacheHit({
|
|
405
524
|
model: summarizationModel,
|
|
406
525
|
messages,
|
|
@@ -417,19 +536,20 @@ async function executeSummarizationWithFallback(params: {
|
|
|
417
536
|
summaryText = result.text;
|
|
418
537
|
summaryUsage = result.usage;
|
|
419
538
|
} catch (primaryError) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
539
|
+
const primaryDescribed = describeProviderError(
|
|
540
|
+
primaryError,
|
|
541
|
+
clientConfig.provider,
|
|
542
|
+
clientConfig.modelName
|
|
543
|
+
);
|
|
544
|
+
log('error', `Summarization LLM call failed ${primaryDescribed.suffix}`, {
|
|
545
|
+
...primaryDescribed.data,
|
|
427
546
|
messagesToRefineCount: messages.length,
|
|
428
547
|
});
|
|
429
548
|
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
549
|
+
const rawFallbacks = (
|
|
550
|
+
clientConfig.clientOptions as unknown as t.LLMConfig | undefined
|
|
551
|
+
)?.fallbacks;
|
|
552
|
+
const fallbacks = Array.isArray(rawFallbacks) ? rawFallbacks : [];
|
|
433
553
|
if (fallbacks.length > 0) {
|
|
434
554
|
try {
|
|
435
555
|
const onChunk = createSummarizationChunkHandler({
|
|
@@ -462,18 +582,21 @@ async function executeSummarizationWithFallback(params: {
|
|
|
462
582
|
);
|
|
463
583
|
}
|
|
464
584
|
} catch (fbErr) {
|
|
465
|
-
|
|
466
|
-
|
|
585
|
+
const fbDescribed = describeFallbackError(fbErr, fallbacks);
|
|
586
|
+
log('warn', `Fallback providers also failed ${fbDescribed.suffix}`, {
|
|
587
|
+
...fbDescribed.data,
|
|
467
588
|
});
|
|
468
589
|
}
|
|
469
590
|
}
|
|
470
591
|
if (!summaryText) {
|
|
471
|
-
log(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
592
|
+
log(
|
|
593
|
+
'warn',
|
|
594
|
+
`Summarization failed, falling back to metadata stub ${primaryDescribed.suffix}`,
|
|
595
|
+
{
|
|
596
|
+
...primaryDescribed.data,
|
|
597
|
+
messagesToRefineCount: messages.length,
|
|
598
|
+
}
|
|
599
|
+
);
|
|
477
600
|
summaryText = generateMetadataStub(messages);
|
|
478
601
|
}
|
|
479
602
|
}
|
|
@@ -532,35 +655,6 @@ async function dispatchCompletionEvents(params: {
|
|
|
532
655
|
);
|
|
533
656
|
}
|
|
534
657
|
|
|
535
|
-
const sessionId = graph.runId ?? '';
|
|
536
|
-
if (graph.hookRegistry?.hasHookFor('PostCompact', sessionId) === true) {
|
|
537
|
-
const threadId = (
|
|
538
|
-
runnableConfig?.configurable as Record<string, unknown> | undefined
|
|
539
|
-
)?.thread_id as string | undefined;
|
|
540
|
-
const firstBlock = summaryBlock.content?.[0];
|
|
541
|
-
const summaryText =
|
|
542
|
-
firstBlock != null &&
|
|
543
|
-
typeof firstBlock === 'object' &&
|
|
544
|
-
'text' in firstBlock &&
|
|
545
|
-
typeof firstBlock.text === 'string'
|
|
546
|
-
? firstBlock.text
|
|
547
|
-
: '';
|
|
548
|
-
await executeHooks({
|
|
549
|
-
registry: graph.hookRegistry,
|
|
550
|
-
input: {
|
|
551
|
-
hook_event_name: 'PostCompact',
|
|
552
|
-
runId: sessionId,
|
|
553
|
-
threadId,
|
|
554
|
-
agentId,
|
|
555
|
-
summary: summaryText,
|
|
556
|
-
messagesAfterCount: 0,
|
|
557
|
-
},
|
|
558
|
-
sessionId,
|
|
559
|
-
}).catch(() => {
|
|
560
|
-
/* PostCompact is observational — swallow errors */
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
658
|
agentContext.rebuildTokenMapAfterSummarization({});
|
|
565
659
|
}
|
|
566
660
|
|
|
@@ -576,7 +670,6 @@ interface CreateSummarizeNodeParams {
|
|
|
576
670
|
config?: RunnableConfig;
|
|
577
671
|
runId?: string;
|
|
578
672
|
isMultiAgent: boolean;
|
|
579
|
-
hookRegistry?: HookRegistry;
|
|
580
673
|
dispatchRunStep: (
|
|
581
674
|
runStep: t.RunStep,
|
|
582
675
|
config?: RunnableConfig
|
|
@@ -682,27 +775,6 @@ export function createSummarizeNode({
|
|
|
682
775
|
);
|
|
683
776
|
}
|
|
684
777
|
|
|
685
|
-
const sessionId = graph.runId ?? '';
|
|
686
|
-
if (graph.hookRegistry?.hasHookFor('PreCompact', sessionId) === true) {
|
|
687
|
-
const threadId = (
|
|
688
|
-
runnableConfig?.configurable as Record<string, unknown> | undefined
|
|
689
|
-
)?.thread_id as string | undefined;
|
|
690
|
-
await executeHooks({
|
|
691
|
-
registry: graph.hookRegistry,
|
|
692
|
-
input: {
|
|
693
|
-
hook_event_name: 'PreCompact',
|
|
694
|
-
runId: sessionId,
|
|
695
|
-
threadId,
|
|
696
|
-
agentId: request.agentId,
|
|
697
|
-
messagesBeforeCount: messagesToRefine.length,
|
|
698
|
-
trigger: agentContext.summarizationConfig?.trigger?.type ?? 'default',
|
|
699
|
-
},
|
|
700
|
-
sessionId,
|
|
701
|
-
}).catch(() => {
|
|
702
|
-
/* PreCompact is observational — swallow errors */
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
|
|
706
778
|
const isSelfSummarizeModel =
|
|
707
779
|
clientConfig.provider === (agentContext.provider as string);
|
|
708
780
|
const hasPromptCache =
|