@pencil-agent/nano-pencil 1.14.3 → 1.14.5

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 (137) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/cli/args.d.ts +11 -1
  3. package/dist/cli/args.js +130 -0
  4. package/dist/core/config/settings-manager.d.ts +8 -1
  5. package/dist/core/config/settings-manager.js +9 -0
  6. package/dist/core/extensions/index.d.ts +1 -1
  7. package/dist/core/extensions/runner.d.ts +70 -4
  8. package/dist/core/extensions/runner.js +188 -8
  9. package/dist/core/extensions/types.d.ts +15 -3
  10. package/dist/core/i18n/slash-commands.d.ts +1 -0
  11. package/dist/core/i18n/slash-commands.js +1 -0
  12. package/dist/core/i18n/slash-commands.zh.d.ts +1 -0
  13. package/dist/core/i18n/slash-commands.zh.js +1 -0
  14. package/dist/core/runtime/agent-session.d.ts +10 -2
  15. package/dist/core/runtime/agent-session.js +117 -27
  16. package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
  17. package/dist/core/runtime/extension-core-bindings.js +73 -20
  18. package/dist/core/runtime/retry-coordinator.d.ts +10 -1
  19. package/dist/core/runtime/retry-coordinator.js +20 -4
  20. package/dist/core/runtime/sdk.d.ts +21 -1
  21. package/dist/core/runtime/sdk.js +12 -2
  22. package/dist/core/slash-commands.d.ts +3 -0
  23. package/dist/core/slash-commands.js +6 -2
  24. package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
  25. package/dist/core/telemetry/batching-dispatcher.js +89 -0
  26. package/dist/core/telemetry/build-meta.d.ts +12 -0
  27. package/dist/core/telemetry/build-meta.js +57 -0
  28. package/dist/core/telemetry/caller-context.d.ts +32 -0
  29. package/dist/core/telemetry/caller-context.js +19 -0
  30. package/dist/core/telemetry/credentials.d.ts +27 -0
  31. package/dist/core/telemetry/credentials.js +87 -0
  32. package/dist/core/telemetry/ext-events.d.ts +89 -0
  33. package/dist/core/telemetry/ext-events.js +189 -0
  34. package/dist/core/telemetry/index.d.ts +13 -0
  35. package/dist/core/telemetry/index.js +6 -0
  36. package/dist/core/telemetry/insforge-base.d.ts +37 -0
  37. package/dist/core/telemetry/insforge-base.js +160 -0
  38. package/dist/core/telemetry/types.d.ts +33 -0
  39. package/dist/core/telemetry/types.js +7 -0
  40. package/dist/extensions/defaults/AGENT.md +3 -3
  41. package/dist/extensions/defaults/CLAUDE.md +1 -1
  42. package/dist/extensions/defaults/browser/index.js +13 -5
  43. package/dist/extensions/defaults/btw/index.js +2 -2
  44. package/dist/extensions/defaults/debug/index.js +18 -8
  45. package/dist/extensions/defaults/diagnostics/index.js +12 -0
  46. package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
  47. package/dist/extensions/defaults/grub/grub-parser.js +31 -1
  48. package/dist/extensions/defaults/grub/index.d.ts +1 -1
  49. package/dist/extensions/defaults/grub/index.js +4 -3
  50. package/dist/extensions/defaults/interview/index.js +2 -2
  51. package/dist/extensions/defaults/link-world/index.js +13 -5
  52. package/dist/extensions/defaults/loop/index.js +35 -0
  53. package/dist/extensions/defaults/mcp/index.js +18 -0
  54. package/dist/extensions/defaults/plan/index.js +29 -11
  55. package/dist/extensions/defaults/presence/index.d.ts +12 -2
  56. package/dist/extensions/defaults/presence/index.js +77 -23
  57. package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
  58. package/dist/extensions/defaults/presence/presence-memory.js +37 -1
  59. package/dist/extensions/defaults/recap/index.js +12 -0
  60. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
  61. package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
  62. package/dist/extensions/defaults/sal/index.d.ts +2 -2
  63. package/dist/extensions/defaults/sal/index.js +47 -10
  64. package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
  65. package/dist/extensions/defaults/sal/sal-config.js +15 -82
  66. package/dist/extensions/defaults/sal/sal-runtime.d.ts +3 -1
  67. package/dist/extensions/defaults/sal/sal-runtime.js +1 -1
  68. package/dist/extensions/defaults/sal/sal-trace.d.ts +1 -1
  69. package/dist/extensions/defaults/sal/sal-trace.js +13 -2
  70. package/dist/extensions/defaults/security-audit/index.js +138 -80
  71. package/dist/extensions/defaults/subagent/index.js +29 -5
  72. package/dist/extensions/defaults/team/index.js +10 -9
  73. package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
  74. package/dist/extensions/defaults/team/team-parser.js +91 -3
  75. package/dist/extensions/defaults/token-save/index.js +11 -5
  76. package/dist/extensions/optional/export-html/index.js +19 -5
  77. package/dist/extensions/optional/simplify/index.js +11 -5
  78. package/dist/main.js +11 -2
  79. package/dist/modes/acp/acp-mode.d.ts +3 -0
  80. package/dist/modes/acp/acp-mode.js +40 -1
  81. package/dist/modes/agent-loop-result-format.d.ts +10 -0
  82. package/dist/modes/agent-loop-result-format.js +69 -0
  83. package/dist/modes/interactive/agent-loop-status.d.ts +8 -0
  84. package/dist/modes/interactive/agent-loop-status.js +41 -0
  85. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  86. package/dist/modes/interactive/interactive-mode.js +75 -25
  87. package/dist/modes/interactive/slash-command-arguments.d.ts +28 -0
  88. package/dist/modes/interactive/slash-command-arguments.js +126 -0
  89. package/dist/modes/print-mode.d.ts +17 -5
  90. package/dist/modes/print-mode.js +77 -5
  91. package/dist/modes/rpc/rpc-client.d.ts +5 -1
  92. package/dist/modes/rpc/rpc-client.js +6 -0
  93. package/dist/modes/rpc/rpc-mode.d.ts +8 -1
  94. package/dist/modes/rpc/rpc-mode.js +92 -48
  95. package/dist/modes/rpc/rpc-types.d.ts +24 -1
  96. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-continuations.d.ts +17 -0
  97. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-continuations.js +60 -0
  98. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-results.d.ts +10 -0
  99. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-results.js +137 -0
  100. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-summaries.d.ts +22 -0
  101. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-summaries.js +64 -0
  102. package/dist/node_modules/@pencil-agent/agent-core/agent-loop.d.ts +4 -4
  103. package/dist/node_modules/@pencil-agent/agent-core/agent-loop.js +440 -39
  104. package/dist/node_modules/@pencil-agent/agent-core/agent-run-result.d.ts +9 -0
  105. package/dist/node_modules/@pencil-agent/agent-core/agent-run-result.js +32 -0
  106. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +42 -2
  107. package/dist/node_modules/@pencil-agent/agent-core/agent.js +86 -1
  108. package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
  109. package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
  110. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +2 -2
  111. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +164 -43
  112. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
  113. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
  114. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
  115. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
  116. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +139 -19
  117. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  118. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +34 -34
  119. package/dist/node_modules/@pencil-agent/ai/models.generated.js +63 -63
  120. package/dist/node_modules/@pencil-agent/ai/providers/transform-messages.d.ts +3 -3
  121. package/dist/node_modules/@pencil-agent/ai/providers/transform-messages.js +42 -27
  122. package/dist/node_modules/@pencil-agent/ai/stream.d.ts +2 -2
  123. package/dist/node_modules/@pencil-agent/ai/stream.js +70 -14
  124. package/dist/node_modules/@pencil-agent/ai/utils/event-stream.d.ts +5 -1
  125. package/dist/node_modules/@pencil-agent/ai/utils/event-stream.js +15 -3
  126. package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
  127. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +12 -1
  128. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  129. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +3 -8
  130. package/dist/node_modules/@pencil-agent/tui/tui.js +33 -67
  131. package/dist/packages/mem-core/extension.js +6 -2
  132. package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
  133. package/dist/packages/mem-core/full-insights-sections.js +231 -0
  134. package/dist/packages/mem-core/full-insights.d.ts +1 -1
  135. package/dist/packages/mem-core/full-insights.js +102 -42
  136. package/docs/agent-loop-frameworks.md +26 -3
  137. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.14.3",
3
- "commitHash": "1a4f55a",
2
+ "version": "1.14.5",
3
+ "commitHash": "5845e2c",
4
4
  "branch": "main",
5
- "builtAt": "2026-05-27T01:43:40.032Z"
5
+ "builtAt": "2026-05-28T06:36:42.739Z"
6
6
  }
@@ -4,7 +4,7 @@
4
4
  * [TO]: Consumed by main.ts, core/model-resolver.ts
5
5
  * [HERE]: cli/args.ts - CLI argument parsing and help display
6
6
  */
7
- import type { ThinkingLevel } from "@pencil-agent/agent-core";
7
+ import { type AgentLoopFrameworkInput, type AgentLoopPolicyOptions, type ThinkingLevel } from "@pencil-agent/agent-core";
8
8
  import { type ToolName } from "../core/tools/index.js";
9
9
  export type Mode = "text" | "json" | "rpc";
10
10
  export interface Args {
@@ -29,6 +29,16 @@ export interface Args {
29
29
  extensions?: string[];
30
30
  noExtensions?: boolean;
31
31
  print?: boolean;
32
+ /** In text print mode, emit the final agent loop result as stderr JSON. */
33
+ printLoopResult?: boolean;
34
+ /** In print mode, exit non-zero when the final agent result is an error. */
35
+ failOnAgentError?: boolean;
36
+ /** In print mode, exit non-zero when any tool permission denial occurred. */
37
+ failOnToolDenial?: boolean;
38
+ /** Non-persistent agent loop framework override for this process/session. */
39
+ agentLoopFramework?: AgentLoopFrameworkInput;
40
+ /** Non-persistent loop policy overrides for this process/session. */
41
+ loopPolicy?: Pick<AgentLoopPolicyOptions, "maxTurnsPerPrompt" | "maxToolCallsPerPrompt" | "maxToolConcurrency" | "maxToolResultBatchSizeChars" | "outputTokenBudget" | "maxOutputTokenRecoveryAttempts" | "maxModelErrorRecoveryAttempts" | "maxStopHookContinuations">;
32
42
  export?: string;
33
43
  noSkills?: boolean;
34
44
  skills?: string[];
package/dist/cli/args.js CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * [WHO]: Args, Mode, parseArgs(), printHelp()
3
+ * [FROM]: Depends on agent-core, chalk, config.ts, core/tools
4
+ * [TO]: Consumed by main.ts, core/model-resolver.ts
5
+ * [HERE]: cli/args.ts - CLI argument parsing and help display
6
+ */
7
+ import { normalizeAgentLoopFramework, } from "@pencil-agent/agent-core";
1
8
  import chalk from "chalk";
2
9
  import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js";
3
10
  import { allTools } from "../core/tools/index.js";
@@ -5,12 +12,45 @@ const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh
5
12
  export function isValidThinkingLevel(level) {
6
13
  return VALID_THINKING_LEVELS.includes(level);
7
14
  }
15
+ function parseAgentLoopFramework(value) {
16
+ const normalized = normalizeAgentLoopFramework(value);
17
+ if (normalized === "standard" || normalized === "weak-model-compatible") {
18
+ return normalized;
19
+ }
20
+ return undefined;
21
+ }
22
+ function parsePositiveIntegerOption(flag, value) {
23
+ const parsed = Number(value);
24
+ if (Number.isInteger(parsed) && parsed > 0)
25
+ return parsed;
26
+ console.error(chalk.yellow(`Warning: Invalid ${flag} value "${value}". Expected a positive integer.`));
27
+ return undefined;
28
+ }
29
+ function parseNonNegativeIntegerOption(flag, value) {
30
+ const parsed = Number(value);
31
+ if (Number.isInteger(parsed) && parsed >= 0)
32
+ return parsed;
33
+ console.error(chalk.yellow(`Warning: Invalid ${flag} value "${value}". Expected a non-negative integer.`));
34
+ return undefined;
35
+ }
36
+ function parseUnitIntervalOption(flag, value) {
37
+ const parsed = Number(value);
38
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 1)
39
+ return parsed;
40
+ console.error(chalk.yellow(`Warning: Invalid ${flag} value "${value}". Expected a number in (0, 1].`));
41
+ return undefined;
42
+ }
43
+ function setLoopPolicyOption(result, key, value) {
44
+ result.loopPolicy = result.loopPolicy ?? {};
45
+ result.loopPolicy[key] = value;
46
+ }
8
47
  export function parseArgs(args, extensionFlags) {
9
48
  const result = {
10
49
  messages: [],
11
50
  fileArgs: [],
12
51
  unknownFlags: new Map(),
13
52
  };
53
+ const outputTokenBudget = {};
14
54
  for (let i = 0; i < args.length; i++) {
15
55
  const arg = args[i];
16
56
  if (arg === "--help" || arg === "-h") {
@@ -89,6 +129,75 @@ export function parseArgs(args, extensionFlags) {
89
129
  else if (arg === "--print" || arg === "-p") {
90
130
  result.print = true;
91
131
  }
132
+ else if (arg === "--print-loop-result") {
133
+ result.printLoopResult = true;
134
+ }
135
+ else if (arg === "--fail-on-agent-error") {
136
+ result.failOnAgentError = true;
137
+ }
138
+ else if (arg === "--fail-on-tool-denial") {
139
+ result.failOnToolDenial = true;
140
+ }
141
+ else if (arg === "--agent-loop" && i + 1 < args.length) {
142
+ const framework = args[++i];
143
+ const normalized = parseAgentLoopFramework(framework);
144
+ if (normalized) {
145
+ result.agentLoopFramework = normalized;
146
+ }
147
+ else {
148
+ console.error(chalk.yellow(`Warning: Invalid agent loop framework "${framework}". Valid values: standard, weak-model-compatible.`));
149
+ }
150
+ }
151
+ else if (arg === "--max-turns-per-prompt" && i + 1 < args.length) {
152
+ const value = parsePositiveIntegerOption(arg, args[++i]);
153
+ if (value !== undefined)
154
+ setLoopPolicyOption(result, "maxTurnsPerPrompt", value);
155
+ }
156
+ else if (arg === "--max-tool-calls-per-prompt" && i + 1 < args.length) {
157
+ const value = parsePositiveIntegerOption(arg, args[++i]);
158
+ if (value !== undefined)
159
+ setLoopPolicyOption(result, "maxToolCallsPerPrompt", value);
160
+ }
161
+ else if (arg === "--max-tool-concurrency" && i + 1 < args.length) {
162
+ const value = parsePositiveIntegerOption(arg, args[++i]);
163
+ if (value !== undefined)
164
+ setLoopPolicyOption(result, "maxToolConcurrency", value);
165
+ }
166
+ else if (arg === "--max-tool-result-batch-size-chars" && i + 1 < args.length) {
167
+ const value = parsePositiveIntegerOption(arg, args[++i]);
168
+ if (value !== undefined)
169
+ setLoopPolicyOption(result, "maxToolResultBatchSizeChars", value);
170
+ }
171
+ else if (arg === "--output-token-budget" && i + 1 < args.length) {
172
+ const value = parsePositiveIntegerOption(arg, args[++i]);
173
+ if (value !== undefined)
174
+ outputTokenBudget.targetTokens = value;
175
+ }
176
+ else if (arg === "--output-token-budget-threshold" && i + 1 < args.length) {
177
+ const value = parseUnitIntervalOption(arg, args[++i]);
178
+ if (value !== undefined)
179
+ outputTokenBudget.thresholdPct = value;
180
+ }
181
+ else if (arg === "--output-token-budget-continuations" && i + 1 < args.length) {
182
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
183
+ if (value !== undefined)
184
+ outputTokenBudget.maxContinuations = value;
185
+ }
186
+ else if (arg === "--max-output-token-recovery-attempts" && i + 1 < args.length) {
187
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
188
+ if (value !== undefined)
189
+ setLoopPolicyOption(result, "maxOutputTokenRecoveryAttempts", value);
190
+ }
191
+ else if (arg === "--max-model-error-recovery-attempts" && i + 1 < args.length) {
192
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
193
+ if (value !== undefined)
194
+ setLoopPolicyOption(result, "maxModelErrorRecoveryAttempts", value);
195
+ }
196
+ else if (arg === "--max-stop-hook-continuations" && i + 1 < args.length) {
197
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
198
+ if (value !== undefined)
199
+ setLoopPolicyOption(result, "maxStopHookContinuations", value);
200
+ }
92
201
  else if (arg === "--export" && i + 1 < args.length) {
93
202
  result.export = args[++i];
94
203
  }
@@ -168,6 +277,13 @@ export function parseArgs(args, extensionFlags) {
168
277
  result.messages.push(arg);
169
278
  }
170
279
  }
280
+ if (outputTokenBudget.targetTokens !== undefined) {
281
+ setLoopPolicyOption(result, "outputTokenBudget", {
282
+ targetTokens: outputTokenBudget.targetTokens,
283
+ thresholdPct: outputTokenBudget.thresholdPct,
284
+ maxContinuations: outputTokenBudget.maxContinuations,
285
+ });
286
+ }
171
287
  return result;
172
288
  }
173
289
  export function printHelp() {
@@ -194,6 +310,20 @@ ${chalk.bold("Options:")}
194
310
  --append-system-prompt <text> Append text or file contents to the system prompt
195
311
  --mode <mode> Output mode: text (default), json, or rpc
196
312
  --print, -p Non-interactive mode: process prompt and exit
313
+ --print-loop-result In text print mode, write final loop result JSON to stderr
314
+ --fail-on-agent-error In print mode, exit non-zero when final loop result is an error
315
+ --fail-on-tool-denial In print mode, exit non-zero when tools were denied
316
+ --agent-loop <framework> Override loop framework: standard or weak-model-compatible
317
+ --max-turns-per-prompt <n> Stop a prompt after n assistant turns
318
+ --max-tool-calls-per-prompt <n> Stop a prompt after n tool calls
319
+ --max-tool-concurrency <n> Max concurrent safe tool calls in compatible loop
320
+ --max-tool-result-batch-size-chars <n> Max aggregate tool result chars per turn
321
+ --output-token-budget <n> Continue when final output is below n tokens
322
+ --output-token-budget-threshold <n> Continuation threshold ratio in (0,1], default loop policy
323
+ --output-token-budget-continuations <n> Max output-budget continuations
324
+ --max-output-token-recovery-attempts <n> Max recovery turns after output-token stops
325
+ --max-model-error-recovery-attempts <n> Max in-loop model error recoveries
326
+ --max-stop-hook-continuations <n> Max stop-hook validation continuations
197
327
  --continue, -c Continue previous session
198
328
  --resume, -r Select a session to resume
199
329
  --session <path> Use specific session file
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: SettingsManager class, two-tier settings (global + project-local)
2
+ * [WHO]: SettingsManager class, two-tier settings (global + project-local), agent loop defaults
3
3
  * [FROM]: Depends on ai, node:fs, proper-lockfile, config.ts
4
4
  * [TO]: Consumed by index.ts, main.ts, core/runtime/sdk.ts, cli/config-selector.ts, extensions/defaults/team/index.ts, modes/interactive/components/model-selector.ts, modes/interactive/components/config-selector.ts, and test files
5
5
  * [HERE]: core/config/settings-manager.ts - user preferences aggregation
@@ -20,6 +20,9 @@ export interface RetrySettings {
20
20
  baseDelayMs?: number;
21
21
  maxDelayMs?: number;
22
22
  }
23
+ export interface AgentLoopSettings {
24
+ maxToolResultBatchSizeChars?: number;
25
+ }
23
26
  export interface TerminalSettings {
24
27
  showImages?: boolean;
25
28
  clearOnShrink?: boolean;
@@ -65,6 +68,7 @@ export interface Settings {
65
68
  defaultModel?: string;
66
69
  defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
67
70
  agentLoopFramework?: AgentLoopFrameworkSettingInput;
71
+ agentLoop?: AgentLoopSettings;
68
72
  transport?: TransportSetting;
69
73
  steeringMode?: "all" | "one-at-a-time";
70
74
  followUpMode?: "all" | "one-at-a-time";
@@ -218,6 +222,9 @@ export declare class SettingsManager {
218
222
  getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined;
219
223
  setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void;
220
224
  getAgentLoopFramework(): AgentLoopFrameworkSetting | undefined;
225
+ getAgentLoopSettings(): {
226
+ maxToolResultBatchSizeChars: number;
227
+ };
221
228
  setAgentLoopFramework(framework: AgentLoopFrameworkSettingInput | undefined): void;
222
229
  getTransport(): TransportSetting;
223
230
  setTransport(transport: TransportSetting): void;
@@ -4,6 +4,7 @@ import lockfile from "proper-lockfile";
4
4
  import { APP_NAME, CONFIG_DIR_NAME } from "../../config.js";
5
5
  import { defaultAgentDirContext } from "../agent-dir/agent-dir-context.js";
6
6
  import { readFile, writeFile, mkdir } from "node:fs/promises";
7
+ const DEFAULT_MAX_TOOL_RESULT_BATCH_SIZE_CHARS = 200_000;
7
8
  export function normalizeAgentLoopFrameworkSetting(value) {
8
9
  if (value === "high-intelligence")
9
10
  return "standard";
@@ -574,6 +575,14 @@ export class SettingsManager {
574
575
  getAgentLoopFramework() {
575
576
  return normalizeAgentLoopFrameworkSetting(this.settings.agentLoopFramework);
576
577
  }
578
+ getAgentLoopSettings() {
579
+ const configured = this.settings.agentLoop?.maxToolResultBatchSizeChars;
580
+ return {
581
+ maxToolResultBatchSizeChars: typeof configured === "number" && Number.isFinite(configured) && configured > 0
582
+ ? Math.floor(configured)
583
+ : DEFAULT_MAX_TOOL_RESULT_BATCH_SIZE_CHARS,
584
+ };
585
+ }
577
586
  setAgentLoopFramework(framework) {
578
587
  const normalized = normalizeAgentLoopFrameworkSetting(framework);
579
588
  if (normalized === undefined) {
@@ -8,6 +8,6 @@ export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from
8
8
  export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.js";
9
9
  export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.js";
10
10
  export { ExtensionRunner } from "./runner.js";
11
- export type { AgentEndEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppAction, AppendEntryHandler, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionForkEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WriteToolCallEvent, WriteToolResultEvent, } from "./types.js";
11
+ export type { AgentEndEvent, AgentResultEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppAction, AppendEntryHandler, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionForkEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WriteToolCallEvent, WriteToolResultEvent, } from "./types.js";
12
12
  export { isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.js";
13
13
  export { wrapRegisteredTool, wrapRegisteredTools, wrapToolsWithExtensions, wrapToolWithExtensions, } from "./wrapper.js";
@@ -1,8 +1,8 @@
1
1
  /**
2
- * [WHO]: ExtensionRunner class, lifecycle management, event emission
3
- * [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts
4
- * [TO]: Consumed by core/extensions/index.ts, core/extensions/wrapper.ts
5
- * [HERE]: core/extensions/runner.ts - extension execution and lifecycle management
2
+ * [WHO]: ExtensionRunner class, lifecycle management, event emission, slash-command dispatch chokepoint (invokeCommand), telemetry sink wiring (setTelemetrySink)
3
+ * [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts, core/telemetry (ExtensionTelemetrySink + classifyArgsSignature for the P1 ext_command_events writer)
4
+ * [TO]: Consumed by core/extensions/index.ts, core/extensions/wrapper.ts, core/runtime/agent-session.ts (delegates command dispatch via invokeCommand)
5
+ * [HERE]: core/extensions/runner.ts - extension execution and lifecycle management; owns the single try/catch around command.handler so telemetry can wrap every invocation regardless of caller mode
6
6
  */
7
7
  import type { AgentMessage } from "@pencil-agent/agent-core";
8
8
  import type { ImageContent } from "@pencil-agent/ai";
@@ -11,6 +11,7 @@ import type { ResourceDiagnostic } from "../diagnostics.js";
11
11
  import type { KeybindingsConfig } from "../keybindings.js";
12
12
  import type { ModelRegistry } from "../model-registry.js";
13
13
  import type { SessionManager } from "../session/session-manager.js";
14
+ import { type ExtensionTelemetrySink, type LlmCallEventInput } from "../telemetry/index.js";
14
15
  import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, InputSource, MessageRenderer, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult } from "./types.js";
15
16
  /** Combined result from all before_agent_start handlers */
16
17
  interface BeforeAgentStartCombinedResult {
@@ -90,6 +91,7 @@ export declare class ExtensionRunner {
90
91
  private shutdownHandler;
91
92
  private shortcutDiagnostics;
92
93
  private commandDiagnostics;
94
+ private telemetrySink?;
93
95
  private _beforeAgentStartTimeoutMs;
94
96
  private get beforeAgentStartTimeoutMs();
95
97
  private readonly beforeAgentStartTimeoutSentinel;
@@ -127,6 +129,70 @@ export declare class ExtensionRunner {
127
129
  extensionPath: string;
128
130
  }>;
129
131
  getCommand(name: string): RegisteredCommand | undefined;
132
+ /**
133
+ * Returns the owning Extension for a given slash command name. Used by the
134
+ * telemetry middleware to stamp `extension_name` on each ext_command_events
135
+ * row. Falls back to undefined when no extension claims the command.
136
+ */
137
+ private findCommandOwner;
138
+ /**
139
+ * Derive a short, stable extension name from an Extension record. For
140
+ * built-ins (extensions/defaults/<name>) and most user extensions
141
+ * (packages/<name>) the directory basename is the right answer.
142
+ */
143
+ private deriveExtensionName;
144
+ /**
145
+ * Attach (or replace) the extension telemetry sink. The runner owns sink
146
+ * lifecycle from this point — invokeCommand() will fire-and-forget one
147
+ * `ext_command_events` row per invocation, and writeLlmCallEvent() (called
148
+ * from extension-core-bindings) writes one `ext_llm_calls` row per
149
+ * extension-initiated LLM call. Passing the noop sink (the factory's
150
+ * default when no insforge credentials exist) is the safe way to disable
151
+ * telemetry without scattering null-checks at the call sites.
152
+ */
153
+ setTelemetrySink(sink: ExtensionTelemetrySink): void;
154
+ /**
155
+ * Passthrough used by core/runtime/extension-core-bindings.ts after each
156
+ * extension-initiated LLM call. The runner owns the sink; the binding
157
+ * doesn't import it directly to keep its concerns scoped to LLM plumbing.
158
+ */
159
+ writeLlmCallEvent(input: LlmCallEventInput): void;
160
+ /**
161
+ * Wrap an extension hook handler invocation in three layers:
162
+ *
163
+ * 1. AsyncLocalStorage frame so any LLM call placed by the handler gets
164
+ * attributed to this extension + hook + isUserInitiated=false in
165
+ * ext_llm_calls.
166
+ * 2. Wall-clock timing measurement (only when the sample roll passes).
167
+ * 3. One fire-and-forget ext_hook_events row per sampled invocation,
168
+ * capturing duration_ms + ok + error_code + sample_rate.
169
+ *
170
+ * High-frequency hooks (tool_*) are sampled at 10% so the table doesn't
171
+ * drown in tool-execution rows; the sample_rate column on each row lets
172
+ * dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision
173
+ * sits inside the AsyncLocalStorage frame so even skipped-emit hooks still
174
+ * attribute their LLM calls.
175
+ */
176
+ private invokeHookHandler;
177
+ /**
178
+ * Single chokepoint for slash command dispatch. All modes (interactive /
179
+ * print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand,
180
+ * which calls this method. The wrapper measures wall-clock duration,
181
+ * captures outcome (ok / error / cancelled), and emits one telemetry row
182
+ * per invocation. Errors are still routed through emitError() so existing
183
+ * UI surfaces (toasts, logs) keep working unchanged.
184
+ *
185
+ * Returns `{ found: false }` when no extension owns the command, letting
186
+ * the caller fall through to built-in command handling without emitting a
187
+ * telemetry row for an unknown command.
188
+ */
189
+ invokeCommand(commandName: string, args: string, ctx: ExtensionCommandContext, metadata?: {
190
+ sessionId?: string | null;
191
+ runId?: string | null;
192
+ variant?: string | null;
193
+ }): Promise<{
194
+ found: boolean;
195
+ }>;
130
196
  /**
131
197
  * Request a graceful shutdown. Called by extension tools and event handlers.
132
198
  * The actual shutdown behavior is provided by the mode via bindExtensions().
@@ -1,4 +1,5 @@
1
1
  import { theme } from "../../modes/interactive/theme/theme.js";
2
+ import { classifyArgsSignature, HOOK_SAMPLE_RATES, runWithExtCallerContext, } from "../telemetry/index.js";
2
3
  // Keybindings for these actions cannot be overridden by extensions
3
4
  const RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS = [
4
5
  "interrupt",
@@ -108,6 +109,7 @@ export class ExtensionRunner {
108
109
  shutdownHandler = () => { };
109
110
  shortcutDiagnostics = [];
110
111
  commandDiagnostics = [];
112
+ telemetrySink;
111
113
  _beforeAgentStartTimeoutMs = 1500;
112
114
  get beforeAgentStartTimeoutMs() { return this._beforeAgentStartTimeoutMs ?? 1500; }
113
115
  beforeAgentStartTimeoutSentinel = Symbol("before_agent_start_timeout");
@@ -378,6 +380,184 @@ export class ExtensionRunner {
378
380
  }
379
381
  return undefined;
380
382
  }
383
+ /**
384
+ * Returns the owning Extension for a given slash command name. Used by the
385
+ * telemetry middleware to stamp `extension_name` on each ext_command_events
386
+ * row. Falls back to undefined when no extension claims the command.
387
+ */
388
+ findCommandOwner(name) {
389
+ for (const ext of this.extensions) {
390
+ if (ext.commands.has(name))
391
+ return ext;
392
+ }
393
+ return undefined;
394
+ }
395
+ /**
396
+ * Derive a short, stable extension name from an Extension record. For
397
+ * built-ins (extensions/defaults/<name>) and most user extensions
398
+ * (packages/<name>) the directory basename is the right answer.
399
+ */
400
+ deriveExtensionName(ext) {
401
+ const path = ext.path || ext.resolvedPath || "";
402
+ const segments = path.replace(/\/+$/, "").split(/[\\/]/);
403
+ return segments[segments.length - 1] || "unknown";
404
+ }
405
+ /**
406
+ * Attach (or replace) the extension telemetry sink. The runner owns sink
407
+ * lifecycle from this point — invokeCommand() will fire-and-forget one
408
+ * `ext_command_events` row per invocation, and writeLlmCallEvent() (called
409
+ * from extension-core-bindings) writes one `ext_llm_calls` row per
410
+ * extension-initiated LLM call. Passing the noop sink (the factory's
411
+ * default when no insforge credentials exist) is the safe way to disable
412
+ * telemetry without scattering null-checks at the call sites.
413
+ */
414
+ setTelemetrySink(sink) {
415
+ this.telemetrySink = sink;
416
+ }
417
+ /**
418
+ * Passthrough used by core/runtime/extension-core-bindings.ts after each
419
+ * extension-initiated LLM call. The runner owns the sink; the binding
420
+ * doesn't import it directly to keep its concerns scoped to LLM plumbing.
421
+ */
422
+ writeLlmCallEvent(input) {
423
+ try {
424
+ this.telemetrySink?.writeLlmCallEvent(input);
425
+ }
426
+ catch {
427
+ // Telemetry must never destabilize the LLM call path.
428
+ }
429
+ }
430
+ /**
431
+ * Wrap an extension hook handler invocation in three layers:
432
+ *
433
+ * 1. AsyncLocalStorage frame so any LLM call placed by the handler gets
434
+ * attributed to this extension + hook + isUserInitiated=false in
435
+ * ext_llm_calls.
436
+ * 2. Wall-clock timing measurement (only when the sample roll passes).
437
+ * 3. One fire-and-forget ext_hook_events row per sampled invocation,
438
+ * capturing duration_ms + ok + error_code + sample_rate.
439
+ *
440
+ * High-frequency hooks (tool_*) are sampled at 10% so the table doesn't
441
+ * drown in tool-execution rows; the sample_rate column on each row lets
442
+ * dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision
443
+ * sits inside the AsyncLocalStorage frame so even skipped-emit hooks still
444
+ * attribute their LLM calls.
445
+ */
446
+ invokeHookHandler(ext, hookName, fn) {
447
+ const extensionName = this.deriveExtensionName(ext);
448
+ const ctx = {
449
+ extensionName,
450
+ callerContext: `hook:${hookName}`,
451
+ isUserInitiated: false,
452
+ };
453
+ return runWithExtCallerContext(ctx, async () => {
454
+ const sampleRate = HOOK_SAMPLE_RATES[hookName] ?? 1.0;
455
+ if (sampleRate < 1.0 && Math.random() >= sampleRate) {
456
+ return await fn();
457
+ }
458
+ const recordedAt = new Date();
459
+ const startPerf = performance.now();
460
+ let ok = true;
461
+ let errorCode = null;
462
+ try {
463
+ return await fn();
464
+ }
465
+ catch (err) {
466
+ ok = false;
467
+ errorCode = err instanceof Error ? err.constructor.name : "unknown";
468
+ throw err;
469
+ }
470
+ finally {
471
+ try {
472
+ this.telemetrySink?.writeHookEvent({
473
+ extensionName,
474
+ hookName,
475
+ durationMs: Math.round(performance.now() - startPerf),
476
+ ok,
477
+ errorCode,
478
+ sampleRate,
479
+ recordedAt,
480
+ });
481
+ }
482
+ catch {
483
+ // Telemetry never destabilizes hook dispatch.
484
+ }
485
+ }
486
+ });
487
+ }
488
+ /**
489
+ * Single chokepoint for slash command dispatch. All modes (interactive /
490
+ * print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand,
491
+ * which calls this method. The wrapper measures wall-clock duration,
492
+ * captures outcome (ok / error / cancelled), and emits one telemetry row
493
+ * per invocation. Errors are still routed through emitError() so existing
494
+ * UI surfaces (toasts, logs) keep working unchanged.
495
+ *
496
+ * Returns `{ found: false }` when no extension owns the command, letting
497
+ * the caller fall through to built-in command handling without emitting a
498
+ * telemetry row for an unknown command.
499
+ */
500
+ async invokeCommand(commandName, args, ctx, metadata) {
501
+ const command = this.getCommand(commandName);
502
+ if (!command)
503
+ return { found: false };
504
+ const ownerExt = this.findCommandOwner(commandName);
505
+ const extensionName = ownerExt ? this.deriveExtensionName(ownerExt) : "unknown";
506
+ const startedAt = new Date();
507
+ const startPerf = performance.now();
508
+ let outcome = "ok";
509
+ let errorCode = null;
510
+ let thrown;
511
+ const argsSig = classifyArgsSignature(args);
512
+ const callerCtx = {
513
+ extensionName,
514
+ callerContext: argsSig === "no-args" ? `command:/${commandName}` : `command:/${commandName} ${argsSig}`,
515
+ isUserInitiated: true,
516
+ sessionId: metadata?.sessionId ?? null,
517
+ runId: metadata?.runId ?? null,
518
+ variant: metadata?.variant ?? null,
519
+ };
520
+ try {
521
+ await runWithExtCallerContext(callerCtx, () => command.handler(args, ctx));
522
+ }
523
+ catch (err) {
524
+ outcome = "error";
525
+ errorCode = err instanceof Error ? err.constructor.name : "unknown";
526
+ thrown = err;
527
+ this.emitError({
528
+ extensionPath: `command:${commandName}`,
529
+ event: "command",
530
+ error: err instanceof Error ? err.message : String(err),
531
+ });
532
+ }
533
+ const durationMs = Math.round(performance.now() - startPerf);
534
+ const endedAt = new Date();
535
+ try {
536
+ this.telemetrySink?.writeCommandEvent({
537
+ extensionName,
538
+ commandName,
539
+ argsSignature: argsSig,
540
+ argsLength: args.length,
541
+ outcome,
542
+ errorCode,
543
+ durationMs,
544
+ startedAt,
545
+ endedAt,
546
+ sessionId: metadata?.sessionId ?? null,
547
+ runId: metadata?.runId ?? null,
548
+ variant: metadata?.variant ?? null,
549
+ });
550
+ }
551
+ catch {
552
+ // Sink emission must never bring down command dispatch.
553
+ }
554
+ // Even though we logged the error to telemetry + emitError, callers
555
+ // expecting the thrown error (e.g. AgentSession previously re-caught it
556
+ // silently) won't see it. This matches the original behaviour: the old
557
+ // _tryExecuteExtensionCommand swallowed the error after emitError too.
558
+ void thrown;
559
+ return { found: true };
560
+ }
381
561
  /**
382
562
  * Request a graceful shutdown. Called by extension tools and event handlers.
383
563
  * The actual shutdown behavior is provided by the mode via bindExtensions().
@@ -444,7 +624,7 @@ export class ExtensionRunner {
444
624
  continue;
445
625
  for (const handler of handlers) {
446
626
  try {
447
- const handlerResult = await handler(event, ctx);
627
+ const handlerResult = await this.invokeHookHandler(ext, event.type, () => handler(event, ctx));
448
628
  if (this.isSessionBeforeEvent(event) && handlerResult) {
449
629
  result = handlerResult;
450
630
  if (result.cancel) {
@@ -476,7 +656,7 @@ export class ExtensionRunner {
476
656
  continue;
477
657
  for (const handler of handlers) {
478
658
  try {
479
- const handlerResult = (await handler(currentEvent, ctx));
659
+ const handlerResult = (await this.invokeHookHandler(ext, "tool_result", () => handler(currentEvent, ctx)));
480
660
  if (!handlerResult)
481
661
  continue;
482
662
  if (handlerResult.content !== undefined) {
@@ -521,7 +701,7 @@ export class ExtensionRunner {
521
701
  if (!handlers || handlers.length === 0)
522
702
  continue;
523
703
  for (const handler of handlers) {
524
- const handlerResult = await handler(event, ctx);
704
+ const handlerResult = await this.invokeHookHandler(ext, "tool_call", () => handler(event, ctx));
525
705
  if (handlerResult) {
526
706
  result = handlerResult;
527
707
  if (result.block) {
@@ -540,7 +720,7 @@ export class ExtensionRunner {
540
720
  continue;
541
721
  for (const handler of handlers) {
542
722
  try {
543
- const handlerResult = await handler(event, ctx);
723
+ const handlerResult = await this.invokeHookHandler(ext, "user_bash", () => handler(event, ctx));
544
724
  if (handlerResult) {
545
725
  return handlerResult;
546
726
  }
@@ -569,7 +749,7 @@ export class ExtensionRunner {
569
749
  for (const handler of handlers) {
570
750
  try {
571
751
  const event = { type: "context", messages: currentMessages };
572
- const handlerResult = await handler(event, ctx);
752
+ const handlerResult = await this.invokeHookHandler(ext, "context", () => handler(event, ctx));
573
753
  if (handlerResult && handlerResult.messages) {
574
754
  currentMessages = handlerResult.messages;
575
755
  }
@@ -605,7 +785,7 @@ export class ExtensionRunner {
605
785
  images,
606
786
  systemPrompt: currentSystemPrompt,
607
787
  };
608
- const handlerResult = await this.withTimeout(handler(event, ctx), this.beforeAgentStartTimeoutMs);
788
+ const handlerResult = await this.withTimeout(this.invokeHookHandler(ext, "before_agent_start", () => handler(event, ctx)), this.beforeAgentStartTimeoutMs);
609
789
  if (handlerResult === this.beforeAgentStartTimeoutSentinel) {
610
790
  this.reportBeforeAgentStartTimeout(ext.path);
611
791
  continue;
@@ -663,7 +843,7 @@ export class ExtensionRunner {
663
843
  for (const handler of handlers) {
664
844
  try {
665
845
  const event = { type: "resources_discover", cwd, reason };
666
- const handlerResult = await handler(event, ctx);
846
+ const handlerResult = await this.invokeHookHandler(ext, "resources_discover", () => handler(event, ctx));
667
847
  const result = handlerResult;
668
848
  if (result?.skillPaths?.length) {
669
849
  skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));
@@ -698,7 +878,7 @@ export class ExtensionRunner {
698
878
  for (const handler of ext.handlers.get("input") ?? []) {
699
879
  try {
700
880
  const event = { type: "input", text: currentText, images: currentImages, source };
701
- const result = (await handler(event, ctx));
881
+ const result = (await this.invokeHookHandler(ext, "input", () => handler(event, ctx)));
702
882
  if (result?.action === "handled")
703
883
  return result;
704
884
  if (result?.action === "transform") {