@namzu/sdk 63.1.0 → 64.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 (101) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -1
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/compaction/hooks.d.ts +57 -0
  6. package/dist/compaction/hooks.d.ts.map +1 -0
  7. package/dist/compaction/hooks.js +44 -0
  8. package/dist/compaction/hooks.js.map +1 -0
  9. package/dist/compaction/manual.d.ts +9 -0
  10. package/dist/compaction/manual.d.ts.map +1 -1
  11. package/dist/compaction/manual.js +74 -5
  12. package/dist/compaction/manual.js.map +1 -1
  13. package/dist/compaction/model-summary.d.ts +6 -0
  14. package/dist/compaction/model-summary.d.ts.map +1 -1
  15. package/dist/compaction/model-summary.js +13 -3
  16. package/dist/compaction/model-summary.js.map +1 -1
  17. package/dist/compaction/verifier.d.ts +8 -2
  18. package/dist/compaction/verifier.d.ts.map +1 -1
  19. package/dist/compaction/verifier.js +23 -7
  20. package/dist/compaction/verifier.js.map +1 -1
  21. package/dist/plugin/hook-context.d.ts +16 -0
  22. package/dist/plugin/hook-context.d.ts.map +1 -1
  23. package/dist/plugin/hook-context.js +40 -0
  24. package/dist/plugin/hook-context.js.map +1 -1
  25. package/dist/plugin/index.d.ts +4 -3
  26. package/dist/plugin/index.d.ts.map +1 -1
  27. package/dist/plugin/index.js +3 -2
  28. package/dist/plugin/index.js.map +1 -1
  29. package/dist/plugin/shell-hook-command.d.ts +25 -0
  30. package/dist/plugin/shell-hook-command.d.ts.map +1 -0
  31. package/dist/plugin/shell-hook-command.js +218 -0
  32. package/dist/plugin/shell-hook-command.js.map +1 -0
  33. package/dist/plugin/shell-hook-tool-names.d.ts +20 -0
  34. package/dist/plugin/shell-hook-tool-names.d.ts.map +1 -0
  35. package/dist/plugin/shell-hook-tool-names.js +43 -0
  36. package/dist/plugin/shell-hook-tool-names.js.map +1 -0
  37. package/dist/plugin/shell-hook.d.ts +62 -20
  38. package/dist/plugin/shell-hook.d.ts.map +1 -1
  39. package/dist/plugin/shell-hook.js +372 -85
  40. package/dist/plugin/shell-hook.js.map +1 -1
  41. package/dist/public-runtime.d.ts +3 -1
  42. package/dist/public-runtime.d.ts.map +1 -1
  43. package/dist/public-runtime.js +2 -1
  44. package/dist/public-runtime.js.map +1 -1
  45. package/dist/public-types.d.ts +2 -1
  46. package/dist/public-types.d.ts.map +1 -1
  47. package/dist/runtime/query/compact-session.d.ts +7 -1
  48. package/dist/runtime/query/compact-session.d.ts.map +1 -1
  49. package/dist/runtime/query/compact-session.js +8 -3
  50. package/dist/runtime/query/compact-session.js.map +1 -1
  51. package/dist/runtime/query/executor/tool-call-admission.d.ts +1 -1
  52. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  53. package/dist/runtime/query/executor/tool-call-admission.js +4 -3
  54. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  55. package/dist/runtime/query/executor.d.ts.map +1 -1
  56. package/dist/runtime/query/executor.js +5 -3
  57. package/dist/runtime/query/executor.js.map +1 -1
  58. package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
  59. package/dist/runtime/query/finalize-turn.js +9 -2
  60. package/dist/runtime/query/finalize-turn.js.map +1 -1
  61. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  62. package/dist/runtime/query/iteration/index.js +2 -1
  63. package/dist/runtime/query/iteration/index.js.map +1 -1
  64. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  65. package/dist/runtime/query/iteration/phases/compaction.js +24 -12
  66. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  67. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  68. package/dist/runtime/query/iteration/phases/tool-review.js +2 -0
  69. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  70. package/dist/runtime/query/plugin-hooks.d.ts +13 -0
  71. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  72. package/dist/runtime/query/plugin-hooks.js +29 -2
  73. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  74. package/dist/types/agent/query.d.ts +8 -0
  75. package/dist/types/agent/query.d.ts.map +1 -1
  76. package/dist/types/plugin/index.d.ts +55 -11
  77. package/dist/types/plugin/index.d.ts.map +1 -1
  78. package/dist/types/plugin/index.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/agents/QueryAgent.ts +2 -1
  81. package/src/compaction/hooks.ts +104 -0
  82. package/src/compaction/manual.ts +117 -4
  83. package/src/compaction/model-summary.ts +19 -3
  84. package/src/compaction/verifier.ts +33 -3
  85. package/src/plugin/hook-context.ts +46 -0
  86. package/src/plugin/index.ts +14 -0
  87. package/src/plugin/shell-hook-command.ts +215 -0
  88. package/src/plugin/shell-hook-tool-names.ts +46 -0
  89. package/src/plugin/shell-hook.ts +405 -79
  90. package/src/public-runtime.ts +12 -0
  91. package/src/public-types.ts +2 -0
  92. package/src/runtime/query/compact-session.ts +16 -4
  93. package/src/runtime/query/executor/tool-call-admission.ts +10 -2
  94. package/src/runtime/query/executor.ts +5 -2
  95. package/src/runtime/query/finalize-turn.ts +9 -2
  96. package/src/runtime/query/iteration/index.ts +2 -1
  97. package/src/runtime/query/iteration/phases/compaction.ts +31 -12
  98. package/src/runtime/query/iteration/phases/tool-review.ts +2 -0
  99. package/src/runtime/query/plugin-hooks.ts +32 -2
  100. package/src/types/agent/query.ts +9 -0
  101. package/src/types/plugin/index.ts +66 -22
@@ -697,8 +697,13 @@ export {
697
697
 
698
698
  export {
699
699
  attachShellHooks,
700
+ boundCompactionSummary,
700
701
  boundHookContext,
701
702
  boundHookReplacement,
703
+ boundLastAssistantMessage,
704
+ compactionHookInstructions,
705
+ MAX_COMPACTION_SUMMARY_HOOK_CHARS,
706
+ MAX_LAST_MESSAGE_HOOK_CHARS,
702
707
  createShellHook,
703
708
  DEFAULT_SHELL_HOOK_TIMEOUT_MS,
704
709
  definePlugin,
@@ -716,8 +721,13 @@ export {
716
721
  PluginResolver,
717
722
  runShellHook,
718
723
  SHELL_HOOK_EVENTS,
724
+ SHELL_HOOK_PASCAL_EVENT_NAMES,
725
+ SHELL_HOOK_WRITTEN_TOOL_NAMES,
719
726
  SHELL_HOOKS_PLUGIN_ID,
727
+ shellHookToolName,
720
728
  shellHookIfMatches,
729
+ shellHookIfProblem,
730
+ shellHookMatcherProblem,
721
731
  shellHookMatches,
722
732
  shellHookVerdict,
723
733
  } from './plugin/index.js'
@@ -1343,6 +1353,7 @@ export { assertTaskStatus, isTerminalTaskStatus } from './types/task/index.js'
1343
1353
  // offer "compact this conversation", could not shrink an idle session
1344
1354
  // between turns, and could not collapse a span it had chosen.
1345
1355
  export { compactNow, compactRegion } from './compaction/manual.js'
1356
+ export { CompactionRefusedError } from './compaction/hooks.js'
1346
1357
  // A host persisting manual compaction has to recognise the system message on
1347
1358
  // resume. Export the identity predicate rather than making every host copy the
1348
1359
  // marker string and eventually disagree with the writer.
@@ -1380,6 +1391,7 @@ export { isWorkingMemoryMessage } from './runtime/query/iteration/phases/working
1380
1391
  // `any` — and the first host to try (`@namzu/cli`'s `/compact`) did exactly
1381
1392
  // that before this line existed.
1382
1393
  export type { CompactNowInput, CompactionResult } from './compaction/manual.js'
1394
+ export type { CompactionHookHost, ManualCompactionHooks } from './compaction/hooks.js'
1383
1395
  export type {
1384
1396
  CompactionSummaryRoute,
1385
1397
  CompactionSummaryRouter,
@@ -242,12 +242,14 @@ export type {
242
242
  ShellHookHandler,
243
243
  ShellHookJudge,
244
244
  ShellHookOnFailure,
245
+ ShellHookCall,
245
246
  ShellHookInput,
246
247
  ShellHookOptions,
247
248
  ShellHookPathFilter,
248
249
  ShellHookOutcome,
249
250
  ShellHooksConfig,
250
251
  } from './plugin/shell-hook.js'
252
+ export type { ShellHookToolNames } from './plugin/shell-hook-tool-names.js'
251
253
 
252
254
  export type {
253
255
  PlanApprovalHandler,
@@ -1,3 +1,4 @@
1
+ import type { ManualCompactionHooks } from '../../compaction/hooks.js'
1
2
  import {
2
3
  type CompactNowInput,
3
4
  type CompactionResult,
@@ -13,8 +14,14 @@ import { generateMessageId } from '../../utils/id.js'
13
14
  import { type SessionLocatorOptions, locateSessionLog, withSessionLease } from './abandon-turn.js'
14
15
 
15
16
  /** A compaction a host asks for between turns. */
16
- export interface CompactSessionParams extends Omit<CompactNowInput, 'messages' | 'onShed'> {
17
+ export interface CompactSessionParams
18
+ extends Omit<CompactNowInput, 'messages' | 'onShed' | 'hooks'> {
17
19
  readonly sessionId: SessionId
20
+ /**
21
+ * Fire `pre_compact` and `post_compact` around the pass. The session and
22
+ * the trigger are this call's own. See {@link CompactNowInput.hooks}.
23
+ */
24
+ readonly hooks?: Omit<ManualCompactionHooks, 'sessionId' | 'trigger'>
18
25
  /**
19
26
  * Who asked. `'manual'` (the default) is a person's request; `'idle'` is a
20
27
  * host compacting a conversation nobody is using before its prompt cache
@@ -37,7 +44,7 @@ export interface CompactSessionParams extends Omit<CompactNowInput, 'messages' |
37
44
  export async function compactSession(
38
45
  params: CompactSessionParams,
39
46
  ): Promise<CompactionResult | null> {
40
- const { sessionId, locator = {}, trigger = 'manual', ...input } = params
47
+ const { sessionId, locator = {}, trigger = 'manual', hooks, ...input } = params
41
48
  const log = await locateSessionLog(sessionId, locator)
42
49
  return withSessionLease(log, locator.lease, async (lease) => {
43
50
  const active = await log.activeTurn({ lease })
@@ -47,7 +54,11 @@ export async function compactSession(
47
54
  const history = await readFoldedHistory(log)
48
55
  const ids = new Map<Message, MessageId>()
49
56
  for (const entry of history) if (entry.messageId) ids.set(entry.message, entry.messageId)
50
- const result = await compactNow({ ...input, messages: history.map((entry) => entry.message) })
57
+ const result = await compactNow({
58
+ ...input,
59
+ messages: history.map((entry) => entry.message),
60
+ ...(hooks ? { hooks: { ...hooks, sessionId, trigger } } : {}),
61
+ })
51
62
  if (!result) return null
52
63
  const head = await log.head()
53
64
  if (!head) {
@@ -105,7 +116,7 @@ export interface CompactSessionRangeParams extends CompactSessionParams {
105
116
  export async function compactSessionRange(
106
117
  params: CompactSessionRangeParams,
107
118
  ): Promise<CompactionResult | null> {
108
- const { sessionId, locator = {}, from, until, ...input } = params
119
+ const { sessionId, locator = {}, from, until, hooks, ...input } = params
109
120
  const log = await locateSessionLog(sessionId, locator)
110
121
  return withSessionLease(log, locator.lease, async (lease) => {
111
122
  const active = await log.activeTurn({ lease })
@@ -134,6 +145,7 @@ export async function compactSessionRange(
134
145
  messages: history.map((entry) => entry.message),
135
146
  start,
136
147
  end,
148
+ ...(hooks ? { hooks: { ...hooks, sessionId, trigger: 'manual' as const } } : {}),
137
149
  })
138
150
  if (!result) return null
139
151
  const head = await log.head()
@@ -78,6 +78,7 @@ export async function runPreToolHook(
78
78
  toolName: string,
79
79
  input: unknown,
80
80
  signal: AbortSignal = host.config.abortSignal,
81
+ toolCallId?: string,
81
82
  ): Promise<PreToolHookOutcome> {
82
83
  signal.throwIfAborted()
83
84
  if (!host.config.pluginManager) return { kind: 'continue', input, modified: false }
@@ -88,6 +89,7 @@ export async function runPreToolHook(
88
89
  sessionId: host.config.sessionId,
89
90
  turnId: host.config.turnId,
90
91
  toolName,
92
+ ...(toolCallId !== undefined ? { toolCallId } : {}),
91
93
  toolInput: input,
92
94
  signal,
93
95
  },
@@ -155,7 +157,7 @@ export async function prepareDirectCall(
155
157
  : {}),
156
158
  }
157
159
  }
158
- const preOutcome = await runPreToolHook(host, toolName, resolved.input)
160
+ const preOutcome = await runPreToolHook(host, toolName, resolved.input, undefined, toolCall.id)
159
161
  if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
160
162
  return {
161
163
  kind: 'synthetic',
@@ -297,7 +299,13 @@ export async function prepareDirectCall(
297
299
  }
298
300
  }
299
301
 
300
- const preOutcome = await runPreToolHook(host, toolName, preparation.prepared.input)
302
+ const preOutcome = await runPreToolHook(
303
+ host,
304
+ toolName,
305
+ preparation.prepared.input,
306
+ undefined,
307
+ toolCall.id,
308
+ )
301
309
  if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
302
310
  return {
303
311
  kind: 'synthetic',
@@ -2790,7 +2790,7 @@ export class ToolExecutor {
2790
2790
  // block had it discarded here — before the wire mapper that was
2791
2791
  // built to carry it ever saw it.
2792
2792
  let result: ToolResult = await this.runOnce(toolName, input, toolContext, prepared)
2793
- let post = await this.runPostToolHook(toolName, input, result)
2793
+ let post = await this.runPostToolHook(toolName, input, result, toolCall.id)
2794
2794
 
2795
2795
  // In-loop retry. A transient failure used to cost a full model round
2796
2796
  // trip: the error went back as a `tool_result`, the model read it and
@@ -2873,7 +2873,7 @@ export class ToolExecutor {
2873
2873
  break
2874
2874
  }
2875
2875
  result = await this.runOnce(toolName, input, toolContext, prepared)
2876
- post = await this.runPostToolHook(toolName, input, result)
2876
+ post = await this.runPostToolHook(toolName, input, result, toolCall.id)
2877
2877
  }
2878
2878
  const durationMs = Date.now() - startMs
2879
2879
 
@@ -3515,6 +3515,7 @@ export class ToolExecutor {
3515
3515
  toolName: string,
3516
3516
  input: unknown,
3517
3517
  toolResult: ToolResult,
3518
+ toolCallId?: string,
3518
3519
  ): Promise<{
3519
3520
  override: PostToolOverride | null
3520
3521
  retry: boolean
@@ -3529,6 +3530,7 @@ export class ToolExecutor {
3529
3530
  sessionId: this.config.sessionId,
3530
3531
  turnId: this.config.turnId,
3531
3532
  toolName,
3533
+ ...(toolCallId !== undefined ? { toolCallId } : {}),
3532
3534
  toolInput: input,
3533
3535
  toolResult,
3534
3536
  signal: this.config.abortSignal,
@@ -3547,6 +3549,7 @@ export class ToolExecutor {
3547
3549
  sessionId: this.config.sessionId,
3548
3550
  turnId: this.config.turnId,
3549
3551
  toolName,
3552
+ ...(toolCallId !== undefined ? { toolCallId } : {}),
3550
3553
  toolInput: input,
3551
3554
  toolResult,
3552
3555
  signal: this.config.abortSignal,
@@ -8,7 +8,7 @@ import type { TurnContext } from './context.js'
8
8
  import type { EventTranslator } from './events.js'
9
9
  import { runOutputGuardrails } from './guardrails.js'
10
10
  import type { QueryParams } from './index.js'
11
- import { applyLifecycleHookResults } from './plugin-hooks.js'
11
+ import { applyLifecycleHookResults, lastAssistantMessageField } from './plugin-hooks.js'
12
12
  import type { ResultAssembler } from './result.js'
13
13
 
14
14
  /**
@@ -86,7 +86,12 @@ export async function* finalizeTurn(
86
86
  if (params.pluginManager && finalization.turnEndHooksFired?.() !== true) {
87
87
  const hookResults = await params.pluginManager.executeHooks(
88
88
  'turn_end',
89
- { sessionId: ctx.sessionId, turnId: ctx.turnId, signal: ctx.abortController.signal },
89
+ {
90
+ sessionId: ctx.sessionId,
91
+ turnId: ctx.turnId,
92
+ ...lastAssistantMessageField(ctx.recorder.messages),
93
+ signal: ctx.abortController.signal,
94
+ },
90
95
  eventTranslator.emitEvent,
91
96
  )
92
97
  applyLifecycleHookResults('turn_end', hookResults)
@@ -100,8 +105,10 @@ export async function* finalizeTurn(
100
105
  {
101
106
  sessionId: ctx.sessionId,
102
107
  turnId: ctx.turnId,
108
+ ...lastAssistantMessageField(ctx.recorder.messages),
103
109
  parentSessionId: params.parentSessionId,
104
110
  ...(params.parentTurnId !== undefined ? { parentTurnId: params.parentTurnId } : {}),
111
+ agentType: params.agentType ?? params.agentName,
105
112
  signal: ctx.abortController.signal,
106
113
  },
107
114
  eventTranslator.emitEvent,
@@ -70,7 +70,7 @@ import { SandboxAcquisitionStopped } from '../deferred-sandbox.js'
70
70
  import { deliverDurableInbound } from '../durable-inbound.js'
71
71
  import type { ToolCallOutcome } from '../executor.js'
72
72
  import { projectObservationContext } from '../observation-context.js'
73
- import { applyLifecycleHookResults } from '../plugin-hooks.js'
73
+ import { applyLifecycleHookResults, lastAssistantMessageField } from '../plugin-hooks.js'
74
74
  import {
75
75
  type RequestContextSnapshot,
76
76
  diffRequestContext,
@@ -419,6 +419,7 @@ export class IterationOrchestrator {
419
419
  sessionId: recorder.sessionId,
420
420
  turnId: recorder.turnId,
421
421
  ...extra,
422
+ ...lastAssistantMessageField(recorder.messages),
422
423
  ...(this.stopHookActive ? { stopHookActive: true } : {}),
423
424
  signal: this.ctx.abortController.signal,
424
425
  },
@@ -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 { askPreCompact, postCompactSummary } from '../../../../compaction/hooks.js'
6
7
  import {
7
8
  LOADED_SKILLS_ENTRY_KEY,
8
9
  type RenderedSkills,
@@ -781,6 +782,10 @@ async function recordShed(
781
782
  interface CompactionPass {
782
783
  attempted: boolean
783
784
  reason: 'threshold' | 'overflow'
785
+ /** What the `pre_compact` hooks said the summary should keep. */
786
+ hookInstructions?: string
787
+ /** The summary the pass installed, for `post_compact`. */
788
+ summary?: string
784
789
  tokensBefore: number
785
790
  contextWindowTokens: number
786
791
  }
@@ -807,9 +812,11 @@ export async function runCompactionCheck(
807
812
  iteration: ctx.recorder.currentIteration,
808
813
  compaction: {
809
814
  reason: pass.reason,
815
+ trigger: 'auto',
810
816
  tokensBefore: pass.tokensBefore,
811
817
  tokensAfter: estimateTokens(ctx),
812
818
  contextWindowTokens: pass.contextWindowTokens,
819
+ ...postCompactSummary(pass.summary),
813
820
  },
814
821
  signal: ctx.abortController.signal,
815
822
  },
@@ -903,24 +910,29 @@ async function runCompactionCheckInner(
903
910
  pass.tokensBefore = estimatedTokens
904
911
  pass.contextWindowTokens = budget
905
912
  if (ctx.pluginManager) {
906
- const results = await ctx.pluginManager.executeHooks(
907
- 'pre_compact',
913
+ const answer = await askPreCompact(
914
+ ctx.pluginManager,
908
915
  {
909
916
  sessionId: ctx.recorder.sessionId,
910
917
  turnId: ctx.recorder.turnId,
911
918
  iteration: ctx.recorder.currentIteration,
912
- compaction: {
913
- reason: pass.reason,
914
- tokensBefore: estimatedTokens,
915
- contextWindowTokens: budget,
916
- },
917
919
  signal: ctx.abortController.signal,
920
+ emitEvent: ctx.emitEvent,
921
+ },
922
+ {
923
+ reason: pass.reason,
924
+ trigger: 'auto',
925
+ instructions: null,
926
+ tokensBefore: estimatedTokens,
927
+ contextWindowTokens: budget,
918
928
  },
919
- ctx.emitEvent,
920
929
  )
921
- applyLifecycleHookResults('pre_compact', results)
922
- const refusal = results.find((result) => result.action === 'skip')
923
- if (refusal?.action === 'skip') {
930
+ // What the hooks said the summary should keep is heard even when a
931
+ // forced pass overrides a refusal. A reducer or the extracted state
932
+ // alone has no place for it (see `docs/sdk/hooks.md`).
933
+ if (answer.instructions !== undefined) pass.hookInstructions = answer.instructions
934
+ const refusal = answer.refusal !== undefined ? { reason: answer.refusal } : undefined
935
+ if (refusal) {
924
936
  // A threshold pass is the context's own housekeeping and a hook may
925
937
  // postpone it. A forced pass runs because the provider rejected the
926
938
  // prompt: a context it will reject is not something a hook may insist
@@ -1130,6 +1142,7 @@ async function runCompactionCheckInner(
1130
1142
  olderMessages,
1131
1143
  previousSummaries: replaceableSummaries(systemMessages),
1132
1144
  config,
1145
+ ...(pass.hookInstructions !== undefined ? { hookInstructions: pass.hookInstructions } : {}),
1133
1146
  resolveRoute: async (signal) => {
1134
1147
  const route = await ctx.compactionSummaryRoute?.(signal)
1135
1148
  if (route) {
@@ -1162,7 +1175,10 @@ async function runCompactionCheckInner(
1162
1175
 
1163
1176
  if (modelWritten !== undefined) {
1164
1177
  compactedContent = modelWritten
1165
- } else if (config.llmVerification && manager.slotCount() < config.richStateThreshold) {
1178
+ } else if (
1179
+ pass.hookInstructions !== undefined ||
1180
+ (config.llmVerification && manager.slotCount() < config.richStateThreshold)
1181
+ ) {
1166
1182
  // Named once and used twice on purpose. The model the summariser is
1167
1183
  // asked for is also the model its tokens are priced at, and computing
1168
1184
  // the two separately is how they drift — a router that sends compaction
@@ -1188,6 +1204,8 @@ async function runCompactionCheckInner(
1188
1204
  // was added, and nothing ever consulted it.
1189
1205
  compactionModel,
1190
1206
  ctx.abortController.signal,
1207
+ undefined,
1208
+ pass.hookInstructions,
1191
1209
  )
1192
1210
  } catch (error) {
1193
1211
  // The model step only ADDS to the extracted state, so a failed one (a
@@ -1308,6 +1326,7 @@ async function runCompactionCheckInner(
1308
1326
  await recordShed(ctx, [...live], newMessages, options?.force ? 'overflow' : 'threshold')
1309
1327
 
1310
1328
  installMessages(live, newMessages)
1329
+ pass.summary = compactedContent
1311
1330
 
1312
1331
  const newEstimate = estimateTokens(ctx)
1313
1332
 
@@ -318,6 +318,7 @@ export async function* runToolReview(
318
318
  sessionId: ctx.recorder.sessionId,
319
319
  turnId: ctx.recorder.turnId,
320
320
  toolName: summary.name,
321
+ toolCallId: summary.id,
321
322
  toolInput: summary.input,
322
323
  reason,
323
324
  signal: ctx.abortController.signal,
@@ -854,6 +855,7 @@ export async function* runToolReview(
854
855
  sessionId: ctx.recorder.sessionId,
855
856
  turnId: ctx.recorder.turnId,
856
857
  toolName: tc.name,
858
+ toolCallId: tc.id,
857
859
  toolInput: tc.input,
858
860
  signal: ctx.abortController.signal,
859
861
  },
@@ -1,5 +1,31 @@
1
+ import { boundLastAssistantMessage } from '../../plugin/hook-context.js'
1
2
  import { NamzuError } from '../../types/errors/index.js'
3
+ import type { Message } from '../../types/message/index.js'
2
4
  import type { PluginHookEvent, PluginHookResult } from '../../types/plugin/index.js'
5
+ import { isOperatorUserMessage } from './steering.js'
6
+
7
+ /**
8
+ * The model's last message of the turn, as a `turn_end` or `subagent_stop`
9
+ * hook receives it: the text of the last assistant message after the
10
+ * operator's latest prompt, bounded. A message that carries only tool calls,
11
+ * a turn that stopped before the model wrote anything, and a turn with no
12
+ * assistant message yet all give `undefined`: the field is then absent, and a
13
+ * hook never gets an earlier turn's words or an earlier step's narration in
14
+ * its place.
15
+ */
16
+ export function lastAssistantMessageField(messages: readonly Message[]): {
17
+ lastAssistantMessage?: string
18
+ } {
19
+ for (let index = messages.length - 1; index >= 0; index--) {
20
+ const message = messages[index]
21
+ if (!message) continue
22
+ if (isOperatorUserMessage(message)) return {}
23
+ if (message.role !== 'assistant') continue
24
+ const text = boundLastAssistantMessage(message.content)
25
+ return text === undefined ? {} : { lastAssistantMessage: text }
26
+ }
27
+ return {}
28
+ }
3
29
 
4
30
  /**
5
31
  * Interpret hook results from non-tool lifecycle events (run_start, run_end,
@@ -19,9 +45,13 @@ export function applyLifecycleHookResults(
19
45
  case 'continue':
20
46
  continue
21
47
  case 'annotate':
22
- if (event !== 'user_prompt_submit' && event !== 'subagent_start') {
48
+ if (
49
+ event !== 'user_prompt_submit' &&
50
+ event !== 'subagent_start' &&
51
+ event !== 'pre_compact'
52
+ ) {
23
53
  throw new Error(
24
- `Plugin hook ${event} returned 'annotate', which only user_prompt_submit and subagent_start accept`,
54
+ `Plugin hook ${event} returned 'annotate', which only user_prompt_submit, subagent_start and pre_compact accept`,
25
55
  )
26
56
  }
27
57
  if (result.text.trim().length > 0) annotations.push(result.text.trim())
@@ -25,6 +25,15 @@ export interface QueryAgentConfig extends BaseAgentConfig {
25
25
 
26
26
  persona?: AgentPersona
27
27
 
28
+ /**
29
+ * The name lifecycle hooks see for this agent (`agentType` on
30
+ * `subagent_start` and `subagent_stop`, `agent_type` for a shell hook).
31
+ * Absent, it is the agent's `type`, which for a host-defined class is the
32
+ * class's marker rather than the agent's name: a host that names its
33
+ * agents passes the name here.
34
+ */
35
+ agentType?: string
36
+
28
37
  /**
29
38
  * Channel a host uses to hand guidance to this turn's current turn.
30
39
  *
@@ -246,12 +246,36 @@ export interface PluginModelResponse {
246
246
 
247
247
  /** What a compaction hook is shown about the pass. */
248
248
  export interface PluginCompactionInfo {
249
- /** `threshold`: the estimate crossed the line. `overflow`: the provider rejected the prompt. */
250
- readonly reason: 'threshold' | 'overflow'
249
+ /**
250
+ * `threshold`: the estimate crossed the line. `overflow`: the provider
251
+ * rejected the prompt. `manual`: a host asked for the pass outside a turn
252
+ * (a person's `/compact`, a summarised span, an idle pass); test `trigger`
253
+ * to tell a person's request from the kernel's own.
254
+ */
255
+ readonly reason: 'threshold' | 'overflow' | 'manual'
251
256
  readonly tokensBefore: number
252
257
  /** Present on `post_compact`. */
253
258
  readonly tokensAfter?: number
254
259
  readonly contextWindowTokens: number
260
+ /**
261
+ * Who started the pass: `manual` for one a person asked for (`/compact`,
262
+ * summarising a span), `auto` for every pass the kernel or a host started
263
+ * on its own (the threshold, a provider overflow, an idle pass). The
264
+ * kernel always sets it; it is optional only so a host that builds this
265
+ * context by hand keeps compiling. A matcher on these two events tests it.
266
+ */
267
+ readonly trigger?: 'manual' | 'auto'
268
+ /**
269
+ * On `pre_compact`: what the person typed with the command, or `null` when
270
+ * they typed nothing or the pass is not one a person asked for.
271
+ */
272
+ readonly instructions?: string | null
273
+ /**
274
+ * On `post_compact`: the summary the pass wrote, cut to
275
+ * `MAX_COMPACTION_SUMMARY_HOOK_CHARS`. Absent when the pass wrote none (a
276
+ * context reducer shed the history, or the pass shed nothing).
277
+ */
278
+ readonly summary?: string
255
279
  }
256
280
 
257
281
  /** One call in a `post_tool_batch` hook's context. */
@@ -275,19 +299,22 @@ export interface PluginNotificationInfo {
275
299
  }
276
300
 
277
301
  /**
278
- * Why a session started, on `session_start`: `startup` for a conversation
302
+ * Why a conversation began, on `session_start`: `startup` for a conversation
279
303
  * with no earlier turn, `resume` for one that already had turns (a resumed
280
- * conversation, a `--continue`, or a session rebuilt under a running one).
304
+ * conversation, a `--continue`, a fork, or a session rebuilt under a running
305
+ * one), `clear` for the empty conversation a host opens when the person
306
+ * clears the old one (`/clear`, `/new`).
281
307
  */
282
- export type PluginSessionSource = 'startup' | 'resume'
308
+ export type PluginSessionSource = 'startup' | 'resume' | 'clear'
283
309
 
284
310
  /**
285
- * Why a session closed, on `session_end`: `exit` when the host is finishing
286
- * with it (the process ends, the person quits), `replaced` when the host
287
- * builds another session in its place (a model or account switch, another
288
- * conversation opened).
311
+ * Why a conversation ended, on `session_end`: `exit` when the host is
312
+ * finishing with it (the process ends, the person quits), `replaced` when the
313
+ * host moves to another session or conversation in its place (a model or
314
+ * account switch, another conversation opened or forked), `clear` when the
315
+ * person cleared it and a new, empty conversation begins (`/clear`, `/new`).
289
316
  */
290
- export type PluginSessionEndReason = 'exit' | 'replaced'
317
+ export type PluginSessionEndReason = 'exit' | 'replaced' | 'clear'
291
318
 
292
319
  export interface PluginHookContext {
293
320
  /** The turn the hook fired in. Absent on `session_start` and `session_end`. */
@@ -311,6 +338,14 @@ export interface PluginHookContext {
311
338
  /** The pass, on `pre_compact` and `post_compact`. */
312
339
  readonly compaction?: PluginCompactionInfo
313
340
  readonly toolName?: string
341
+ /**
342
+ * The id of the call, on `pre_tool_use`, `post_tool_use`,
343
+ * `post_tool_use_failure`, `permission_request` and `permission_denied`: the
344
+ * same id `post_tool_batch` carries as `toolCallId`, so a hook can pair a
345
+ * before with an after. Absent on a call a tool makes through
346
+ * `dispatchTool` (it has no model-issued id).
347
+ */
348
+ readonly toolCallId?: string
314
349
  readonly toolInput?: unknown
315
350
  readonly toolResult?: ToolResult
316
351
  /** The calls of one model step with their outcomes, on `post_tool_batch`. */
@@ -328,6 +363,13 @@ export interface PluginHookContext {
328
363
  * keep the model working until the turn's own limits stop it.
329
364
  */
330
365
  readonly stopHookActive?: boolean
366
+ /**
367
+ * On `turn_end` and `subagent_stop`: the text of the model's last message
368
+ * of the turn, cut to `MAX_LAST_MESSAGE_HOOK_CHARS`. Absent when the turn
369
+ * produced no text (it ended on a tool call, was interrupted before the
370
+ * model wrote anything, or failed).
371
+ */
372
+ readonly lastAssistantMessage?: string
331
373
  readonly iteration?: number
332
374
  /**
333
375
  * Why the turn was stopped, on `turn_interrupt`.
@@ -501,18 +543,20 @@ export type PluginHookResultFor<E extends PluginHookEvent> = E extends 'user_pro
501
543
  ? Extract<PluginHookResult, { action: 'continue' | 'error' }>
502
544
  : E extends 'permission_request'
503
545
  ? Extract<PluginHookResult, { action: 'continue' | 'skip' }>
504
- : E extends 'turn_end' | 'subagent_stop' | 'pre_compact'
505
- ? Extract<PluginHookResult, { action: 'continue' | 'skip' | 'error' }>
506
- : E extends 'session_start'
507
- ? Extract<PluginHookResult, { action: 'continue' | 'annotate' }>
508
- : E extends
509
- | 'turn_interrupt'
510
- | 'session_end'
511
- | 'notification'
512
- | 'turn_failure'
513
- | 'permission_denied'
514
- ? Extract<PluginHookResult, { action: 'continue' }>
515
- : Extract<PluginHookResult, { action: 'continue' | 'error' }>
546
+ : E extends 'pre_compact'
547
+ ? Extract<PluginHookResult, { action: 'continue' | 'annotate' | 'skip' | 'error' }>
548
+ : E extends 'turn_end' | 'subagent_stop'
549
+ ? Extract<PluginHookResult, { action: 'continue' | 'skip' | 'error' }>
550
+ : E extends 'session_start'
551
+ ? Extract<PluginHookResult, { action: 'continue' | 'annotate' }>
552
+ : E extends
553
+ | 'turn_interrupt'
554
+ | 'session_end'
555
+ | 'notification'
556
+ | 'turn_failure'
557
+ | 'permission_denied'
558
+ ? Extract<PluginHookResult, { action: 'continue' }>
559
+ : Extract<PluginHookResult, { action: 'continue' | 'error' }>
516
560
 
517
561
  /** The event and its handler verdict stay correlated, including in hook arrays. */
518
562
  export type PluginHookDefinition<E extends PluginHookEvent = PluginHookEvent> =