@namzu/sdk 62.15.0 → 63.1.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 (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/plugin/hook-context.d.ts +30 -0
  3. package/dist/plugin/hook-context.d.ts.map +1 -0
  4. package/dist/plugin/hook-context.js +51 -0
  5. package/dist/plugin/hook-context.js.map +1 -0
  6. package/dist/plugin/index.d.ts +2 -1
  7. package/dist/plugin/index.d.ts.map +1 -1
  8. package/dist/plugin/index.js +1 -0
  9. package/dist/plugin/index.js.map +1 -1
  10. package/dist/plugin/lifecycle.d.ts +9 -0
  11. package/dist/plugin/lifecycle.d.ts.map +1 -1
  12. package/dist/plugin/lifecycle.js +13 -0
  13. package/dist/plugin/lifecycle.js.map +1 -1
  14. package/dist/plugin/shell-hook.d.ts +87 -12
  15. package/dist/plugin/shell-hook.d.ts.map +1 -1
  16. package/dist/plugin/shell-hook.js +222 -31
  17. package/dist/plugin/shell-hook.js.map +1 -1
  18. package/dist/public-runtime.d.ts +1 -1
  19. package/dist/public-runtime.d.ts.map +1 -1
  20. package/dist/public-runtime.js +1 -1
  21. package/dist/public-runtime.js.map +1 -1
  22. package/dist/public-types.d.ts +1 -1
  23. package/dist/public-types.d.ts.map +1 -1
  24. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  25. package/dist/runtime/query/executor/tool-call-admission.js +2 -3
  26. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  27. package/dist/runtime/query/executor.d.ts +10 -0
  28. package/dist/runtime/query/executor.d.ts.map +1 -1
  29. package/dist/runtime/query/executor.js +25 -5
  30. package/dist/runtime/query/executor.js.map +1 -1
  31. package/dist/runtime/query/index.d.ts.map +1 -1
  32. package/dist/runtime/query/index.js +3 -2
  33. package/dist/runtime/query/index.js.map +1 -1
  34. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  35. package/dist/runtime/query/iteration/index.js +2 -0
  36. package/dist/runtime/query/iteration/index.js.map +1 -1
  37. package/dist/runtime/query/turn/outcome-ledger.d.ts +4 -0
  38. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
  39. package/dist/runtime/query/turn/outcome-ledger.js +2 -1
  40. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
  41. package/dist/tools/builtins/bash.d.ts.map +1 -1
  42. package/dist/tools/builtins/bash.js +10 -2
  43. package/dist/tools/builtins/bash.js.map +1 -1
  44. package/dist/tools/coordinator/index.d.ts.map +1 -1
  45. package/dist/tools/coordinator/index.js +1 -0
  46. package/dist/tools/coordinator/index.js.map +1 -1
  47. package/dist/types/plugin/index.d.ts +37 -10
  48. package/dist/types/plugin/index.d.ts.map +1 -1
  49. package/dist/types/plugin/index.js.map +1 -1
  50. package/dist/types/session/events.d.ts +2 -0
  51. package/dist/types/session/events.d.ts.map +1 -1
  52. package/dist/types/session/events.js.map +1 -1
  53. package/dist/types/tool/index.d.ts +13 -1
  54. package/dist/types/tool/index.d.ts.map +1 -1
  55. package/dist/types/tool/index.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/plugin/hook-context.ts +58 -0
  58. package/src/plugin/index.ts +9 -0
  59. package/src/plugin/lifecycle.ts +13 -0
  60. package/src/plugin/shell-hook.ts +288 -42
  61. package/src/public-runtime.ts +6 -0
  62. package/src/public-types.ts +1 -0
  63. package/src/runtime/query/executor/tool-call-admission.ts +2 -4
  64. package/src/runtime/query/executor.ts +43 -9
  65. package/src/runtime/query/index.ts +3 -3
  66. package/src/runtime/query/iteration/index.ts +2 -0
  67. package/src/runtime/query/turn/outcome-ledger.ts +6 -1
  68. package/src/tools/builtins/bash.ts +16 -3
  69. package/src/tools/coordinator/index.ts +1 -0
  70. package/src/types/plugin/index.ts +48 -16
  71. package/src/types/session/events.ts +2 -0
  72. package/src/types/tool/index.ts +14 -1
@@ -63,6 +63,7 @@ import type {
63
63
  ToolDispatchOptions,
64
64
  ToolFacts,
65
65
  ToolHandoff,
66
+ ToolProgressDetails,
66
67
  ToolResult,
67
68
  } from '../../types/tool/index.js'
68
69
  import type { RepairToolCall } from '../../types/tool/repair.js'
@@ -272,7 +273,9 @@ function boundedToolProgress(message: string): string {
272
273
  * complete useful working set; intermediate states may be replaced.
273
274
  */
274
275
  class ToolProgressPublisher {
275
- private pending: { readonly message: string; readonly fraction?: number } | undefined
276
+ private pending:
277
+ | { readonly message: string; readonly fraction?: number; readonly outputLines?: number }
278
+ | undefined
276
279
  private draining: Promise<void> | null = null
277
280
  private accepting = true
278
281
 
@@ -280,15 +283,18 @@ class ToolProgressPublisher {
280
283
  private readonly emitEvent: EmitEvent,
281
284
  private readonly base: Omit<
282
285
  Extract<SessionEventDraft, { type: 'tool_progress' }>,
283
- 'message' | 'fraction'
286
+ 'message' | 'fraction' | 'outputLines'
284
287
  >,
285
288
  ) {}
286
289
 
287
- report(message: string, fraction?: number): void {
290
+ report(message: string, fraction?: number, details?: ToolProgressDetails): void {
288
291
  if (!this.accepting) return
289
292
  this.pending = {
290
293
  message,
291
294
  ...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
295
+ ...(details?.outputLines !== undefined && Number.isFinite(details.outputLines)
296
+ ? { outputLines: Math.max(0, Math.floor(details.outputLines)) }
297
+ : {}),
292
298
  }
293
299
  this.startDrain()
294
300
  }
@@ -323,6 +329,7 @@ class ToolProgressPublisher {
323
329
  ...this.base,
324
330
  message: boundedToolProgress(update.message),
325
331
  ...(update.fraction !== undefined ? { fraction: update.fraction } : {}),
332
+ ...(update.outputLines !== undefined ? { outputLines: update.outputLines } : {}),
326
333
  }).catch(() => {})
327
334
  }
328
335
  }
@@ -564,8 +571,14 @@ interface PostToolOverride {
564
571
  readonly output: string
565
572
  readonly isError: boolean
566
573
  readonly content?: ToolResultContent
574
+ /** A hook replaced a result that stood; the record says so beside it. */
575
+ readonly replacedByHook?: true
567
576
  }
568
577
 
578
+ /** What the record and the model are told beside a result a hook replaced. */
579
+ export const HOOK_REPLACED_RESULT_NOTE =
580
+ "A hook replaced this tool result: the text above is the hook's, not the tool's own output."
581
+
569
582
  /** What `pre_tool_use` hooks said about a call beyond running it or not. */
570
583
  export interface PreToolHookNotes {
571
584
  /** The strictest permission answer among the hooks: `ask` outranks `allow`. */
@@ -599,6 +612,14 @@ export interface ToolCallOutcome {
599
612
  inputFailure?: CompletedToolRecord['inputFailure']
600
613
  /** A pre-tool hook deliberately skipped execution; its receipt is not an output candidate. */
601
614
  skipped?: true
615
+ /**
616
+ * The call never ran: the person declined it, a rule refused it, its input
617
+ * was refused before it started, or the turn ended first. The outcome record
618
+ * does not count it as a call that may have changed files.
619
+ */
620
+ notRun?: true
621
+ /** The call only started work that goes on after it returned; see `ToolResult.launched`. */
622
+ launched?: true
602
623
  /** Opt-in execution evidence, never provider content or accepted output. */
603
624
  structuredResultJson?: string
604
625
  /** The tool asked for a person; see `ToolResult.handoff`. */
@@ -1848,7 +1869,8 @@ export class ToolExecutor {
1848
1869
  // Per-call for the same reason: a pause has to be routed back
1849
1870
  // to the call that raised it, and a batch can raise several.
1850
1871
  ...(this.config.toolPause ? { requestPause: this.config.toolPause(toolCall.id) } : {}),
1851
- report: (message: string, fraction?: number) => progress.report(message, fraction),
1872
+ report: (message: string, fraction?: number, details?: ToolProgressDetails) =>
1873
+ progress.report(message, fraction, details),
1852
1874
  }
1853
1875
  if (preparedCall) {
1854
1876
  const cwd = this.preparedBashDirectories.get(preparedCall)
@@ -2341,7 +2363,8 @@ export class ToolExecutor {
2341
2363
  // ephemeral child id; minting a pause route for `nestedId` makes the
2342
2364
  // answer impossible to match after process restart. `...context`
2343
2365
  // intentionally preserves the ancestor route here.
2344
- report: (message: string, fraction?: number) => progress.report(message, fraction),
2366
+ report: (message: string, fraction?: number, details?: ToolProgressDetails) =>
2367
+ progress.report(message, fraction, details),
2345
2368
  }
2346
2369
 
2347
2370
  const nestedCwd = this.preparedBashDirectories.get(preparedCall)
@@ -2944,9 +2967,12 @@ export class ToolExecutor {
2944
2967
  // receipt and the host's alike, framed as a hook's words. Added after
2945
2968
  // the output budget so a bounded result is never trimmed to make room
2946
2969
  // for it; the note has its own bound (`hookNote`).
2947
- const hookTexts = [preparedHookContext, post.context].filter(
2948
- (text): text is string => text !== undefined && text.length > 0,
2949
- )
2970
+ // The mark comes first so the bound on hook text never cuts it off.
2971
+ const hookTexts = [
2972
+ postOverride?.replacedByHook === true ? HOOK_REPLACED_RESULT_NOTE : undefined,
2973
+ preparedHookContext,
2974
+ post.context,
2975
+ ].filter((text): text is string => text !== undefined && text.length > 0)
2950
2976
  if (hookTexts.length > 0) {
2951
2977
  const note = hookNote(hookTexts)
2952
2978
  output = `${output}\n\n${note}`
@@ -3104,6 +3130,7 @@ export class ToolExecutor {
3104
3130
  ...(result.handoff !== undefined && !this.config.abortSignal.aborted
3105
3131
  ? { handoff: result.handoff }
3106
3132
  : {}),
3133
+ ...(result.launched === true && !effectiveIsError ? { launched: true as const } : {}),
3107
3134
  ...(revealedTools !== undefined ? { revealedTools } : {}),
3108
3135
  ...(result.facts !== undefined && !postOverride ? { facts: result.facts } : {}),
3109
3136
  }
@@ -3562,8 +3589,11 @@ export class ToolExecutor {
3562
3589
  override = {
3563
3590
  output: result.output,
3564
3591
  isError: false,
3592
+ ...(result.disclose === true ? { replacedByHook: true as const } : {}),
3565
3593
  ...(result.content !== undefined ? { content: result.content } : {}),
3566
3594
  }
3595
+ if (result.context) contexts.push(result.context)
3596
+ if (result.halt) this.requestHalt(result.halt.reason)
3567
3597
  continue
3568
3598
  // `retry` was a declared variant with no implementation: every
3569
3599
  // site that consumed it threw. Here it finally means something
@@ -3659,7 +3689,7 @@ export class ToolExecutor {
3659
3689
  isError: true,
3660
3690
  })
3661
3691
 
3662
- return { toolCallId: toolCall.id, toolName, output, isError: true }
3692
+ return { toolCallId: toolCall.id, toolName, output, isError: true, notRun: true }
3663
3693
  }
3664
3694
 
3665
3695
  /**
@@ -3710,6 +3740,7 @@ export class ToolExecutor {
3710
3740
  const reason = abortReasonText(this.config.abortSignal.reason)
3711
3741
  return this.recordSyntheticHookOutcome(toolCallId, toolName, input, {
3712
3742
  kind: 'error',
3743
+ notRun: true,
3713
3744
  output: `Tool "${toolName}" was not started because the turn was cancelled${reason ? `: ${reason}` : '.'}`,
3714
3745
  })
3715
3746
  }
@@ -3723,6 +3754,7 @@ export class ToolExecutor {
3723
3754
  output: string
3724
3755
  inputFailure?: ToolCallOutcome['inputFailure']
3725
3756
  skipped?: true
3757
+ notRun?: true
3726
3758
  facts?: ToolFacts
3727
3759
  },
3728
3760
  ): Promise<ToolCallOutcome> {
@@ -3767,12 +3799,14 @@ export class ToolExecutor {
3767
3799
  ...(outcome.facts ? { facts: outcome.facts } : {}),
3768
3800
  ...(outcome.inputFailure ? { inputFailure: outcome.inputFailure } : {}),
3769
3801
  ...(outcome.skipped ? { skipped: true as const } : {}),
3802
+ ...(outcome.notRun ? { notRun: true as const } : {}),
3770
3803
  }
3771
3804
  }
3772
3805
 
3773
3806
  private recordSyntheticPreparation(call: Extract<PreparedDirectCall, { kind: 'synthetic' }>) {
3774
3807
  return this.recordSyntheticHookOutcome(call.toolCall.id, call.toolName, call.input, {
3775
3808
  kind: call.isError ? 'error' : 'skip',
3809
+ ...(call.isError ? { notRun: true as const } : {}),
3776
3810
  output: call.message,
3777
3811
  ...(call.facts ? { facts: call.facts } : {}),
3778
3812
  ...(call.inputFailure ? { inputFailure: call.inputFailure } : {}),
@@ -18,6 +18,7 @@ import { restoreWorkingState, snapshotWorkingState } from '../../compaction/wire
18
18
  import { type CompactionConfig, CompactionConfigSchema } from '../../config/runtime.js'
19
19
  import { childSessionLog } from '../../manager/agent/child-session.js'
20
20
  import { type TurnRecorder, recordedTurnStart } from '../../manager/session/turn-recorder.js'
21
+ import { frameOperatorHookContext } from '../../plugin/hook-context.js'
21
22
  import { PromptContributionRegistry } from '../../prompt/contributions.js'
22
23
  import { resolveProviderCapabilities } from '../../provider/capabilities.js'
23
24
  import type { ProviderChainMember } from '../../provider/fallback.js'
@@ -2199,8 +2200,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2199
2200
  promptContributions.replace({
2200
2201
  id: 'hooks:subagent_start',
2201
2202
  placement: 'context',
2202
- render: () =>
2203
- `Context from the operator's hooks:\n\n${startAnnotations.join('\n\n')}`,
2203
+ render: () => frameOperatorHookContext(startAnnotations),
2204
2204
  })
2205
2205
  }
2206
2206
  yield* eventTranslator.drainPending()
@@ -2226,7 +2226,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2226
2226
  promptContributions.replace({
2227
2227
  id: 'hooks:user_prompt_submit',
2228
2228
  placement: 'context',
2229
- render: () => `Context from the operator's hooks:\n\n${annotations.join('\n\n')}`,
2229
+ render: () => frameOperatorHookContext(annotations),
2230
2230
  })
2231
2231
  }
2232
2232
  yield* eventTranslator.drainPending()
@@ -3505,6 +3505,8 @@ export class IterationOrchestrator {
3505
3505
  input,
3506
3506
  isError: result.isError,
3507
3507
  ...(result.skipped ? { skipped: true } : {}),
3508
+ ...(result.notRun ? { notRun: true } : {}),
3509
+ ...(result.launched ? { launched: true } : {}),
3508
3510
  readOnly: this.lookedOnly(result.toolName, input),
3509
3511
  ...(result.facts ? { facts: result.facts } : {}),
3510
3512
  }
@@ -36,6 +36,10 @@ export interface OutcomeCall {
36
36
  readonly isError?: boolean
37
37
  /** A hook stopped it before it ran. */
38
38
  readonly skipped?: boolean
39
+ /** It never ran: declined by the person, refused by a rule or by its own input, or the turn ended first. */
40
+ readonly notRun?: boolean
41
+ /** It only started work that goes on after it returned; the call itself changed nothing. */
42
+ readonly launched?: boolean
39
43
  /** The tool declared this call read-only (or it only keeps the task list). */
40
44
  readonly readOnly?: boolean
41
45
  /** What the call did, as typed fields. Absent when a hook replaced the output. */
@@ -393,7 +397,8 @@ export class OutcomeLedger {
393
397
  // answers for exactly the state they left.
394
398
  let pending: CheckState[] = []
395
399
  for (const call of calls) {
396
- if (call.skipped === true) continue
400
+ // A call that did not run, or only started work elsewhere, changed nothing here.
401
+ if (call.skipped === true || call.notRun === true || call.launched === true) continue
397
402
  const written = WRITING_TOOLS.has(call.toolName)
398
403
  if (written && call.isError !== true) {
399
404
  if (call.facts?.kind === 'write' && call.facts.operation === 'unchanged') continue
@@ -205,14 +205,26 @@ function progressLine(value: string): string {
205
205
  * Reports stay synchronous: the executor owns coalescing and backpressure.
206
206
  * This projection adds neither an output log nor a queue of pending reports.
207
207
  */
208
- function shellProgress(report?: (message: string) => void) {
208
+ function shellProgress(
209
+ report?: (
210
+ message: string,
211
+ fraction?: number,
212
+ details?: { readonly outputLines?: number },
213
+ ) => void,
214
+ ) {
209
215
  if (!report) return undefined
210
216
  const partial = { stdout: '', stderr: '' }
211
217
  let lastMessage = ''
218
+ // Lines of output so far, both streams: every line feed seen. Counted here, on
219
+ // every chunk, because the reports below are coalesced and a viewer counting
220
+ // the messages it received would undercount a fast command.
221
+ let outputLines = 0
222
+ let lastLines = 0
212
223
  return ({ stream, data }: { stream: 'stdout' | 'stderr'; data: string }): void => {
213
224
  let line = partial[stream]
214
225
  let latest = ''
215
226
  let start = 0
227
+ for (let at = data.indexOf('\n'); at >= 0; at = data.indexOf('\n', at + 1)) outputLines++
216
228
  // Scan delimiters without allocating an array for arbitrarily chatty output.
217
229
  for (const match of data.matchAll(/[\r\n]/g)) {
218
230
  line = appendProgressTail(line, data, start, match.index)
@@ -223,10 +235,11 @@ function shellProgress(report?: (message: string) => void) {
223
235
  line = appendProgressTail(line, data, start, data.length)
224
236
  partial[stream] = line
225
237
  const message = progressLine(line) || latest
226
- if (!message || message === lastMessage) return
238
+ if (!message || (message === lastMessage && outputLines === lastLines)) return
227
239
  lastMessage = message
240
+ lastLines = outputLines
228
241
  try {
229
- report(message)
242
+ report(message, undefined, { outputLines })
230
243
  } catch {
231
244
  // A diagnostic observer cannot break the process output reader.
232
245
  }
@@ -791,6 +791,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
791
791
  plan_task_id: resolvedPlanTaskId,
792
792
  background: true,
793
793
  },
794
+ launched: true,
794
795
  }
795
796
  }
796
797
 
@@ -96,7 +96,13 @@ export type PluginHookEvent =
96
96
  * to what the model is told (`annotate`).
97
97
  */
98
98
  | 'user_prompt_submit'
99
- /** A host's session opened or closed. Carries `sessionId` and no `turnId`: these hooks run outside any turn. */
99
+ /**
100
+ * A host's session opened or closed. Carries `sessionId` and no `turnId`:
101
+ * these hooks run outside any turn. `session_start` carries `source`;
102
+ * `session_end` carries `endReason`. A `session_start` hook may answer with
103
+ * `context` (on `continue`) or `annotate`: text the model reads, from the
104
+ * first turn of the session on.
105
+ */
100
106
  | 'session_start'
101
107
  | 'session_end'
102
108
  /** A compaction pass is about to run / has run. Carries `compaction`. */
@@ -110,11 +116,6 @@ export type PluginHookEvent =
110
116
  | 'subagent_start'
111
117
  /** A child session's turn ended. Fired after its own `turn_end`; carries `parentSessionId`, `parentTurnId` and `agentType`. */
112
118
  | 'subagent_stop'
113
- /**
114
- * A child session's turn is starting. Fired right after its own
115
- * `turn_start`; carries `parentSessionId` and `parentTurnId`. Observational.
116
- */
117
- | 'subagent_start'
118
119
  | 'turn_start'
119
120
  | 'turn_end'
120
121
  /**
@@ -273,6 +274,21 @@ export interface PluginNotificationInfo {
273
274
  readonly message: string
274
275
  }
275
276
 
277
+ /**
278
+ * Why a session started, on `session_start`: `startup` for a conversation
279
+ * 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).
281
+ */
282
+ export type PluginSessionSource = 'startup' | 'resume'
283
+
284
+ /**
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).
289
+ */
290
+ export type PluginSessionEndReason = 'exit' | 'replaced'
291
+
276
292
  export interface PluginHookContext {
277
293
  /** The turn the hook fired in. Absent on `session_start` and `session_end`. */
278
294
  readonly turnId?: TurnId
@@ -286,6 +302,10 @@ export interface PluginHookContext {
286
302
  readonly parentTurnId?: TurnId
287
303
  /** The name of the agent the child runs, on `subagent_start` and `subagent_stop`. */
288
304
  readonly agentType?: string
305
+ /** Why the session started, on `session_start`. */
306
+ readonly source?: PluginSessionSource
307
+ /** Why the session closed, on `session_end`. */
308
+ readonly endReason?: PluginSessionEndReason
289
309
  /** The operator's prompt, on `user_prompt_submit`. */
290
310
  readonly prompt?: string
291
311
  /** The pass, on `pre_compact` and `post_compact`. */
@@ -437,7 +457,18 @@ export type PluginHookResult =
437
457
  * hook redacting a secret that also appears in an image must — this variant
438
458
  * cannot inspect what it is preserving.
439
459
  */
440
- | { action: 'replace'; output: string; content?: ToolResultContent }
460
+ | ({
461
+ action: 'replace'
462
+ output: string
463
+ content?: ToolResultContent
464
+ /**
465
+ * Say, beside the result the model and the record carry, that a hook
466
+ * replaced it, so rewritten text is not passed off as the tool's own
467
+ * words. Off by default: a plugin that redacts quietly keeps doing so.
468
+ * A shell hook's replacement always sets it.
469
+ */
470
+ disclose?: boolean
471
+ } & PluginHookNotes)
441
472
 
442
473
  export function assertPluginHookResult(result: PluginHookResult): asserts result {
443
474
  const action = result.action
@@ -472,15 +503,16 @@ export type PluginHookResultFor<E extends PluginHookEvent> = E extends 'user_pro
472
503
  ? Extract<PluginHookResult, { action: 'continue' | 'skip' }>
473
504
  : E extends 'turn_end' | 'subagent_stop' | 'pre_compact'
474
505
  ? Extract<PluginHookResult, { action: 'continue' | 'skip' | 'error' }>
475
- : E extends
476
- | 'turn_interrupt'
477
- | 'session_start'
478
- | 'session_end'
479
- | 'notification'
480
- | 'turn_failure'
481
- | 'permission_denied'
482
- ? Extract<PluginHookResult, { action: 'continue' }>
483
- : Extract<PluginHookResult, { action: 'continue' | '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' }>
484
516
 
485
517
  /** The event and its handler verdict stay correlated, including in hook arrays. */
486
518
  export type PluginHookDefinition<E extends PluginHookEvent = PluginHookEvent> =
@@ -435,6 +435,8 @@ type CoreSessionEvent =
435
435
  message: string
436
436
  /** Optional completion in [0,1] when the tool genuinely knows it. */
437
437
  fraction?: number
438
+ /** Lines of output the call has produced so far, when the tool counts them (a shell command). Cumulative: the newest report wins. */
439
+ outputLines?: number
438
440
  }
439
441
  /** Provider-executed activity, retained as evidence rather than a local tool request. */
440
442
  | {
@@ -940,7 +940,13 @@ export interface ToolContext {
940
940
  *
941
941
  * Absent when the executing surface has no event stream to write to.
942
942
  */
943
- report?: (message: string, fraction?: number) => void
943
+ report?: (message: string, fraction?: number, details?: ToolProgressDetails) => void
944
+ }
945
+
946
+ /** Facts a tool can attach to a progress report besides its words. */
947
+ export interface ToolProgressDetails {
948
+ /** Lines of output the call has produced so far, counted by the tool itself (never by a renderer from what it was shown). */
949
+ readonly outputLines?: number
944
950
  }
945
951
 
946
952
  export interface ToolResult {
@@ -1009,6 +1015,13 @@ export interface ToolResult {
1009
1015
  * the child's turn fails with {@link ToolHandoff.reason} instead.
1010
1016
  */
1011
1017
  handoff?: ToolHandoff
1018
+ /**
1019
+ * This call only started work that goes on after it returned (a sub-agent
1020
+ * launched in the background, or one whose wait was released): the call itself
1021
+ * changed nothing. The turn's outcome record does not count it as a call that
1022
+ * may have changed files; what the started work does is its own record.
1023
+ */
1024
+ launched?: true
1012
1025
  /**
1013
1026
  * What this call did, as typed fields for the PERSON's view: the exit code
1014
1027
  * and the two streams of a command, the path and size of a read, whether a