@namzu/sdk 57.0.0 → 58.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 (278) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +3 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/file-definitions.d.ts +35 -4
  6. package/dist/agents/file-definitions.d.ts.map +1 -1
  7. package/dist/agents/file-definitions.js +98 -8
  8. package/dist/agents/file-definitions.js.map +1 -1
  9. package/dist/authorization/protected-paths.d.ts +9 -0
  10. package/dist/authorization/protected-paths.d.ts.map +1 -1
  11. package/dist/authorization/protected-paths.js +29 -3
  12. package/dist/authorization/protected-paths.js.map +1 -1
  13. package/dist/compaction/index.d.ts +1 -0
  14. package/dist/compaction/index.d.ts.map +1 -1
  15. package/dist/compaction/index.js.map +1 -1
  16. package/dist/compaction/manual.d.ts +7 -0
  17. package/dist/compaction/manual.d.ts.map +1 -1
  18. package/dist/compaction/manual.js +31 -12
  19. package/dist/compaction/manual.js.map +1 -1
  20. package/dist/compaction/model-summary.d.ts +101 -0
  21. package/dist/compaction/model-summary.d.ts.map +1 -0
  22. package/dist/compaction/model-summary.js +333 -0
  23. package/dist/compaction/model-summary.js.map +1 -0
  24. package/dist/compaction/verifier.d.ts +6 -1
  25. package/dist/compaction/verifier.d.ts.map +1 -1
  26. package/dist/compaction/verifier.js +3 -3
  27. package/dist/compaction/verifier.js.map +1 -1
  28. package/dist/config/runtime.d.ts +62 -0
  29. package/dist/config/runtime.d.ts.map +1 -1
  30. package/dist/config/runtime.js +16 -0
  31. package/dist/config/runtime.js.map +1 -1
  32. package/dist/connector/mcp/client.d.ts +8 -0
  33. package/dist/connector/mcp/client.d.ts.map +1 -1
  34. package/dist/connector/mcp/client.js +75 -2
  35. package/dist/connector/mcp/client.js.map +1 -1
  36. package/dist/constants/mcp/index.d.ts +2 -0
  37. package/dist/constants/mcp/index.d.ts.map +1 -1
  38. package/dist/constants/mcp/index.js +2 -0
  39. package/dist/constants/mcp/index.js.map +1 -1
  40. package/dist/constants/plugin/index.d.ts +2 -0
  41. package/dist/constants/plugin/index.d.ts.map +1 -1
  42. package/dist/constants/plugin/index.js +2 -0
  43. package/dist/constants/plugin/index.js.map +1 -1
  44. package/dist/manager/agent/lifecycle.d.ts +1 -1
  45. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  46. package/dist/manager/agent/lifecycle.js +25 -4
  47. package/dist/manager/agent/lifecycle.js.map +1 -1
  48. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  49. package/dist/manager/session/turn-recorder.js +2 -1
  50. package/dist/manager/session/turn-recorder.js.map +1 -1
  51. package/dist/persona/assembler.d.ts.map +1 -1
  52. package/dist/persona/assembler.js +2 -2
  53. package/dist/persona/assembler.js.map +1 -1
  54. package/dist/plugin/index.d.ts +1 -1
  55. package/dist/plugin/index.d.ts.map +1 -1
  56. package/dist/plugin/lifecycle.d.ts +8 -1
  57. package/dist/plugin/lifecycle.d.ts.map +1 -1
  58. package/dist/plugin/lifecycle.js +9 -2
  59. package/dist/plugin/lifecycle.js.map +1 -1
  60. package/dist/plugin/shell-hook-http.d.ts +39 -0
  61. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  62. package/dist/plugin/shell-hook-http.js +163 -0
  63. package/dist/plugin/shell-hook-http.js.map +1 -0
  64. package/dist/plugin/shell-hook.d.ts +93 -11
  65. package/dist/plugin/shell-hook.d.ts.map +1 -1
  66. package/dist/plugin/shell-hook.js +223 -39
  67. package/dist/plugin/shell-hook.js.map +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  69. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  70. package/dist/prompt/coding-agent-doctrine.js +2 -1
  71. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  72. package/dist/public-runtime.d.ts +3 -2
  73. package/dist/public-runtime.d.ts.map +1 -1
  74. package/dist/public-runtime.js +2 -2
  75. package/dist/public-runtime.js.map +1 -1
  76. package/dist/public-tools.d.ts +1 -0
  77. package/dist/public-tools.d.ts.map +1 -1
  78. package/dist/public-tools.js +2 -0
  79. package/dist/public-tools.js.map +1 -1
  80. package/dist/public-types.d.ts +1 -1
  81. package/dist/public-types.d.ts.map +1 -1
  82. package/dist/runtime/query/executor.d.ts +4 -0
  83. package/dist/runtime/query/executor.d.ts.map +1 -1
  84. package/dist/runtime/query/executor.js +47 -9
  85. package/dist/runtime/query/executor.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +29 -1
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  91. package/dist/runtime/query/iteration/index.js +6 -2
  92. package/dist/runtime/query/iteration/index.js.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  94. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  96. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/phases/compaction.js +52 -5
  99. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  101. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  102. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  105. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  106. package/dist/runtime/query/path-escalation.d.ts +2 -0
  107. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  108. package/dist/runtime/query/path-escalation.js +7 -4
  109. package/dist/runtime/query/path-escalation.js.map +1 -1
  110. package/dist/runtime/query/plugin-hooks.js +2 -2
  111. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  112. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  113. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  114. package/dist/runtime/query/prepare-turn.js +25 -0
  115. package/dist/runtime/query/prepare-turn.js.map +1 -1
  116. package/dist/runtime/query/review-policy.d.ts +3 -2
  117. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  118. package/dist/runtime/query/review-policy.js +32 -13
  119. package/dist/runtime/query/review-policy.js.map +1 -1
  120. package/dist/scheduler/local.js +1 -1
  121. package/dist/scheduler/local.js.map +1 -1
  122. package/dist/session/retention/archive.js +1 -1
  123. package/dist/session/retention/archive.js.map +1 -1
  124. package/dist/session/workspace/driver.d.ts +6 -0
  125. package/dist/session/workspace/driver.d.ts.map +1 -1
  126. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  127. package/dist/session/workspace/git-worktree.js +28 -0
  128. package/dist/session/workspace/git-worktree.js.map +1 -1
  129. package/dist/skills/index.d.ts +1 -1
  130. package/dist/skills/index.d.ts.map +1 -1
  131. package/dist/skills/index.js +1 -1
  132. package/dist/skills/index.js.map +1 -1
  133. package/dist/skills/loader.d.ts +7 -0
  134. package/dist/skills/loader.d.ts.map +1 -1
  135. package/dist/skills/loader.js +53 -0
  136. package/dist/skills/loader.js.map +1 -1
  137. package/dist/skills/registry.d.ts +1 -1
  138. package/dist/skills/registry.d.ts.map +1 -1
  139. package/dist/skills/registry.js +2 -1
  140. package/dist/skills/registry.js.map +1 -1
  141. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  142. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  143. package/dist/tools/builtins/bash-cwd.js +127 -0
  144. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  145. package/dist/tools/builtins/bash.d.ts.map +1 -1
  146. package/dist/tools/builtins/bash.js +41 -6
  147. package/dist/tools/builtins/bash.js.map +1 -1
  148. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  149. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  150. package/dist/tools/builtins/bounded-file-read.js +67 -0
  151. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  152. package/dist/tools/builtins/grep.d.ts +6 -1
  153. package/dist/tools/builtins/grep.d.ts.map +1 -1
  154. package/dist/tools/builtins/grep.js +187 -24
  155. package/dist/tools/builtins/grep.js.map +1 -1
  156. package/dist/tools/builtins/read-file.d.ts +1 -0
  157. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  158. package/dist/tools/builtins/read-file.js +113 -28
  159. package/dist/tools/builtins/read-file.js.map +1 -1
  160. package/dist/tools/builtins/read-media.d.ts +68 -0
  161. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  162. package/dist/tools/builtins/read-media.js +397 -0
  163. package/dist/tools/builtins/read-media.js.map +1 -0
  164. package/dist/tools/builtins/search-tools.js +1 -1
  165. package/dist/tools/builtins/search-tools.js.map +1 -1
  166. package/dist/tools/builtins/skill.d.ts +4 -2
  167. package/dist/tools/builtins/skill.d.ts.map +1 -1
  168. package/dist/tools/builtins/skill.js +61 -2
  169. package/dist/tools/builtins/skill.js.map +1 -1
  170. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  171. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  172. package/dist/tools/builtins/sleep-guard.js +33 -0
  173. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  174. package/dist/tools/builtins/view-image.d.ts +23 -1
  175. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  176. package/dist/tools/builtins/view-image.js +66 -58
  177. package/dist/tools/builtins/view-image.js.map +1 -1
  178. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  179. package/dist/tools/file-read-tracker.js +31 -0
  180. package/dist/tools/file-read-tracker.js.map +1 -1
  181. package/dist/toolsets/manager.js +3 -3
  182. package/dist/toolsets/manager.js.map +1 -1
  183. package/dist/types/agent/base.d.ts +8 -0
  184. package/dist/types/agent/base.d.ts.map +1 -1
  185. package/dist/types/agent/query.d.ts +2 -0
  186. package/dist/types/agent/query.d.ts.map +1 -1
  187. package/dist/types/agent/scheduler.d.ts +8 -0
  188. package/dist/types/agent/scheduler.d.ts.map +1 -1
  189. package/dist/types/agent/task.d.ts +6 -2
  190. package/dist/types/agent/task.d.ts.map +1 -1
  191. package/dist/types/agent/task.js.map +1 -1
  192. package/dist/types/connector/mcp.d.ts +46 -0
  193. package/dist/types/connector/mcp.d.ts.map +1 -1
  194. package/dist/types/hitl/index.d.ts +1 -1
  195. package/dist/types/plugin/index.d.ts +74 -7
  196. package/dist/types/plugin/index.d.ts.map +1 -1
  197. package/dist/types/plugin/index.js +12 -1
  198. package/dist/types/plugin/index.js.map +1 -1
  199. package/dist/types/session/sub-session.d.ts +6 -2
  200. package/dist/types/session/sub-session.d.ts.map +1 -1
  201. package/dist/types/skills/index.d.ts +36 -0
  202. package/dist/types/skills/index.d.ts.map +1 -1
  203. package/dist/types/skills/index.js +9 -0
  204. package/dist/types/skills/index.js.map +1 -1
  205. package/dist/types/tool/index.d.ts +37 -0
  206. package/dist/types/tool/index.d.ts.map +1 -1
  207. package/dist/types/tool/index.js.map +1 -1
  208. package/dist/types/workspace/ref.d.ts +2 -0
  209. package/dist/types/workspace/ref.d.ts.map +1 -1
  210. package/dist/utils/frontmatter.d.ts +12 -0
  211. package/dist/utils/frontmatter.d.ts.map +1 -1
  212. package/dist/utils/frontmatter.js +6 -0
  213. package/dist/utils/frontmatter.js.map +1 -1
  214. package/package.json +1 -1
  215. package/src/agents/QueryAgent.ts +3 -0
  216. package/src/agents/file-definitions.ts +126 -6
  217. package/src/authorization/protected-paths.ts +30 -2
  218. package/src/compaction/index.ts +1 -0
  219. package/src/compaction/manual.ts +68 -30
  220. package/src/compaction/model-summary.ts +412 -0
  221. package/src/compaction/verifier.ts +3 -3
  222. package/src/config/runtime.ts +16 -0
  223. package/src/connector/mcp/client.ts +87 -1
  224. package/src/constants/mcp/index.ts +3 -0
  225. package/src/constants/plugin/index.ts +2 -0
  226. package/src/manager/agent/lifecycle.ts +24 -5
  227. package/src/manager/session/turn-recorder.ts +2 -1
  228. package/src/persona/assembler.ts +2 -2
  229. package/src/plugin/index.ts +5 -0
  230. package/src/plugin/lifecycle.ts +16 -2
  231. package/src/plugin/shell-hook-http.ts +181 -0
  232. package/src/plugin/shell-hook.ts +386 -89
  233. package/src/prompt/coding-agent-doctrine.ts +2 -1
  234. package/src/public-runtime.ts +6 -0
  235. package/src/public-tools.ts +2 -0
  236. package/src/public-types.ts +5 -0
  237. package/src/runtime/query/executor.ts +54 -10
  238. package/src/runtime/query/index.ts +44 -0
  239. package/src/runtime/query/iteration/index.ts +11 -3
  240. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  241. package/src/runtime/query/iteration/phases/compaction.ts +67 -5
  242. package/src/runtime/query/iteration/phases/context.ts +6 -0
  243. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  244. package/src/runtime/query/path-escalation.ts +11 -5
  245. package/src/runtime/query/plugin-hooks.ts +2 -2
  246. package/src/runtime/query/prepare-turn.ts +33 -0
  247. package/src/runtime/query/review-policy.ts +45 -16
  248. package/src/scheduler/local.ts +1 -1
  249. package/src/session/retention/archive.ts +1 -1
  250. package/src/session/workspace/driver.ts +6 -0
  251. package/src/session/workspace/git-worktree.ts +31 -0
  252. package/src/skills/index.ts +6 -1
  253. package/src/skills/loader.ts +54 -0
  254. package/src/skills/registry.ts +7 -6
  255. package/src/tools/builtins/bash-cwd.ts +156 -0
  256. package/src/tools/builtins/bash.ts +79 -34
  257. package/src/tools/builtins/bounded-file-read.ts +80 -0
  258. package/src/tools/builtins/grep.ts +198 -28
  259. package/src/tools/builtins/read-file.ts +151 -30
  260. package/src/tools/builtins/read-media.ts +483 -0
  261. package/src/tools/builtins/search-tools.ts +1 -1
  262. package/src/tools/builtins/skill.ts +91 -2
  263. package/src/tools/builtins/sleep-guard.ts +31 -0
  264. package/src/tools/builtins/view-image.ts +74 -62
  265. package/src/tools/file-read-tracker.ts +30 -0
  266. package/src/toolsets/manager.ts +3 -3
  267. package/src/types/agent/base.ts +9 -0
  268. package/src/types/agent/query.ts +2 -0
  269. package/src/types/agent/scheduler.ts +9 -0
  270. package/src/types/agent/task.ts +6 -2
  271. package/src/types/connector/mcp.ts +48 -0
  272. package/src/types/hitl/index.ts +1 -1
  273. package/src/types/plugin/index.ts +102 -16
  274. package/src/types/session/sub-session.ts +6 -2
  275. package/src/types/skills/index.ts +48 -0
  276. package/src/types/tool/index.ts +38 -0
  277. package/src/types/workspace/ref.ts +2 -0
  278. package/src/utils/frontmatter.ts +21 -0
@@ -79,7 +79,8 @@ export const CODING_AGENT_WORKING_DOCTRINE = `## How you work
79
79
  */
80
80
  export const CODING_AGENT_DELEGATION_DOCTRINE = `### Planning and delegating
81
81
  - For work of three or more steps — several files, several distinct stages, anything you would write a checklist for — open a task list with \`task_create\` and keep it current with \`task_update\`, marking each step done when it is done rather than at the end. Skip it for a single inspect-edit-test cycle or a two-step job: the list is for the user to follow, and a list of one or two items tells them nothing.
82
- - When the \`Agent\` tool is available, delegate independent work through it and give each delegation a short, specific description; the user watches those descriptions, not the prompts. Brief each sub-agent with four things: the objective; what is out of bounds; which tools and sources to use; and the shape of the reply you want, such as a short summary with a file path for anything long. It starts with none of your context and returns what you ask for.
82
+ - When the \`Agent\` tool is available, delegate independent work through it and give each delegation a short, specific description; the user watches those descriptions, not the prompts. Brief each sub-agent with four things: the objective; what is out of bounds; which tools and sources to use; and the shape of the reply you want, such as a short summary with a file path for anything long. A fresh sub-agent starts with none of your context and returns what you ask for.
83
+ - When the \`Agent\` tool lists a \`fork\` type, use \`subagent_type: "fork"\` when a side task would fill your context with output you will not need again, such as an open-ended survey. A fork starts from this conversation under your own instructions, so its prompt is only the directive: what to do and where its scope ends, without the background. Never write or guess what a fork will find before its report arrives. A fork cannot start forks of its own.
83
84
  - When delegated agents finish, tell the user what each one found in your next message. Their results reach you and not the user, so a result you do not pass on stays unseen.
84
85
  - Delegate lookups — where something is defined, which files reference it, how a module works — with \`subagent_type: "explore"\`: it has reading and searching tools only and never interrupts the user for permission. Reserve the default sub-agent for work that changes files or runs commands.`
85
86
 
@@ -485,6 +485,7 @@ export {
485
485
  loadSkill,
486
486
  resolveSkillChain,
487
487
  SKILL_FRONTMATTER_KEYS,
488
+ SKILL_FRONTMATTER_TOLERATED_KEYS,
488
489
  SkillCollisionError,
489
490
  SkillRegistry,
490
491
  } from './skills/index.js'
@@ -1323,6 +1324,10 @@ export { isWorkingMemoryMessage } from './runtime/query/iteration/phases/working
1323
1324
  // `any` — and the first host to try (`@namzu/cli`'s `/compact`) did exactly
1324
1325
  // that before this line existed.
1325
1326
  export type { CompactNowInput, CompactionResult } from './compaction/manual.js'
1327
+ export type {
1328
+ CompactionSummaryRoute,
1329
+ CompactionSummaryRouter,
1330
+ } from './compaction/model-summary.js'
1326
1331
 
1327
1332
  export {
1328
1333
  buildVerifiedSummary,
@@ -1517,6 +1522,7 @@ export {
1517
1522
  SKILL_INVOCATION_DEFAULT,
1518
1523
  isInvocableBy,
1519
1524
  skillInvocation,
1525
+ skillModelDescription,
1520
1526
  } from './types/skills/index.js'
1521
1527
 
1522
1528
  // Reaching the web. Fetching ships guarded because its rules are about the
@@ -79,6 +79,8 @@ export {
79
79
  backgroundTasksDisabled,
80
80
  execHostShell,
81
81
  } from './tools/builtins/bash.js'
82
+ // Authorization and execution must agree after a session carries its shell cwd.
83
+ export { effectiveBashWorkingDirectory } from './tools/builtins/bash-cwd.js'
82
84
  export {
83
85
  CODE_ANALYSIS_TOOL_NAME,
84
86
  CodeAnalysisTool,
@@ -231,8 +231,13 @@ export type {
231
231
  SkippedAgentFile,
232
232
  } from './agents/file-definitions.js'
233
233
  export type {
234
+ HttpHookEntry,
235
+ PromptHookEntry,
234
236
  ShellHookEntry,
235
237
  ShellHookEvent,
238
+ ShellHookHandler,
239
+ ShellHookJudge,
240
+ ShellHookOnFailure,
236
241
  ShellHookInput,
237
242
  ShellHookOptions,
238
243
  ShellHookOutcome,
@@ -14,6 +14,11 @@ import { ProbeVetoError } from '../../probe/errors.js'
14
14
  import { probe as defaultProbeRegistry } from '../../probe/registry.js'
15
15
  import type { ProbeEnforcement } from '../../probe/registry.js'
16
16
  import type { ActivityStore } from '../../store/activity/memory.js'
17
+ import {
18
+ effectiveBashWorkingDirectory,
19
+ pinBashWorkingDirectory,
20
+ } from '../../tools/builtins/bash-cwd.js'
21
+ import { BashTool } from '../../tools/builtins/bash.js'
17
22
  import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
18
23
  import { sandboxShellSpawn, withoutBashStartup } from '../../tools/command-shell.js'
19
24
  import { isAlwaysDestructive } from '../../tools/defineTool.js'
@@ -99,6 +104,9 @@ import {
99
104
  export type { EmitEvent } from './events.js'
100
105
  import type { EmitEvent, SessionEventDraft } from './events.js'
101
106
 
107
+ /** Input and identity needed by the shared direct/nested safety checks. */
108
+ type PreparedSafetyCall = Pick<PreparedDirectCall, 'kind' | 'toolName' | 'input'>
109
+
102
110
  export type PreparedDirectCall =
103
111
  | {
104
112
  readonly kind: 'ready'
@@ -703,10 +711,12 @@ export class ToolExecutor {
703
711
  private log: Logger
704
712
  private workingStateManager?: WorkingStateManager
705
713
  private historyView?: () => readonly Message[]
714
+ private systemPromptView?: () => string | undefined
706
715
  private probes: ProbeEnforcement
707
716
  private parentSpan?: Span
708
717
  private readonly toolCallBudget?: ToolCallBudget
709
718
  private readonly preparedBatches = new WeakSet<PreparedToolBatch>()
719
+ private readonly preparedBashDirectories = new WeakMap<PreparedSafetyCall, string>()
710
720
  /** Ephemeral proof of actual execution; restored messages do not acquire it. */
711
721
  private readonly recordedObservations = new Map<
712
722
  string,
@@ -761,6 +771,11 @@ export class ToolExecutor {
761
771
  this.historyView = view
762
772
  }
763
773
 
774
+ /** The system prompt assembled for the current iteration, for `ToolContext.conversation`. */
775
+ setSystemPromptView(view: () => string | undefined): void {
776
+ this.systemPromptView = view
777
+ }
778
+
764
779
  private receiptVisible = (callId: string, renderedFingerprint: string): boolean => {
765
780
  const history = this.historyView?.()
766
781
  if (!history) return false
@@ -1174,7 +1189,7 @@ export class ToolExecutor {
1174
1189
  * safety checks did not run is the one a person must look at.
1175
1190
  */
1176
1191
  private async escalationOrUnfinished(
1177
- call: PreparedDirectCall,
1192
+ call: PreparedSafetyCall,
1178
1193
  ): Promise<ToolCallEscalation | undefined> {
1179
1194
  try {
1180
1195
  return await this.escalationOf(call)
@@ -1206,12 +1221,19 @@ export class ToolExecutor {
1206
1221
  * roots is not mounted there to be approved. The escape is looked at only
1207
1222
  * WITH one, since without one there is nothing to escape.
1208
1223
  */
1209
- private async escalationOf(call: PreparedDirectCall): Promise<ToolCallEscalation | undefined> {
1224
+ private async escalationOf(call: PreparedSafetyCall): Promise<ToolCallEscalation | undefined> {
1210
1225
  if (call.kind === 'synthetic') return undefined
1211
1226
  const tool = this.config.tools.get(call.toolName)
1212
1227
  if (!tool || call.input === null || typeof call.input !== 'object') return undefined
1213
1228
  const input = call.input as Record<string, unknown>
1214
1229
  const sandboxed = this.sandboxed
1230
+ const shellCwd =
1231
+ tool.execute === BashTool.execute
1232
+ ? effectiveBashWorkingDirectory(this.config.sessionId, this.config.workingDirectory, {
1233
+ sandboxed,
1234
+ })
1235
+ : undefined
1236
+ if (shellCwd !== undefined) this.preparedBashDirectories.set(call, shellCwd)
1215
1237
  let outsidePaths: string[] | undefined
1216
1238
  if (
1217
1239
  this.config.outsideRootAccess === 'review' &&
@@ -1234,7 +1256,7 @@ export class ToolExecutor {
1234
1256
  tool.sandboxEscapeArgument !== undefined &&
1235
1257
  input[tool.sandboxEscapeArgument] === true
1236
1258
  const unknownProgram = this.unknownProgramOf(tool, input, sandboxed)
1237
- const reaches = await this.pathEscalationsOf(tool, input, sandboxed)
1259
+ const reaches = await this.pathEscalationsOf(tool, input, sandboxed, shellCwd)
1238
1260
  if (reaches.outsidePaths.length > 0) {
1239
1261
  outsidePaths = [...new Set([...(outsidePaths ?? []), ...reaches.outsidePaths])]
1240
1262
  }
@@ -1275,6 +1297,7 @@ export class ToolExecutor {
1275
1297
  >,
1276
1298
  input: Record<string, unknown>,
1277
1299
  sandboxed: boolean,
1300
+ shellCwd?: string,
1278
1301
  ): Promise<{
1279
1302
  outsidePaths: readonly string[]
1280
1303
  protectedPaths: readonly string[]
@@ -1297,6 +1320,7 @@ export class ToolExecutor {
1297
1320
  const dialect = tool.commandDialect?.({ sandboxed }) ?? 'sh'
1298
1321
  const found = await shellEscalations(line, dialect, {
1299
1322
  roots,
1323
+ ...(shellCwd !== undefined ? { cwd: shellCwd } : {}),
1300
1324
  reviewOutside: this.config.outsideRootAccess === 'review' && !sandboxed,
1301
1325
  })
1302
1326
  protectedPaths.push(...found.protectedPaths)
@@ -1592,6 +1616,10 @@ export class ToolExecutor {
1592
1616
  ...(this.config.toolPause ? { requestPause: this.config.toolPause(toolCall.id) } : {}),
1593
1617
  report: (message: string, fraction?: number) => progress.report(message, fraction),
1594
1618
  }
1619
+ if (preparedCall) {
1620
+ const cwd = this.preparedBashDirectories.get(preparedCall)
1621
+ if (cwd !== undefined) pinBashWorkingDirectory(ctx, cwd)
1622
+ }
1595
1623
  const run = async () => {
1596
1624
  try {
1597
1625
  results[i] = await this.executeSingle(
@@ -1843,7 +1871,10 @@ export class ToolExecutor {
1843
1871
  signal.throwIfAborted()
1844
1872
  // Preparation detaches input synchronously before its first await.
1845
1873
  // Admission must not add an earlier yield where the caller can mutate it.
1846
- const preparedCall = await this.prepareNestedCall(name, input, signal)
1874
+ const preparedCall = {
1875
+ ...(await this.prepareNestedCall(name, input, signal)),
1876
+ toolName: name,
1877
+ }
1847
1878
  signal.throwIfAborted()
1848
1879
  if (this.toolCallBudget) {
1849
1880
  const refusal = await this.toolCallBudget.admit(1, 'nested', signal)
@@ -1926,12 +1957,15 @@ export class ToolExecutor {
1926
1957
  } catch {
1927
1958
  needsApproval = true
1928
1959
  }
1929
- if (needsApproval || (gateResult && gateResult.decision !== 'allow')) {
1930
- const reason = needsApproval
1931
- ? `Blocked: "${name}" requires the person's approval for every call, and an approval cannot be requested from inside another tool. Call it directly.`
1932
- : gateResult?.decision === 'deny'
1933
- ? `Blocked by the authorization gate: ${gateResult.reason}`
1934
- : `Blocked by the authorization gate: this nested call requires an explicit allow rule because an operator review cannot be opened from inside another tool. ${gateResult?.reason}`
1960
+ const nestedEscalation = await this.escalationOrUnfinished(preparedCall)
1961
+ if (nestedEscalation || needsApproval || (gateResult && gateResult.decision !== 'allow')) {
1962
+ const reason = nestedEscalation
1963
+ ? 'Blocked: this nested call reaches a protected path, removal floor, outside-root boundary or another safety escalation. Call it directly so it can be reviewed.'
1964
+ : needsApproval
1965
+ ? `Blocked: "${name}" requires the person's approval for every call, and an approval cannot be requested from inside another tool. Call it directly.`
1966
+ : gateResult?.decision === 'deny'
1967
+ ? `Blocked by the authorization gate: ${gateResult.reason}`
1968
+ : `Blocked by the authorization gate: this nested call requires an explicit allow rule because an operator review cannot be opened from inside another tool. ${gateResult?.reason}`
1935
1969
  const output = deniedToolOutput(name, reason)
1936
1970
  // Same fail-closed durability rule as a direct gate denial: if the
1937
1971
  // configured session log cannot record the refusal, do not quietly carry
@@ -2004,6 +2038,8 @@ export class ToolExecutor {
2004
2038
  report: (message: string, fraction?: number) => progress.report(message, fraction),
2005
2039
  }
2006
2040
 
2041
+ const nestedCwd = this.preparedBashDirectories.get(preparedCall)
2042
+ if (nestedCwd !== undefined) pinBashWorkingDirectory(childContext, nestedCwd)
2007
2043
  await this.emitEvent({
2008
2044
  type: 'tool_executing',
2009
2045
  turnId: this.config.turnId,
@@ -2213,6 +2249,14 @@ export class ToolExecutor {
2213
2249
  sandbox: this.config.sandbox,
2214
2250
  fileReadTracker: this.fileReadTracker,
2215
2251
  receiptVisible: this.receiptVisible,
2252
+ ...(this.historyView
2253
+ ? {
2254
+ conversation: {
2255
+ messages: () => this.historyView?.() ?? [],
2256
+ systemPrompt: () => this.systemPromptView?.(),
2257
+ },
2258
+ }
2259
+ : {}),
2216
2260
  ...(this.config.codeAnalysis ? { codeAnalysis: this.config.codeAnalysis } : {}),
2217
2261
  // Bound to this turn, once. Binding here rather than passing the
2218
2262
  // owner from the tool is what makes the scoping structural: there
@@ -10,6 +10,7 @@ import { SkillGrantSet } from '../../authorization/skill-grant.js'
10
10
  import { repairToolMessageHistory, toolHistoryRepairChanged } from '../../compaction/dangling.js'
11
11
  import { extractFromUserMessage } from '../../compaction/extractor.js'
12
12
  import { WorkingStateManager } from '../../compaction/manager.js'
13
+ import type { CompactionSummaryRouter } from '../../compaction/model-summary.js'
13
14
  import type { ContextReducer } from '../../compaction/reducer.js'
14
15
  import { serializeState as serializeWorkingState } from '../../compaction/serializer.js'
15
16
  import { restoreWorkingState, snapshotWorkingState } from '../../compaction/wire.js'
@@ -552,6 +553,8 @@ export interface QueryParams {
552
553
  allowedTools?: string[]
553
554
  agentId: string
554
555
  agentName: string
556
+ /** Application-owned agent kind for lifecycle hooks; defaults to agentName. */
557
+ agentType?: string
555
558
  workingDirectory?: string
556
559
  /**
557
560
  * Directories besides the working directory the file tools may reach,
@@ -821,6 +824,12 @@ export interface QueryParams {
821
824
  */
822
825
  postCompactionContext?: () => string | undefined | Promise<string | undefined>
823
826
 
827
+ /**
828
+ * Names the model that writes a compaction summary. Asked once per pass;
829
+ * resolving nothing, or a call that fails, keeps the extracted summary.
830
+ */
831
+ compactionSummaryRoute?: CompactionSummaryRouter
832
+
824
833
  /**
825
834
  * Where this conversation's durable state lives.
826
835
  *
@@ -1064,6 +1073,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1064
1073
  approvalPolicy,
1065
1074
  eventTranslator,
1066
1075
  executeUserInterruptHooks,
1076
+ executeTurnFailureHooks,
1067
1077
  pendingHistoryRepairs,
1068
1078
  initialMessages,
1069
1079
  historyIds,
@@ -1841,6 +1851,9 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1841
1851
  ...(params.postCompactionContext
1842
1852
  ? { postCompactionContext: params.postCompactionContext }
1843
1853
  : {}),
1854
+ ...(params.compactionSummaryRoute
1855
+ ? { compactionSummaryRoute: params.compactionSummaryRoute }
1856
+ : {}),
1844
1857
  ...(providerContextWindow !== undefined ? { providerContextWindow } : {}),
1845
1858
  resolveModelContextWindow: async (model) => {
1846
1859
  if (!modelContextWindows.has(model)) {
@@ -1867,6 +1880,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1867
1880
  pluginManager: params.pluginManager,
1868
1881
  ...(params.parentSessionId !== undefined ? { parentSessionId: params.parentSessionId } : {}),
1869
1882
  ...(params.parentTurnId !== undefined ? { parentTurnId: params.parentTurnId } : {}),
1883
+ agentType: params.agentType ?? params.agentName,
1870
1884
  })
1871
1885
 
1872
1886
  const tracer = getTracer()
@@ -2031,6 +2045,33 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2031
2045
  'namzu.runtime.resume_from_checkpoint': params.resumeFromCheckpoint ?? null,
2032
2046
  })
2033
2047
 
2048
+ // A child session is starting: its hooks may add context for the
2049
+ // child's turn, before the child's first prompt is seen.
2050
+ if (params.pluginManager && params.parentSessionId !== undefined) {
2051
+ const startResults = await params.pluginManager.executeHooks(
2052
+ 'subagent_start',
2053
+ {
2054
+ turnId: ctx.turnId,
2055
+ sessionId: params.sessionId,
2056
+ parentSessionId: params.parentSessionId,
2057
+ ...(params.parentTurnId !== undefined ? { parentTurnId: params.parentTurnId } : {}),
2058
+ agentType: params.agentType ?? params.agentName,
2059
+ signal: ctx.abortController.signal,
2060
+ },
2061
+ eventTranslator.emitEvent,
2062
+ )
2063
+ const startAnnotations = applyLifecycleHookResults('subagent_start', startResults)
2064
+ if (startAnnotations.length > 0) {
2065
+ promptContributions.replace({
2066
+ id: 'hooks:subagent_start',
2067
+ placement: 'context',
2068
+ render: () =>
2069
+ `Context from the operator's hooks:\n\n${startAnnotations.join('\n\n')}`,
2070
+ })
2071
+ }
2072
+ yield* eventTranslator.drainPending()
2073
+ }
2074
+
2034
2075
  // The operator's prompt, before the model sees it. A hook may
2035
2076
  // refuse it — the turn ends here, before a prompt is built — or add
2036
2077
  // to what the model is told; the addition rides the prompt as a
@@ -2351,6 +2392,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2351
2392
  ? `${segments.static}\n\n---\n\n${segments.dynamic}`
2352
2393
  : segments.static
2353
2394
 
2395
+ toolExecutor.setSystemPromptView(() => assembledPrompt)
2354
2396
  ctx.recorder.markRunning()
2355
2397
  // A resumed turn already began; `turn_resuming` above continued it.
2356
2398
  if (!params.resumeFromCheckpoint) {
@@ -2788,6 +2830,8 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2788
2830
  ctx.recorder.setSteps(iterationOrchestrator.getSteps())
2789
2831
  await executeUserInterruptHooks(err)
2790
2832
  yield* eventTranslator.drainPending()
2833
+ await executeTurnFailureHooks(err)
2834
+ yield* eventTranslator.drainPending()
2791
2835
  yield* resultAssembler.handleError(err, rootSpan)
2792
2836
  } finally {
2793
2837
  // A pending acquisition is withdrawn and awaited first, so a handle
@@ -79,7 +79,7 @@ import {
79
79
  } from './outstanding-work.js'
80
80
  import { runAdvisoryPhase } from './phases/advisory.js'
81
81
  import { runIterationCheckpoint } from './phases/checkpoint.js'
82
- import { compactionBreaker } from './phases/compaction-breaker.js'
82
+ import { compactionBreaker, estimateRefillTokens } from './phases/compaction-breaker.js'
83
83
  import {
84
84
  AUTOMATIC_COMPACTION_FAILED,
85
85
  OVERFLOW_RELIEF_LIMIT,
@@ -319,7 +319,7 @@ export class IterationOrchestrator {
319
319
  const feedback: string[] = []
320
320
  const ask = async (
321
321
  event: 'turn_end' | 'subagent_stop',
322
- extra: { parentSessionId?: SessionId; parentTurnId?: TurnId },
322
+ extra: { parentSessionId?: SessionId; parentTurnId?: TurnId; agentType?: string },
323
323
  ): Promise<void> => {
324
324
  const results = await manager.executeHooks(
325
325
  event,
@@ -346,6 +346,7 @@ export class IterationOrchestrator {
346
346
  await ask('subagent_stop', {
347
347
  parentSessionId: this.ctx.parentSessionId,
348
348
  ...(this.ctx.parentTurnId !== undefined ? { parentTurnId: this.ctx.parentTurnId } : {}),
349
+ ...(this.ctx.agentType !== undefined ? { agentType: this.ctx.agentType } : {}),
349
350
  })
350
351
  yield* this.ctx.drainPending()
351
352
  }
@@ -2176,6 +2177,7 @@ export class IterationOrchestrator {
2176
2177
  }
2177
2178
  // A rung that sheds too little to change the verdict is not worth a
2178
2179
  // provider call; climb to the next one at once.
2180
+ const tokensBeforeRelief = estimateRefillTokens(recorder.messages)
2179
2181
  let shed = false
2180
2182
  while (!shed && overflowReliefs < OVERFLOW_RELIEF_LIMIT) {
2181
2183
  overflowReliefs++
@@ -2185,7 +2187,13 @@ export class IterationOrchestrator {
2185
2187
  // Clearing results is a compaction too: only summaries and reducers
2186
2188
  // recorded themselves, so a clear-only relief was invisible to the
2187
2189
  // breaker.
2188
- compactionBreaker(this.ctx)?.recordCompaction(iterationNum)
2190
+ // Record the whole ladder once, including any insufficient first
2191
+ // rung, against the same estimator the refill check uses.
2192
+ compactionBreaker(this.ctx)?.recordCompaction(
2193
+ iterationNum,
2194
+ tokensBeforeRelief,
2195
+ estimateRefillTokens(recorder.messages),
2196
+ )
2189
2197
  this.ctx.log.info('Retrying the turn after relieving a context overflow', {
2190
2198
  [NAMZU.TURN_ID]: recorder.turnId,
2191
2199
  [NAMZU.ITERATION]: iterationNum,
@@ -7,10 +7,10 @@
7
7
  * and the turn carries on, silently as far as the person is concerned (a log
8
8
  * line and a `breaker_open` event, no sentence); a pass the provider forces
9
9
  * by rejecting the prompt is not gated, so the reactive ladder still runs.
10
- * - Three compactions in a row, each followed by the context filling back up
11
- * within three iterations: something in the loop (a huge file read, a tool
12
- * that prints without limit) refills the window faster than any summary can
13
- * empty it. The turn stops, because carrying on only buys the same wall.
10
+ * - Three compactions in a row, each followed by the history regaining all
11
+ * space shed within three iterations: something in the loop (a huge file
12
+ * read, a tool that prints without limit) refills the window faster than a
13
+ * summary can empty it. The turn stops, because carrying on buys the same wall.
14
14
  *
15
15
  * Off unless `compactionConfig.circuitBreaker` is true: stopping compaction,
16
16
  * and above all stopping a turn, is not something a host had before.
@@ -19,11 +19,31 @@
19
19
  * share a count.
20
20
  */
21
21
 
22
+ import { estimateMessageTokens } from '../../../../compaction/token-estimate.js'
23
+ import { type Message, isTurnContextEntry } from '../../../../types/message/index.js'
24
+ import { isReminderMessage } from '../../reminders.js'
22
25
  import type { IterationContext } from './context.js'
23
26
 
27
+ /**
28
+ * Conversation growth for the refill guard. Compaction sheds host reminders
29
+ * and recorded context so the host can announce them again; their return is
30
+ * not evidence of a tool refilling the window. Budgeting still includes them.
31
+ * Use provenance, not text that an operator or tool could happen to quote.
32
+ */
33
+ export function estimateRefillTokens(messages: readonly Message[]): number {
34
+ return messages.reduce(
35
+ (total, message) =>
36
+ total +
37
+ (isReminderMessage(message) || isTurnContextEntry(message)
38
+ ? 0
39
+ : estimateMessageTokens(message)),
40
+ 0,
41
+ )
42
+ }
43
+
24
44
  /** Consecutive failed passes after which automatic compaction stops. */
25
45
  export const COMPACTION_FAILURE_LIMIT = 3
26
- /** A compaction followed by a trigger sooner than this many iterations is a refill. */
46
+ /** A full refill sooner than this many iterations counts toward the limit. */
27
47
  export const COMPACTION_REFILL_ITERATIONS = 3
28
48
  /** Consecutive refills that stop the turn. */
29
49
  export const COMPACTION_REFILL_LIMIT = 3
@@ -41,6 +61,8 @@ export class CompactionBreaker {
41
61
  private refills = 0
42
62
  private pendingRefills = 0
43
63
  private lastCompactedIteration: number | undefined
64
+ private lastTokensBefore = 0
65
+ private lastTokensAfter = 0
44
66
  private failureNoticeSent = false
45
67
  private stopReason: string | undefined
46
68
 
@@ -69,18 +91,25 @@ export class CompactionBreaker {
69
91
  * A summary or reducer pass is about to run at `iteration`. True when it
70
92
  * would be the third refill in a row, in which case it must not run.
71
93
  */
72
- wouldTrip(iteration: number): boolean {
94
+ wouldTrip(iteration: number, tokens: number): boolean {
95
+ // Iteration distance alone confuses continued pressure with a refill:
96
+ // a small reduction may leave the prompt above its trigger. Compare the
97
+ // same history estimator on both sides, never provider usage against an
98
+ // estimate. Count only when new context has replaced ALL the space shed.
99
+ const refilled = this.lastTokensAfter < this.lastTokensBefore && tokens >= this.lastTokensBefore
73
100
  const quick =
74
101
  this.lastCompactedIteration !== undefined &&
75
102
  iteration - this.lastCompactedIteration < COMPACTION_REFILL_ITERATIONS
76
- this.pendingRefills = quick ? this.refills + 1 : 0
103
+ this.pendingRefills = quick && refilled ? this.refills + 1 : 0
77
104
  return this.pendingRefills >= COMPACTION_REFILL_LIMIT
78
105
  }
79
106
 
80
- /** The pass that `wouldTrip` allowed completed at `iteration`. */
81
- recordCompaction(iteration: number): void {
107
+ /** The allowed pass completed; both sizes must use the same history estimator. */
108
+ recordCompaction(iteration: number, tokensBefore: number, tokensAfter: number): void {
82
109
  this.refills = this.pendingRefills
83
110
  this.lastCompactedIteration = iteration
111
+ this.lastTokensBefore = tokensBefore
112
+ this.lastTokensAfter = tokensAfter
84
113
  this.recordRelief()
85
114
  }
86
115
 
@@ -3,6 +3,7 @@ import {
3
3
  resolveTriggerThreshold,
4
4
  } from '../../../../compaction/context-window.js'
5
5
  import { findDanglingMessages } from '../../../../compaction/dangling.js'
6
+ import { replaceableSummaries, summarizeWithModel } from '../../../../compaction/model-summary.js'
6
7
  import {
7
8
  type CompactionPlan,
8
9
  DEFAULT_SOFT_TARGET,
@@ -28,7 +29,11 @@ import { invariants } from '../../../../invariants/index.js'
28
29
  import { resolveTaskModel } from '../../../../model-router/task-router.js'
29
30
  import type { Message } from '../../../../types/message/index.js'
30
31
  import { applyLifecycleHookResults } from '../../plugin-hooks.js'
31
- import { COMPACTION_REFILL_MESSAGE, compactionBreaker } from './compaction-breaker.js'
32
+ import {
33
+ COMPACTION_REFILL_MESSAGE,
34
+ compactionBreaker,
35
+ estimateRefillTokens,
36
+ } from './compaction-breaker.js'
32
37
  import type { IterationContext } from './context.js'
33
38
  import { isWorkingMemoryMessage } from './working-memory.js'
34
39
 
@@ -442,7 +447,10 @@ export async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
442
447
  if (!breaker) return false
443
448
  // Already announced: the turn is on its way out; do not say it twice.
444
449
  if (breaker.stop !== undefined) return true
445
- if (!breaker.wouldTrip(ctx.recorder.currentIteration)) return false
450
+ if (
451
+ !breaker.wouldTrip(ctx.recorder.currentIteration, estimateRefillTokens(ctx.recorder.messages))
452
+ )
453
+ return false
446
454
  ctx.log.warn('Compaction keeps refilling — stopping the turn', {
447
455
  [NAMZU.TURN_ID]: ctx.recorder.turnId,
448
456
  [NAMZU.ITERATION]: ctx.recorder.currentIteration,
@@ -508,6 +516,7 @@ async function applyReducer(
508
516
  const messages = ctx.recorder.messages
509
517
  const before = messages.length
510
518
  const beforeChars = totalChars(messages)
519
+ const beforeTokens = estimateRefillTokens(messages)
511
520
 
512
521
  let next: readonly Message[] | undefined
513
522
  try {
@@ -594,7 +603,11 @@ async function applyReducer(
594
603
  reduction.contextWindowTokens,
595
604
  measurement.windowSource,
596
605
  )
597
- compactionBreaker(ctx)?.recordCompaction(ctx.recorder.currentIteration)
606
+ compactionBreaker(ctx)?.recordCompaction(
607
+ ctx.recorder.currentIteration,
608
+ beforeTokens,
609
+ estimateRefillTokens(messages),
610
+ )
598
611
  }
599
612
 
600
613
  /**
@@ -960,7 +973,51 @@ async function runCompactionCheckInner(
960
973
 
961
974
  let compactedContent: string
962
975
 
963
- if (config.llmVerification && manager.slotCount() < config.richStateThreshold) {
976
+ // A model the host set aside writes the summary when it can. Its own cost
977
+ // goes on the turn's ledger at the price of the model that answered. When
978
+ // there is no such model, or its call does not give a summary, the
979
+ // extracted state below is used, with its verifier step where that applies.
980
+ let modelRoute: { providerId: string; model: string } | undefined
981
+ const modelWritten = !ctx.compactionSummaryRoute
982
+ ? undefined
983
+ : await summarizeWithModel({
984
+ manager,
985
+ olderMessages,
986
+ previousSummaries: replaceableSummaries(systemMessages),
987
+ config,
988
+ resolveRoute: async (signal) => {
989
+ const route = await ctx.compactionSummaryRoute?.(signal)
990
+ if (route) {
991
+ modelRoute = {
992
+ providerId: route.providerId ?? ctx.recorder.servingProviderId,
993
+ model: route.model,
994
+ }
995
+ }
996
+ return route
997
+ },
998
+ onUsage: (usage) => {
999
+ if (modelRoute) ctx.recorder.accumulateUsage(usage, modelRoute)
1000
+ },
1001
+ signal: ctx.abortController.signal,
1002
+ onFallback: (reason, error) => {
1003
+ if (reason === 'no_route') return
1004
+ if (error !== undefined) noteCompactionError(ctx, error)
1005
+ ctx.log.warn(
1006
+ 'The model-written compaction summary was not used — using the extracted state',
1007
+ {
1008
+ [NAMZU.TURN_ID]: ctx.recorder.turnId,
1009
+ 'namzu.runtime.reason': reason,
1010
+ ...(error !== undefined
1011
+ ? { 'exception.message': error instanceof Error ? error.message : String(error) }
1012
+ : {}),
1013
+ },
1014
+ )
1015
+ },
1016
+ })
1017
+
1018
+ if (modelWritten !== undefined) {
1019
+ compactedContent = modelWritten
1020
+ } else if (config.llmVerification && manager.slotCount() < config.richStateThreshold) {
964
1021
  // Named once and used twice on purpose. The model the summariser is
965
1022
  // asked for is also the model its tokens are priced at, and computing
966
1023
  // the two separately is how they drift — a router that sends compaction
@@ -1077,6 +1134,7 @@ async function runCompactionCheckInner(
1077
1134
 
1078
1135
  const live = ctx.recorder.messages
1079
1136
  const oldCount = live.length
1137
+ const beforeTokens = estimateRefillTokens(live)
1080
1138
  // Retained history can survive in full while a summary adds another copy
1081
1139
  // of its facts. Even an unretained span may be smaller than the cumulative
1082
1140
  // summary. Compare both candidates with the same estimator, rather than
@@ -1151,5 +1209,9 @@ async function runCompactionCheckInner(
1151
1209
  reachedResetThreshold: reachedReset,
1152
1210
  })
1153
1211
  await emitContextUsageSnapshot(ctx, newEstimate, budget, window.source)
1154
- compactionBreaker(ctx)?.recordCompaction(ctx.recorder.currentIteration)
1212
+ compactionBreaker(ctx)?.recordCompaction(
1213
+ ctx.recorder.currentIteration,
1214
+ beforeTokens,
1215
+ estimateRefillTokens(live),
1216
+ )
1155
1217
  }
@@ -2,6 +2,7 @@ import type { AdvisoryContext } from '../../../../advisory/context.js'
2
2
  import type { SkillGrantSet } from '../../../../authorization/skill-grant.js'
3
3
  import type { AgentBus } from '../../../../bus/index.js'
4
4
  import type { WorkingStateManager } from '../../../../compaction/manager.js'
5
+ import type { CompactionSummaryRouter } from '../../../../compaction/model-summary.js'
5
6
  import type { ContextReducer } from '../../../../compaction/reducer.js'
6
7
  import type { CompactionConfig } from '../../../../config/runtime.js'
7
8
  import { NAMZU } from '../../../../constants/telemetry/index.js'
@@ -259,6 +260,9 @@ export interface IterationContext {
259
260
  /** Host text appended to the restored context after a compaction. */
260
261
  readonly postCompactionContext?: () => string | undefined | Promise<string | undefined>
261
262
 
263
+ /** Where a model-written compaction summary is asked for; see {@link CompactionSummaryRouter}. */
264
+ readonly compactionSummaryRoute?: CompactionSummaryRouter
265
+
262
266
  readonly workingStateManager?: WorkingStateManager
263
267
 
264
268
  /**
@@ -280,6 +284,8 @@ export interface IterationContext {
280
284
  /** The delegating session, when this turn is a child's: `subagent_stop` names it. */
281
285
  readonly parentSessionId?: import('../../../../types/ids/index.js').SessionId
282
286
  readonly parentTurnId?: import('../../../../types/ids/index.js').TurnId
287
+ /** The agent this turn runs, named to `subagent_stop` hooks of a child. */
288
+ readonly agentType?: string
283
289
 
284
290
  /**
285
291
  * Override for {@link PARK_RECORD_DELAY_MS}. Internal; tests set `0` to