@namzu/sdk 0.1.4 → 0.1.5-rc.1-fix
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/CHANGELOG.md +8 -0
- package/dist/advisory/executor.d.ts +2 -2
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js.map +1 -1
- package/dist/agents/AbstractAgent.d.ts +20 -2
- package/dist/agents/AbstractAgent.d.ts.map +1 -1
- package/dist/agents/AbstractAgent.js +23 -1
- package/dist/agents/AbstractAgent.js.map +1 -1
- package/dist/agents/PipelineAgent.d.ts.map +1 -1
- package/dist/agents/PipelineAgent.js +1 -1
- package/dist/agents/PipelineAgent.js.map +1 -1
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +1 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/RouterAgent.d.ts.map +1 -1
- package/dist/agents/RouterAgent.js +4 -2
- package/dist/agents/RouterAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +4 -1
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/lock.test.d.ts +2 -0
- package/dist/agents/__tests__/lock.test.d.ts.map +1 -0
- package/dist/agents/__tests__/lock.test.js +131 -0
- package/dist/agents/__tests__/lock.test.js.map +1 -0
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/lock.d.ts +42 -0
- package/dist/agents/lock.d.ts.map +1 -0
- package/dist/agents/lock.js +54 -0
- package/dist/agents/lock.js.map +1 -0
- package/dist/bridge/a2a/message.d.ts.map +1 -1
- package/dist/bridge/a2a/message.js.map +1 -1
- package/dist/bridge/tools/connector/router.d.ts +4 -5
- package/dist/bridge/tools/connector/router.d.ts.map +1 -1
- package/dist/bridge/tools/connector/router.js.map +1 -1
- package/dist/compaction/__tests__/SlidingWindowManager.test.d.ts +2 -0
- package/dist/compaction/__tests__/SlidingWindowManager.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/SlidingWindowManager.test.js +113 -0
- package/dist/compaction/__tests__/SlidingWindowManager.test.js.map +1 -0
- package/dist/compaction/__tests__/dangling.test.d.ts +2 -0
- package/dist/compaction/__tests__/dangling.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/dangling.test.js +356 -0
- package/dist/compaction/__tests__/dangling.test.js.map +1 -0
- package/dist/compaction/__tests__/factory.test.d.ts +2 -0
- package/dist/compaction/__tests__/factory.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/factory.test.js +43 -0
- package/dist/compaction/__tests__/factory.test.js.map +1 -0
- package/dist/compaction/dangling.d.ts +96 -0
- package/dist/compaction/dangling.d.ts.map +1 -0
- package/dist/compaction/dangling.js +274 -0
- package/dist/compaction/dangling.js.map +1 -0
- package/dist/compaction/factory.d.ts +20 -0
- package/dist/compaction/factory.d.ts.map +1 -0
- package/dist/compaction/factory.js +35 -0
- package/dist/compaction/factory.js.map +1 -0
- package/dist/compaction/index.d.ts +5 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +3 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +33 -0
- package/dist/compaction/interface.d.ts.map +1 -0
- package/dist/compaction/interface.js +2 -0
- package/dist/compaction/interface.js.map +1 -0
- package/dist/compaction/managers/index.d.ts +4 -0
- package/dist/compaction/managers/index.d.ts.map +1 -0
- package/dist/compaction/managers/index.js +4 -0
- package/dist/compaction/managers/index.js.map +1 -0
- package/dist/compaction/managers/null.d.ts +12 -0
- package/dist/compaction/managers/null.d.ts.map +1 -0
- package/dist/compaction/managers/null.js +15 -0
- package/dist/compaction/managers/null.js.map +1 -0
- package/dist/compaction/managers/slidingWindow.d.ts +27 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -0
- package/dist/compaction/managers/slidingWindow.js +41 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -0
- package/dist/compaction/managers/structured.d.ts +23 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -0
- package/dist/compaction/managers/structured.js +144 -0
- package/dist/compaction/managers/structured.js.map +1 -0
- package/dist/compaction/types.d.ts +1 -1
- package/dist/compaction/types.d.ts.map +1 -1
- package/dist/config/runtime.d.ts +16 -16
- package/dist/config/runtime.js +1 -1
- package/dist/config/runtime.js.map +1 -1
- package/dist/constants/agent/index.d.ts +1 -1
- package/dist/constants/agent/index.d.ts.map +1 -1
- package/dist/gateway/local.d.ts +2 -2
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +10 -1
- package/dist/gateway/local.js.map +1 -1
- package/dist/index.d.ts +18 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +3 -2
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +1 -2
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +2 -1
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/plugin/__tests__/lifecycle.test.d.ts +2 -0
- package/dist/plugin/__tests__/lifecycle.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/lifecycle.test.js +332 -0
- package/dist/plugin/__tests__/lifecycle.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +2 -2
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +28 -2
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/resolver.d.ts +2 -2
- package/dist/plugin/resolver.d.ts.map +1 -1
- package/dist/plugin/resolver.js.map +1 -1
- package/dist/registry/agent/definitions.d.ts +3 -2
- package/dist/registry/agent/definitions.d.ts.map +1 -1
- package/dist/registry/agent/definitions.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +2 -5
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/decision/parser.d.ts.map +1 -1
- package/dist/runtime/decision/parser.js +15 -40
- package/dist/runtime/decision/parser.js.map +1 -1
- package/dist/runtime/query/context-cache.d.ts +3 -3
- package/dist/runtime/query/context-cache.d.ts.map +1 -1
- package/dist/runtime/query/context-cache.js.map +1 -1
- package/dist/runtime/query/context.d.ts +1 -1
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/events.js +11 -11
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +4 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +1 -0
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +5 -3
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +2 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +2 -2
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.d.ts +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -2
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/plan.d.ts +1 -1
- package/dist/runtime/query/iteration/phases/plan.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts +2 -2
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/runtime/query/result.d.ts +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +4 -2
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/store/conversation/memory.d.ts +1 -1
- package/dist/store/conversation/memory.d.ts.map +1 -1
- package/dist/store/conversation/memory.js +15 -3
- package/dist/store/conversation/memory.js.map +1 -1
- package/dist/store/run/disk.d.ts +1 -2
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js +21 -13
- package/dist/store/run/disk.js.map +1 -1
- package/dist/tools/builtins/__tests__/structuredOutput.example.d.ts +140 -0
- package/dist/tools/builtins/__tests__/structuredOutput.example.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/structuredOutput.example.js +183 -0
- package/dist/tools/builtins/__tests__/structuredOutput.example.js.map +1 -0
- package/dist/tools/builtins/__tests__/structuredOutput.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/structuredOutput.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/structuredOutput.test.js +224 -0
- package/dist/tools/builtins/__tests__/structuredOutput.test.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +1 -2
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +3 -0
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/ls.d.ts +1 -1
- package/dist/tools/builtins/structuredOutput.d.ts +27 -0
- package/dist/tools/builtins/structuredOutput.d.ts.map +1 -0
- package/dist/tools/builtins/structuredOutput.js +46 -0
- package/dist/tools/builtins/structuredOutput.js.map +1 -0
- package/dist/tools/task/list.d.ts +1 -1
- package/dist/tools/task/list.d.ts.map +1 -1
- package/dist/tools/task/list.js.map +1 -1
- package/dist/types/agent/base.d.ts +4 -1
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/index.d.ts +1 -0
- package/dist/types/agent/index.d.ts.map +1 -1
- package/dist/types/agent/index.js +1 -0
- package/dist/types/agent/index.js.map +1 -1
- package/dist/types/agent/manager.d.ts +27 -0
- package/dist/types/agent/manager.d.ts.map +1 -0
- package/dist/types/agent/manager.js +2 -0
- package/dist/types/agent/manager.js.map +1 -0
- package/dist/types/agent/reactive.d.ts +2 -2
- package/dist/types/agent/reactive.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +2 -2
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +0 -2
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/agent/task.js +0 -2
- package/dist/types/agent/task.js.map +1 -1
- package/dist/types/common/index.d.ts +0 -1
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +0 -1
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/hitl/index.d.ts +1 -2
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/invocation/__tests__/state.test.d.ts +2 -0
- package/dist/types/invocation/__tests__/state.test.d.ts.map +1 -0
- package/dist/types/invocation/__tests__/state.test.js +167 -0
- package/dist/types/invocation/__tests__/state.test.js.map +1 -0
- package/dist/types/invocation/index.d.ts +37 -0
- package/dist/types/invocation/index.d.ts.map +1 -0
- package/dist/types/invocation/index.js +23 -0
- package/dist/types/invocation/index.js.map +1 -0
- package/dist/types/plugin/index.d.ts +6 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +16 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/run/events.d.ts +1 -1
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/index.d.ts +1 -0
- package/dist/types/run/index.d.ts.map +1 -1
- package/dist/types/run/index.js +1 -0
- package/dist/types/run/index.js.map +1 -1
- package/dist/types/run/metadata.d.ts +1 -1
- package/dist/types/run/metadata.d.ts.map +1 -1
- package/dist/types/run/state.d.ts +1 -1
- package/dist/types/run/state.d.ts.map +1 -1
- package/dist/types/run/stop-reason.d.ts +2 -0
- package/dist/types/run/stop-reason.d.ts.map +1 -0
- package/dist/types/run/stop-reason.js +2 -0
- package/dist/types/run/stop-reason.js.map +1 -0
- package/dist/types/structured-output/index.d.ts +51 -0
- package/dist/types/structured-output/index.d.ts.map +1 -0
- package/dist/types/structured-output/index.js +2 -0
- package/dist/types/structured-output/index.js.map +1 -0
- package/dist/types/tool/index.d.ts +36 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/executor.ts +2 -4
- package/src/agents/AbstractAgent.ts +26 -3
- package/src/agents/PipelineAgent.ts +1 -1
- package/src/agents/ReactiveAgent.ts +1 -0
- package/src/agents/RouterAgent.ts +8 -2
- package/src/agents/SupervisorAgent.ts +5 -1
- package/src/agents/__tests__/lock.test.ts +158 -0
- package/src/agents/index.ts +2 -0
- package/src/agents/lock.ts +66 -0
- package/src/bridge/a2a/message.ts +1 -2
- package/src/bridge/tools/connector/router.ts +4 -5
- package/src/compaction/__tests__/SlidingWindowManager.test.ts +139 -0
- package/src/compaction/__tests__/dangling.test.ts +447 -0
- package/src/compaction/__tests__/factory.test.ts +53 -0
- package/src/compaction/dangling.ts +321 -0
- package/src/compaction/factory.ts +41 -0
- package/src/compaction/index.ts +14 -0
- package/src/compaction/interface.ts +35 -0
- package/src/compaction/managers/index.ts +3 -0
- package/src/compaction/managers/null.ts +19 -0
- package/src/compaction/managers/slidingWindow.ts +57 -0
- package/src/compaction/managers/structured.ts +169 -0
- package/src/compaction/types.ts +1 -1
- package/src/config/runtime.ts +1 -1
- package/src/constants/agent/index.ts +1 -1
- package/src/gateway/local.ts +13 -4
- package/src/index.ts +38 -1
- package/src/manager/agent/lifecycle.ts +3 -2
- package/src/manager/run/persistence.ts +3 -8
- package/src/plugin/__tests__/lifecycle.test.ts +430 -0
- package/src/plugin/lifecycle.ts +32 -6
- package/src/plugin/resolver.ts +3 -3
- package/src/registry/agent/definitions.ts +3 -2
- package/src/registry/tool/execute.ts +2 -5
- package/src/runtime/decision/parser.ts +15 -40
- package/src/runtime/query/context-cache.ts +3 -4
- package/src/runtime/query/context.ts +1 -2
- package/src/runtime/query/events.ts +11 -11
- package/src/runtime/query/executor.ts +5 -3
- package/src/runtime/query/index.ts +11 -4
- package/src/runtime/query/iteration/index.ts +2 -2
- package/src/runtime/query/iteration/phases/advisory.ts +1 -2
- package/src/runtime/query/iteration/phases/checkpoint.ts +1 -2
- package/src/runtime/query/iteration/phases/context.ts +2 -2
- package/src/runtime/query/iteration/phases/plan.ts +1 -2
- package/src/runtime/query/prompt.ts +3 -3
- package/src/runtime/query/result.ts +1 -2
- package/src/runtime/query/tooling.ts +5 -2
- package/src/store/conversation/memory.ts +21 -5
- package/src/store/run/disk.ts +18 -16
- package/src/tools/builtins/__tests__/structuredOutput.example.ts +221 -0
- package/src/tools/builtins/__tests__/structuredOutput.test.ts +275 -0
- package/src/tools/builtins/grep.ts +1 -2
- package/src/tools/builtins/index.ts +3 -0
- package/src/tools/builtins/structuredOutput.ts +55 -0
- package/src/tools/task/list.ts +1 -2
- package/src/types/agent/base.ts +5 -1
- package/src/types/agent/index.ts +1 -0
- package/src/types/agent/manager.ts +36 -0
- package/src/types/agent/reactive.ts +2 -2
- package/src/types/agent/supervisor.ts +2 -2
- package/src/types/agent/task.ts +0 -4
- package/src/types/common/index.ts +0 -2
- package/src/types/hitl/index.ts +1 -2
- package/src/types/invocation/__tests__/state.test.ts +210 -0
- package/src/types/invocation/index.ts +55 -0
- package/src/types/plugin/index.ts +19 -0
- package/src/types/run/events.ts +1 -10
- package/src/types/run/index.ts +1 -0
- package/src/types/run/metadata.ts +1 -1
- package/src/types/run/state.ts +1 -1
- package/src/types/run/stop-reason.ts +10 -0
- package/src/types/structured-output/index.ts +56 -0
- package/src/types/tool/index.ts +45 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { Message } from '../types/message/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents the result of scanning messages for dangling tool call/result pairs.
|
|
5
|
+
* Used to identify which messages should be removed to ensure message validity.
|
|
6
|
+
*/
|
|
7
|
+
export interface DanglingResult {
|
|
8
|
+
/** Indices of assistant messages with unmatched tool calls */
|
|
9
|
+
assistantsWithUnmatchedCalls: number[]
|
|
10
|
+
/** Indices of tool messages with no matching assistant tool call */
|
|
11
|
+
orphanedToolMessages: number[]
|
|
12
|
+
/** Whether the message sequence is valid (no dangling messages) */
|
|
13
|
+
isValid: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Named constants for dangling message detection logic.
|
|
18
|
+
*/
|
|
19
|
+
const CONSTANTS = {
|
|
20
|
+
/** Role sentinel for tool message identification */
|
|
21
|
+
TOOL_ROLE: 'tool',
|
|
22
|
+
/** Role sentinel for assistant message identification */
|
|
23
|
+
ASSISTANT_ROLE: 'assistant',
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a message is an assistant message with tool calls.
|
|
28
|
+
* @param message - Message to inspect
|
|
29
|
+
* @returns true if message has role 'assistant' and contains toolCalls array
|
|
30
|
+
*/
|
|
31
|
+
function hasToolCalls(message: Message): boolean {
|
|
32
|
+
return (
|
|
33
|
+
message.role === CONSTANTS.ASSISTANT_ROLE &&
|
|
34
|
+
'toolCalls' in message &&
|
|
35
|
+
Array.isArray(message.toolCalls) &&
|
|
36
|
+
message.toolCalls.length > 0
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a message is a tool result message.
|
|
42
|
+
* @param message - Message to inspect
|
|
43
|
+
* @returns true if message has role 'tool'
|
|
44
|
+
*/
|
|
45
|
+
function isToolMessage(message: Message): boolean {
|
|
46
|
+
return message.role === CONSTANTS.TOOL_ROLE
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scans a message sequence and identifies dangling tool call/result pairs.
|
|
51
|
+
*
|
|
52
|
+
* A dangling pair occurs when:
|
|
53
|
+
* 1. An assistant message has tool calls but no matching tool message follows
|
|
54
|
+
* 2. A tool message exists but its toolCallId doesn't match any preceding assistant tool call
|
|
55
|
+
*
|
|
56
|
+
* @param messages - Array of messages to scan
|
|
57
|
+
* @returns DanglingResult with indices of invalid messages
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const messages = [
|
|
62
|
+
* { role: 'user', content: 'test' },
|
|
63
|
+
* { role: 'assistant', content: null, toolCalls: [{ id: '1', type: 'function', function: { name: 'test', arguments: '{}' } }] },
|
|
64
|
+
* // Missing tool message for call id '1'
|
|
65
|
+
* { role: 'user', content: 'next' }
|
|
66
|
+
* ]
|
|
67
|
+
* const result = findDanglingMessages(messages)
|
|
68
|
+
* // result.assistantsWithUnmatchedCalls = [1] (index 1 has unmatched tool call)
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function findDanglingMessages(messages: Message[]): DanglingResult {
|
|
72
|
+
const assistantsWithUnmatchedCalls: number[] = []
|
|
73
|
+
const orphanedToolMessages: number[] = []
|
|
74
|
+
|
|
75
|
+
// Build a set of all tool call IDs that exist in assistant messages
|
|
76
|
+
// along with their coverage map (which tool messages satisfy them)
|
|
77
|
+
const toolCallIds = new Map<string, { assistantIndex: number; satisfied: boolean }>()
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < messages.length; i++) {
|
|
80
|
+
const message = messages[i]
|
|
81
|
+
if (!message) continue
|
|
82
|
+
|
|
83
|
+
if (hasToolCalls(message)) {
|
|
84
|
+
// Record all tool calls from this assistant message
|
|
85
|
+
const assistantMsg = message as { toolCalls?: Array<{ id: string }> }
|
|
86
|
+
if (assistantMsg.toolCalls) {
|
|
87
|
+
for (const toolCall of assistantMsg.toolCalls) {
|
|
88
|
+
toolCallIds.set(toolCall.id, { assistantIndex: i, satisfied: false })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Second pass: mark satisfied tool calls and find orphaned tool messages
|
|
95
|
+
for (let i = 0; i < messages.length; i++) {
|
|
96
|
+
const message = messages[i]
|
|
97
|
+
if (!message) continue
|
|
98
|
+
|
|
99
|
+
if (isToolMessage(message)) {
|
|
100
|
+
const toolMsg = message as { toolCallId: string }
|
|
101
|
+
if (toolCallIds.has(toolMsg.toolCallId)) {
|
|
102
|
+
// Tool message satisfies a preceding tool call
|
|
103
|
+
const entry = toolCallIds.get(toolMsg.toolCallId)
|
|
104
|
+
if (entry) {
|
|
105
|
+
entry.satisfied = true
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Tool message has no matching tool call
|
|
109
|
+
orphanedToolMessages.push(i)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Third pass: identify unsatisfied tool calls
|
|
115
|
+
for (const entry of toolCallIds.values()) {
|
|
116
|
+
if (!entry.satisfied) {
|
|
117
|
+
assistantsWithUnmatchedCalls.push(entry.assistantIndex)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
assistantsWithUnmatchedCalls,
|
|
123
|
+
orphanedToolMessages,
|
|
124
|
+
isValid: assistantsWithUnmatchedCalls.length === 0 && orphanedToolMessages.length === 0,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Removes dangling messages from a message sequence, preserving order.
|
|
130
|
+
*
|
|
131
|
+
* This function removes the minimum set of messages needed to ensure
|
|
132
|
+
* all remaining tool call/result pairs are valid and complete.
|
|
133
|
+
*
|
|
134
|
+
* Algorithm:
|
|
135
|
+
* 1. Identify dangling assistant messages and orphaned tool messages
|
|
136
|
+
* 2. Remove orphaned tool messages
|
|
137
|
+
* 3. For assistant messages with unmatched calls, remove both the assistant
|
|
138
|
+
* message AND any following tool messages that attempt to satisfy it
|
|
139
|
+
*
|
|
140
|
+
* @param messages - Array of messages to clean
|
|
141
|
+
* @returns New array with dangling messages removed, original order preserved
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* const messages = [
|
|
146
|
+
* { role: 'user', content: 'test' },
|
|
147
|
+
* { role: 'assistant', content: null, toolCalls: [{ id: '1', ... }] },
|
|
148
|
+
* // Missing tool response
|
|
149
|
+
* { role: 'user', content: 'next' }
|
|
150
|
+
* ]
|
|
151
|
+
* const clean = removeDanglingMessages(messages)
|
|
152
|
+
* // Result: [{ role: 'user', content: 'test' }, { role: 'user', content: 'next' }]
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function removeDanglingMessages(messages: Message[]): Message[] {
|
|
156
|
+
const result = findDanglingMessages(messages)
|
|
157
|
+
|
|
158
|
+
if (result.isValid) {
|
|
159
|
+
return messages.slice() // Return shallow copy if already valid
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build a set of indices to remove
|
|
163
|
+
const indicesToRemove = new Set<number>()
|
|
164
|
+
|
|
165
|
+
// Mark orphaned tool messages for removal
|
|
166
|
+
for (const idx of result.orphanedToolMessages) {
|
|
167
|
+
indicesToRemove.add(idx)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// For unsatisfied assistant messages:
|
|
171
|
+
// 1. Remove the assistant message itself
|
|
172
|
+
// 2. Remove any immediately following tool messages (they can't match)
|
|
173
|
+
|
|
174
|
+
for (const assistantIdx of result.assistantsWithUnmatchedCalls) {
|
|
175
|
+
indicesToRemove.add(assistantIdx)
|
|
176
|
+
|
|
177
|
+
// Collect the tool call IDs from this unsatisfied assistant message
|
|
178
|
+
const assistantMsg = messages[assistantIdx] as {
|
|
179
|
+
toolCalls?: Array<{ id: string }>
|
|
180
|
+
}
|
|
181
|
+
const toolCallIds = new Set<string>()
|
|
182
|
+
if (assistantMsg.toolCalls) {
|
|
183
|
+
for (const toolCall of assistantMsg.toolCalls) {
|
|
184
|
+
toolCallIds.add(toolCall.id)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Remove any following tool messages that match these tool call IDs
|
|
189
|
+
// (they are orphaned now that the assistant message is removed)
|
|
190
|
+
for (let i = assistantIdx + 1; i < messages.length; i++) {
|
|
191
|
+
const msg = messages[i]
|
|
192
|
+
if (!msg) continue
|
|
193
|
+
if (isToolMessage(msg)) {
|
|
194
|
+
const toolMsg = msg as { toolCallId: string }
|
|
195
|
+
if (toolCallIds.has(toolMsg.toolCallId)) {
|
|
196
|
+
indicesToRemove.add(i)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Return messages not marked for removal, preserving order
|
|
203
|
+
return messages.filter((_, idx) => !indicesToRemove.has(idx))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Finds a safe index for trimming messages while preserving tool call/result atomicity.
|
|
208
|
+
*
|
|
209
|
+
* Given a desired trim point (maxIndex), adjusts it forward to ensure:
|
|
210
|
+
* 1. The trim doesn't split a tool call/result pair
|
|
211
|
+
* 2. The first message after the trim point is not a ToolMessage (orphaned result)
|
|
212
|
+
* 3. All tool call/result pairs are kept intact (either fully included or fully excluded)
|
|
213
|
+
*
|
|
214
|
+
* Algorithm:
|
|
215
|
+
* 1. Start from desired index
|
|
216
|
+
* 2. Check if there's an incomplete tool call/result pair that started before the trim point
|
|
217
|
+
* 3. If so, advance trim point past the complete pair
|
|
218
|
+
* 4. If the new trim point starts with a tool message, advance past it
|
|
219
|
+
*
|
|
220
|
+
* @param messages - Array of messages to analyze
|
|
221
|
+
* @param targetIndex - Desired trim point (exclusive upper bound)
|
|
222
|
+
* @returns Safe trim index where message sequence is valid (at least 0, at most messages.length)
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* const messages = [
|
|
227
|
+
* { role: 'user', content: 'test' },
|
|
228
|
+
* { role: 'assistant', content: null, toolCalls: [{ id: '1', ... }] },
|
|
229
|
+
* { role: 'tool', content: 'result', toolCallId: '1' },
|
|
230
|
+
* { role: 'user', content: 'next' }
|
|
231
|
+
* ]
|
|
232
|
+
* const safeIdx = findSafeTrimIndex(messages, 2)
|
|
233
|
+
* // Result: 3 (skips the incomplete pair at index 1-2)
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function findSafeTrimIndex(messages: Message[], targetIndex: number): number {
|
|
237
|
+
// Clamp to valid bounds
|
|
238
|
+
const clampedTarget = Math.max(0, Math.min(targetIndex, messages.length))
|
|
239
|
+
|
|
240
|
+
// If no messages after trim point, safe to trim here
|
|
241
|
+
if (clampedTarget >= messages.length) {
|
|
242
|
+
return messages.length
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for incomplete tool call/result pairs that cross the trim boundary
|
|
246
|
+
// Build a map of tool call IDs and whether they have results in the kept portion
|
|
247
|
+
let currentIndex = clampedTarget
|
|
248
|
+
let attempts = 0
|
|
249
|
+
const maxAttempts = messages.length // Prevent infinite loops
|
|
250
|
+
|
|
251
|
+
while (attempts < maxAttempts) {
|
|
252
|
+
attempts++
|
|
253
|
+
|
|
254
|
+
if (currentIndex >= messages.length) {
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check if message at currentIndex is a tool message (orphaned result)
|
|
259
|
+
const currentMsg = messages[currentIndex]
|
|
260
|
+
if (currentMsg && isToolMessage(currentMsg)) {
|
|
261
|
+
// Skip orphaned tool message
|
|
262
|
+
currentIndex++
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for incomplete tool call/result pairs in the kept portion [0, currentIndex)
|
|
267
|
+
const keptMessages = messages.slice(0, currentIndex)
|
|
268
|
+
const incompleteResult = findDanglingMessages(keptMessages)
|
|
269
|
+
|
|
270
|
+
if (!incompleteResult.isValid) {
|
|
271
|
+
// Find the maximum dangling message index
|
|
272
|
+
const allDanglingIndices = [
|
|
273
|
+
...incompleteResult.assistantsWithUnmatchedCalls,
|
|
274
|
+
...incompleteResult.orphanedToolMessages,
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
if (allDanglingIndices.length === 0) {
|
|
278
|
+
// No dangling messages found, but isValid is false — shouldn't happen
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const maxDanglingIdx = Math.max(...allDanglingIndices)
|
|
283
|
+
|
|
284
|
+
// Move trim point past the dangling message
|
|
285
|
+
currentIndex = maxDanglingIdx + 1
|
|
286
|
+
|
|
287
|
+
// For assistant messages, also skip following tool messages from that call
|
|
288
|
+
const assistantAtDanglingIdx = messages[maxDanglingIdx]
|
|
289
|
+
if (assistantAtDanglingIdx && hasToolCalls(assistantAtDanglingIdx)) {
|
|
290
|
+
const toolCallIds = new Set<string>()
|
|
291
|
+
const assistantMsg = assistantAtDanglingIdx as {
|
|
292
|
+
toolCalls?: Array<{ id: string }>
|
|
293
|
+
}
|
|
294
|
+
if (assistantMsg.toolCalls) {
|
|
295
|
+
for (const toolCall of assistantMsg.toolCalls) {
|
|
296
|
+
toolCallIds.add(toolCall.id)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Skip following tool messages from this assistant
|
|
301
|
+
while (currentIndex < messages.length) {
|
|
302
|
+
const nextMsg = messages[currentIndex]
|
|
303
|
+
if (!nextMsg) break
|
|
304
|
+
if (isToolMessage(nextMsg)) {
|
|
305
|
+
const toolMsg = nextMsg as { toolCallId: string }
|
|
306
|
+
if (toolCallIds.has(toolMsg.toolCallId)) {
|
|
307
|
+
currentIndex++
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
break
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// No dangling messages in the kept portion, we're safe
|
|
316
|
+
break
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return Math.min(currentIndex, messages.length)
|
|
321
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CompactionConfig } from '../config/runtime.js'
|
|
2
|
+
import type { ConversationManager } from './interface.js'
|
|
3
|
+
import { NullManager } from './managers/null.js'
|
|
4
|
+
import { SlidingWindowManager } from './managers/slidingWindow.js'
|
|
5
|
+
import { StructuredCompactionManager } from './managers/structured.js'
|
|
6
|
+
import type { CompactionStrategy } from './types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Factory function to create a ConversationManager based on the configured strategy.
|
|
10
|
+
*
|
|
11
|
+
* @param strategy - Selected compaction strategy: 'structured', 'sliding-window', or 'disabled'
|
|
12
|
+
* @param config - CompactionConfig with keepRecentMessages and other settings
|
|
13
|
+
* @returns Instantiated ConversationManager
|
|
14
|
+
*
|
|
15
|
+
* @throws If strategy is not recognized (exhaustiveness check ensures this never happens at runtime)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const manager = createConversationManager('structured', config)
|
|
20
|
+
* const trimmed = manager.applyManagement(messages)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createConversationManager(
|
|
24
|
+
strategy: CompactionStrategy,
|
|
25
|
+
config: CompactionConfig,
|
|
26
|
+
): ConversationManager {
|
|
27
|
+
switch (strategy) {
|
|
28
|
+
case 'structured':
|
|
29
|
+
return new StructuredCompactionManager(config)
|
|
30
|
+
case 'sliding-window':
|
|
31
|
+
return new SlidingWindowManager({
|
|
32
|
+
keepRecentMessages: config.keepRecentMessages,
|
|
33
|
+
})
|
|
34
|
+
case 'disabled':
|
|
35
|
+
return new NullManager()
|
|
36
|
+
default: {
|
|
37
|
+
const _exhaustive: never = strategy
|
|
38
|
+
throw new Error(`Unknown compaction strategy: ${_exhaustive}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/compaction/index.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type {
|
|
|
7
7
|
CompactionStrategy,
|
|
8
8
|
} from './types.js'
|
|
9
9
|
|
|
10
|
+
export type { DanglingResult } from './dangling.js'
|
|
11
|
+
|
|
12
|
+
export type { ConversationManager } from './interface.js'
|
|
13
|
+
|
|
10
14
|
export { WorkingStateManager } from './manager.js'
|
|
11
15
|
|
|
12
16
|
export { serializeState } from './serializer.js'
|
|
@@ -19,3 +23,13 @@ export {
|
|
|
19
23
|
} from './extractor.js'
|
|
20
24
|
|
|
21
25
|
export { buildVerifiedSummary } from './verifier.js'
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
findDanglingMessages,
|
|
29
|
+
removeDanglingMessages,
|
|
30
|
+
findSafeTrimIndex,
|
|
31
|
+
} from './dangling.js'
|
|
32
|
+
|
|
33
|
+
export { NullManager, SlidingWindowManager, StructuredCompactionManager } from './managers/index.js'
|
|
34
|
+
|
|
35
|
+
export { createConversationManager } from './factory.js'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Message } from '../types/message/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Strategy interface for managing conversation context.
|
|
5
|
+
* Implementations decide how to handle context overflow and message trimming.
|
|
6
|
+
*
|
|
7
|
+
* A manager applies two strategies:
|
|
8
|
+
* 1. **Routine management** (applyManagement): Called after each iteration to proactively optimize context.
|
|
9
|
+
* 2. **Overflow reduction** (reduceContext): Called when the LLM reports context window exceeded.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConversationManager {
|
|
12
|
+
/** Unique name for this manager (e.g., 'structured', 'sliding-window', 'disabled') */
|
|
13
|
+
readonly name: string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Apply routine management after each iteration.
|
|
17
|
+
* Called proactively, not in response to an error.
|
|
18
|
+
* Returns modified messages array (or same reference if no changes).
|
|
19
|
+
*
|
|
20
|
+
* @param messages - Current message history
|
|
21
|
+
* @returns Modified messages array (may be same reference if no changes made)
|
|
22
|
+
*/
|
|
23
|
+
applyManagement(messages: Message[]): Message[]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reduce context when overflow is detected.
|
|
27
|
+
* Called when the LLM reports context window exceeded.
|
|
28
|
+
* Returns true if context was successfully reduced, false if no reduction possible.
|
|
29
|
+
*
|
|
30
|
+
* @param messages - Current message history
|
|
31
|
+
* @param overflowTokens - Approximate number of tokens over budget
|
|
32
|
+
* @returns true if context was successfully reduced, false if no reduction possible
|
|
33
|
+
*/
|
|
34
|
+
reduceContext(messages: Message[], overflowTokens: number): boolean
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Message } from '../../types/message/index.js'
|
|
2
|
+
import type { ConversationManager } from '../interface.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* No-op conversation manager implementation.
|
|
6
|
+
* Never modifies messages, useful for testing or when context management is disabled.
|
|
7
|
+
*/
|
|
8
|
+
export class NullManager implements ConversationManager {
|
|
9
|
+
readonly name = 'null'
|
|
10
|
+
|
|
11
|
+
applyManagement(messages: Message[]): Message[] {
|
|
12
|
+
return messages
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
reduceContext(_messages: Message[], _overflowTokens: number): boolean {
|
|
16
|
+
// Cannot reduce; no operations performed
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Message } from '../../types/message/index.js'
|
|
2
|
+
import { findSafeTrimIndex } from '../dangling.js'
|
|
3
|
+
import type { ConversationManager } from '../interface.js'
|
|
4
|
+
|
|
5
|
+
export interface SlidingWindowManagerConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Number of most recent messages to always keep.
|
|
8
|
+
* Default: 4
|
|
9
|
+
*/
|
|
10
|
+
keepRecentMessages?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Simple window-based conversation manager.
|
|
15
|
+
* Trims old messages to maintain a fixed-size window of recent context.
|
|
16
|
+
* Fastest strategy, least context preservation.
|
|
17
|
+
*
|
|
18
|
+
* Algorithm:
|
|
19
|
+
* 1. If message count exceeds window size, trim excess from the start
|
|
20
|
+
* 2. Use findSafeTrimIndex to ensure tool call/result pairs remain atomic
|
|
21
|
+
* 3. applyManagement runs proactively each iteration; reduceContext on overflow
|
|
22
|
+
*/
|
|
23
|
+
export class SlidingWindowManager implements ConversationManager {
|
|
24
|
+
readonly name = 'sliding-window'
|
|
25
|
+
private readonly keepRecentMessages: number
|
|
26
|
+
|
|
27
|
+
constructor(config: SlidingWindowManagerConfig = {}) {
|
|
28
|
+
this.keepRecentMessages = config.keepRecentMessages ?? 4
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
applyManagement(messages: Message[]): Message[] {
|
|
32
|
+
// Trim to window size if needed
|
|
33
|
+
if (messages.length <= this.keepRecentMessages) {
|
|
34
|
+
return messages
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const desiredTrimPoint = messages.length - this.keepRecentMessages
|
|
38
|
+
const safeTrimIdx = findSafeTrimIndex(messages, desiredTrimPoint)
|
|
39
|
+
return messages.slice(safeTrimIdx)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reduceContext(messages: Message[], _overflowTokens: number): boolean {
|
|
43
|
+
// Try to reduce by trimming more aggressively
|
|
44
|
+
// Target: keep even fewer messages than normal window
|
|
45
|
+
const targetWindow = Math.max(1, Math.floor(this.keepRecentMessages * 0.5))
|
|
46
|
+
const desiredTrimPoint = messages.length - targetWindow
|
|
47
|
+
const safeTrimIdx = findSafeTrimIndex(messages, desiredTrimPoint)
|
|
48
|
+
|
|
49
|
+
if (safeTrimIdx >= messages.length) {
|
|
50
|
+
// Cannot trim further
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Successfully trimmed if we removed at least one message
|
|
55
|
+
return safeTrimIdx > 0
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { CompactionConfig } from '../../config/runtime.js'
|
|
2
|
+
import type { Message } from '../../types/message/index.js'
|
|
3
|
+
import { findSafeTrimIndex } from '../dangling.js'
|
|
4
|
+
import {
|
|
5
|
+
extractFromAssistantMessage,
|
|
6
|
+
extractFromToolCall,
|
|
7
|
+
extractFromUserMessage,
|
|
8
|
+
} from '../extractor.js'
|
|
9
|
+
import type { ConversationManager } from '../interface.js'
|
|
10
|
+
import { WorkingStateManager } from '../manager.js'
|
|
11
|
+
import { serializeState } from '../serializer.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Structured conversation manager wrapping the existing WorkingStateManager.
|
|
15
|
+
* Extracts context from messages into a structured state, then compacts via serialization.
|
|
16
|
+
*
|
|
17
|
+
* Flow:
|
|
18
|
+
* 1. Extract state from input messages (tracks tasks, decisions, files, etc.)
|
|
19
|
+
* 2. Serialize state to XML-like format
|
|
20
|
+
* 3. Inject compacted state as system message
|
|
21
|
+
* 4. Trim older messages, keeping the state message + recent history
|
|
22
|
+
*
|
|
23
|
+
* Provides maximum context preservation but more computational overhead.
|
|
24
|
+
*/
|
|
25
|
+
export class StructuredCompactionManager implements ConversationManager {
|
|
26
|
+
readonly name = 'structured'
|
|
27
|
+
private readonly config: CompactionConfig
|
|
28
|
+
|
|
29
|
+
constructor(config: CompactionConfig) {
|
|
30
|
+
this.config = config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
applyManagement(messages: Message[]): Message[] {
|
|
34
|
+
// Build working state from all messages
|
|
35
|
+
const stateManager = new WorkingStateManager(this.config)
|
|
36
|
+
|
|
37
|
+
let isFirstUserMessage = true
|
|
38
|
+
for (const message of messages) {
|
|
39
|
+
if (message.role === 'user') {
|
|
40
|
+
extractFromUserMessage(stateManager, message.content, isFirstUserMessage)
|
|
41
|
+
isFirstUserMessage = false
|
|
42
|
+
} else if (message.role === 'assistant' && message.content) {
|
|
43
|
+
extractFromAssistantMessage(stateManager, message.content, this.config)
|
|
44
|
+
} else if (message.role === 'tool') {
|
|
45
|
+
// Extract from tool messages if needed (optional refinement)
|
|
46
|
+
// For now, we focus on assistant and user content
|
|
47
|
+
} else if (message.role === 'assistant' && 'toolCalls' in message && message.toolCalls) {
|
|
48
|
+
// Extract from tool calls
|
|
49
|
+
for (const toolCall of message.toolCalls) {
|
|
50
|
+
extractFromToolCall(stateManager, toolCall.function.name, toolCall.function.arguments)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If not much context accumulated, don't inject state yet
|
|
56
|
+
if (stateManager.slotCount() < this.config.richStateThreshold) {
|
|
57
|
+
return messages
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Serialize state to string
|
|
61
|
+
const state = stateManager.getState()
|
|
62
|
+
const serialized = serializeState(state)
|
|
63
|
+
|
|
64
|
+
// Inject as system message at the start (after first user message if present)
|
|
65
|
+
const result: Message[] = []
|
|
66
|
+
let injected = false
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < messages.length; i++) {
|
|
69
|
+
const msg = messages[i]
|
|
70
|
+
if (!msg) continue
|
|
71
|
+
|
|
72
|
+
// Keep system messages and first user message
|
|
73
|
+
result.push(msg)
|
|
74
|
+
|
|
75
|
+
// Inject after first user message
|
|
76
|
+
if (!injected && msg.role === 'user') {
|
|
77
|
+
result.push({
|
|
78
|
+
role: 'system',
|
|
79
|
+
content: serialized,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
})
|
|
82
|
+
injected = true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If no user message found, inject at start
|
|
87
|
+
if (!injected) {
|
|
88
|
+
result.unshift({
|
|
89
|
+
role: 'system',
|
|
90
|
+
content: serialized,
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Trim old messages while keeping state message
|
|
96
|
+
if (result.length > this.config.keepRecentMessages + 2) {
|
|
97
|
+
const desiredTrimPoint = result.length - this.config.keepRecentMessages
|
|
98
|
+
const safeTrimIdx = findSafeTrimIndex(result, desiredTrimPoint)
|
|
99
|
+
|
|
100
|
+
// Always keep the injected system message
|
|
101
|
+
const trimPoint = Math.min(
|
|
102
|
+
safeTrimIdx,
|
|
103
|
+
Math.max(0, result.length - this.config.keepRecentMessages),
|
|
104
|
+
)
|
|
105
|
+
return result.slice(trimPoint)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reduceContext(messages: Message[], _overflowTokens: number): boolean {
|
|
112
|
+
// Use the same approach as applyManagement but more aggressive trimming
|
|
113
|
+
const stateManager = new WorkingStateManager(this.config)
|
|
114
|
+
|
|
115
|
+
let isFirstUserMessage = true
|
|
116
|
+
for (const message of messages) {
|
|
117
|
+
if (message.role === 'user') {
|
|
118
|
+
extractFromUserMessage(stateManager, message.content, isFirstUserMessage)
|
|
119
|
+
isFirstUserMessage = false
|
|
120
|
+
} else if (message.role === 'assistant' && message.content) {
|
|
121
|
+
extractFromAssistantMessage(stateManager, message.content, this.config)
|
|
122
|
+
} else if (message.role === 'assistant' && 'toolCalls' in message && message.toolCalls) {
|
|
123
|
+
for (const toolCall of message.toolCalls) {
|
|
124
|
+
extractFromToolCall(stateManager, toolCall.function.name, toolCall.function.arguments)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const state = stateManager.getState()
|
|
130
|
+
const serialized = serializeState(state)
|
|
131
|
+
|
|
132
|
+
// Build new messages: keep system messages, inject state, keep fewer recent messages
|
|
133
|
+
const result: Message[] = []
|
|
134
|
+
let injected = false
|
|
135
|
+
|
|
136
|
+
for (const msg of messages) {
|
|
137
|
+
if (msg.role === 'system') {
|
|
138
|
+
result.push(msg)
|
|
139
|
+
} else if (!injected && msg.role === 'user') {
|
|
140
|
+
result.push(msg)
|
|
141
|
+
result.push({
|
|
142
|
+
role: 'system',
|
|
143
|
+
content: serialized,
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
})
|
|
146
|
+
injected = true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add recent messages
|
|
151
|
+
const recentCount = Math.max(1, Math.floor(this.config.keepRecentMessages * 0.5))
|
|
152
|
+
const recentMessages = messages.filter((m) => m.role !== 'system').slice(-recentCount)
|
|
153
|
+
|
|
154
|
+
for (const msg of recentMessages) {
|
|
155
|
+
if (!result.includes(msg)) {
|
|
156
|
+
result.push(msg)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Use safe trim to preserve tool atomicity
|
|
161
|
+
if (result.length > recentCount + 2) {
|
|
162
|
+
const desiredTrimPoint = result.length - recentCount
|
|
163
|
+
const safeTrimIdx = findSafeTrimIndex(result, desiredTrimPoint)
|
|
164
|
+
return safeTrimIdx > 0 && safeTrimIdx < result.length
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result.length < messages.length
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/compaction/types.ts
CHANGED
package/src/config/runtime.ts
CHANGED
|
@@ -17,7 +17,7 @@ export const TaskRouterConfigSchema = z
|
|
|
17
17
|
.optional()
|
|
18
18
|
|
|
19
19
|
export const CompactionConfigSchema = z.object({
|
|
20
|
-
strategy: z.enum(['structured', 'disabled']).default('structured'),
|
|
20
|
+
strategy: z.enum(['structured', 'sliding-window', 'disabled']).default('structured'),
|
|
21
21
|
triggerThreshold: z.number().min(0).max(1).default(0.7),
|
|
22
22
|
resetThreshold: z.number().min(0).max(1).default(0.4),
|
|
23
23
|
keepRecentMessages: z.number().positive().default(4),
|