@gajae-code/coding-agent 0.16.4 → 0.16.6

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 (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Extension runner - executes extensions and manages their lifecycle.
3
3
  */
4
+
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { randomUUID } from "node:crypto";
7
+ import * as path from "node:path";
4
8
  import type { AgentMessage } from "@gajae-code/agent-core";
5
9
  import type { AttemptScope } from "@gajae-code/agent-core/attempt-scope";
6
10
  import type {
@@ -18,6 +22,37 @@ import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/workflow
18
22
  import { type Theme, theme } from "../../modes/theme/theme";
19
23
  import type { AttemptRecordStore } from "../../session/attempt-record-store";
20
24
  import { createReadonlySessionManager, type SessionManager } from "../../session/session-manager";
25
+ import {
26
+ attenuateFunctionHookGrant,
27
+ cloneFunctionHookDataStrict,
28
+ compatibilityPayloadForFunctionHook,
29
+ createFunctionHookCapabilities,
30
+ type FunctionHook,
31
+ type FunctionHookAuditRecord,
32
+ type FunctionHookCapability,
33
+ type FunctionHookCapabilityBindings,
34
+ type FunctionHookGrant,
35
+ type FunctionHookInvocation,
36
+ type FunctionHookNext,
37
+ type FunctionHookRegistration,
38
+ type FunctionHookResult,
39
+ functionHookDenyAllowed,
40
+ functionHookEventIdentityMatches,
41
+ functionHookGrantHash,
42
+ functionHookInspectAllowed,
43
+ functionHookPayloadHash,
44
+ functionHookTransformAllowed,
45
+ isPlainFunctionHookData,
46
+ isValidFunctionHookEventValue,
47
+ isValidFunctionHookReturnValue,
48
+ redactFunctionHookValue,
49
+ sanitizeFunctionHookReason,
50
+ } from "./function-hooks";
51
+ import {
52
+ getExtensionHandlerRegistrationOrder,
53
+ getFunctionHookRegistration,
54
+ readConstrainedFunctionHookFile,
55
+ } from "./function-hooks-internal";
21
56
  import type {
22
57
  AfterProviderResponseEvent,
23
58
  BeforeAgentStartEvent,
@@ -67,6 +102,8 @@ import { createExtensionSettings } from "./types";
67
102
  interface BeforeAgentStartCombinedResult {
68
103
  messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
69
104
  systemPrompt?: string[];
105
+ prompt?: string;
106
+ images?: ImageContent[];
70
107
  }
71
108
 
72
109
  const BEFORE_AGENT_START_RESULT_KEYS = new Set(["message", "systemPrompt"]);
@@ -153,7 +190,15 @@ type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "
153
190
  ? SessionCompactingResult | undefined
154
191
  : undefined;
155
192
  type Handler = Extension["handlers"] extends Map<string, Array<infer T>> ? T : never;
156
- type IndexedHandler = { ext: Extension; handler: Handler };
193
+ type IndexedHandler = { ext: Extension; handler: Handler; registrationOrder: number };
194
+ type IndexedFunctionHook = IndexedHandler & { registration: FunctionHookRegistration };
195
+
196
+ export type FunctionHookDispatchResult<TEvent extends ExtensionEvent> =
197
+ | { action: "continue"; event: TEvent; transformed?: boolean }
198
+ | { action: "deny"; reason: string }
199
+ | { action: "return"; value: unknown };
200
+
201
+ export type FunctionHookAuditSink = (record: FunctionHookAuditRecord) => void;
157
202
 
158
203
  export type NewSessionHandler = (options?: {
159
204
  parentSession?: string;
@@ -218,6 +263,10 @@ export class ExtensionRunner {
218
263
  #errorListeners: Set<ExtensionErrorListener> = new Set();
219
264
  #handlersByEvent: Map<string, IndexedHandler[]> = new Map();
220
265
  #attemptRecordStore: AttemptRecordStore | undefined;
266
+ #functionHookAuditSequence = 0;
267
+ #functionHookAudit: FunctionHookAuditRecord[] = [];
268
+ #functionHookAuditSink: FunctionHookAuditSink | undefined;
269
+ #functionHookDepth = new AsyncLocalStorage<number>();
221
270
 
222
271
  #getModel: () => Model | undefined = () => undefined;
223
272
  #getCredentialSessionId: () => string = () => "";
@@ -263,6 +312,7 @@ export class ExtensionRunner {
263
312
 
264
313
  #getJobsFn: ExtensionContextActions["getJobs"] = undefined;
265
314
  #onJobFoldFn: ExtensionContextActions["onJobFold"] = undefined;
315
+ #onSessionEventFn: ExtensionContextActions["onSessionEvent"] = undefined;
266
316
  #sdkControlFn: ExtensionContextActions["sdkControl"] = undefined;
267
317
  #setSdkPermissionProviderFn: ExtensionContextActions["setSdkPermissionProvider"] = undefined;
268
318
  #setSdkClientBridgeFn: ExtensionContextActions["setSdkClientBridge"] = undefined;
@@ -305,16 +355,36 @@ export class ExtensionRunner {
305
355
 
306
356
  static #indexHandlers(extensions: Extension[]): Map<string, IndexedHandler[]> {
307
357
  const handlersByEvent = new Map<string, IndexedHandler[]>();
358
+ let registrationOrder = 0;
308
359
  for (const ext of extensions) {
360
+ const extensionHandlers: Array<{
361
+ eventType: string;
362
+ handler: Handler;
363
+ index: number;
364
+ explicitOrder?: number;
365
+ }> = [];
366
+ let index = 0;
309
367
  for (const [eventType, handlers] of ext.handlers) {
368
+ for (const handler of handlers) {
369
+ const explicitOrder =
370
+ getExtensionHandlerRegistrationOrder(handler) ??
371
+ getFunctionHookRegistration(handler)?.registrationOrder;
372
+ extensionHandlers.push({ eventType, handler, index: index++, explicitOrder });
373
+ }
374
+ }
375
+ extensionHandlers.sort((a, b) => {
376
+ if (a.explicitOrder !== undefined && b.explicitOrder !== undefined)
377
+ return a.explicitOrder - b.explicitOrder;
378
+ return a.index - b.index;
379
+ });
380
+ for (const { eventType, handler } of extensionHandlers) {
310
381
  let indexedHandlers = handlersByEvent.get(eventType);
311
382
  if (!indexedHandlers) {
312
383
  indexedHandlers = [];
313
384
  handlersByEvent.set(eventType, indexedHandlers);
314
385
  }
315
- for (const handler of handlers) {
316
- indexedHandlers.push({ ext, handler });
317
- }
386
+ indexedHandlers.push({ ext, handler, registrationOrder });
387
+ registrationOrder += 1;
318
388
  }
319
389
  }
320
390
  return handlersByEvent;
@@ -395,6 +465,7 @@ export class ExtensionRunner {
395
465
 
396
466
  this.#getJobsFn = contextActions.getJobs;
397
467
  this.#onJobFoldFn = contextActions.onJobFold;
468
+ this.#onSessionEventFn = contextActions.onSessionEvent;
398
469
  this.#sdkControlFn = contextActions.sdkControl;
399
470
  this.#setSdkPermissionProviderFn = contextActions.setSdkPermissionProvider;
400
471
  this.#setSdkClientBridgeFn = contextActions.setSdkClientBridge;
@@ -556,7 +627,45 @@ export class ExtensionRunner {
556
627
  }
557
628
 
558
629
  hasHandlers(eventType: string): boolean {
559
- return (this.#handlersByEvent.get(eventType)?.length ?? 0) > 0;
630
+ return (
631
+ (this.#handlersByEvent.get(eventType)?.length ?? 0) > 0 ||
632
+ (this.#handlersByEvent.get("*")?.some(({ handler }) => getFunctionHookRegistration(handler) !== undefined) ??
633
+ false)
634
+ );
635
+ }
636
+
637
+ hasToolResultMediation(toolName: string): boolean {
638
+ for (const indexed of this.#matchingHandlers({ type: "tool_result", toolName } as ToolResultEvent, true)) {
639
+ const registration = getFunctionHookRegistration(indexed.handler);
640
+ if (!registration) return true;
641
+ const capabilities = new Set(registration.grant.capabilities);
642
+ if (capabilities.has("tool") || capabilities.has("tool.transform") || capabilities.has("tool.deny")) {
643
+ return true;
644
+ }
645
+ }
646
+ return false;
647
+ }
648
+
649
+ /** Return immutable registration metadata in authoritative registration order. */
650
+ getFunctionHookRegistrations(): readonly FunctionHookRegistration[] {
651
+ const registrations: Array<{ order: number; registration: FunctionHookRegistration }> = [];
652
+ for (const handlers of this.#handlersByEvent.values()) {
653
+ for (const indexed of handlers) {
654
+ const registration = getFunctionHookRegistration(indexed.handler);
655
+ if (registration) registrations.push({ order: indexed.registrationOrder, registration });
656
+ }
657
+ }
658
+ registrations.sort((a, b) => a.order - b.order);
659
+ return registrations.map(({ registration }) => registration);
660
+ }
661
+
662
+ /** Install the optional deterministic audit sink used by hosts and tests. */
663
+ setFunctionHookAuditSink(sink: FunctionHookAuditSink | undefined): void {
664
+ this.#functionHookAuditSink = sink;
665
+ }
666
+
667
+ getFunctionHookAudit(): readonly FunctionHookAuditRecord[] {
668
+ return this.#functionHookAudit.map(record => cloneFunctionHookDataStrict(record));
560
669
  }
561
670
 
562
671
  setAttemptRecordStore(store: AttemptRecordStore): void {
@@ -680,6 +789,7 @@ export class ExtensionRunner {
680
789
 
681
790
  getJobs: () => this.#getJobsFn?.(),
682
791
  onJobFold: listener => this.#onJobFoldFn?.(listener) ?? (() => {}),
792
+ onSessionEvent: listener => this.#onSessionEventFn?.(listener) ?? (() => {}),
683
793
  sdkControl: (operation, input) => this.#sdkControlFn?.(operation, input),
684
794
  setSdkPermissionProvider: provider => this.#setSdkPermissionProviderFn?.(provider),
685
795
  setSdkClientBridge: bridge => this.#setSdkClientBridgeFn?.(bridge),
@@ -732,6 +842,496 @@ export class ExtensionRunner {
732
842
  };
733
843
  }
734
844
 
845
+ #matchingFunctionHooks(event: ExtensionEvent): IndexedFunctionHook[] {
846
+ return this.#matchingHandlers(event, false).map(indexed => ({
847
+ ...indexed,
848
+ registration: getFunctionHookRegistration(indexed.handler)!,
849
+ }));
850
+ }
851
+
852
+ #matchingHandlers(event: ExtensionEvent, includeLegacyToolHandlers: boolean): IndexedHandler[] {
853
+ const exact = this.#handlersByEvent.get(event.type) ?? [];
854
+ const wildcard = this.#handlersByEvent.get("*") ?? [];
855
+ const matches: IndexedHandler[] = [];
856
+ for (const indexed of [...exact, ...wildcard]) {
857
+ const registration = getFunctionHookRegistration(indexed.handler);
858
+ if (!registration) {
859
+ if (includeLegacyToolHandlers && indexed.registrationOrder >= 0 && exact.includes(indexed))
860
+ matches.push(indexed);
861
+ continue;
862
+ }
863
+ if (registration.event !== "*" && registration.event !== event.type) continue;
864
+ if (
865
+ registration.target !== undefined &&
866
+ registration.target !== "*" &&
867
+ ((event.type !== "tool_call" && event.type !== "tool_result") || event.toolName !== registration.target)
868
+ ) {
869
+ continue;
870
+ }
871
+ matches.push(indexed);
872
+ }
873
+ matches.sort((a, b) => a.registrationOrder - b.registrationOrder);
874
+ return matches;
875
+ }
876
+
877
+ #functionHookAuditPath(registration: FunctionHookRegistration): string {
878
+ return path.basename(registration.provenance.path).slice(0, 256);
879
+ }
880
+
881
+ #appendFunctionHookAudit(
882
+ registration: FunctionHookRegistration,
883
+ invocation: FunctionHookInvocation,
884
+ action: FunctionHookAuditRecord["action"],
885
+ reason?: string,
886
+ effectiveGrant: FunctionHookGrant = registration.grant,
887
+ evidence?: unknown,
888
+ ): void {
889
+ const record: FunctionHookAuditRecord = {
890
+ sequence: this.#functionHookAuditSequence,
891
+ eventId: invocation.eventId,
892
+ correlationId: invocation.correlationId,
893
+ eventType: invocation.eventType,
894
+ action,
895
+ registrationOrder: invocation.registrationOrder,
896
+ provenance: {
897
+ source: registration.provenance.source,
898
+ scope: registration.provenance.scope,
899
+ plugin: registration.provenance.plugin,
900
+ extensionId: registration.provenance.extensionId,
901
+ path: this.#functionHookAuditPath(registration),
902
+ },
903
+ payloadHash: functionHookPayloadHash(invocation.payload),
904
+ requestedCapabilities: [...registration.grant.capabilities],
905
+ effectiveCapabilities: [...effectiveGrant.capabilities],
906
+ capabilityHash: functionHookGrantHash(effectiveGrant),
907
+ ...(registration.provenance.activationGeneration === undefined
908
+ ? {}
909
+ : { activationGeneration: registration.provenance.activationGeneration }),
910
+ ...(reason === undefined ? {} : { reason: sanitizeFunctionHookReason(reason, "Function hook decision") }),
911
+ ...(evidence === undefined ? {} : { evidence: redactFunctionHookValue(evidence) }),
912
+ };
913
+ this.#functionHookAuditSequence += 1;
914
+ const frozenEvidence =
915
+ record.evidence !== null && typeof record.evidence === "object"
916
+ ? Object.freeze(cloneFunctionHookDataStrict(record.evidence))
917
+ : record.evidence;
918
+ const frozenRecord = Object.freeze({
919
+ ...record,
920
+ provenance: Object.freeze(record.provenance),
921
+ ...(record.evidence === undefined ? {} : { evidence: frozenEvidence }),
922
+ });
923
+ this.#functionHookAudit.push(frozenRecord);
924
+ if (this.#functionHookAudit.length > 1024) this.#functionHookAudit.shift();
925
+ this.#functionHookAuditSink?.(frozenRecord);
926
+ }
927
+
928
+ #functionHookErrorResult(event: ExtensionEvent, reason: string): FunctionHookDispatchResult<ExtensionEvent> {
929
+ if (
930
+ event.type === "tool_call" ||
931
+ event.type === "tool_result" ||
932
+ event.type === "before_provider_request" ||
933
+ event.type.startsWith("session_before_")
934
+ ) {
935
+ return { action: "deny", reason };
936
+ }
937
+ return { action: "continue", event };
938
+ }
939
+
940
+ async #runFunctionHookWithTimeout(
941
+ hook: FunctionHook,
942
+ invocation: FunctionHookInvocation,
943
+ capabilities: ReturnType<typeof createFunctionHookCapabilities>,
944
+ next: FunctionHookNext,
945
+ parentSignal: AbortSignal,
946
+ ext: Extension,
947
+ controller: AbortController,
948
+ ): Promise<
949
+ { status: "ok"; value: unknown } | { status: "timeout" } | { status: "error"; error: string; stack?: string }
950
+ > {
951
+ const abortFromParent = () => controller.abort(parentSignal.reason);
952
+ if (parentSignal.aborted) abortFromParent();
953
+ else parentSignal.addEventListener("abort", abortFromParent, { once: true });
954
+ const childInvocation = Object.freeze({ ...invocation, signal: controller.signal });
955
+ let timer: NodeJS.Timeout | undefined;
956
+ const timeout = new Promise<"timeout">(resolve => {
957
+ timer = setTimeout(() => {
958
+ controller.abort(new Error("Function hook timed out"));
959
+ resolve("timeout");
960
+ }, extensionHandlerTimeoutMs);
961
+ });
962
+ const { promise: aborted, resolve: resolveAborted } = Promise.withResolvers<"aborted">();
963
+ const resolveAbort = () => resolveAborted("aborted");
964
+ if (parentSignal.aborted) resolveAborted("aborted");
965
+ else parentSignal.addEventListener("abort", resolveAbort, { once: true });
966
+ try {
967
+ const value = await Promise.race([
968
+ Promise.resolve()
969
+ .then(() => hook(childInvocation, capabilities, next))
970
+ .then(result => ({ status: "ok" as const, value: result })),
971
+ timeout.then(status => ({ status })),
972
+ aborted.then(status => ({ status })),
973
+ ]);
974
+ if (value.status === "timeout" || value.status === "aborted") {
975
+ const error =
976
+ value.status === "timeout"
977
+ ? `handler timed out after ${extensionHandlerTimeoutMs}ms`
978
+ : "handler aborted";
979
+ this.emitError({ extensionPath: ext.path, event: invocation.eventType, error });
980
+ return { status: "timeout" };
981
+ }
982
+ return value;
983
+ } catch (error) {
984
+ const message = sanitizeFunctionHookReason(
985
+ error instanceof Error ? error.message : String(error),
986
+ "Function hook failed",
987
+ );
988
+ const stack =
989
+ error instanceof Error && error.stack
990
+ ? sanitizeFunctionHookReason(error.stack, "Function hook stack unavailable")
991
+ : undefined;
992
+ this.emitError({ extensionPath: ext.path, event: invocation.eventType, error: message, stack });
993
+ return { status: "error", error: message, stack };
994
+ } finally {
995
+ if (timer !== undefined) clearTimeout(timer);
996
+ parentSignal.removeEventListener("abort", abortFromParent);
997
+ parentSignal.removeEventListener("abort", resolveAbort);
998
+ }
999
+ }
1000
+
1001
+ /** Execute capability-scoped middleware stored in Extension.handlers. */
1002
+ async emitFunctionHooks<TEvent extends ExtensionEvent>(
1003
+ event: TEvent,
1004
+ options: {
1005
+ signal?: AbortSignal;
1006
+ correlationId?: string;
1007
+ scope?: AttemptScopeRef;
1008
+ legacyHandler?: (handler: IndexedHandler, event: TEvent) => Promise<FunctionHookDispatchResult<TEvent>>;
1009
+ } = {},
1010
+ ): Promise<FunctionHookDispatchResult<TEvent>> {
1011
+ const handlers = options.legacyHandler ? this.#matchingHandlers(event, true) : this.#matchingFunctionHooks(event);
1012
+ if (handlers.length === 0) return { action: "continue", event };
1013
+ const includesLegacy = handlers.some(indexed => getFunctionHookRegistration(indexed.handler) === undefined);
1014
+ if (includesLegacy) this.#requireScopeOrFailClosed(options.scope, event.type);
1015
+ this.#markAttemptExecuted(options.scope);
1016
+ const functionHookDepth = this.#functionHookDepth.getStore() ?? 0;
1017
+ if (functionHookDepth >= 16) {
1018
+ const reason = "Function hook re-entry depth exceeded";
1019
+ this.emitError({ extensionPath: "<function-hooks>", event: event.type, error: reason });
1020
+ return this.#functionHookErrorResult(event, reason) as FunctionHookDispatchResult<TEvent>;
1021
+ }
1022
+ const parentSignal = options.signal ?? new AbortController().signal;
1023
+ const eventId = randomUUID();
1024
+ const correlationId = options.correlationId ?? eventId;
1025
+ return await this.#functionHookDepth.run(functionHookDepth + 1, async () => {
1026
+ const invoke = async (
1027
+ index: number,
1028
+ currentEvent: TEvent,
1029
+ removedCapabilities: readonly FunctionHookCapability[],
1030
+ chainSignal: AbortSignal,
1031
+ ): Promise<FunctionHookDispatchResult<TEvent>> => {
1032
+ if (index >= handlers.length) return { action: "continue", event: currentEvent, transformed: false };
1033
+ if (chainSignal.aborted)
1034
+ return this.#functionHookErrorResult(
1035
+ currentEvent,
1036
+ "Function hook chain aborted",
1037
+ ) as FunctionHookDispatchResult<TEvent>;
1038
+ const indexed = handlers[index];
1039
+ if (!indexed) return { action: "continue", event: currentEvent, transformed: false };
1040
+ const registration = getFunctionHookRegistration(indexed.handler);
1041
+ if (!registration) {
1042
+ const legacyDispatch = await options.legacyHandler!(indexed, currentEvent);
1043
+ if (legacyDispatch.action !== "continue") return legacyDispatch;
1044
+ const downstream = await invoke(index + 1, legacyDispatch.event, removedCapabilities, chainSignal);
1045
+ return legacyDispatch.transformed === true && downstream.action === "continue"
1046
+ ? { ...downstream, transformed: true }
1047
+ : downstream;
1048
+ }
1049
+ const effectiveGrant = attenuateFunctionHookGrant(registration.grant, removedCapabilities);
1050
+ const downstreamRemoved = [
1051
+ ...new Set([...removedCapabilities, ...(registration.grant.attenuateDownstream ?? [])]),
1052
+ ];
1053
+ const wildcard = registration.event === "*" || registration.target === "*";
1054
+ let nextCalled = false;
1055
+ let nextFailureReason: string | undefined;
1056
+ let nextPromise: Promise<FunctionHookDispatchResult<TEvent>> | undefined;
1057
+ let nextReturnValue: FunctionHookResult | undefined;
1058
+ const failureResult = async (reason: string): Promise<FunctionHookDispatchResult<TEvent>> => {
1059
+ if (functionHookDenyAllowed(currentEvent, effectiveGrant)) return { action: "deny", reason };
1060
+ if (wildcard) {
1061
+ if (nextPromise) return await nextPromise;
1062
+ return await invoke(index + 1, currentEvent, downstreamRemoved, chainSignal);
1063
+ }
1064
+ return this.#functionHookErrorResult(currentEvent, reason) as FunctionHookDispatchResult<TEvent>;
1065
+ };
1066
+ let payload: Readonly<TEvent>;
1067
+ try {
1068
+ payload = Object.freeze(
1069
+ compatibilityPayloadForFunctionHook(currentEvent, effectiveGrant, wildcard),
1070
+ ) as Readonly<TEvent>;
1071
+ } catch {
1072
+ const reason = "Function hook payload could not be snapshotted";
1073
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1074
+ return failureResult(reason);
1075
+ }
1076
+ const controller = new AbortController();
1077
+ const abortFromChain = () => controller.abort(chainSignal.reason);
1078
+ if (chainSignal.aborted) abortFromChain();
1079
+ else chainSignal.addEventListener("abort", abortFromChain, { once: true });
1080
+ const invocation = Object.freeze({
1081
+ eventId,
1082
+ correlationId,
1083
+ eventType: currentEvent.type,
1084
+ provenance: Object.freeze({ ...registration.provenance }),
1085
+ registrationOrder: indexed.registrationOrder,
1086
+ payload,
1087
+ signal: controller.signal,
1088
+ }) as FunctionHookInvocation<TEvent>;
1089
+ let active = true;
1090
+ const bindings: FunctionHookCapabilityBindings = {
1091
+ cwd: this.cwd,
1092
+ ui: this.#uiContext,
1093
+ sessionMetadata: this.sessionMetadata,
1094
+ isActive: () => active && !chainSignal.aborted && !controller.signal.aborted,
1095
+ emitMessage: (message, options) => {
1096
+ const { triggerTurn: _triggerTurn, deliverAs: _deliverAs, ...payload } = message;
1097
+ this.runtime.sendMessage(
1098
+ {
1099
+ ...payload,
1100
+ display: message.display ?? false,
1101
+ },
1102
+ options,
1103
+ );
1104
+ },
1105
+ appendAudit: evidence => {
1106
+ this.#appendFunctionHookAudit(
1107
+ registration,
1108
+ invocation,
1109
+ "continue",
1110
+ undefined,
1111
+ effectiveGrant,
1112
+ evidence,
1113
+ );
1114
+ },
1115
+ fetch: (input, init) => fetch(input, init),
1116
+ readFile: (filePath, roots) => readConstrainedFunctionHookFile(filePath, this.cwd, roots),
1117
+ };
1118
+ const capabilities = createFunctionHookCapabilities(effectiveGrant, bindings);
1119
+ const next: FunctionHookNext = async nextEvent => {
1120
+ if (nextCalled) throw new Error("Function hook next() may only be called once");
1121
+ nextCalled = true;
1122
+ const candidate = (nextEvent ?? currentEvent) as TEvent;
1123
+ if (
1124
+ nextEvent !== undefined &&
1125
+ (!isPlainFunctionHookData(candidate) ||
1126
+ !functionHookEventIdentityMatches(currentEvent, candidate) ||
1127
+ !isValidFunctionHookEventValue(candidate) ||
1128
+ !functionHookTransformAllowed(currentEvent, effectiveGrant))
1129
+ ) {
1130
+ nextFailureReason = "Function hook passed an invalid or unauthorized event to next()";
1131
+ throw new Error(nextFailureReason);
1132
+ }
1133
+ let candidateSnapshot = currentEvent;
1134
+ if (nextEvent !== undefined) {
1135
+ try {
1136
+ candidateSnapshot = cloneFunctionHookDataStrict(candidate);
1137
+ } catch {
1138
+ nextFailureReason = "Function hook replacement event could not be snapshotted";
1139
+ throw new Error(nextFailureReason);
1140
+ }
1141
+ }
1142
+ nextPromise = invoke(index + 1, candidateSnapshot, downstreamRemoved, controller.signal).then(
1143
+ downstreamResult =>
1144
+ nextEvent !== undefined && downstreamResult.action === "continue"
1145
+ ? { ...downstreamResult, transformed: true }
1146
+ : downstreamResult,
1147
+ );
1148
+ const continuedResult = await nextPromise;
1149
+ try {
1150
+ const publicResult =
1151
+ continuedResult.action === "continue"
1152
+ ? {
1153
+ action: "continue" as const,
1154
+ event: compatibilityPayloadForFunctionHook(
1155
+ continuedResult.event,
1156
+ effectiveGrant,
1157
+ wildcard,
1158
+ ),
1159
+ }
1160
+ : continuedResult.action === "return" &&
1161
+ !functionHookInspectAllowed(currentEvent, effectiveGrant, wildcard)
1162
+ ? { action: "return" as const, value: "<redacted>" }
1163
+ : continuedResult;
1164
+ nextReturnValue = cloneFunctionHookDataStrict(publicResult) as FunctionHookResult;
1165
+ return nextReturnValue;
1166
+ } catch {
1167
+ nextFailureReason = "Function hook continuation result could not be snapshotted";
1168
+ throw new Error(nextFailureReason);
1169
+ }
1170
+ };
1171
+ const outcome = await this.#runFunctionHookWithTimeout(
1172
+ registration.handler,
1173
+ invocation,
1174
+ capabilities,
1175
+ next as FunctionHookNext,
1176
+ chainSignal,
1177
+ indexed.ext,
1178
+ controller,
1179
+ );
1180
+ if (outcome.status === "timeout") {
1181
+ controller.abort(new Error("Function hook invocation timed out"));
1182
+ active = false;
1183
+ chainSignal.removeEventListener("abort", abortFromChain);
1184
+ this.#appendFunctionHookAudit(registration, invocation, "timeout");
1185
+ return failureResult("Function hook timed out");
1186
+ }
1187
+ if (outcome.status === "error") {
1188
+ controller.abort(new Error("Function hook invocation failed"));
1189
+ active = false;
1190
+ chainSignal.removeEventListener("abort", abortFromChain);
1191
+ this.#appendFunctionHookAudit(registration, invocation, "error", outcome.error);
1192
+ return failureResult(outcome.error);
1193
+ }
1194
+ if (nextFailureReason) {
1195
+ controller.abort(new Error(nextFailureReason));
1196
+ active = false;
1197
+ chainSignal.removeEventListener("abort", abortFromChain);
1198
+ this.#appendFunctionHookAudit(registration, invocation, "error", nextFailureReason);
1199
+ return failureResult(nextFailureReason);
1200
+ }
1201
+ if (nextCalled) {
1202
+ if (outcome.value !== undefined && outcome.value !== nextReturnValue) {
1203
+ const reason = "Function hook returned a conflicting decision after next()";
1204
+ controller.abort(new Error(reason));
1205
+ active = false;
1206
+ chainSignal.removeEventListener("abort", abortFromChain);
1207
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1208
+ return failureResult(reason);
1209
+ }
1210
+ const nextResult = (await nextPromise) as FunctionHookDispatchResult<TEvent>;
1211
+ controller.abort(new Error("Function hook invocation completed"));
1212
+ active = false;
1213
+ chainSignal.removeEventListener("abort", abortFromChain);
1214
+ this.#appendFunctionHookAudit(registration, invocation, "continue", undefined, effectiveGrant);
1215
+ return nextResult;
1216
+ }
1217
+ controller.abort(new Error("Function hook invocation completed"));
1218
+ active = false;
1219
+ chainSignal.removeEventListener("abort", abortFromChain);
1220
+ const rawResult = outcome.value;
1221
+ if (rawResult === undefined) {
1222
+ this.#appendFunctionHookAudit(registration, invocation, "continue");
1223
+ return await invoke(index + 1, currentEvent, downstreamRemoved, chainSignal);
1224
+ }
1225
+ if (
1226
+ rawResult === null ||
1227
+ !isPlainFunctionHookData(rawResult) ||
1228
+ typeof rawResult !== "object" ||
1229
+ Array.isArray(rawResult)
1230
+ ) {
1231
+ const reason = "Function hook returned a non-plain result";
1232
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1233
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1234
+ return failureResult(reason);
1235
+ }
1236
+ const action = (rawResult as { action?: unknown }).action;
1237
+ const allowedResultKeys =
1238
+ action === "continue"
1239
+ ? new Set(["action", "event"])
1240
+ : action === "deny"
1241
+ ? new Set(["action", "reason"])
1242
+ : action === "return"
1243
+ ? new Set(["action", "value"])
1244
+ : undefined;
1245
+ if (!allowedResultKeys || Object.keys(rawResult).some(key => !allowedResultKeys.has(key))) {
1246
+ const reason = "Function hook returned unknown result fields";
1247
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1248
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1249
+ return failureResult(reason);
1250
+ }
1251
+ if (action === "continue") {
1252
+ const candidate = (rawResult as { event?: unknown }).event;
1253
+ let nextEvent = currentEvent;
1254
+ if (candidate !== undefined) {
1255
+ if (
1256
+ !isPlainFunctionHookData(candidate) ||
1257
+ !functionHookEventIdentityMatches(currentEvent, candidate as ExtensionEvent) ||
1258
+ !isValidFunctionHookEventValue(candidate as ExtensionEvent)
1259
+ ) {
1260
+ const reason = "Function hook returned an invalid transformed event";
1261
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1262
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1263
+ return failureResult(reason);
1264
+ }
1265
+ if (!functionHookTransformAllowed(currentEvent, effectiveGrant)) {
1266
+ const reason = "Function hook transform capability was not granted";
1267
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1268
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1269
+ return failureResult(reason);
1270
+ }
1271
+ try {
1272
+ nextEvent = cloneFunctionHookDataStrict(candidate as TEvent);
1273
+ } catch {
1274
+ const reason = "Function hook replacement event could not be snapshotted";
1275
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1276
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1277
+ return failureResult(reason);
1278
+ }
1279
+ }
1280
+ this.#appendFunctionHookAudit(registration, invocation, "continue");
1281
+ const downstreamResult = await invoke(index + 1, nextEvent, downstreamRemoved, chainSignal);
1282
+ return candidate !== undefined && downstreamResult.action === "continue"
1283
+ ? { ...downstreamResult, transformed: true }
1284
+ : downstreamResult;
1285
+ }
1286
+ if (action === "deny") {
1287
+ const reason = sanitizeFunctionHookReason(
1288
+ (rawResult as { reason?: unknown }).reason,
1289
+ "Function hook denied the event",
1290
+ );
1291
+ if (!functionHookDenyAllowed(currentEvent, effectiveGrant)) {
1292
+ const invalidReason = "Function hook deny capability was not granted";
1293
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: invalidReason });
1294
+ this.#appendFunctionHookAudit(registration, invocation, "error", invalidReason);
1295
+ return failureResult(invalidReason);
1296
+ }
1297
+ this.#appendFunctionHookAudit(registration, invocation, "deny", reason);
1298
+ return { action: "deny", reason } as FunctionHookDispatchResult<TEvent>;
1299
+ }
1300
+ if (action === "return" && Object.hasOwn(rawResult, "value")) {
1301
+ if (!functionHookDenyAllowed(currentEvent, effectiveGrant)) {
1302
+ const reason = "Function hook short-circuit capability was not granted";
1303
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1304
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1305
+ return failureResult(reason);
1306
+ }
1307
+ const value = (rawResult as { value: unknown }).value;
1308
+ if (value === undefined || !isValidFunctionHookReturnValue(currentEvent, value)) {
1309
+ const reason = "Function hook returned an invalid terminal value";
1310
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1311
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1312
+ return failureResult(reason);
1313
+ }
1314
+ let valueSnapshot: unknown;
1315
+ try {
1316
+ valueSnapshot = cloneFunctionHookDataStrict(value);
1317
+ } catch {
1318
+ const reason = "Function hook terminal value could not be snapshotted";
1319
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1320
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1321
+ return failureResult(reason);
1322
+ }
1323
+ this.#appendFunctionHookAudit(registration, invocation, "return");
1324
+ return { action: "return", value: valueSnapshot } as FunctionHookDispatchResult<TEvent>;
1325
+ }
1326
+ const reason = "Function hook returned an invalid action";
1327
+ this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
1328
+ this.#appendFunctionHookAudit(registration, invocation, "error", reason);
1329
+ return failureResult(reason);
1330
+ };
1331
+ return await invoke(0, event, [], parentSignal);
1332
+ });
1333
+ }
1334
+
735
1335
  #isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
736
1336
  return (
737
1337
  event.type === "session_before_switch" ||
@@ -800,84 +1400,88 @@ export class ExtensionRunner {
800
1400
  continueWhile?: () => boolean,
801
1401
  scope?: AttemptScopeRef,
802
1402
  ): Promise<RunnerEmitResult<TEvent>> {
803
- const handlers = this.#handlersByEvent.get(event.type) ?? [];
804
- if (handlers.length === 0) return undefined as RunnerEmitResult<TEvent>;
805
- this.#requireScopeOrFailClosed(scope, event.type);
806
-
807
- const ctx = this.createContext();
1403
+ const eventSignal = "signal" in event && event.signal instanceof AbortSignal ? event.signal : undefined;
1404
+ let ctx: ExtensionContext | undefined;
808
1405
  let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
809
- let marked = false;
810
-
811
- for (const { ext, handler } of handlers) {
812
- if (continueWhile && !continueWhile()) return result as RunnerEmitResult<TEvent>;
813
- if (!marked) {
814
- this.#markAttemptExecuted(scope);
815
- marked = true;
816
- }
817
- const handlerResult = await this.#runHandlerWithTimeout(
818
- handler,
819
- event,
820
- ctx,
821
- ext,
822
- event.type === "session_shutdown" ? sessionShutdownHandlerTimeoutMs : extensionHandlerTimeoutMs,
823
- );
824
- if (continueWhile && !continueWhile()) return result as RunnerEmitResult<TEvent>;
825
-
826
- if (this.#isSessionBeforeEvent(event) && handlerResult) {
827
- result = handlerResult as SessionBeforeEventResult;
828
- if (result.cancel) {
829
- return result as RunnerEmitResult<TEvent>;
1406
+ const functionDispatch = await this.emitFunctionHooks(event, {
1407
+ signal: eventSignal,
1408
+ scope,
1409
+ legacyHandler: async (indexed, currentEvent) => {
1410
+ if (continueWhile && !continueWhile()) return { action: "return", value: result };
1411
+ ctx ??= this.createContext();
1412
+ const handlerResult = await this.#runHandlerWithTimeout(
1413
+ indexed.handler,
1414
+ currentEvent,
1415
+ ctx,
1416
+ indexed.ext,
1417
+ currentEvent.type === "session_shutdown" ? sessionShutdownHandlerTimeoutMs : extensionHandlerTimeoutMs,
1418
+ );
1419
+ if (continueWhile && !continueWhile()) return { action: "return", value: result };
1420
+ if (this.#isSessionBeforeEvent(currentEvent) && handlerResult) {
1421
+ result = handlerResult as SessionBeforeEventResult;
1422
+ if (result.cancel) return { action: "return", value: result };
830
1423
  }
831
- }
832
-
833
- if (event.type === "session.compacting" && handlerResult) {
834
- result = handlerResult as SessionCompactingResult;
835
- }
1424
+ if (currentEvent.type === "session.compacting" && handlerResult) {
1425
+ result = handlerResult as SessionCompactingResult;
1426
+ }
1427
+ return { action: "continue", event: currentEvent };
1428
+ },
1429
+ });
1430
+ if (functionDispatch.action === "deny") {
1431
+ if (this.#isSessionBeforeEvent(event)) return { cancel: true } as RunnerEmitResult<TEvent>;
1432
+ return undefined as RunnerEmitResult<TEvent>;
836
1433
  }
837
-
1434
+ if (functionDispatch.action === "return") return functionDispatch.value as RunnerEmitResult<TEvent>;
838
1435
  return result as RunnerEmitResult<TEvent>;
839
1436
  }
840
1437
 
841
- async emitToolResult(event: ToolResultEvent, scope?: AttemptScopeRef): Promise<ToolResultEventResult | undefined> {
842
- const handlers = this.#handlersByEvent.get("tool_result") ?? [];
843
- if (handlers.length === 0) return undefined;
844
- this.#requireScopeOrFailClosed(scope, "tool_result");
845
-
846
- const ctx = this.createContext();
847
- const currentEvent: ToolResultEvent = { ...event };
848
- let modified = false;
849
- let marked = false;
850
-
851
- for (const { ext, handler } of handlers) {
852
- if (!marked) {
853
- this.#markAttemptExecuted(scope);
854
- marked = true;
855
- }
856
- const handlerResult = (await this.#runHandlerWithTimeout(
857
- handler,
858
- currentEvent,
859
- ctx,
860
- ext,
861
- extensionHandlerTimeoutMs,
862
- )) as ToolResultEventResult | undefined;
863
- if (!handlerResult) continue;
864
-
865
- if (handlerResult.content !== undefined) {
866
- currentEvent.content = handlerResult.content;
867
- modified = true;
868
- }
869
- if (handlerResult.details !== undefined) {
870
- currentEvent.details = handlerResult.details;
871
- modified = true;
872
- }
873
- if (handlerResult.isError !== undefined) {
874
- currentEvent.isError = handlerResult.isError;
875
- modified = true;
876
- }
1438
+ async emitToolResult(
1439
+ event: ToolResultEvent,
1440
+ scope?: AttemptScopeRef,
1441
+ options: { signal?: AbortSignal; correlationId?: string } = {},
1442
+ ): Promise<ToolResultEventResult | undefined> {
1443
+ let ctx: ExtensionContext | undefined;
1444
+ const functionDispatch = await this.emitFunctionHooks(
1445
+ { ...event },
1446
+ {
1447
+ ...options,
1448
+ scope,
1449
+ legacyHandler: async (indexed, currentEvent) => {
1450
+ ctx ??= this.createContext();
1451
+ const result = (await this.#runHandlerWithTimeout(
1452
+ indexed.handler,
1453
+ currentEvent,
1454
+ ctx,
1455
+ indexed.ext,
1456
+ extensionHandlerTimeoutMs,
1457
+ )) as ToolResultEventResult | undefined;
1458
+ let transformed = false;
1459
+ if (result?.content !== undefined) {
1460
+ currentEvent.content = result.content;
1461
+ transformed = true;
1462
+ }
1463
+ if (result?.details !== undefined) {
1464
+ currentEvent.details = result.details;
1465
+ transformed = true;
1466
+ }
1467
+ if (result?.isError !== undefined) {
1468
+ currentEvent.isError = result.isError;
1469
+ transformed = true;
1470
+ }
1471
+ return { action: "continue", event: currentEvent, transformed };
1472
+ },
1473
+ },
1474
+ );
1475
+ if (functionDispatch.action === "deny") {
1476
+ return {
1477
+ content: [{ type: "text", text: functionDispatch.reason }],
1478
+ details: event.details,
1479
+ isError: true,
1480
+ };
877
1481
  }
878
-
879
- if (!modified) return undefined;
880
-
1482
+ if (functionDispatch.action === "return") return functionDispatch.value as ToolResultEventResult;
1483
+ if (functionDispatch.transformed !== true) return undefined;
1484
+ const currentEvent = functionDispatch.event;
881
1485
  return {
882
1486
  content: currentEvent.content,
883
1487
  details: currentEvent.details,
@@ -885,43 +1489,36 @@ export class ExtensionRunner {
885
1489
  };
886
1490
  }
887
1491
 
888
- async emitToolCall(event: ToolCallEvent, scope?: AttemptScopeRef): Promise<ToolCallEventResult | undefined> {
889
- const handlers = this.#handlersByEvent.get("tool_call") ?? [];
890
- if (handlers.length === 0) return undefined;
891
- this.#requireScopeOrFailClosed(scope, "tool_call");
892
-
1492
+ async emitToolCall(
1493
+ event: ToolCallEvent,
1494
+ scope?: AttemptScopeRef,
1495
+ options: { signal?: AbortSignal; correlationId?: string } = {},
1496
+ ): Promise<ToolCallEventResult | undefined> {
893
1497
  const ctx = this.createContext();
894
- let result: ToolCallEventResult | undefined;
895
- let marked = false;
896
-
897
- for (const { ext, handler } of handlers) {
898
- if (!marked) {
899
- this.#markAttemptExecuted(scope);
900
- marked = true;
901
- }
902
- try {
903
- const handlerResult = await handler(event, ctx);
904
-
905
- if (handlerResult) {
906
- result = handlerResult as ToolCallEventResult;
907
- if (result.block) {
908
- return result;
909
- }
1498
+ let legacyResult: ToolCallEventResult | undefined;
1499
+ const functionDispatch = await this.emitFunctionHooks(event, {
1500
+ ...options,
1501
+ scope,
1502
+ legacyHandler: async (indexed, currentEvent) => {
1503
+ try {
1504
+ const result = (await indexed.handler(currentEvent, ctx)) as ToolCallEventResult | undefined;
1505
+ if (result?.block) return { action: "return", value: result };
1506
+ if (result) legacyResult = result;
1507
+ return { action: "continue", event: currentEvent };
1508
+ } catch (error) {
1509
+ const message = error instanceof Error ? error.message : String(error);
1510
+ this.emitError({ extensionPath: indexed.ext.path, event: "tool_call", error: message });
1511
+ return {
1512
+ action: "return",
1513
+ value: { block: true, reason: `Extension ${indexed.ext.path} failed: ${message}` },
1514
+ };
910
1515
  }
911
- } catch (err) {
912
- const message = err instanceof Error ? err.message : String(err);
913
- const stack = err instanceof Error ? err.stack : undefined;
914
- this.emitError({
915
- extensionPath: ext.path,
916
- event: "tool_call",
917
- error: message,
918
- stack,
919
- });
920
- return { block: true, reason: `Extension ${ext.path} failed: ${message}` };
921
- }
922
- }
923
-
924
- return result;
1516
+ },
1517
+ });
1518
+ if (functionDispatch.action === "deny") return { block: true, reason: functionDispatch.reason };
1519
+ if (functionDispatch.action === "return") return functionDispatch.value as ToolCallEventResult;
1520
+ event.input = functionDispatch.event.input;
1521
+ return legacyResult;
925
1522
  }
926
1523
 
927
1524
  async emitUserBash(event: UserBashEvent): Promise<UserBashEventResult | undefined> {
@@ -936,18 +1533,25 @@ export class ExtensionRunner {
936
1533
  event: UserBashEvent | UserPythonEvent,
937
1534
  eventName: "user_bash" | "user_python",
938
1535
  ): Promise<R | undefined> {
939
- const handlers = this.#handlersByEvent.get(eventName) ?? [];
940
- if (handlers.length === 0) return undefined;
941
-
942
1536
  const ctx = this.createContext();
943
-
944
- for (const { ext, handler } of handlers) {
945
- const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
946
- if (handlerResult) {
947
- return handlerResult as R;
948
- }
1537
+ const functionDispatch = await this.emitFunctionHooks(event, {
1538
+ legacyHandler: async (indexed, currentEvent) => {
1539
+ const result = await this.#runHandlerWithTimeout(
1540
+ indexed.handler,
1541
+ currentEvent,
1542
+ ctx,
1543
+ indexed.ext,
1544
+ extensionHandlerTimeoutMs,
1545
+ );
1546
+ return result === undefined
1547
+ ? { action: "continue", event: currentEvent }
1548
+ : { action: "return", value: result };
1549
+ },
1550
+ });
1551
+ if (functionDispatch.action === "deny" || functionDispatch.action === "return") {
1552
+ return functionDispatch.action === "return" ? (functionDispatch.value as R) : undefined;
949
1553
  }
950
-
1554
+ void eventName;
951
1555
  return undefined;
952
1556
  }
953
1557
 
@@ -959,29 +1563,39 @@ export class ExtensionRunner {
959
1563
  promptPaths: Array<{ path: string; extensionPath: string }>;
960
1564
  themePaths: Array<{ path: string; extensionPath: string }>;
961
1565
  }> {
962
- const handlers = this.#handlersByEvent.get("resources_discover") ?? [];
963
- if (handlers.length === 0) return { skillPaths: [], promptPaths: [], themePaths: [] };
964
- const ctx = this.createContext();
1566
+ let ctx: ExtensionContext | undefined;
965
1567
  const skillPaths: Array<{ path: string; extensionPath: string }> = [];
966
1568
  const promptPaths: Array<{ path: string; extensionPath: string }> = [];
967
1569
  const themePaths: Array<{ path: string; extensionPath: string }> = [];
968
-
969
- for (const { ext, handler } of handlers) {
970
- const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
971
- const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
972
- const result = handlerResult as ResourcesDiscoverResult | undefined;
973
-
974
- if (result?.skillPaths?.length) {
975
- skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: ext.path })));
976
- }
977
- if (result?.promptPaths?.length) {
978
- promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: ext.path })));
979
- }
980
- if (result?.themePaths?.length) {
981
- themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: ext.path })));
982
- }
983
- }
984
-
1570
+ const functionDispatch = await this.emitFunctionHooks(
1571
+ { type: "resources_discover", cwd, reason },
1572
+ {
1573
+ legacyHandler: async (indexed, currentEvent) => {
1574
+ ctx ??= this.createContext();
1575
+ const result = (await this.#runHandlerWithTimeout(
1576
+ indexed.handler,
1577
+ currentEvent,
1578
+ ctx,
1579
+ indexed.ext,
1580
+ extensionHandlerTimeoutMs,
1581
+ )) as ResourcesDiscoverResult | undefined;
1582
+ if (result?.skillPaths?.length)
1583
+ skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: indexed.ext.path })));
1584
+ if (result?.promptPaths?.length)
1585
+ promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: indexed.ext.path })));
1586
+ if (result?.themePaths?.length)
1587
+ themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: indexed.ext.path })));
1588
+ return { action: "continue", event: currentEvent };
1589
+ },
1590
+ },
1591
+ );
1592
+ if (functionDispatch.action === "deny") return { skillPaths: [], promptPaths: [], themePaths: [] };
1593
+ if (functionDispatch.action === "return")
1594
+ return functionDispatch.value as {
1595
+ skillPaths: Array<{ path: string; extensionPath: string }>;
1596
+ promptPaths: Array<{ path: string; extensionPath: string }>;
1597
+ themePaths: Array<{ path: string; extensionPath: string }>;
1598
+ };
985
1599
  return { skillPaths, promptPaths, themePaths };
986
1600
  }
987
1601
 
@@ -991,165 +1605,182 @@ export class ExtensionRunner {
991
1605
  images: ImageContent[] | undefined,
992
1606
  source: "interactive" | "sdk" | "extension",
993
1607
  ): Promise<InputEventResult> {
994
- const handlers = this.#handlersByEvent.get("input") ?? [];
995
- if (handlers.length === 0) return {};
996
-
997
1608
  const ctx = this.createContext();
998
- let currentText = text;
999
- let currentImages = images;
1000
-
1001
- for (const { ext, handler } of handlers) {
1002
- const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
1003
- const result = (await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs)) as
1004
- | InputEventResult
1005
- | undefined;
1006
- if (result?.handled) return result;
1007
- if (result?.text !== undefined) {
1008
- currentText = result.text;
1009
- currentImages = result.images ?? currentImages;
1010
- }
1011
- }
1012
-
1013
- return currentText !== text || currentImages !== images ? { text: currentText, images: currentImages } : {};
1609
+ const functionDispatch = await this.emitFunctionHooks(
1610
+ { type: "input", text, images, source },
1611
+ {
1612
+ legacyHandler: async (indexed, currentEvent) => {
1613
+ const result = (await this.#runHandlerWithTimeout(
1614
+ indexed.handler,
1615
+ currentEvent,
1616
+ ctx,
1617
+ indexed.ext,
1618
+ extensionHandlerTimeoutMs,
1619
+ )) as InputEventResult | undefined;
1620
+ if (result?.handled) return { action: "return", value: result };
1621
+ if (result?.text !== undefined) {
1622
+ currentEvent.text = result.text;
1623
+ currentEvent.images = result.images ?? currentEvent.images;
1624
+ return { action: "continue", event: currentEvent, transformed: true };
1625
+ }
1626
+ return { action: "continue", event: currentEvent };
1627
+ },
1628
+ },
1629
+ );
1630
+ if (functionDispatch.action === "deny") return { handled: true };
1631
+ if (functionDispatch.action === "return") return functionDispatch.value as InputEventResult;
1632
+ const transformedInput = functionDispatch.event;
1633
+ return transformedInput.text !== text || transformedInput.images !== images
1634
+ ? { text: transformedInput.text, images: transformedInput.images }
1635
+ : {};
1014
1636
  }
1015
1637
 
1016
- async emitContext(messages: AgentMessage[], scope?: AttemptScopeRef): Promise<AgentMessage[]> {
1017
- const handlers = this.#handlersByEvent.get("context") ?? [];
1018
- if (handlers.length === 0) return messages;
1019
- this.#requireScopeOrFailClosed(scope, "context");
1020
-
1021
- const ctx = this.createContext();
1022
- let currentMessages: AgentMessage[];
1023
- try {
1024
- currentMessages = structuredClone(messages);
1025
- } catch {
1026
- // Messages may contain non-cloneable objects (e.g. in ToolResultMessage.details
1027
- // or ProviderPayload). Fall back to a shallow array clone — extensions should
1028
- // return new message arrays rather than mutating in place.
1029
- currentMessages = [...messages];
1030
- }
1031
- let marked = false;
1032
-
1033
- for (const { ext, handler } of handlers) {
1034
- if (!marked) {
1035
- this.#markAttemptExecuted(scope);
1036
- marked = true;
1037
- }
1038
- const event: ContextEvent = { type: "context", messages: currentMessages };
1039
- const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
1040
-
1041
- if (handlerResult && (handlerResult as ContextEventResult).messages) {
1042
- currentMessages = (handlerResult as ContextEventResult).messages!;
1043
- }
1044
- }
1045
-
1046
- return currentMessages;
1638
+ async emitContext(messages: AgentMessage[], scope?: AttemptScopeRef, signal?: AbortSignal): Promise<AgentMessage[]> {
1639
+ let ctx: ExtensionContext | undefined;
1640
+ const functionDispatch = await this.emitFunctionHooks(
1641
+ { type: "context", messages },
1642
+ {
1643
+ signal,
1644
+ scope,
1645
+ legacyHandler: async (indexed, currentEvent) => {
1646
+ ctx ??= this.createContext();
1647
+ const result = (await this.#runHandlerWithTimeout(
1648
+ indexed.handler,
1649
+ currentEvent,
1650
+ ctx,
1651
+ indexed.ext,
1652
+ extensionHandlerTimeoutMs,
1653
+ )) as ContextEventResult | undefined;
1654
+ if (!result?.messages) return { action: "continue", event: currentEvent };
1655
+ currentEvent.messages = result.messages;
1656
+ return { action: "continue", event: currentEvent, transformed: true };
1657
+ },
1658
+ },
1659
+ );
1660
+ if (functionDispatch.action === "deny") throw new Error(functionDispatch.reason);
1661
+ if (functionDispatch.action === "return") return functionDispatch.value as AgentMessage[];
1662
+ return functionDispatch.event.messages;
1047
1663
  }
1048
1664
 
1049
1665
  async emitBeforeProviderRequest(
1050
1666
  payload: unknown,
1051
1667
  scope?: AttemptScopeRef,
1668
+ signal?: AbortSignal,
1052
1669
  ): Promise<BeforeProviderRequestEventResult> {
1053
- const handlers = this.#handlersByEvent.get("before_provider_request") ?? [];
1054
- if (handlers.length === 0) return payload;
1055
- this.#requireScopeOrFailClosed(scope, "before_provider_request");
1056
-
1057
1670
  const ctx = this.createContext();
1058
- let currentPayload = payload;
1059
- let marked = false;
1060
-
1061
- for (const { ext, handler } of handlers) {
1062
- if (!marked) {
1063
- this.#markAttemptExecuted(scope);
1064
- marked = true;
1065
- }
1066
- const event: BeforeProviderRequestEvent = {
1067
- type: "before_provider_request",
1068
- payload: currentPayload,
1069
- };
1070
- const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
1071
- if (handlerResult !== undefined) {
1072
- currentPayload = handlerResult;
1073
- }
1074
- }
1075
-
1076
- return currentPayload;
1671
+ const functionDispatch = await this.emitFunctionHooks(
1672
+ { type: "before_provider_request", payload },
1673
+ {
1674
+ signal,
1675
+ scope,
1676
+ legacyHandler: async (indexed, currentEvent) => {
1677
+ const result = await this.#runHandlerWithTimeout(
1678
+ indexed.handler,
1679
+ currentEvent,
1680
+ ctx,
1681
+ indexed.ext,
1682
+ extensionHandlerTimeoutMs,
1683
+ );
1684
+ if (result === undefined) return { action: "continue", event: currentEvent };
1685
+ currentEvent.payload = result;
1686
+ return { action: "continue", event: currentEvent, transformed: true };
1687
+ },
1688
+ },
1689
+ );
1690
+ if (functionDispatch.action === "deny") throw new Error(functionDispatch.reason);
1691
+ if (functionDispatch.action === "return") return functionDispatch.value;
1692
+ return functionDispatch.event.payload;
1077
1693
  }
1078
1694
 
1079
1695
  async emitAfterProviderResponse(
1080
1696
  response: ProviderResponseMetadata,
1081
1697
  _model?: Model,
1082
1698
  scope?: AttemptScopeRef,
1699
+ signal?: AbortSignal,
1083
1700
  ): Promise<void> {
1084
- const handlers = this.#handlersByEvent.get("after_provider_response") ?? [];
1085
- if (handlers.length === 0) return;
1086
- this.#requireScopeOrFailClosed(scope, "after_provider_response");
1087
-
1088
1701
  const ctx = this.createContext();
1089
- let marked = false;
1090
-
1091
- for (const { ext, handler } of handlers) {
1092
- if (!marked) {
1093
- this.#markAttemptExecuted(scope);
1094
- marked = true;
1095
- }
1096
- const event: AfterProviderResponseEvent = {
1702
+ const functionDispatch = await this.emitFunctionHooks(
1703
+ {
1097
1704
  type: "after_provider_response",
1098
1705
  status: response.status,
1099
1706
  headers: response.headers,
1100
1707
  requestId: response.requestId,
1101
1708
  metadata: response.metadata,
1102
- };
1103
- await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
1104
- }
1709
+ },
1710
+ {
1711
+ signal,
1712
+ scope,
1713
+ legacyHandler: async (indexed, currentEvent) => {
1714
+ await this.#runHandlerWithTimeout(
1715
+ indexed.handler,
1716
+ currentEvent,
1717
+ ctx,
1718
+ indexed.ext,
1719
+ extensionHandlerTimeoutMs,
1720
+ );
1721
+ return { action: "continue", event: currentEvent };
1722
+ },
1723
+ },
1724
+ );
1725
+ if (functionDispatch.action !== "continue") return;
1105
1726
  }
1106
1727
 
1107
1728
  async emitBeforeAgentStart(
1108
1729
  prompt: string,
1109
1730
  images: ImageContent[] | undefined,
1110
1731
  systemPrompt: string[],
1732
+ signal?: AbortSignal,
1111
1733
  ): Promise<BeforeAgentStartCombinedResult | undefined> {
1112
- const handlers = this.#handlersByEvent.get("before_agent_start") ?? [];
1113
- if (handlers.length === 0) return undefined;
1114
-
1115
1734
  const ctx = this.createContext();
1116
1735
  const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
1117
- let currentSystemPrompt = systemPrompt;
1118
- let systemPromptModified = false;
1119
-
1120
- for (const { ext, handler } of handlers) {
1121
- const event: BeforeAgentStartEvent = {
1736
+ const functionDispatch = await this.emitFunctionHooks(
1737
+ {
1122
1738
  type: "before_agent_start",
1123
1739
  prompt,
1124
1740
  images,
1125
- systemPrompt: currentSystemPrompt,
1126
- };
1127
- const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
1128
-
1129
- if (handlerResult) {
1130
- const unsupportedFields = unsupportedBeforeAgentStartResultFields(handlerResult);
1131
- if (unsupportedFields.length > 0) {
1132
- this.emitError({
1133
- extensionPath: ext.path,
1134
- event: "before_agent_start",
1135
- error: `Unsupported before_agent_start result field(s): ${unsupportedFields.join(", ")}. Supported fields: message, systemPrompt.`,
1136
- });
1137
- }
1138
- const result = handlerResult as BeforeAgentStartEventResult;
1139
- if (result.message) {
1140
- messages.push(result.message);
1141
- }
1142
- if (result.systemPrompt !== undefined) {
1143
- currentSystemPrompt = result.systemPrompt;
1144
- systemPromptModified = true;
1145
- }
1146
- }
1147
- }
1148
-
1149
- if (messages.length > 0 || systemPromptModified) {
1741
+ systemPrompt,
1742
+ },
1743
+ {
1744
+ signal,
1745
+ legacyHandler: async (indexed, currentEvent) => {
1746
+ const result = await this.#runHandlerWithTimeout(
1747
+ indexed.handler,
1748
+ currentEvent,
1749
+ ctx,
1750
+ indexed.ext,
1751
+ extensionHandlerTimeoutMs,
1752
+ );
1753
+ if (!result) return { action: "continue", event: currentEvent };
1754
+ const unsupportedFields = unsupportedBeforeAgentStartResultFields(result);
1755
+ if (unsupportedFields.length > 0) {
1756
+ this.emitError({
1757
+ extensionPath: indexed.ext.path,
1758
+ event: "before_agent_start",
1759
+ error: `Unsupported before_agent_start result field(s): ${unsupportedFields.join(", ")}. Supported fields: message, systemPrompt.`,
1760
+ });
1761
+ }
1762
+ const legacyResult = result as BeforeAgentStartEventResult;
1763
+ if (legacyResult.message) messages.push(legacyResult.message);
1764
+ if (legacyResult.systemPrompt === undefined) return { action: "continue", event: currentEvent };
1765
+ currentEvent.systemPrompt = legacyResult.systemPrompt;
1766
+ return { action: "continue", event: currentEvent, transformed: true };
1767
+ },
1768
+ },
1769
+ );
1770
+ if (functionDispatch.action === "deny") throw new Error(functionDispatch.reason);
1771
+ if (functionDispatch.action === "return") return functionDispatch.value as BeforeAgentStartCombinedResult;
1772
+ const transformedPrompt = functionDispatch.event.prompt;
1773
+ const transformedImages = functionDispatch.event.images;
1774
+ const transformedSystemPrompt = functionDispatch.event.systemPrompt;
1775
+ const promptModified = transformedPrompt !== prompt;
1776
+ const imagesModified = JSON.stringify(transformedImages) !== JSON.stringify(images);
1777
+ const systemPromptModified = JSON.stringify(transformedSystemPrompt) !== JSON.stringify(systemPrompt);
1778
+ if (messages.length > 0 || systemPromptModified || promptModified || imagesModified) {
1150
1779
  return {
1151
- messages: messages.length > 0 ? messages : undefined,
1152
- systemPrompt: systemPromptModified ? currentSystemPrompt : undefined,
1780
+ ...(messages.length > 0 ? { messages } : {}),
1781
+ ...(systemPromptModified ? { systemPrompt: transformedSystemPrompt } : {}),
1782
+ ...(promptModified ? { prompt: transformedPrompt } : {}),
1783
+ ...(imagesModified ? { images: transformedImages } : {}),
1153
1784
  };
1154
1785
  }
1155
1786