@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.
Files changed (325) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/advisory/executor.d.ts +2 -2
  3. package/dist/advisory/executor.d.ts.map +1 -1
  4. package/dist/advisory/executor.js.map +1 -1
  5. package/dist/agents/AbstractAgent.d.ts +20 -2
  6. package/dist/agents/AbstractAgent.d.ts.map +1 -1
  7. package/dist/agents/AbstractAgent.js +23 -1
  8. package/dist/agents/AbstractAgent.js.map +1 -1
  9. package/dist/agents/PipelineAgent.d.ts.map +1 -1
  10. package/dist/agents/PipelineAgent.js +1 -1
  11. package/dist/agents/PipelineAgent.js.map +1 -1
  12. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  13. package/dist/agents/ReactiveAgent.js +1 -0
  14. package/dist/agents/ReactiveAgent.js.map +1 -1
  15. package/dist/agents/RouterAgent.d.ts.map +1 -1
  16. package/dist/agents/RouterAgent.js +4 -2
  17. package/dist/agents/RouterAgent.js.map +1 -1
  18. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  19. package/dist/agents/SupervisorAgent.js +4 -1
  20. package/dist/agents/SupervisorAgent.js.map +1 -1
  21. package/dist/agents/__tests__/lock.test.d.ts +2 -0
  22. package/dist/agents/__tests__/lock.test.d.ts.map +1 -0
  23. package/dist/agents/__tests__/lock.test.js +131 -0
  24. package/dist/agents/__tests__/lock.test.js.map +1 -0
  25. package/dist/agents/index.d.ts +2 -0
  26. package/dist/agents/index.d.ts.map +1 -1
  27. package/dist/agents/index.js +1 -0
  28. package/dist/agents/index.js.map +1 -1
  29. package/dist/agents/lock.d.ts +42 -0
  30. package/dist/agents/lock.d.ts.map +1 -0
  31. package/dist/agents/lock.js +54 -0
  32. package/dist/agents/lock.js.map +1 -0
  33. package/dist/bridge/a2a/message.d.ts.map +1 -1
  34. package/dist/bridge/a2a/message.js.map +1 -1
  35. package/dist/bridge/tools/connector/router.d.ts +4 -5
  36. package/dist/bridge/tools/connector/router.d.ts.map +1 -1
  37. package/dist/bridge/tools/connector/router.js.map +1 -1
  38. package/dist/compaction/__tests__/SlidingWindowManager.test.d.ts +2 -0
  39. package/dist/compaction/__tests__/SlidingWindowManager.test.d.ts.map +1 -0
  40. package/dist/compaction/__tests__/SlidingWindowManager.test.js +113 -0
  41. package/dist/compaction/__tests__/SlidingWindowManager.test.js.map +1 -0
  42. package/dist/compaction/__tests__/dangling.test.d.ts +2 -0
  43. package/dist/compaction/__tests__/dangling.test.d.ts.map +1 -0
  44. package/dist/compaction/__tests__/dangling.test.js +356 -0
  45. package/dist/compaction/__tests__/dangling.test.js.map +1 -0
  46. package/dist/compaction/__tests__/factory.test.d.ts +2 -0
  47. package/dist/compaction/__tests__/factory.test.d.ts.map +1 -0
  48. package/dist/compaction/__tests__/factory.test.js +43 -0
  49. package/dist/compaction/__tests__/factory.test.js.map +1 -0
  50. package/dist/compaction/dangling.d.ts +96 -0
  51. package/dist/compaction/dangling.d.ts.map +1 -0
  52. package/dist/compaction/dangling.js +274 -0
  53. package/dist/compaction/dangling.js.map +1 -0
  54. package/dist/compaction/factory.d.ts +20 -0
  55. package/dist/compaction/factory.d.ts.map +1 -0
  56. package/dist/compaction/factory.js +35 -0
  57. package/dist/compaction/factory.js.map +1 -0
  58. package/dist/compaction/index.d.ts +5 -0
  59. package/dist/compaction/index.d.ts.map +1 -1
  60. package/dist/compaction/index.js +3 -0
  61. package/dist/compaction/index.js.map +1 -1
  62. package/dist/compaction/interface.d.ts +33 -0
  63. package/dist/compaction/interface.d.ts.map +1 -0
  64. package/dist/compaction/interface.js +2 -0
  65. package/dist/compaction/interface.js.map +1 -0
  66. package/dist/compaction/managers/index.d.ts +4 -0
  67. package/dist/compaction/managers/index.d.ts.map +1 -0
  68. package/dist/compaction/managers/index.js +4 -0
  69. package/dist/compaction/managers/index.js.map +1 -0
  70. package/dist/compaction/managers/null.d.ts +12 -0
  71. package/dist/compaction/managers/null.d.ts.map +1 -0
  72. package/dist/compaction/managers/null.js +15 -0
  73. package/dist/compaction/managers/null.js.map +1 -0
  74. package/dist/compaction/managers/slidingWindow.d.ts +27 -0
  75. package/dist/compaction/managers/slidingWindow.d.ts.map +1 -0
  76. package/dist/compaction/managers/slidingWindow.js +41 -0
  77. package/dist/compaction/managers/slidingWindow.js.map +1 -0
  78. package/dist/compaction/managers/structured.d.ts +23 -0
  79. package/dist/compaction/managers/structured.d.ts.map +1 -0
  80. package/dist/compaction/managers/structured.js +144 -0
  81. package/dist/compaction/managers/structured.js.map +1 -0
  82. package/dist/compaction/types.d.ts +1 -1
  83. package/dist/compaction/types.d.ts.map +1 -1
  84. package/dist/config/runtime.d.ts +16 -16
  85. package/dist/config/runtime.js +1 -1
  86. package/dist/config/runtime.js.map +1 -1
  87. package/dist/constants/agent/index.d.ts +1 -1
  88. package/dist/constants/agent/index.d.ts.map +1 -1
  89. package/dist/gateway/local.d.ts +2 -2
  90. package/dist/gateway/local.d.ts.map +1 -1
  91. package/dist/gateway/local.js +10 -1
  92. package/dist/gateway/local.js.map +1 -1
  93. package/dist/index.d.ts +18 -4
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +12 -2
  96. package/dist/index.js.map +1 -1
  97. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  98. package/dist/manager/agent/lifecycle.js +3 -2
  99. package/dist/manager/agent/lifecycle.js.map +1 -1
  100. package/dist/manager/run/persistence.d.ts +1 -2
  101. package/dist/manager/run/persistence.d.ts.map +1 -1
  102. package/dist/manager/run/persistence.js +2 -1
  103. package/dist/manager/run/persistence.js.map +1 -1
  104. package/dist/plugin/__tests__/lifecycle.test.d.ts +2 -0
  105. package/dist/plugin/__tests__/lifecycle.test.d.ts.map +1 -0
  106. package/dist/plugin/__tests__/lifecycle.test.js +332 -0
  107. package/dist/plugin/__tests__/lifecycle.test.js.map +1 -0
  108. package/dist/plugin/lifecycle.d.ts +2 -2
  109. package/dist/plugin/lifecycle.d.ts.map +1 -1
  110. package/dist/plugin/lifecycle.js +28 -2
  111. package/dist/plugin/lifecycle.js.map +1 -1
  112. package/dist/plugin/resolver.d.ts +2 -2
  113. package/dist/plugin/resolver.d.ts.map +1 -1
  114. package/dist/plugin/resolver.js.map +1 -1
  115. package/dist/registry/agent/definitions.d.ts +3 -2
  116. package/dist/registry/agent/definitions.d.ts.map +1 -1
  117. package/dist/registry/agent/definitions.js.map +1 -1
  118. package/dist/registry/tool/execute.d.ts +2 -5
  119. package/dist/registry/tool/execute.d.ts.map +1 -1
  120. package/dist/registry/tool/execute.js.map +1 -1
  121. package/dist/runtime/decision/parser.d.ts.map +1 -1
  122. package/dist/runtime/decision/parser.js +15 -40
  123. package/dist/runtime/decision/parser.js.map +1 -1
  124. package/dist/runtime/query/context-cache.d.ts +3 -3
  125. package/dist/runtime/query/context-cache.d.ts.map +1 -1
  126. package/dist/runtime/query/context-cache.js.map +1 -1
  127. package/dist/runtime/query/context.d.ts +1 -1
  128. package/dist/runtime/query/context.d.ts.map +1 -1
  129. package/dist/runtime/query/context.js.map +1 -1
  130. package/dist/runtime/query/events.js +11 -11
  131. package/dist/runtime/query/events.js.map +1 -1
  132. package/dist/runtime/query/executor.d.ts +4 -2
  133. package/dist/runtime/query/executor.d.ts.map +1 -1
  134. package/dist/runtime/query/executor.js +1 -0
  135. package/dist/runtime/query/executor.js.map +1 -1
  136. package/dist/runtime/query/index.d.ts +5 -3
  137. package/dist/runtime/query/index.d.ts.map +1 -1
  138. package/dist/runtime/query/index.js +2 -1
  139. package/dist/runtime/query/index.js.map +1 -1
  140. package/dist/runtime/query/iteration/index.d.ts +2 -2
  141. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  142. package/dist/runtime/query/iteration/index.js.map +1 -1
  143. package/dist/runtime/query/iteration/phases/advisory.d.ts.map +1 -1
  144. package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
  145. package/dist/runtime/query/iteration/phases/checkpoint.d.ts +1 -1
  146. package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
  147. package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
  148. package/dist/runtime/query/iteration/phases/context.d.ts +2 -2
  149. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  150. package/dist/runtime/query/iteration/phases/plan.d.ts +1 -1
  151. package/dist/runtime/query/iteration/phases/plan.d.ts.map +1 -1
  152. package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
  153. package/dist/runtime/query/prompt.d.ts +2 -2
  154. package/dist/runtime/query/prompt.d.ts.map +1 -1
  155. package/dist/runtime/query/prompt.js.map +1 -1
  156. package/dist/runtime/query/result.d.ts +1 -1
  157. package/dist/runtime/query/result.d.ts.map +1 -1
  158. package/dist/runtime/query/result.js.map +1 -1
  159. package/dist/runtime/query/tooling.d.ts +4 -2
  160. package/dist/runtime/query/tooling.d.ts.map +1 -1
  161. package/dist/runtime/query/tooling.js +1 -0
  162. package/dist/runtime/query/tooling.js.map +1 -1
  163. package/dist/store/conversation/memory.d.ts +1 -1
  164. package/dist/store/conversation/memory.d.ts.map +1 -1
  165. package/dist/store/conversation/memory.js +15 -3
  166. package/dist/store/conversation/memory.js.map +1 -1
  167. package/dist/store/run/disk.d.ts +1 -2
  168. package/dist/store/run/disk.d.ts.map +1 -1
  169. package/dist/store/run/disk.js +21 -13
  170. package/dist/store/run/disk.js.map +1 -1
  171. package/dist/tools/builtins/__tests__/structuredOutput.example.d.ts +140 -0
  172. package/dist/tools/builtins/__tests__/structuredOutput.example.d.ts.map +1 -0
  173. package/dist/tools/builtins/__tests__/structuredOutput.example.js +183 -0
  174. package/dist/tools/builtins/__tests__/structuredOutput.example.js.map +1 -0
  175. package/dist/tools/builtins/__tests__/structuredOutput.test.d.ts +2 -0
  176. package/dist/tools/builtins/__tests__/structuredOutput.test.d.ts.map +1 -0
  177. package/dist/tools/builtins/__tests__/structuredOutput.test.js +224 -0
  178. package/dist/tools/builtins/__tests__/structuredOutput.test.js.map +1 -0
  179. package/dist/tools/builtins/grep.d.ts.map +1 -1
  180. package/dist/tools/builtins/grep.js +1 -2
  181. package/dist/tools/builtins/grep.js.map +1 -1
  182. package/dist/tools/builtins/index.d.ts +1 -0
  183. package/dist/tools/builtins/index.d.ts.map +1 -1
  184. package/dist/tools/builtins/index.js +3 -0
  185. package/dist/tools/builtins/index.js.map +1 -1
  186. package/dist/tools/builtins/ls.d.ts +1 -1
  187. package/dist/tools/builtins/structuredOutput.d.ts +27 -0
  188. package/dist/tools/builtins/structuredOutput.d.ts.map +1 -0
  189. package/dist/tools/builtins/structuredOutput.js +46 -0
  190. package/dist/tools/builtins/structuredOutput.js.map +1 -0
  191. package/dist/tools/task/list.d.ts +1 -1
  192. package/dist/tools/task/list.d.ts.map +1 -1
  193. package/dist/tools/task/list.js.map +1 -1
  194. package/dist/types/agent/base.d.ts +4 -1
  195. package/dist/types/agent/base.d.ts.map +1 -1
  196. package/dist/types/agent/index.d.ts +1 -0
  197. package/dist/types/agent/index.d.ts.map +1 -1
  198. package/dist/types/agent/index.js +1 -0
  199. package/dist/types/agent/index.js.map +1 -1
  200. package/dist/types/agent/manager.d.ts +27 -0
  201. package/dist/types/agent/manager.d.ts.map +1 -0
  202. package/dist/types/agent/manager.js +2 -0
  203. package/dist/types/agent/manager.js.map +1 -0
  204. package/dist/types/agent/reactive.d.ts +2 -2
  205. package/dist/types/agent/reactive.d.ts.map +1 -1
  206. package/dist/types/agent/supervisor.d.ts +2 -2
  207. package/dist/types/agent/supervisor.d.ts.map +1 -1
  208. package/dist/types/agent/task.d.ts +0 -2
  209. package/dist/types/agent/task.d.ts.map +1 -1
  210. package/dist/types/agent/task.js +0 -2
  211. package/dist/types/agent/task.js.map +1 -1
  212. package/dist/types/common/index.d.ts +0 -1
  213. package/dist/types/common/index.d.ts.map +1 -1
  214. package/dist/types/common/index.js +0 -1
  215. package/dist/types/common/index.js.map +1 -1
  216. package/dist/types/hitl/index.d.ts +1 -2
  217. package/dist/types/hitl/index.d.ts.map +1 -1
  218. package/dist/types/hitl/index.js.map +1 -1
  219. package/dist/types/invocation/__tests__/state.test.d.ts +2 -0
  220. package/dist/types/invocation/__tests__/state.test.d.ts.map +1 -0
  221. package/dist/types/invocation/__tests__/state.test.js +167 -0
  222. package/dist/types/invocation/__tests__/state.test.js.map +1 -0
  223. package/dist/types/invocation/index.d.ts +37 -0
  224. package/dist/types/invocation/index.d.ts.map +1 -0
  225. package/dist/types/invocation/index.js +23 -0
  226. package/dist/types/invocation/index.js.map +1 -0
  227. package/dist/types/plugin/index.d.ts +6 -0
  228. package/dist/types/plugin/index.d.ts.map +1 -1
  229. package/dist/types/plugin/index.js +16 -0
  230. package/dist/types/plugin/index.js.map +1 -1
  231. package/dist/types/run/events.d.ts +1 -1
  232. package/dist/types/run/events.d.ts.map +1 -1
  233. package/dist/types/run/index.d.ts +1 -0
  234. package/dist/types/run/index.d.ts.map +1 -1
  235. package/dist/types/run/index.js +1 -0
  236. package/dist/types/run/index.js.map +1 -1
  237. package/dist/types/run/metadata.d.ts +1 -1
  238. package/dist/types/run/metadata.d.ts.map +1 -1
  239. package/dist/types/run/state.d.ts +1 -1
  240. package/dist/types/run/state.d.ts.map +1 -1
  241. package/dist/types/run/stop-reason.d.ts +2 -0
  242. package/dist/types/run/stop-reason.d.ts.map +1 -0
  243. package/dist/types/run/stop-reason.js +2 -0
  244. package/dist/types/run/stop-reason.js.map +1 -0
  245. package/dist/types/structured-output/index.d.ts +51 -0
  246. package/dist/types/structured-output/index.d.ts.map +1 -0
  247. package/dist/types/structured-output/index.js +2 -0
  248. package/dist/types/structured-output/index.js.map +1 -0
  249. package/dist/types/tool/index.d.ts +36 -0
  250. package/dist/types/tool/index.d.ts.map +1 -1
  251. package/package.json +1 -1
  252. package/src/advisory/executor.ts +2 -4
  253. package/src/agents/AbstractAgent.ts +26 -3
  254. package/src/agents/PipelineAgent.ts +1 -1
  255. package/src/agents/ReactiveAgent.ts +1 -0
  256. package/src/agents/RouterAgent.ts +8 -2
  257. package/src/agents/SupervisorAgent.ts +5 -1
  258. package/src/agents/__tests__/lock.test.ts +158 -0
  259. package/src/agents/index.ts +2 -0
  260. package/src/agents/lock.ts +66 -0
  261. package/src/bridge/a2a/message.ts +1 -2
  262. package/src/bridge/tools/connector/router.ts +4 -5
  263. package/src/compaction/__tests__/SlidingWindowManager.test.ts +139 -0
  264. package/src/compaction/__tests__/dangling.test.ts +447 -0
  265. package/src/compaction/__tests__/factory.test.ts +53 -0
  266. package/src/compaction/dangling.ts +321 -0
  267. package/src/compaction/factory.ts +41 -0
  268. package/src/compaction/index.ts +14 -0
  269. package/src/compaction/interface.ts +35 -0
  270. package/src/compaction/managers/index.ts +3 -0
  271. package/src/compaction/managers/null.ts +19 -0
  272. package/src/compaction/managers/slidingWindow.ts +57 -0
  273. package/src/compaction/managers/structured.ts +169 -0
  274. package/src/compaction/types.ts +1 -1
  275. package/src/config/runtime.ts +1 -1
  276. package/src/constants/agent/index.ts +1 -1
  277. package/src/gateway/local.ts +13 -4
  278. package/src/index.ts +38 -1
  279. package/src/manager/agent/lifecycle.ts +3 -2
  280. package/src/manager/run/persistence.ts +3 -8
  281. package/src/plugin/__tests__/lifecycle.test.ts +430 -0
  282. package/src/plugin/lifecycle.ts +32 -6
  283. package/src/plugin/resolver.ts +3 -3
  284. package/src/registry/agent/definitions.ts +3 -2
  285. package/src/registry/tool/execute.ts +2 -5
  286. package/src/runtime/decision/parser.ts +15 -40
  287. package/src/runtime/query/context-cache.ts +3 -4
  288. package/src/runtime/query/context.ts +1 -2
  289. package/src/runtime/query/events.ts +11 -11
  290. package/src/runtime/query/executor.ts +5 -3
  291. package/src/runtime/query/index.ts +11 -4
  292. package/src/runtime/query/iteration/index.ts +2 -2
  293. package/src/runtime/query/iteration/phases/advisory.ts +1 -2
  294. package/src/runtime/query/iteration/phases/checkpoint.ts +1 -2
  295. package/src/runtime/query/iteration/phases/context.ts +2 -2
  296. package/src/runtime/query/iteration/phases/plan.ts +1 -2
  297. package/src/runtime/query/prompt.ts +3 -3
  298. package/src/runtime/query/result.ts +1 -2
  299. package/src/runtime/query/tooling.ts +5 -2
  300. package/src/store/conversation/memory.ts +21 -5
  301. package/src/store/run/disk.ts +18 -16
  302. package/src/tools/builtins/__tests__/structuredOutput.example.ts +221 -0
  303. package/src/tools/builtins/__tests__/structuredOutput.test.ts +275 -0
  304. package/src/tools/builtins/grep.ts +1 -2
  305. package/src/tools/builtins/index.ts +3 -0
  306. package/src/tools/builtins/structuredOutput.ts +55 -0
  307. package/src/tools/task/list.ts +1 -2
  308. package/src/types/agent/base.ts +5 -1
  309. package/src/types/agent/index.ts +1 -0
  310. package/src/types/agent/manager.ts +36 -0
  311. package/src/types/agent/reactive.ts +2 -2
  312. package/src/types/agent/supervisor.ts +2 -2
  313. package/src/types/agent/task.ts +0 -4
  314. package/src/types/common/index.ts +0 -2
  315. package/src/types/hitl/index.ts +1 -2
  316. package/src/types/invocation/__tests__/state.test.ts +210 -0
  317. package/src/types/invocation/index.ts +55 -0
  318. package/src/types/plugin/index.ts +19 -0
  319. package/src/types/run/events.ts +1 -10
  320. package/src/types/run/index.ts +1 -0
  321. package/src/types/run/metadata.ts +1 -1
  322. package/src/types/run/state.ts +1 -1
  323. package/src/types/run/stop-reason.ts +10 -0
  324. package/src/types/structured-output/index.ts +56 -0
  325. 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
+ }
@@ -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,3 @@
1
+ export { NullManager } from './null.js'
2
+ export { SlidingWindowManager, type SlidingWindowManagerConfig } from './slidingWindow.js'
3
+ export { StructuredCompactionManager } from './structured.js'
@@ -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
+ }
@@ -34,4 +34,4 @@ export interface ToolResultSlot {
34
34
  timestamp: number
35
35
  }
36
36
 
37
- export type CompactionStrategy = 'structured' | 'disabled'
37
+ export type CompactionStrategy = 'structured' | 'sliding-window' | 'disabled'
@@ -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),