@namzu/sdk 56.1.0 → 57.0.0

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 (247) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/index.d.ts +1 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/index.js +1 -0
  8. package/dist/agents/index.js.map +1 -1
  9. package/dist/agents/plan.d.ts +20 -0
  10. package/dist/agents/plan.d.ts.map +1 -0
  11. package/dist/agents/plan.js +27 -0
  12. package/dist/agents/plan.js.map +1 -0
  13. package/dist/compaction/context-breakdown.d.ts +1 -1
  14. package/dist/compaction/context-breakdown.d.ts.map +1 -1
  15. package/dist/compaction/context-breakdown.js +9 -2
  16. package/dist/compaction/context-breakdown.js.map +1 -1
  17. package/dist/compaction/dangling.d.ts +12 -1
  18. package/dist/compaction/dangling.d.ts.map +1 -1
  19. package/dist/compaction/dangling.js +17 -3
  20. package/dist/compaction/dangling.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -2
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js.map +1 -1
  24. package/dist/compaction/manager.d.ts +16 -0
  25. package/dist/compaction/manager.d.ts.map +1 -1
  26. package/dist/compaction/manager.js +55 -0
  27. package/dist/compaction/manager.js.map +1 -1
  28. package/dist/compaction/manual.d.ts +2 -0
  29. package/dist/compaction/manual.d.ts.map +1 -1
  30. package/dist/compaction/manual.js +33 -4
  31. package/dist/compaction/manual.js.map +1 -1
  32. package/dist/compaction/restore.d.ts +51 -0
  33. package/dist/compaction/restore.d.ts.map +1 -0
  34. package/dist/compaction/restore.js +123 -0
  35. package/dist/compaction/restore.js.map +1 -0
  36. package/dist/compaction/types.d.ts +22 -0
  37. package/dist/compaction/types.d.ts.map +1 -1
  38. package/dist/compaction/verifier.d.ts.map +1 -1
  39. package/dist/compaction/verifier.js +3 -1
  40. package/dist/compaction/verifier.js.map +1 -1
  41. package/dist/compaction/wire.d.ts +3 -1
  42. package/dist/compaction/wire.d.ts.map +1 -1
  43. package/dist/compaction/wire.js +16 -0
  44. package/dist/compaction/wire.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/plugin/shell-hook.d.ts +19 -1
  48. package/dist/plugin/shell-hook.d.ts.map +1 -1
  49. package/dist/plugin/shell-hook.js +21 -5
  50. package/dist/plugin/shell-hook.js.map +1 -1
  51. package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
  52. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  53. package/dist/prompt/coding-agent-doctrine.js +3 -2
  54. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  55. package/dist/provider/errors.d.ts +3 -0
  56. package/dist/provider/errors.d.ts.map +1 -1
  57. package/dist/provider/errors.js +23 -3
  58. package/dist/provider/errors.js.map +1 -1
  59. package/dist/provider/fallback.d.ts.map +1 -1
  60. package/dist/provider/fallback.js +34 -3
  61. package/dist/provider/fallback.js.map +1 -1
  62. package/dist/provider/retry.d.ts +12 -5
  63. package/dist/provider/retry.d.ts.map +1 -1
  64. package/dist/provider/retry.js +22 -8
  65. package/dist/provider/retry.js.map +1 -1
  66. package/dist/public-runtime.d.ts +5 -3
  67. package/dist/public-runtime.d.ts.map +1 -1
  68. package/dist/public-runtime.js +5 -2
  69. package/dist/public-runtime.js.map +1 -1
  70. package/dist/public-types.d.ts +1 -1
  71. package/dist/public-types.d.ts.map +1 -1
  72. package/dist/runtime/jobs/registry.d.ts +42 -0
  73. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  74. package/dist/runtime/jobs/registry.js +77 -0
  75. package/dist/runtime/jobs/registry.js.map +1 -1
  76. package/dist/runtime/query/calls-that-never-started.d.ts +12 -0
  77. package/dist/runtime/query/calls-that-never-started.d.ts.map +1 -0
  78. package/dist/runtime/query/calls-that-never-started.js +41 -0
  79. package/dist/runtime/query/calls-that-never-started.js.map +1 -0
  80. package/dist/runtime/query/executor/tool-call-admission.d.ts +2 -2
  81. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  82. package/dist/runtime/query/executor/tool-call-admission.js +13 -9
  83. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  84. package/dist/runtime/query/executor.d.ts +16 -1
  85. package/dist/runtime/query/executor.d.ts.map +1 -1
  86. package/dist/runtime/query/executor.js +45 -2
  87. package/dist/runtime/query/executor.js.map +1 -1
  88. package/dist/runtime/query/file-evidence-context.d.ts +21 -4
  89. package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
  90. package/dist/runtime/query/file-evidence-context.js +70 -8
  91. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  92. package/dist/runtime/query/index.d.ts +6 -0
  93. package/dist/runtime/query/index.d.ts.map +1 -1
  94. package/dist/runtime/query/index.js +7 -11
  95. package/dist/runtime/query/index.js.map +1 -1
  96. package/dist/runtime/query/iteration/index.d.ts +1 -2
  97. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/index.js +30 -19
  99. package/dist/runtime/query/iteration/index.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  101. package/dist/runtime/query/iteration/phases/compaction.js +20 -0
  102. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  104. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  105. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  106. package/dist/runtime/query/iteration/recorded-context.d.ts +18 -0
  107. package/dist/runtime/query/iteration/recorded-context.d.ts.map +1 -0
  108. package/dist/runtime/query/iteration/recorded-context.js +37 -0
  109. package/dist/runtime/query/iteration/recorded-context.js.map +1 -0
  110. package/dist/runtime/query/iteration/step-context.d.ts +15 -51
  111. package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
  112. package/dist/runtime/query/iteration/step-context.js +24 -134
  113. package/dist/runtime/query/iteration/step-context.js.map +1 -1
  114. package/dist/runtime/query/iteration/step-shaping.d.ts +20 -8
  115. package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
  116. package/dist/runtime/query/iteration/step-shaping.js +52 -26
  117. package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
  118. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  119. package/dist/runtime/query/iteration/stream-turn.js +45 -25
  120. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  121. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  122. package/dist/runtime/query/prepare-turn.js +10 -1
  123. package/dist/runtime/query/prepare-turn.js.map +1 -1
  124. package/dist/scheduler/completion-inbox.d.ts +27 -0
  125. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  126. package/dist/scheduler/completion-inbox.js +90 -23
  127. package/dist/scheduler/completion-inbox.js.map +1 -1
  128. package/dist/store/session-index/fts.d.ts.map +1 -1
  129. package/dist/store/session-index/fts.js +6 -0
  130. package/dist/store/session-index/fts.js.map +1 -1
  131. package/dist/test-support/file-evidence-held.d.ts +11 -0
  132. package/dist/test-support/file-evidence-held.d.ts.map +1 -0
  133. package/dist/test-support/file-evidence-held.js +28 -0
  134. package/dist/test-support/file-evidence-held.js.map +1 -0
  135. package/dist/test-support/request-only.d.ts +10 -0
  136. package/dist/test-support/request-only.d.ts.map +1 -0
  137. package/dist/test-support/request-only.js +15 -0
  138. package/dist/test-support/request-only.js.map +1 -0
  139. package/dist/tools/admit-facts.d.ts.map +1 -1
  140. package/dist/tools/admit-facts.js +1 -0
  141. package/dist/tools/admit-facts.js.map +1 -1
  142. package/dist/tools/builtins/bash.d.ts +42 -1
  143. package/dist/tools/builtins/bash.d.ts.map +1 -1
  144. package/dist/tools/builtins/bash.js +115 -48
  145. package/dist/tools/builtins/bash.js.map +1 -1
  146. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  147. package/dist/tools/builtins/read-file.js +55 -5
  148. package/dist/tools/builtins/read-file.js.map +1 -1
  149. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  150. package/dist/tools/file-read-tracker.js +28 -0
  151. package/dist/tools/file-read-tracker.js.map +1 -1
  152. package/dist/tools/untrusted-envelope.d.ts +7 -0
  153. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  154. package/dist/tools/untrusted-envelope.js +3 -1
  155. package/dist/tools/untrusted-envelope.js.map +1 -1
  156. package/dist/turn/evidence-recall.d.ts +19 -8
  157. package/dist/turn/evidence-recall.d.ts.map +1 -1
  158. package/dist/turn/evidence-recall.js +134 -53
  159. package/dist/turn/evidence-recall.js.map +1 -1
  160. package/dist/turn/memory-recall.d.ts +18 -0
  161. package/dist/turn/memory-recall.d.ts.map +1 -1
  162. package/dist/turn/memory-recall.js +43 -4
  163. package/dist/turn/memory-recall.js.map +1 -1
  164. package/dist/types/agent/base.d.ts +6 -0
  165. package/dist/types/agent/base.d.ts.map +1 -1
  166. package/dist/types/errors/catalog.d.ts +3 -0
  167. package/dist/types/errors/catalog.d.ts.map +1 -1
  168. package/dist/types/errors/catalog.js +24 -2
  169. package/dist/types/errors/catalog.js.map +1 -1
  170. package/dist/types/message/index.d.ts +46 -1
  171. package/dist/types/message/index.d.ts.map +1 -1
  172. package/dist/types/message/index.js +78 -1
  173. package/dist/types/message/index.js.map +1 -1
  174. package/dist/types/provider/error.d.ts +7 -0
  175. package/dist/types/provider/error.d.ts.map +1 -1
  176. package/dist/types/provider/errors.d.ts +24 -0
  177. package/dist/types/provider/errors.d.ts.map +1 -1
  178. package/dist/types/provider/errors.js +101 -1
  179. package/dist/types/provider/errors.js.map +1 -1
  180. package/dist/types/provider/stream.d.ts +6 -0
  181. package/dist/types/provider/stream.d.ts.map +1 -1
  182. package/dist/types/session/events.d.ts +8 -0
  183. package/dist/types/session/events.d.ts.map +1 -1
  184. package/dist/types/session/events.js.map +1 -1
  185. package/dist/types/session/prepare-step.d.ts +24 -0
  186. package/dist/types/session/prepare-step.d.ts.map +1 -1
  187. package/dist/types/tool/index.d.ts +39 -0
  188. package/dist/types/tool/index.d.ts.map +1 -1
  189. package/dist/types/tool/index.js.map +1 -1
  190. package/dist/types/tool/presentation.d.ts +4 -0
  191. package/dist/types/tool/presentation.d.ts.map +1 -1
  192. package/package.json +1 -1
  193. package/src/agents/QueryAgent.ts +1 -0
  194. package/src/agents/index.ts +1 -0
  195. package/src/agents/plan.ts +30 -0
  196. package/src/compaction/context-breakdown.ts +11 -3
  197. package/src/compaction/dangling.ts +35 -3
  198. package/src/compaction/index.ts +4 -0
  199. package/src/compaction/manager.ts +53 -0
  200. package/src/compaction/manual.ts +42 -10
  201. package/src/compaction/restore.ts +159 -0
  202. package/src/compaction/types.ts +25 -0
  203. package/src/compaction/verifier.ts +3 -1
  204. package/src/compaction/wire.ts +26 -1
  205. package/src/index.ts +2 -0
  206. package/src/plugin/shell-hook.ts +33 -5
  207. package/src/prompt/coding-agent-doctrine.ts +3 -2
  208. package/src/provider/errors.ts +27 -3
  209. package/src/provider/fallback.ts +34 -2
  210. package/src/provider/retry.ts +30 -10
  211. package/src/public-runtime.ts +15 -0
  212. package/src/public-types.ts +3 -0
  213. package/src/runtime/jobs/registry.ts +105 -0
  214. package/src/runtime/query/calls-that-never-started.ts +37 -0
  215. package/src/runtime/query/executor/tool-call-admission.ts +12 -9
  216. package/src/runtime/query/executor.ts +57 -2
  217. package/src/runtime/query/file-evidence-context.ts +79 -11
  218. package/src/runtime/query/index.ts +14 -10
  219. package/src/runtime/query/iteration/index.ts +32 -26
  220. package/src/runtime/query/iteration/phases/compaction.ts +19 -0
  221. package/src/runtime/query/iteration/phases/context.ts +3 -0
  222. package/src/runtime/query/iteration/recorded-context.ts +48 -0
  223. package/src/runtime/query/iteration/step-context.ts +27 -155
  224. package/src/runtime/query/iteration/step-shaping.ts +64 -40
  225. package/src/runtime/query/iteration/stream-turn.ts +47 -25
  226. package/src/runtime/query/prepare-turn.ts +10 -1
  227. package/src/scheduler/completion-inbox.ts +89 -22
  228. package/src/store/session-index/fts.ts +5 -0
  229. package/src/test-support/file-evidence-held.ts +27 -0
  230. package/src/test-support/request-only.ts +19 -0
  231. package/src/tools/admit-facts.ts +1 -0
  232. package/src/tools/builtins/bash.ts +149 -54
  233. package/src/tools/builtins/read-file.ts +66 -4
  234. package/src/tools/file-read-tracker.ts +25 -1
  235. package/src/tools/untrusted-envelope.ts +10 -1
  236. package/src/turn/evidence-recall.ts +189 -58
  237. package/src/turn/memory-recall.ts +59 -5
  238. package/src/types/agent/base.ts +7 -0
  239. package/src/types/errors/catalog.ts +31 -2
  240. package/src/types/message/index.ts +108 -1
  241. package/src/types/provider/error.ts +7 -0
  242. package/src/types/provider/errors.ts +120 -1
  243. package/src/types/provider/stream.ts +6 -0
  244. package/src/types/session/events.ts +8 -0
  245. package/src/types/session/prepare-step.ts +26 -0
  246. package/src/types/tool/index.ts +40 -0
  247. package/src/types/tool/presentation.ts +4 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The delegate that plans without touching anything.
3
+ *
4
+ * A parent that wants a design before it changes code has two bad options:
5
+ * plan in its own context, which spends the window on exploration, or hand
6
+ * the whole job to a delegate that can also edit. This type is the third:
7
+ * the same roster as `explore` (`EXPLORE_AGENT_ID`, built by filtering each
8
+ * contributing toolset with `isTrustedReadOnly`), under a prompt that asks
9
+ * for a step plan naming the files it touches and the risks it sees. The
10
+ * roster is the host's to build; only the identity and the prompt live here.
11
+ */
12
+
13
+ export const PLAN_AGENT_ID = 'plan'
14
+
15
+ export const PLAN_AGENT_DESCRIPTION =
16
+ 'A read-only sub-agent that studies the code and returns a step plan with the files it touches and the risks.'
17
+
18
+ /**
19
+ * What a plan delegate is told. It states its limits in the first person so a
20
+ * model does not describe a change as made, and fixes the shape of the answer
21
+ * because the parent acts on the steps and opens the files.
22
+ */
23
+ export const PLAN_AGENT_PROMPT = [
24
+ 'You are a read-only sub-agent dispatched to plan a change: you study the code and return a plan, never the change itself.',
25
+ 'You cannot see the parent conversation — work only from the prompt you were given.',
26
+ 'You have reading and searching tools only. You cannot edit, write or run commands, and you must not describe a change as made.',
27
+ 'Read first: find the code the change touches, the patterns it should follow and the tests that cover it. Prefer what the code already does over a design of your own.',
28
+ 'Return the plan in this shape: a one-paragraph approach; numbered steps in the order to do them, each naming the files it changes; the files to read or change first, as paths with line numbers where it helps; the risks and open questions, including what could break and how to check it.',
29
+ 'Say plainly what you could not determine. End with the plan; do not ask the parent questions.',
30
+ ].join('\n')
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { CHARS_PER_TOKEN } from '../constants/limits.js'
15
- import type { Message } from '../types/message/index.js'
15
+ import { type Message, isTurnContextEntry } from '../types/message/index.js'
16
16
  import type { LLMToolSchema } from '../types/tool/index.js'
17
17
  import { TRIGGER_BUFFER_TOKENS, TRIGGER_OUTPUT_RESERVE_CAP_TOKENS } from './context-window.js'
18
18
  import { estimateMessageTokens, estimateTextTokens } from './token-estimate.js'
@@ -166,6 +166,9 @@ function breakdownMessages(messages: readonly Message[]): ContextMessageBreakdow
166
166
  const name = nameOfCall.get(message.toolCallId) ?? 'unknown'
167
167
  toolResultsByType[name] = (toolResultsByType[name] ?? 0) + tokensOfContent(message)
168
168
  } else if (message.role === 'user') {
169
+ // Recorded context is what the host recalled for the model, not
170
+ // something the operator said, so it is counted under memory.
171
+ if (isTurnContextEntry(message)) continue
169
172
  userTokens += tokensOfContent(message)
170
173
  }
171
174
  }
@@ -175,13 +178,15 @@ function breakdownMessages(messages: readonly Message[]): ContextMessageBreakdow
175
178
  /** Split a window into the parts that fill it. */
176
179
  export function analyzeContextUsage(input: ContextUsageInput): ContextBreakdown {
177
180
  const windowTokens = Math.max(0, Math.round(input.windowTokens))
181
+ const recorded = (input.messages ?? []).filter(isTurnContextEntry)
182
+ const recordedTokens = recorded.reduce((sum, message) => sum + tokensOfContent(message), 0)
178
183
  const fixed: Record<(typeof CONTEXT_CATEGORY_ORDER)[number], number> = {
179
184
  'system-prompt': nonNegative(input.systemPromptTokens),
180
185
  'system-tools': nonNegative(input.systemToolTokens),
181
186
  'mcp-tools': nonNegative(input.mcpToolTokens),
182
187
  'mcp-instructions': nonNegative(input.mcpInstructionTokens),
183
188
  agents: nonNegative(input.agentTokens),
184
- memory: nonNegative(input.memoryTokens),
189
+ memory: nonNegative(input.memoryTokens) + recordedTokens,
185
190
  skills: nonNegative(input.skillTokens),
186
191
  messages: 0,
187
192
  }
@@ -195,7 +200,10 @@ export function analyzeContextUsage(input: ContextUsageInput): ContextBreakdown
195
200
  const rawMessages =
196
201
  measured > 0
197
202
  ? Math.max(0, measured - fixedTotal)
198
- : messages.reduce((sum, message) => sum + tokensOfContent(message), 0)
203
+ : messages.reduce(
204
+ (sum, message) => (isTurnContextEntry(message) ? sum : sum + tokensOfContent(message)),
205
+ 0,
206
+ )
199
207
  fixed.messages = Math.min(rawMessages, Math.max(0, windowTokens - fixedTotal - bufferTokens))
200
208
 
201
209
  const totalTokens = measured > 0 ? Math.max(measured, fixedTotal) : fixedTotal + rawMessages
@@ -231,6 +231,8 @@ export interface ToolHistoryRepairReport {
231
231
  readonly orphanedToolResultsRemoved: number
232
232
  /** Error results inserted for calls whose durable outcome is unavailable. */
233
233
  readonly syntheticToolResultsInserted: number
234
+ /** Of those, the results that say the call never started. Absent when none. */
235
+ readonly neverRanToolResultsInserted?: number
234
236
  }
235
237
 
236
238
  /** A repaired copy and the exact changes made to it. */
@@ -274,7 +276,30 @@ function validateToolCallIds(messages: readonly Message[]): void {
274
276
  }
275
277
  }
276
278
 
277
- function unknownOutcomeResult(call: ToolCall, assistant: AssistantMessage): ToolMessage {
279
+ /** What is known about the unanswered calls of a history being repaired. */
280
+ export interface ToolHistoryRepairOptions {
281
+ /**
282
+ * Ids of calls known never to have started (queued, or waiting for a
283
+ * person's approval, when the session ended). Their synthetic result says
284
+ * nothing ran, instead of calling the outcome unknown.
285
+ */
286
+ readonly neverRan?: ReadonlySet<string>
287
+ }
288
+
289
+ function unknownOutcomeResult(
290
+ call: ToolCall,
291
+ assistant: AssistantMessage,
292
+ neverRan: boolean,
293
+ ): ToolMessage {
294
+ if (neverRan) {
295
+ return {
296
+ role: CONSTANTS.TOOL_ROLE,
297
+ toolCallId: call.id,
298
+ isError: true,
299
+ content: `The \`${call.function.name}\` call did not run: the session ended before it started (it may have been waiting for the user's approval, which was never given). Nothing was changed. If it is still needed, call it again.`,
300
+ ...(assistant.timestamp !== undefined ? { timestamp: assistant.timestamp } : {}),
301
+ }
302
+ }
278
303
  return {
279
304
  role: CONSTANTS.TOOL_ROLE,
280
305
  toolCallId: call.id,
@@ -294,13 +319,17 @@ function unknownOutcomeResult(call: ToolCall, assistant: AssistantMessage): Tool
294
319
  * fail closed: changing ids or tool calls would invalidate opaque native replay
295
320
  * state carried by the assistant message.
296
321
  */
297
- export function repairToolMessageHistory(messages: readonly Message[]): ToolHistoryRepairResult {
322
+ export function repairToolMessageHistory(
323
+ messages: readonly Message[],
324
+ options: ToolHistoryRepairOptions = {},
325
+ ): ToolHistoryRepairResult {
298
326
  validateToolCallIds(messages)
299
327
 
300
328
  const repaired: Message[] = []
301
329
  let duplicateToolResultsRemoved = 0
302
330
  let orphanedToolResultsRemoved = 0
303
331
  let syntheticToolResultsInserted = 0
332
+ let neverRanToolResultsInserted = 0
304
333
  let index = 0
305
334
 
306
335
  while (index < messages.length) {
@@ -352,8 +381,10 @@ export function repairToolMessageHistory(messages: readonly Message[]): ToolHist
352
381
 
353
382
  for (const call of calls) {
354
383
  if (answered.has(call.id)) continue
355
- repaired.push(unknownOutcomeResult(call, assistant))
384
+ const neverRan = options.neverRan?.has(call.id) === true
385
+ repaired.push(unknownOutcomeResult(call, assistant, neverRan))
356
386
  syntheticToolResultsInserted++
387
+ if (neverRan) neverRanToolResultsInserted++
357
388
  }
358
389
  index = resultEnd
359
390
  }
@@ -364,6 +395,7 @@ export function repairToolMessageHistory(messages: readonly Message[]): ToolHist
364
395
  duplicateToolResultsRemoved,
365
396
  orphanedToolResultsRemoved,
366
397
  syntheticToolResultsInserted,
398
+ ...(neverRanToolResultsInserted > 0 ? { neverRanToolResultsInserted } : {}),
367
399
  },
368
400
  }
369
401
  }
@@ -4,6 +4,9 @@ export type {
4
4
  FileSlot,
5
5
  PinSlot,
6
6
  PlanSlot,
7
+ RestorableFile,
8
+ RestorableSkill,
9
+ RestorableState,
7
10
  ToolResultSlot,
8
11
  WorkingState,
9
12
  WorkingStatePin,
@@ -11,6 +14,7 @@ export type {
11
14
 
12
15
  export type {
13
16
  DanglingResult,
17
+ ToolHistoryRepairOptions,
14
18
  ToolHistoryRepairReport,
15
19
  ToolHistoryRepairResult,
16
20
  } from './dangling.js'
@@ -4,10 +4,18 @@ import type {
4
4
  FileSlot,
5
5
  PinSlot,
6
6
  PlanSlot,
7
+ RestorableState,
7
8
  ToolResultSlot,
8
9
  WorkingState,
9
10
  } from './types.js'
10
11
 
12
+ /** Skills kept for restoring; the oldest is dropped past this. */
13
+ export const MAX_RESTORABLE_SKILLS = 8
14
+ /** Files kept for restoring; the oldest is dropped past this. */
15
+ export const MAX_RESTORABLE_FILES = 12
16
+ /** Characters of one skill or file kept for restoring (a little over the 5,000 tokens a restore shows, so the cut is visible). */
17
+ export const MAX_RESTORABLE_CHARS = 24_000
18
+
11
19
  /** Pins kept at most; the oldest is dropped past this, and the drop is counted. */
12
20
  export const MAX_PINS = 40
13
21
  /** Characters one pin may carry; a pin is a fact, not a document. */
@@ -23,6 +31,10 @@ export function truncateStateText(text: string, max: number): string {
23
31
  return `${head}${marker}`
24
32
  }
25
33
 
34
+ function normalizeRestorablePath(path: string): string {
35
+ return path.trim().replace(/^\.\//, '')
36
+ }
37
+
26
38
  function createEmptyState(): WorkingState {
27
39
  return {
28
40
  task: '',
@@ -90,6 +102,47 @@ export class WorkingStateManager {
90
102
  }
91
103
  }
92
104
 
105
+ private restorableState(): RestorableState {
106
+ if (!this.state.restorable) this.state.restorable = { skills: [], files: [] }
107
+ return this.state.restorable
108
+ }
109
+
110
+ /**
111
+ * Remember a skill's instructions so a compaction can put them back.
112
+ * `append` adds a later page of a skill that arrives in pages.
113
+ */
114
+ recordSkillBody(name: string, body: string, append = false): void {
115
+ const key = name.trim()
116
+ if (key.length === 0 || body.trim().length === 0) return
117
+ const restorable = this.restorableState()
118
+ const index = restorable.skills.findIndex((skill) => skill.name === key)
119
+ const previous = index >= 0 ? restorable.skills[index] : undefined
120
+ const joined = append && previous ? `${previous.body}${body}` : body
121
+ if (index >= 0) restorable.skills.splice(index, 1)
122
+ restorable.skills.push({ name: key, body: joined.slice(0, MAX_RESTORABLE_CHARS) })
123
+ while (restorable.skills.length > MAX_RESTORABLE_SKILLS) restorable.skills.shift()
124
+ }
125
+
126
+ /** Remember what a read returned so a compaction can put it back. */
127
+ recordFileText(path: string, text: string): void {
128
+ const key = normalizeRestorablePath(path)
129
+ if (key.length === 0 || text.trim().length === 0) return
130
+ const restorable = this.restorableState()
131
+ const index = restorable.files.findIndex((file) => file.path === key)
132
+ if (index >= 0) restorable.files.splice(index, 1)
133
+ restorable.files.push({ path: key, text: text.slice(0, MAX_RESTORABLE_CHARS) })
134
+ while (restorable.files.length > MAX_RESTORABLE_FILES) restorable.files.shift()
135
+ }
136
+
137
+ /** A file that was changed after it was read no longer has a body worth restoring. */
138
+ forgetFileText(path: string): void {
139
+ const files = this.state.restorable?.files
140
+ if (!files) return
141
+ const key = normalizeRestorablePath(path)
142
+ const index = files.findIndex((file) => file.path === key)
143
+ if (index >= 0) files.splice(index, 1)
144
+ }
145
+
93
146
  addDecision(decision: string): void {
94
147
  this.pushWithEviction('decisions', this.state.decisions, decision, this.config.maxListSize)
95
148
  }
@@ -17,6 +17,7 @@ import {
17
17
  } from './extractor.js'
18
18
  import { WorkingStateManager } from './manager.js'
19
19
  import { planCompaction } from './plan.js'
20
+ import { recordRestorable, renderRestoredContext } from './restore.js'
20
21
  import { findRetainedIndices } from './retention.js'
21
22
  import { buildCompactionMessage, isCompactionMessage } from './summary.js'
22
23
  import { type CompactionVerificationOptions, buildVerifiedSummary } from './verifier.js'
@@ -69,6 +70,8 @@ export interface CompactNowInput extends CompactionVerificationOptions {
69
70
  * model at the originals ({@link COMPACTION_RECOVERY_NOTE}). Absent adds none.
70
71
  */
71
72
  readonly recoveryNote?: string
73
+ /** Text the host wants carried across, such as where its plan file is; added to the restored context. */
74
+ readonly restoreNote?: string
72
75
  }
73
76
 
74
77
  async function publishManualCompaction(
@@ -116,6 +119,23 @@ function splice(
116
119
  return { messages: [...preservedSystem, summary, ...retainedOlder, ...recentMessages], summary }
117
120
  }
118
121
 
122
+ /** The summary with the loaded skills and read files put back after it. */
123
+ function withRestoredContext(
124
+ body: string,
125
+ manager: WorkingStateManager,
126
+ input: CompactNowInput,
127
+ ): string {
128
+ const window = resolveContextWindow(
129
+ input.contextWindowTokens ?? input.config.contextWindowTokens,
130
+ input.model,
131
+ )
132
+ const restored = renderRestoredContext(manager.getState(), {
133
+ contextWindowTokens: window.tokens,
134
+ ...(input.restoreNote ? { hostNote: input.restoreNote } : {}),
135
+ })
136
+ return restored ? `${body}\n\n${restored}` : body
137
+ }
138
+
119
139
  /** Build the state a host-triggered pass is about to replace. */
120
140
  function populateWorkingState(
121
141
  manager: WorkingStateManager,
@@ -124,6 +144,7 @@ function populateWorkingState(
124
144
  ): void {
125
145
  let firstUser = true
126
146
  const toolNames = new Map<string, string>()
147
+ const toolInputs = new Map<string, unknown>()
127
148
 
128
149
  for (const message of messages) {
129
150
  switch (message.role) {
@@ -139,17 +160,26 @@ function populateWorkingState(
139
160
  if (message.content) extractFromAssistantMessage(manager, message.content, config)
140
161
  for (const call of message.toolCalls ?? []) {
141
162
  toolNames.set(call.id, call.function.name)
163
+ try {
164
+ toolInputs.set(call.id, JSON.parse(call.function.arguments))
165
+ } catch {
166
+ // Unparseable arguments restore nothing.
167
+ }
142
168
  extractFromToolCall(manager, call.function.name, call.function.arguments)
143
169
  }
144
170
  break
145
- case 'tool':
146
- extractFromToolResult(
147
- manager,
148
- toolNames.get(message.toolCallId) ?? 'tool',
149
- toolResultToText(message.content),
150
- message.isError === true,
151
- )
171
+ case 'tool': {
172
+ const toolName = toolNames.get(message.toolCallId) ?? 'tool'
173
+ const text = toolResultToText(message.content)
174
+ extractFromToolResult(manager, toolName, text, message.isError === true)
175
+ recordRestorable(manager, {
176
+ toolName,
177
+ input: toolInputs.get(message.toolCallId),
178
+ output: text,
179
+ success: message.isError !== true,
180
+ })
152
181
  break
182
+ }
153
183
  case 'system':
154
184
  break
155
185
  }
@@ -218,7 +248,7 @@ export async function compactNow(input: CompactNowInput): Promise<CompactionResu
218
248
 
219
249
  const { messages, summary } = splice(
220
250
  preservedSystem,
221
- body,
251
+ withRestoredContext(body, manager, input),
222
252
  retainedOlder,
223
253
  plan.recentMessages,
224
254
  input.recoveryNote,
@@ -230,7 +260,6 @@ export async function compactNow(input: CompactNowInput): Promise<CompactionResu
230
260
  usage,
231
261
  })
232
262
  }
233
-
234
263
  export interface CompactRegionInput extends CompactNowInput {
235
264
  /** First index to summarise, inclusive. */
236
265
  readonly start: number
@@ -311,7 +340,10 @@ export async function compactRegion(input: CompactRegionInput): Promise<Compacti
311
340
 
312
341
  // Same cross-run ownership as compactNow: a selected region has been
313
342
  // replaced outside a turn, so its only surviving account is pinned.
314
- const summary = { ...buildCompactionMessage(body, input.recoveryNote), retain: true }
343
+ const summary = {
344
+ ...buildCompactionMessage(withRestoredContext(body, manager, input), input.recoveryNote),
345
+ retain: true,
346
+ }
315
347
  const out = [...messages.slice(0, start), summary, ...retainedSelected, ...messages.slice(end)]
316
348
 
317
349
  // Checked after the splice, not only before it. The edges being
@@ -0,0 +1,159 @@
1
+ import { resolve as resolvePath } from 'node:path'
2
+
3
+ import { CHARS_PER_TOKEN } from '../constants/limits.js'
4
+ import type { WorkingStateManager } from './manager.js'
5
+ import type { WorkingState } from './types.js'
6
+
7
+ /**
8
+ * What a compaction puts back after the summary.
9
+ *
10
+ * A summary says a skill was loaded and a file was read; it cannot give the
11
+ * model the skill's steps or the file's lines, so the next step either guesses
12
+ * from a paraphrase or loads and reads the same things again. This puts the
13
+ * most recent of each back, verbatim, inside fixed caps — and inside a share
14
+ * of the window, so a small window is not refilled by the thing that was meant
15
+ * to free it.
16
+ */
17
+
18
+ /** Skills restored per pass, most recent first. */
19
+ export const RESTORE_MAX_SKILLS = 8
20
+ /** Tokens of one restored skill. */
21
+ export const RESTORE_SKILL_TOKENS = 5_000
22
+ /** Tokens of all restored skills together. */
23
+ export const RESTORE_SKILLS_TOTAL_TOKENS = 25_000
24
+ /** Files restored per pass, most recent first. */
25
+ export const RESTORE_MAX_FILES = 5
26
+ /** Tokens of one restored file. */
27
+ export const RESTORE_FILE_TOKENS = 5_000
28
+ /** Tokens of all restored files together. */
29
+ export const RESTORE_FILES_TOTAL_TOKENS = 50_000
30
+ /** The share of the window all restored files may take. */
31
+ export const RESTORE_FILES_WINDOW_SHARE = 0.1
32
+ /** The share of the window all restored skills may take. */
33
+ export const RESTORE_SKILLS_WINDOW_SHARE = 0.05
34
+
35
+ const CUT_MARKER = '\n… [cut to fit the restore budget]'
36
+ const SKILL_CONTINUATION = /\n\n\[More skill instructions remain\.[^\]]*\]/g
37
+
38
+ function isInstructionPath(path: string): boolean {
39
+ const normal = path.replace(/\\/g, '/')
40
+ const name = normal.slice(normal.lastIndexOf('/') + 1)
41
+ return name === 'AGENTS.md' || name === 'AGENTS.local.md' || normal.includes('/.namzu/rules/')
42
+ }
43
+
44
+ function resolveAgainst(path: string, workingDirectory: string | undefined): string {
45
+ return workingDirectory ? resolvePath(workingDirectory, path) : path
46
+ }
47
+
48
+ function isPlanPath(path: string): boolean {
49
+ return /(^|\/)plans\/plan\.md$/.test(path.replace(/\\/g, '/'))
50
+ }
51
+
52
+ /**
53
+ * Note a finished tool call, if it is one whose result a compaction restores.
54
+ * Called with the call's input and the text the model was shown.
55
+ */
56
+ export function recordRestorable(
57
+ manager: WorkingStateManager,
58
+ call: {
59
+ readonly toolName: string
60
+ readonly input: unknown
61
+ readonly output: string
62
+ readonly success: boolean
63
+ /** The read was answered as unchanged: the body is already stored from the earlier read. */
64
+ readonly unchanged?: boolean
65
+ /** Resolves a relative path, so one file is one entry however the model spelled it. */
66
+ readonly workingDirectory?: string
67
+ },
68
+ ): void {
69
+ const input =
70
+ typeof call.input === 'object' && call.input !== null
71
+ ? (call.input as Record<string, unknown>)
72
+ : undefined
73
+ if (!input) return
74
+ if (call.toolName === 'skill') {
75
+ if (!call.success || typeof input.name !== 'string') return
76
+ manager.recordSkillBody(
77
+ input.name,
78
+ call.output.replace(SKILL_CONTINUATION, ''),
79
+ typeof input.cursor === 'string' && input.cursor.length > 0,
80
+ )
81
+ return
82
+ }
83
+ if (call.toolName === 'read') {
84
+ const path = typeof input.path === 'string' ? input.path : undefined
85
+ if (!path || !call.success || call.unchanged) return
86
+ manager.recordFileText(resolveAgainst(path, call.workingDirectory), call.output)
87
+ return
88
+ }
89
+ if (call.toolName === 'write' || call.toolName === 'edit') {
90
+ const path = typeof input.path === 'string' ? input.path : undefined
91
+ // The body a read showed is behind the file now.
92
+ if (path && call.success) manager.forgetFileText(resolveAgainst(path, call.workingDirectory))
93
+ }
94
+ }
95
+
96
+ function fit(text: string, tokens: number): string {
97
+ const room = tokens * CHARS_PER_TOKEN
98
+ return text.length <= room
99
+ ? text
100
+ : `${text.slice(0, Math.max(0, room - CUT_MARKER.length))}${CUT_MARKER}`
101
+ }
102
+
103
+ export interface RestoreRenderOptions {
104
+ /** The window the pass is working against; caps scale down on a small one. */
105
+ readonly contextWindowTokens?: number
106
+ /** A host's own line to carry across, such as where its plan file is. */
107
+ readonly hostNote?: string
108
+ }
109
+
110
+ /** The section to append after a summary, or `undefined` when there is nothing to put back. */
111
+ export function renderRestoredContext(
112
+ state: WorkingState,
113
+ options: RestoreRenderOptions = {},
114
+ ): string | undefined {
115
+ const window = options.contextWindowTokens
116
+ const skillsTotal =
117
+ window === undefined
118
+ ? RESTORE_SKILLS_TOTAL_TOKENS
119
+ : Math.min(RESTORE_SKILLS_TOTAL_TOKENS, Math.floor(window * RESTORE_SKILLS_WINDOW_SHARE))
120
+ const filesTotal =
121
+ window === undefined
122
+ ? RESTORE_FILES_TOTAL_TOKENS
123
+ : Math.min(RESTORE_FILES_TOTAL_TOKENS, Math.floor(window * RESTORE_FILES_WINDOW_SHARE))
124
+
125
+ const sections: string[] = []
126
+
127
+ let skillBudget = skillsTotal
128
+ const skills = [...(state.restorable?.skills ?? [])].reverse()
129
+ for (const skill of skills) {
130
+ if (sections.length >= RESTORE_MAX_SKILLS || skillBudget <= 0) break
131
+ const body = fit(skill.body, Math.min(RESTORE_SKILL_TOKENS, skillBudget))
132
+ skillBudget -= Math.ceil(body.length / CHARS_PER_TOKEN)
133
+ sections.push(`### Skill: ${skill.name}\n\n${body}`)
134
+ }
135
+
136
+ let fileBudget = filesTotal
137
+ let files = 0
138
+ const candidates = [...(state.restorable?.files ?? [])].reverse()
139
+ for (const file of candidates) {
140
+ if (files >= RESTORE_MAX_FILES || fileBudget <= 0) break
141
+ if (isInstructionPath(file.path) || isPlanPath(file.path)) continue
142
+ const text = fit(file.text, Math.min(RESTORE_FILE_TOKENS, fileBudget))
143
+ fileBudget -= Math.ceil(text.length / CHARS_PER_TOKEN)
144
+ files += 1
145
+ sections.push(`### File: ${file.path}\n\n${text}`)
146
+ }
147
+
148
+ const note = options.hostNote?.trim()
149
+ if (note) sections.push(note)
150
+ if (sections.length === 0) return undefined
151
+
152
+ return [
153
+ '## Restored after compaction',
154
+ '',
155
+ 'These were loaded or read earlier in this conversation and are put back here so you can keep using them. A file below may have changed on disk since it was read; read it again before you rely on it for an edit.',
156
+ '',
157
+ sections.join('\n\n'),
158
+ ].join('\n')
159
+ }
@@ -28,6 +28,31 @@ export interface WorkingState {
28
28
  * of the state".
29
29
  */
30
30
  evicted: Record<string, number>
31
+ /**
32
+ * Bodies a compaction puts back after the summary: the skills the model
33
+ * loaded and the files it read. Kept apart from the slots above because
34
+ * they are verbatim text, not a summary of it, and are never rendered into
35
+ * the working-memory block.
36
+ */
37
+ restorable?: RestorableState
38
+ }
39
+
40
+ /** A skill the model loaded, as its instructions read. */
41
+ export interface RestorableSkill {
42
+ readonly name: string
43
+ readonly body: string
44
+ }
45
+
46
+ /** A file the model read, as the read returned it. */
47
+ export interface RestorableFile {
48
+ readonly path: string
49
+ readonly text: string
50
+ }
51
+
52
+ /** Oldest first; a later load or read of the same name or path replaces and moves to the end. */
53
+ export interface RestorableState {
54
+ skills: RestorableSkill[]
55
+ files: RestorableFile[]
31
56
  }
32
57
 
33
58
  export interface PinSlot {
@@ -55,7 +55,9 @@ function isStandingContext(message: Message): boolean {
55
55
  return (
56
56
  source?.type === 'project-instructions' ||
57
57
  (source?.type === 'runtime-context' &&
58
- (source.kind === 'step-context' || source.kind === 'reminder'))
58
+ (source.kind === 'step-context' ||
59
+ source.kind === 'reminder' ||
60
+ source.kind === 'turn-context'))
59
61
  )
60
62
  }
61
63
 
@@ -1,6 +1,13 @@
1
1
  import type { CompactionConfig } from '../config/runtime.js'
2
2
  import { WorkingStateManager } from './manager.js'
3
- import type { FileSlot, PinSlot, PlanSlot, ToolResultSlot, WorkingState } from './types.js'
3
+ import type {
4
+ FileSlot,
5
+ PinSlot,
6
+ PlanSlot,
7
+ RestorableState,
8
+ ToolResultSlot,
9
+ WorkingState,
10
+ } from './types.js'
4
11
 
5
12
  /**
6
13
  * The working state in a form that survives a process boundary.
@@ -42,6 +49,8 @@ export interface WorkingStateSnapshot {
42
49
  * completeness it does not have.
43
50
  */
44
51
  readonly evicted: Readonly<Record<string, number>>
52
+ /** Absent in snapshots written before bodies were restored; restored as none. */
53
+ readonly restorable?: RestorableState
45
54
  }
46
55
 
47
56
  export function snapshotWorkingState(manager: WorkingStateManager): WorkingStateSnapshot {
@@ -59,6 +68,14 @@ export function snapshotWorkingState(manager: WorkingStateManager): WorkingState
59
68
  userRequirements: [...state.userRequirements],
60
69
  assistantNotes: [...state.assistantNotes],
61
70
  evicted: { ...state.evicted },
71
+ ...(state.restorable
72
+ ? {
73
+ restorable: {
74
+ skills: state.restorable.skills.map((skill) => ({ ...skill })),
75
+ files: state.restorable.files.map((file) => ({ ...file })),
76
+ },
77
+ }
78
+ : {}),
62
79
  }
63
80
  }
64
81
 
@@ -89,6 +106,14 @@ export function restoreWorkingState(
89
106
  userRequirements: [...snapshot.userRequirements],
90
107
  assistantNotes: [...snapshot.assistantNotes],
91
108
  evicted: { ...snapshot.evicted },
109
+ ...(snapshot.restorable
110
+ ? {
111
+ restorable: {
112
+ skills: snapshot.restorable.skills.map((skill) => ({ ...skill })),
113
+ files: snapshot.restorable.files.map((file) => ({ ...file })),
114
+ },
115
+ }
116
+ : {}),
92
117
  }
93
118
  manager.replaceState(state)
94
119
  return manager
package/src/index.ts CHANGED
@@ -47,6 +47,8 @@ export {
47
47
  } from './runtime/jobs/job-id.js'
48
48
  export type {
49
49
  BackgroundJob,
50
+ ForegroundCommand,
51
+ ForegroundHandoffReason,
50
52
  BackgroundJobOutput,
51
53
  BackgroundJobRegistryConfig,
52
54
  BackgroundJobStatus,