From ea742297d05c7b699d824b40cbf9d57264190439 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Tue, 11 Aug 2026 14:10:14 +0800 Subject: [PATCH] refactor(agent-core-v2): extract externalHooks into a scope-organized feature Move the external hook services out of app/externalHooksRunner, session/externalHooks, and agent/externalHooks into features/externalHooks, assembled as the ExternalHooksFeature unit: - services live under per-scope subdirectories (app/, session/, agent/); shared pure helpers (types, hook matching/dispatch, process spawn, prompt result rendering) live under internal/ - the runner and the two observers are contributed through the Feature seams (ScopeUnits materialization); the hooks config section stays on the static import=register channel - update the package entry leaf exports, the plugin domain imports, the kap-server events-zod import, and the affected tests; regenerate the state manifest --- packages/agent-core-v2/AGENTS.md | 2 +- .../agent-core-v2/docs/config-manifest.toml | 4 +- .../agent-core-v2/docs/state-manifest.d.ts | 6 +- .../src/app/externalHooksRunner/index.ts | 9 -- .../agent-core-v2/src/app/plugin/manager.ts | 2 +- .../agent-core-v2/src/app/plugin/manifest.ts | 2 +- .../agent-core-v2/src/app/plugin/plugin.ts | 2 +- .../src/app/plugin/pluginService.ts | 2 +- .../agent-core-v2/src/app/plugin/types.ts | 2 +- .../agent/agentExternalHooks.ts} | 6 -- .../agent/agentExternalHooksService.ts} | 19 +--- .../externalHooks/app}/externalHooksRunner.ts | 2 +- .../app}/externalHooksRunnerService.ts | 21 ++-- .../externalHooks/configSection.ts | 2 +- .../externalHooks/externalHooksFeature.ts | 48 +++++++++ .../externalHooks/internal/matchHooks.ts} | 9 +- .../externalHooks/internal/runHook.ts} | 9 ++ .../externalHooks/internal}/types.ts | 9 ++ .../externalHooks/internal/userPrompt.ts} | 8 ++ .../session/sessionExternalHooks.ts} | 0 .../session/sessionExternalHooksService.ts} | 16 +-- packages/agent-core-v2/src/index.ts | 15 +-- .../src/session/externalHooks/index.ts | 8 -- .../fullCompaction/fullCompaction.test.ts | 4 +- .../test/agent/task/rpc-events.test.ts | 2 +- .../externalHooksFeature.test.ts | 99 +++++++++++++++++++ .../externalHooksRunner.test.ts | 2 +- .../externalHooks}/integration.test.ts | 18 ++-- .../externalHooks/runner-stub.ts | 6 +- .../externalHooks/runner.test.ts | 2 +- packages/agent-core-v2/test/harness/agent.ts | 11 +++ packages/agent-core-v2/test/tool/tool.test.ts | 2 +- .../sessionLifecycle/sessionLifecycle.test.ts | 2 +- .../kap-server/src/protocol/events-zod.ts | 2 +- .../klient/src/contract/global/plugins.ts | 2 +- 35 files changed, 245 insertions(+), 110 deletions(-) delete mode 100644 packages/agent-core-v2/src/app/externalHooksRunner/index.ts rename packages/agent-core-v2/src/{agent/externalHooks/externalHooks.ts => features/externalHooks/agent/agentExternalHooks.ts} (81%) rename packages/agent-core-v2/src/{agent/externalHooks/externalHooksService.ts => features/externalHooks/agent/agentExternalHooksService.ts} (96%) rename packages/agent-core-v2/src/{app/externalHooksRunner => features/externalHooks/app}/externalHooksRunner.ts (97%) rename packages/agent-core-v2/src/{app/externalHooksRunner => features/externalHooks/app}/externalHooksRunnerService.ts (86%) rename packages/agent-core-v2/src/{agent => features}/externalHooks/configSection.ts (96%) create mode 100644 packages/agent-core-v2/src/features/externalHooks/externalHooksFeature.ts rename packages/agent-core-v2/src/{app/externalHooksRunner/runner.ts => features/externalHooks/internal/matchHooks.ts} (95%) rename packages/agent-core-v2/src/{agent/externalHooks/runner.ts => features/externalHooks/internal/runHook.ts} (94%) rename packages/agent-core-v2/src/{agent/externalHooks => features/externalHooks/internal}/types.ts (77%) rename packages/agent-core-v2/src/{agent/externalHooks/user-prompt.ts => features/externalHooks/internal/userPrompt.ts} (88%) rename packages/agent-core-v2/src/{session/externalHooks/externalHooks.ts => features/externalHooks/session/sessionExternalHooks.ts} (100%) rename packages/agent-core-v2/src/{session/externalHooks/externalHooksService.ts => features/externalHooks/session/sessionExternalHooksService.ts} (93%) delete mode 100644 packages/agent-core-v2/src/session/externalHooks/index.ts create mode 100644 packages/agent-core-v2/test/features/externalHooks/externalHooksFeature.test.ts rename packages/agent-core-v2/test/{app/externalHooksRunner => features/externalHooks}/externalHooksRunner.test.ts (99%) rename packages/agent-core-v2/test/{app/externalHooksRunner => features/externalHooks}/integration.test.ts (98%) rename packages/agent-core-v2/test/{agent => features}/externalHooks/runner-stub.ts (88%) rename packages/agent-core-v2/test/{agent => features}/externalHooks/runner.test.ts (98%) diff --git a/packages/agent-core-v2/AGENTS.md b/packages/agent-core-v2/AGENTS.md index d7509008ce..0413638c05 100644 --- a/packages/agent-core-v2/AGENTS.md +++ b/packages/agent-core-v2/AGENTS.md @@ -19,7 +19,7 @@ The DI kernel (`src/_base/di/`) owns the unit layer on top of the scoped registr The four contribution seams (token → fold): config sections — `ConfigSectionContribution` → `ConfigRegistry` fold (`src/app/config/`; module-level `registerConfigSection` stays the static built-in channel drained at construction, a withdrawn runtime record unregisters the section while user TOML values survive); agent tools — `AgentToolContribution` → `AgentToolActivationService` fold (built-in records provided once at App scope by `builtinToolAssemblyService`; `registerAgentToolService` stays the static channel: Agent-scope DI `OnDemand` registration + module table); agent profiles — `AgentProfileContribution` → `IAgentProfileRegistry` fold (see Scopes); wire vocabulary — `WireModelContribution` → `WireService` fold (a record bundles `models` / `ops` / `crossReducers` / `checkpointedModels`; the built-in layer is the module tables drained at fold time — `defineOp` / `defineModel` / `defineCheckpointedModel` stay the static channel — and replaying a withdrawn domain's history lands on the unknown-op skip-and-count path). A fifth seam: executable commands — `CommandContribution` → `IAgentCommandService` fold (`src/agent/command/`; a contributed command runs engine-side — `run(ctx)` gets `ctx.get` resolving through the agent container, valid only during the synchronous part of `run`; name-level dedup, last record wins; surfaced over RPC as `agentRPCService.listCommands` / `runCommand`). -`src/features/` — built-in capabilities authored as self-contained Feature units (`plan` is the first, extracted from `agent/plan` + `agent/tools/plan`). A `Feature` (`src/features/feature.ts`) is an App-scope unit recipe with a `static override readonly name` and `contribute*` helpers composing the seams: `contributeService(scope, id, ctor)` / `contributeAgentService` (per-scope materialization via `ScopeUnits` — provider death retracts everywhere, 连坐), `contributeTool` (per-agent `OnDemand` registration + the `AgentToolContribution` record), `contributeProfiles`, `contributeConfig`, `contributeCommand`, plus `onDispose`. Feature modules self-register at import (`registerFeature`, `src/features/featureRegistry.ts`); the App-scope `IFeatureAssemblyService` drains the table through `IFeatureManager.provideUnit`, so every feature is a named, introspectable, retractable managed unit. Built-in features keep user-facing static contracts — config sections, agent profiles, wire vocabulary — on the static import=register channels (the config/state manifest generators read static tables / call sites; wire records must stay replayable); the Feature unit carries the runtime capabilities (services, tools, commands). The string form of the unit `on(...)` capability (`this.on('turn.ended', …)`) is backed by the production `FiberEventResolver` registered in `src/app/event/fiberEventResolver.ts`, resolving against the scope's `IEventBus`. +`src/features/` — built-in capabilities authored as self-contained Feature units (`plan` was the first, extracted from `agent/plan` + `agent/tools/plan`; `externalHooks` from `app/externalHooksRunner` + `session/externalHooks` + `agent/externalHooks`). A `Feature` (`src/features/feature.ts`) is an App-scope unit recipe with a `static override readonly name` and `contribute*` helpers composing the seams: `contributeService(scope, id, ctor)` / `contributeAgentService` (per-scope materialization via `ScopeUnits` — provider death retracts everywhere, 连坐), `contributeTool` (per-agent `OnDemand` registration + the `AgentToolContribution` record), `contributeProfiles`, `contributeConfig`, `contributeCommand`, plus `onDispose`. Feature modules self-register at import (`registerFeature`, `src/features/featureRegistry.ts`); the App-scope `IFeatureAssemblyService` drains the table through `IFeatureManager.provideUnit`, so every feature is a named, introspectable, retractable managed unit. Built-in features keep user-facing static contracts — config sections, agent profiles, wire vocabulary — on the static import=register channels (the config/state manifest generators read static tables / call sites; wire records must stay replayable); the Feature unit carries the runtime capabilities (services, tools, commands). The string form of the unit `on(...)` capability (`this.on('turn.ended', …)`) is backed by the production `FiberEventResolver` registered in `src/app/event/fiberEventResolver.ts`, resolving against the scope's `IEventBus`. ## Ledger and cascade (L0/L2) diff --git a/packages/agent-core-v2/docs/config-manifest.toml b/packages/agent-core-v2/docs/config-manifest.toml index a638728399..877c5ec4de 100644 --- a/packages/agent-core-v2/docs/config-manifest.toml +++ b/packages/agent-core-v2/docs/config-manifest.toml @@ -17,7 +17,7 @@ # experimental src/app/flag/flag.ts # extraAgentDirs src/workspace/workspaceAgentProfileLoader/configSection.ts # extraSkillDirs src/app/skillCatalog/configSection.ts -# hooks src/agent/externalHooks/configSection.ts +# hooks src/features/externalHooks/configSection.ts # identity src/app/agentIdentity/configSection.ts # image src/agent/media/configSection.ts # loopControl src/agent/loop/configSection.ts @@ -136,7 +136,7 @@ extra_skill_dirs = [] # ########################################################################## # hooks -# owner: src/agent/externalHooks/configSection.ts +# owner: src/features/externalHooks/configSection.ts # scope: core # hooks: custom fromToml · custom toToml # ########################################################################## diff --git a/packages/agent-core-v2/docs/state-manifest.d.ts b/packages/agent-core-v2/docs/state-manifest.d.ts index bdc9795519..a131e8238a 100644 --- a/packages/agent-core-v2/docs/state-manifest.d.ts +++ b/packages/agent-core-v2/docs/state-manifest.d.ts @@ -63,7 +63,7 @@ // contextInjector.isNewTurn src/agent/contextInjector/contextInjectorService.ts // contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts // dateChange.seed src/agent/dateChange/dateChangeService.ts -// externalHooks.stopHookContinuationUsed src/agent/externalHooks/externalHooksService.ts +// externalHooks.stopHookContinuationUsed src/features/externalHooks/agent/agentExternalHooksService.ts // fullCompaction.activeTurnId src/agent/fullCompaction/fullCompactionService.ts // fullCompaction.compactionCountInTurn src/agent/fullCompaction/fullCompactionService.ts // fullCompaction.consecutiveOverflowCompactions src/agent/fullCompaction/fullCompactionService.ts @@ -1022,8 +1022,6 @@ export interface AgentStateSnapshot { readonly timeZone: string; readonly renderGeneration: number; } | undefined; - // src/agent/externalHooks/externalHooksService.ts - 'externalHooks.stopHookContinuationUsed': boolean; // src/agent/fullCompaction/fullCompactionService.ts 'fullCompaction.activeTurnId': number | undefined; 'fullCompaction.compactionCountInTurn': number; @@ -1209,6 +1207,8 @@ export interface AgentStateSnapshot { inputCacheCreation: number; } | undefined; 'usage.currentTurnId': number | undefined; + // src/features/externalHooks/agent/agentExternalHooksService.ts + 'externalHooks.stopHookContinuationUsed': boolean; // src/features/plan/injection/planModeInjection.ts 'plan.wasActive': boolean; } diff --git a/packages/agent-core-v2/src/app/externalHooksRunner/index.ts b/packages/agent-core-v2/src/app/externalHooksRunner/index.ts deleted file mode 100644 index e747fee312..0000000000 --- a/packages/agent-core-v2/src/app/externalHooksRunner/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * `externalHooksRunner` domain barrel — re-exports the App-scope - * `IExternalHooksRunnerService` contract and its implementation, plus the - * argument shape shared by callers. Importing this barrel registers the - * App-scope runner binding into the scope registry. - */ - -export * from './externalHooksRunner'; -export * from './externalHooksRunnerService'; diff --git a/packages/agent-core-v2/src/app/plugin/manager.ts b/packages/agent-core-v2/src/app/plugin/manager.ts index 2b6fae8549..d360d4826a 100644 --- a/packages/agent-core-v2/src/app/plugin/manager.ts +++ b/packages/agent-core-v2/src/app/plugin/manager.ts @@ -10,7 +10,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { BugIndicatingError, Error2, ErrorCodes, PluginErrors } from '#/errors'; -import type { HookDef } from '#/agent/externalHooks/types'; +import type { HookDef } from '#/features/externalHooks/internal/types'; import type { McpServerConfig } from '#/mcpCore/config-schema'; import type { PluginAgentRoot } from './types'; import { discoverFileSkills } from '#/app/skillCatalog/fileSkillDiscovery'; diff --git a/packages/agent-core-v2/src/app/plugin/manifest.ts b/packages/agent-core-v2/src/app/plugin/manifest.ts index 3a3a7bae0b..5af61c8c9b 100644 --- a/packages/agent-core-v2/src/app/plugin/manifest.ts +++ b/packages/agent-core-v2/src/app/plugin/manifest.ts @@ -1,7 +1,7 @@ import { readdir, readFile, realpath, stat } from 'node:fs/promises'; import path from 'node:path'; -import { HookDefSchema, type HookDefConfig } from '#/agent/externalHooks/configSection'; +import { HookDefSchema, type HookDefConfig } from '#/features/externalHooks/configSection'; import { McpServerConfigSchema, type McpServerConfig } from '#/mcpCore/config-schema'; import { diff --git a/packages/agent-core-v2/src/app/plugin/plugin.ts b/packages/agent-core-v2/src/app/plugin/plugin.ts index 3cfad9f883..ede09fd51e 100644 --- a/packages/agent-core-v2/src/app/plugin/plugin.ts +++ b/packages/agent-core-v2/src/app/plugin/plugin.ts @@ -9,7 +9,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; import type { Event } from '#/_base/event'; -import type { HookDef } from '#/agent/externalHooks/types'; +import type { HookDef } from '#/features/externalHooks/internal/types'; import type { McpServerConfig } from '#/mcpCore/config-schema'; import type { SkillRoot } from '#/app/skillCatalog/types'; diff --git a/packages/agent-core-v2/src/app/plugin/pluginService.ts b/packages/agent-core-v2/src/app/plugin/pluginService.ts index 0a8d6901c8..f9f9f2d930 100644 --- a/packages/agent-core-v2/src/app/plugin/pluginService.ts +++ b/packages/agent-core-v2/src/app/plugin/pluginService.ts @@ -26,7 +26,7 @@ import { BugIndicatingError, Error2, PluginErrors } from '#/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IProviderService } from '#/kosong/provider/provider'; import { ISkillDiscovery } from '#/app/skillCatalog/skillDiscovery'; -import type { HookDef } from '#/agent/externalHooks/types'; +import type { HookDef } from '#/features/externalHooks/internal/types'; import type { McpServerConfig } from '#/mcpCore/config-schema'; import type { SkillRoot } from '#/app/skillCatalog/types'; diff --git a/packages/agent-core-v2/src/app/plugin/types.ts b/packages/agent-core-v2/src/app/plugin/types.ts index ad426972e6..e7ce77f993 100644 --- a/packages/agent-core-v2/src/app/plugin/types.ts +++ b/packages/agent-core-v2/src/app/plugin/types.ts @@ -1,4 +1,4 @@ -import type { HookDefConfig } from '#/agent/externalHooks/configSection'; +import type { HookDefConfig } from '#/features/externalHooks/configSection'; import type { McpServerConfig } from '#/mcpCore/config-schema'; export type PluginDiagnosticSeverity = 'error' | 'warn' | 'info'; diff --git a/packages/agent-core-v2/src/agent/externalHooks/externalHooks.ts b/packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooks.ts similarity index 81% rename from packages/agent-core-v2/src/agent/externalHooks/externalHooks.ts rename to packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooks.ts index f56571ea13..aef4aa0e6d 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/externalHooks.ts +++ b/packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooks.ts @@ -9,12 +9,6 @@ import { createDecorator } from '#/_base/di/instantiation'; -export interface RenderedExternalHookResult { - readonly event: string; - readonly message: string; - readonly text: string; -} - export interface IAgentExternalHooksService { readonly _serviceBrand: undefined; } diff --git a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts b/packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooksService.ts similarity index 96% rename from packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts rename to packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooksService.ts index 56ddf2aa0f..1ef9eff205 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts +++ b/packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooksService.ts @@ -19,12 +19,11 @@ * (`stopHookContinuationUsed`, the Stop-hook re-entry guard) is registered * into `agentState` (`IAgentStateService`) and read/written through it; the * hook listener registrations stay ordinary disposables on the instance. + * Contributed at Agent scope by `ExternalHooksFeature`. */ import { IInstantiationService } from '#/_base/di/instantiation'; import { Service } from '#/_base/di/service'; -import { LifecycleScope } from '#/app/scopes'; -import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { defineState } from '#/_base/state/stateRegistry'; import { isPlainRecord } from '#/_base/utils/canonical-args'; import { IAgentStateService } from '#/agent/state/agentState'; @@ -52,13 +51,13 @@ import { toKimiErrorPayload } from '#/errors'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata'; -import { IAgentExternalHooksService } from './externalHooks'; -import { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner'; -import type { HookMatcherValue } from './types'; +import { IAgentExternalHooksService } from './agentExternalHooks'; +import { IExternalHooksRunnerService } from '../app/externalHooksRunner'; +import type { HookMatcherValue } from '../internal/types'; import { renderUserPromptHookBlockResult, renderUserPromptHookResult, -} from './user-prompt'; +} from '../internal/userPrompt'; export interface HookResultEvent { readonly type: 'hook.result'; @@ -482,11 +481,3 @@ function toolOutputText(output: ExecutableToolResult['output']): string { .map((part) => part.text) .join(''); } - -registerScopedService( - LifecycleScope.Agent, - IAgentExternalHooksService, - AgentExternalHooksService, - ScopeActivation.OnScopeCreated, - 'externalHooks', -); diff --git a/packages/agent-core-v2/src/app/externalHooksRunner/externalHooksRunner.ts b/packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunner.ts similarity index 97% rename from packages/agent-core-v2/src/app/externalHooksRunner/externalHooksRunner.ts rename to packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunner.ts index 95d273dde0..765b983eb0 100644 --- a/packages/agent-core-v2/src/app/externalHooksRunner/externalHooksRunner.ts +++ b/packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunner.ts @@ -11,7 +11,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; import type { Event } from '#/_base/event'; -import type { HookBlockDecision, HookMatcherValue, HookResult } from '#/agent/externalHooks/types'; +import type { HookBlockDecision, HookMatcherValue, HookResult } from '../internal/types'; export interface ExternalHooksRunnerTriggerArgs { readonly matcherValue?: HookMatcherValue; diff --git a/packages/agent-core-v2/src/app/externalHooksRunner/externalHooksRunnerService.ts b/packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunnerService.ts similarity index 86% rename from packages/agent-core-v2/src/app/externalHooksRunner/externalHooksRunnerService.ts rename to packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunnerService.ts index 6bacea0fde..e7f03ddb2a 100644 --- a/packages/agent-core-v2/src/app/externalHooksRunner/externalHooksRunnerService.ts +++ b/packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunnerService.ts @@ -11,26 +11,25 @@ * bootstrap cwd, `sessionId`, `signal`, payload) flow in through the args, so * this service keeps no per-scope state; the one payload field it contributes * itself is `clientType` (the host platform from bootstrap client identity), - * merged under the caller's `inputData`. Bound at App scope. + * merged under the caller's `inputData`. Contributed at App scope by + * `ExternalHooksFeature`. */ import { Disposable } from '#/_base/di/lifecycle'; -import { LifecycleScope } from '#/app/scopes'; -import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Emitter, type Event } from '#/_base/event'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IConfigService } from '#/app/config/config'; import { IPluginService } from '#/app/plugin/plugin'; -import { HOOKS_SECTION, type HookDefConfig } from '#/agent/externalHooks/configSection'; -import type { HookBlockDecision, HookDef, HookResult } from '#/agent/externalHooks/types'; import { IHostProcessService } from '#/os/interface/hostProcess'; +import { HOOKS_SECTION, type HookDefConfig } from '../configSection'; import { IExternalHooksRunnerService, type ExternalHooksRunnerTriggerArgs, } from './externalHooksRunner'; -import { blockDecision, indexHooks, runMatchedHooks } from './runner'; -import type { HookRunCallbacks } from './runner'; +import { blockDecision, indexHooks, runMatchedHooks } from '../internal/matchHooks'; +import type { HookRunCallbacks } from '../internal/matchHooks'; +import type { HookBlockDecision, HookDef, HookResult } from '../internal/types'; // NOTE: stays Disposable — its own 'config' collides with the Fiber export class ExternalHooksRunnerService extends Disposable implements IExternalHooksRunnerService { @@ -137,11 +136,3 @@ export class ExternalHooksRunnerService extends Disposable implements IExternalH this._onDidReload.fire(); } } - -registerScopedService( - LifecycleScope.App, - IExternalHooksRunnerService, - ExternalHooksRunnerService, - ScopeActivation.OnScopeCreated, - 'externalHooksRunner', -); diff --git a/packages/agent-core-v2/src/agent/externalHooks/configSection.ts b/packages/agent-core-v2/src/features/externalHooks/configSection.ts similarity index 96% rename from packages/agent-core-v2/src/agent/externalHooks/configSection.ts rename to packages/agent-core-v2/src/features/externalHooks/configSection.ts index a84b322fcc..5d5f59b94b 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/configSection.ts +++ b/packages/agent-core-v2/src/features/externalHooks/configSection.ts @@ -12,7 +12,7 @@ import { z } from 'zod'; import { registerConfigSection } from '#/app/config/configSectionContributions'; import { isPlainObject, plainObjectToToml, transformPlainObject } from '#/app/config/toml'; -import { HOOK_EVENT_TYPES } from './types'; +import { HOOK_EVENT_TYPES } from './internal/types'; export const HOOKS_SECTION = 'hooks'; diff --git a/packages/agent-core-v2/src/features/externalHooks/externalHooksFeature.ts b/packages/agent-core-v2/src/features/externalHooks/externalHooksFeature.ts new file mode 100644 index 0000000000..d7e3fc7eae --- /dev/null +++ b/packages/agent-core-v2/src/features/externalHooks/externalHooksFeature.ts @@ -0,0 +1,48 @@ +/** + * `externalHooks` domain — `ExternalHooksFeature`: the external-hook + * capability assembled as one App-scope Feature unit. + * + * Contributes the App-scope `IExternalHooksRunnerService` (the configured-hook + * engine), the Session-scope `ISessionExternalHooksService` observer (session + * lifecycle / subagent / heartbeat hooks), and the per-Agent + * `IAgentExternalHooksService` observer (tool / prompt / turn / compaction / + * task hooks) through the `features` base-class seams; retracting the unit + * withdraws all of them across the scope tree. The `hooks` config section + * (`features/externalHooks/configSection`) stays on its static + * import=register channel — user-facing contracts must remain statically + * discoverable (config manifest) even when the feature unit is retracted. + * Registered into the feature table at import. + */ + +import { LifecycleScope } from '#/app/scopes'; +import { Feature } from '#/features/feature'; +import { registerFeature } from '#/features/featureRegistry'; + +import './configSection'; +import { IAgentExternalHooksService } from './agent/agentExternalHooks'; +import { AgentExternalHooksService } from './agent/agentExternalHooksService'; +import { IExternalHooksRunnerService } from './app/externalHooksRunner'; +import { ExternalHooksRunnerService } from './app/externalHooksRunnerService'; +import { ISessionExternalHooksService } from './session/sessionExternalHooks'; +import { SessionExternalHooksService } from './session/sessionExternalHooksService'; + +export class ExternalHooksFeature extends Feature { + static override readonly name = 'externalHooks'; + + constructor() { + super(); + this.contributeService( + LifecycleScope.App, + IExternalHooksRunnerService, + ExternalHooksRunnerService, + ); + this.contributeService( + LifecycleScope.Session, + ISessionExternalHooksService, + SessionExternalHooksService, + ); + this.contributeAgentService(IAgentExternalHooksService, AgentExternalHooksService); + } +} + +registerFeature(ExternalHooksFeature); diff --git a/packages/agent-core-v2/src/app/externalHooksRunner/runner.ts b/packages/agent-core-v2/src/features/externalHooks/internal/matchHooks.ts similarity index 95% rename from packages/agent-core-v2/src/app/externalHooksRunner/runner.ts rename to packages/agent-core-v2/src/features/externalHooks/internal/matchHooks.ts index 745846065c..3c5d67f2e3 100644 --- a/packages/agent-core-v2/src/app/externalHooksRunner/runner.ts +++ b/packages/agent-core-v2/src/features/externalHooks/internal/matchHooks.ts @@ -10,16 +10,17 @@ * helper module, not a scoped Service. */ -import { runHook } from '#/agent/externalHooks/runner'; +import type { IHostProcessService } from '#/os/interface/hostProcess'; + +import { runHook } from './runHook'; import type { HookBlockDecision, HookDef, HookMatcherValue, HookResult, -} from '#/agent/externalHooks/types'; -import type { IHostProcessService } from '#/os/interface/hostProcess'; +} from './types'; -import type { ExternalHooksRunnerTriggerArgs } from './externalHooksRunner'; +import type { ExternalHooksRunnerTriggerArgs } from '../app/externalHooksRunner'; const DEFAULT_HOOK_TIMEOUT_SECONDS = 30; diff --git a/packages/agent-core-v2/src/agent/externalHooks/runner.ts b/packages/agent-core-v2/src/features/externalHooks/internal/runHook.ts similarity index 94% rename from packages/agent-core-v2/src/agent/externalHooks/runner.ts rename to packages/agent-core-v2/src/features/externalHooks/internal/runHook.ts index 7f50380e1f..6001940eac 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/runner.ts +++ b/packages/agent-core-v2/src/features/externalHooks/internal/runHook.ts @@ -1,3 +1,12 @@ +/** + * `externalHooksRunner` domain — single-hook process spawner. + * + * Owns running one configured hook command through the caller-supplied + * App-scope `IHostProcessService`: the JSON payload on stdin, timeout and + * abort-signal kill, and the exit-code / stdout-JSON (`hookSpecificOutput`) + * to `HookResult` mapping. Pure helper module, not a scoped Service. + */ + import { type SpawnOptionsWithoutStdio } from 'node:child_process'; import { z } from 'zod'; diff --git a/packages/agent-core-v2/src/agent/externalHooks/types.ts b/packages/agent-core-v2/src/features/externalHooks/internal/types.ts similarity index 77% rename from packages/agent-core-v2/src/agent/externalHooks/types.ts rename to packages/agent-core-v2/src/features/externalHooks/internal/types.ts index 1cab03130b..e3b77a90e2 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/types.ts +++ b/packages/agent-core-v2/src/features/externalHooks/internal/types.ts @@ -1,3 +1,12 @@ +/** + * `externalHooks` domain — shared hook types and event vocabulary. + * + * Owns the `HOOK_EVENT_TYPES` event-name union, the `HookDef` configured-hook + * shape, and the `HookResult` / `HookBlockDecision` / `HookMatcherValue` + * types shared by every scope's contract and the pure dispatch helpers. + * Pure type module, not a scoped Service. + */ + import type { ContentPart } from '#/kosong/contract/message'; export const HOOK_EVENT_TYPES = [ diff --git a/packages/agent-core-v2/src/agent/externalHooks/user-prompt.ts b/packages/agent-core-v2/src/features/externalHooks/internal/userPrompt.ts similarity index 88% rename from packages/agent-core-v2/src/agent/externalHooks/user-prompt.ts rename to packages/agent-core-v2/src/features/externalHooks/internal/userPrompt.ts index 1b81c9f615..e0209e65a9 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/user-prompt.ts +++ b/packages/agent-core-v2/src/features/externalHooks/internal/userPrompt.ts @@ -1,3 +1,11 @@ +/** + * `externalHooks` domain — `UserPromptSubmit` hook-result rendering. + * + * Owns folding `UserPromptSubmit` hook results into the display message and + * the `` context text the Agent-scope adapter appends. Pure + * helper module, not a scoped Service. + */ + import type { HookResult } from './types'; export function renderHookResult(event: string, message: string): string { diff --git a/packages/agent-core-v2/src/session/externalHooks/externalHooks.ts b/packages/agent-core-v2/src/features/externalHooks/session/sessionExternalHooks.ts similarity index 100% rename from packages/agent-core-v2/src/session/externalHooks/externalHooks.ts rename to packages/agent-core-v2/src/features/externalHooks/session/sessionExternalHooks.ts diff --git a/packages/agent-core-v2/src/session/externalHooks/externalHooksService.ts b/packages/agent-core-v2/src/features/externalHooks/session/sessionExternalHooksService.ts similarity index 93% rename from packages/agent-core-v2/src/session/externalHooks/externalHooksService.ts rename to packages/agent-core-v2/src/features/externalHooks/session/sessionExternalHooksService.ts index cd82ec7a67..6982aec443 100644 --- a/packages/agent-core-v2/src/session/externalHooks/externalHooksService.ts +++ b/packages/agent-core-v2/src/features/externalHooks/session/sessionExternalHooksService.ts @@ -20,14 +20,11 @@ * the Agent-scope adapter follows against the agent behavior services. The * actual hook execution is delegated to the shared App-scope * `IExternalHooksRunnerService`; all config/plugin loading and engine lifecycle - * live in the runner. Bound at Session scope. + * live in the runner. Contributed at Session scope by `ExternalHooksFeature`. */ import { Service } from '#/_base/di/service'; -import { LifecycleScope } from '#/app/scopes'; -import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { IntervalTimer } from '#/_base/utils/timer'; -import { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner'; import type { Hooks } from '#/hooks'; import { IModelService } from '#/kosong/model/model'; import { @@ -47,7 +44,8 @@ import { ISessionSubagentService, } from '#/session/subagent/subagent'; -import { ISessionExternalHooksService } from './externalHooks'; +import { IExternalHooksRunnerService } from '../app/externalHooksRunner'; +import { ISessionExternalHooksService } from './sessionExternalHooks'; type SessionStartHookSource = Exclude; @@ -209,11 +207,3 @@ export class SessionExternalHooksService }); } } - -registerScopedService( - LifecycleScope.Session, - ISessionExternalHooksService, - SessionExternalHooksService, - ScopeActivation.OnScopeCreated, - 'externalHooks', -); diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 6d910fcaba..8c471b5e9d 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -302,6 +302,14 @@ export * from '#/features/plan/plan'; export * from '#/features/plan/planOps'; export * from '#/features/plan/planService'; import '#/features/plan/planFeature'; +export * from '#/features/externalHooks/configSection'; +export * from '#/features/externalHooks/app/externalHooksRunner'; +export * from '#/features/externalHooks/app/externalHooksRunnerService'; +export * from '#/features/externalHooks/session/sessionExternalHooks'; +export * from '#/features/externalHooks/session/sessionExternalHooksService'; +export * from '#/features/externalHooks/agent/agentExternalHooks'; +export * from '#/features/externalHooks/agent/agentExternalHooksService'; +import '#/features/externalHooks/externalHooksFeature'; export * from '#/agent/tools/goal/create-goal/create-goal'; import '#/agent/tools/goal/create-goal/createGoalTool'; export * from '#/agent/tools/goal/get-goal/get-goal'; @@ -409,8 +417,6 @@ export * from '#/workspace/sessionLifecycle/sessionLifecycle'; export * from '#/workspace/sessionLifecycle/sessionLifecycleService'; export * from '#/workspace/sessionLifecycle/internal/addressing'; export * from '#/session/sessionLifecycleHooks/sessionLifecycleHooks'; -export * from '#/session/externalHooks/externalHooks'; -export * from '#/session/externalHooks/externalHooksService'; import '#/app/sessionExport/errors'; export * from '#/app/sessionExport/sessionExport'; export * from '#/app/sessionExport/sessionExportService'; @@ -533,8 +539,6 @@ export * from '#/app/edit/editService'; export * from '#/app/edit/textModel'; export * from '#/agent/tools/edit/edit'; import '#/agent/tools/edit/editTool'; -export * from '#/app/externalHooksRunner/externalHooksRunner'; -export * from '#/app/externalHooksRunner/externalHooksRunnerService'; export * from '#/agent/tools/fetch-url/fetch-url'; import '#/agent/tools/fetch-url/fetchUrlTool'; export * from '#/app/web/web'; @@ -567,9 +571,6 @@ export * from '#/agent/contextInjector/contextInjector'; export * from '#/agent/contextInjector/contextInjectorService'; export * from '#/agent/plugin/agentPlugin'; export * from '#/agent/plugin/agentPluginService'; -import '#/agent/externalHooks/configSection'; -export * from '#/agent/externalHooks/externalHooks'; -export * from '#/agent/externalHooks/externalHooksService'; export * from '#/agent/fullCompaction/strategy'; export * from '#/agent/fullCompaction/fullCompaction'; export * from '#/agent/fullCompaction/fullCompactionService'; diff --git a/packages/agent-core-v2/src/session/externalHooks/index.ts b/packages/agent-core-v2/src/session/externalHooks/index.ts deleted file mode 100644 index 0c7145dbc9..0000000000 --- a/packages/agent-core-v2/src/session/externalHooks/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * `externalHooks` domain barrel — re-exports the Session-scope external hooks - * contract and its scoped service. Importing this barrel registers the - * `ISessionExternalHooksService` binding into the scope registry. - */ - -export * from './externalHooks'; -export * from './externalHooksService'; diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index f32a9c1959..213bb7efdd 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -30,8 +30,8 @@ import { DefaultCompactionStrategy, } from '#/agent/fullCompaction/strategy'; import { COMPACTION_SUMMARY_PREFIX } from '#/agent/contextMemory/compactionHandoff'; -import { makeHookRunner } from '../externalHooks/runner-stub'; -import type { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner'; +import { makeHookRunner } from '../../features/externalHooks/runner-stub'; +import type { IExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunner'; import { MASTER_ENV } from '#/app/flag/flagService'; import { estimateTokensForMessages } from '#/kosong/contract/tokens'; import { recordingTelemetry, type TelemetryRecord } from '../../app/telemetry/stubs'; diff --git a/packages/agent-core-v2/test/agent/task/rpc-events.test.ts b/packages/agent-core-v2/test/agent/task/rpc-events.test.ts index 628d9bd7cb..83ea8c0762 100644 --- a/packages/agent-core-v2/test/agent/task/rpc-events.test.ts +++ b/packages/agent-core-v2/test/agent/task/rpc-events.test.ts @@ -23,7 +23,7 @@ import { import { ProcessTask } from '#/agent/tools/os/bash/process-task'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import { IEventBus } from '#/app/event/eventBus'; -import type { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner'; +import type { IExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunner'; import { IAgentLoopService } from '#/agent/loop/loop'; import { MessageStepRequest } from '#/agent/loop/stepRequest'; import { IAgentConversationUndoService } from '#/agent/undo/undo'; diff --git a/packages/agent-core-v2/test/features/externalHooks/externalHooksFeature.test.ts b/packages/agent-core-v2/test/features/externalHooks/externalHooksFeature.test.ts new file mode 100644 index 0000000000..1562a952ab --- /dev/null +++ b/packages/agent-core-v2/test/features/externalHooks/externalHooksFeature.test.ts @@ -0,0 +1,99 @@ +/** + * `externalHooks` feature test — verifies `ExternalHooksFeature` assembly + * wiring. + * + * Registers the production feature recipe into a cleared scoped registry and + * asserts the App-scope runner materializes eagerly (with stub App + * dependencies), the Session/Agent service contributions land in the + * `ScopeUnits` collections, and retracting the unit withdraws everything. + */ + +import { beforeEach, describe, expect, it } from 'vitest'; + +import { type CollectionToken, type CollectionView } from '#/_base/di/collection'; +import { ScopeUnits } from '#/_base/di/fiber'; +import { ScopeActivation } from '#/_base/di/instantiation'; +import { type InstantiationService } from '#/_base/di/instantiationService'; +import { _clearScopedRegistryForTests, registerScopedService, type Scope } from '#/_base/di/scope'; +import { createScopedTestHost } from '#/_base/di/test'; +import { Event } from '#/_base/event'; +import { IBootstrapService } from '#/app/bootstrap/bootstrap'; +import { IConfigService } from '#/app/config/config'; +import { IFeatureManager } from '#/app/feature/featureManager'; +import { FeatureManagerService } from '#/app/feature/featureManagerService'; +import { IPluginService } from '#/app/plugin/plugin'; +import { LifecycleScope } from '#/app/scopes'; +import { IFeatureAssemblyService } from '#/features/featureAssembly'; +import { FeatureAssemblyService } from '#/features/featureAssemblyService'; +import { IAgentExternalHooksService } from '#/features/externalHooks/agent/agentExternalHooks'; +import { IExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunner'; +import { ExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunnerService'; +import '#/features/externalHooks/externalHooksFeature'; +import { ISessionExternalHooksService } from '#/features/externalHooks/session/sessionExternalHooks'; +import { IHostProcessService } from '#/os/interface/hostProcess'; + +import { stubBootstrap } from '../../app/bootstrap/stubs'; + +function collectionViewOf(scope: Scope, token: CollectionToken): CollectionView { + return (scope.instantiation as InstantiationService).fiberHost.collectionView(token); +} + +describe('ExternalHooksFeature — assembly (src/features/externalHooks)', () => { + beforeEach(() => { + _clearScopedRegistryForTests(); + registerScopedService( + LifecycleScope.App, + IFeatureManager, + FeatureManagerService, + ScopeActivation.OnScopeCreated, + 'feature', + ); + registerScopedService( + LifecycleScope.App, + IFeatureAssemblyService, + FeatureAssemblyService, + ScopeActivation.OnScopeCreated, + 'features', + ); + }); + + it('assembles the feature and retracts all contributions on unprovide', async () => { + const host = createScopedTestHost([ + [IBootstrapService, stubBootstrap()], + [ + IConfigService, + { _serviceBrand: undefined, ready: Promise.resolve(), get: () => undefined }, + ], + [ + IPluginService, + { _serviceBrand: undefined, enabledHooks: async () => [], onDidReload: Event.None }, + ], + [IHostProcessService, { _serviceBrand: undefined }], + ]); + const manager = host.app.accessor.get(IFeatureManager); + expect(manager.units().map((unit) => unit.name)).toContain('externalHooks'); + + const runner = host.app.accessor.get(IExternalHooksRunnerService); + expect(runner).toBeInstanceOf(ExternalHooksRunnerService); + + const sessionUnits = collectionViewOf(host.app, ScopeUnits(LifecycleScope.Session)); + expect(sessionUnits.items.map((item) => item.name)).toEqual([ + `externalHooks:${String(ISessionExternalHooksService)}`, + ]); + const agentUnits = collectionViewOf(host.app, ScopeUnits(LifecycleScope.Agent)); + expect(agentUnits.items.map((item) => item.name)).toEqual([ + `externalHooks:${String(IAgentExternalHooksService)}`, + ]); + + await manager.unprovideUnit('externalHooks'); + await host.app.instantiation.cascade.whenIdle(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(manager.units()).toHaveLength(0); + expect(() => host.app.accessor.get(IExternalHooksRunnerService)).toThrow(); + expect(sessionUnits.items).toHaveLength(0); + expect(agentUnits.items).toHaveLength(0); + + host.dispose(); + }); +}); diff --git a/packages/agent-core-v2/test/app/externalHooksRunner/externalHooksRunner.test.ts b/packages/agent-core-v2/test/features/externalHooks/externalHooksRunner.test.ts similarity index 99% rename from packages/agent-core-v2/test/app/externalHooksRunner/externalHooksRunner.test.ts rename to packages/agent-core-v2/test/features/externalHooks/externalHooksRunner.test.ts index 86809fa587..4d35a12ed1 100644 --- a/packages/agent-core-v2/test/app/externalHooksRunner/externalHooksRunner.test.ts +++ b/packages/agent-core-v2/test/features/externalHooks/externalHooksRunner.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import type { ContentPart } from '#/kosong/contract/message'; import { describe, expect, it, vi } from 'vitest'; -import { makeHookRunner } from '../../agent/externalHooks/runner-stub'; +import { makeHookRunner } from './runner-stub'; function nodeCommand(source: string): string { return `node -e ${JSON.stringify(source.replaceAll(/\s*\n\s*/g, ' '))}`; diff --git a/packages/agent-core-v2/test/app/externalHooksRunner/integration.test.ts b/packages/agent-core-v2/test/features/externalHooks/integration.test.ts similarity index 98% rename from packages/agent-core-v2/test/app/externalHooksRunner/integration.test.ts rename to packages/agent-core-v2/test/features/externalHooks/integration.test.ts index a7b5fbfd94..3d72e54c29 100644 --- a/packages/agent-core-v2/test/app/externalHooksRunner/integration.test.ts +++ b/packages/agent-core-v2/test/features/externalHooks/integration.test.ts @@ -26,18 +26,18 @@ import { HOOKS_SECTION, hooksFromToml, hooksToToml, -} from '#/agent/externalHooks/configSection'; -import { IAgentExternalHooksService } from '#/agent/externalHooks/externalHooks'; -import { AgentExternalHooksService } from '#/agent/externalHooks/externalHooksService'; +} from '#/features/externalHooks/configSection'; +import { IAgentExternalHooksService } from '#/features/externalHooks/agent/agentExternalHooks'; +import { AgentExternalHooksService } from '#/features/externalHooks/agent/agentExternalHooksService'; import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction'; import { IAgentLoopService, type AfterStepContext } from '#/agent/loop/loop'; import { IAgentPermissionGate } from '#/agent/permissionGate/permissionGate'; import { IAgentPromptService } from '#/agent/prompt/prompt'; import { IAgentTaskService } from '#/agent/task/task'; import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor'; -import { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner'; -import { ExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunnerService'; -import { makeHookRunner } from '../../agent/externalHooks/runner-stub'; +import { IExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunner'; +import { ExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunnerService'; +import { makeHookRunner } from './runner-stub'; import type { AgentTaskInfo } from '#/agent/task/task'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IConfigService } from '#/app/config/config'; @@ -57,15 +57,15 @@ import { type AgentTaskStopHookContext, ISessionSubagentService, } from '#/session/subagent/subagent'; -import { ISessionExternalHooksService } from '#/session/externalHooks/externalHooks'; -import { SessionExternalHooksService } from '#/session/externalHooks/externalHooksService'; +import { ISessionExternalHooksService } from '#/features/externalHooks/session/sessionExternalHooks'; +import { SessionExternalHooksService } from '#/features/externalHooks/session/sessionExternalHooksService'; import { ISessionAgentProfileCatalog, } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata'; import { IModelService } from '#/kosong/model/model'; -import { stubBootstrap } from '../bootstrap/stubs'; +import { stubBootstrap } from '../../app/bootstrap/stubs'; import { stubLoopWithHooks, stubToolExecutor } from '../../agent/loop/stubs'; import { registerStateServices } from '../../state/stubs'; import { registerTestAgentWireServices } from '../../wire/stubs'; diff --git a/packages/agent-core-v2/test/agent/externalHooks/runner-stub.ts b/packages/agent-core-v2/test/features/externalHooks/runner-stub.ts similarity index 88% rename from packages/agent-core-v2/test/agent/externalHooks/runner-stub.ts rename to packages/agent-core-v2/test/features/externalHooks/runner-stub.ts index f2052b52b0..1bdd242317 100644 --- a/packages/agent-core-v2/test/agent/externalHooks/runner-stub.ts +++ b/packages/agent-core-v2/test/features/externalHooks/runner-stub.ts @@ -11,9 +11,9 @@ */ import { Event } from '#/_base/event'; -import { ExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunnerService'; -import { HOOKS_SECTION } from '#/agent/externalHooks/configSection'; -import type { HookDef } from '#/agent/externalHooks/types'; +import { ExternalHooksRunnerService } from '#/features/externalHooks/app/externalHooksRunnerService'; +import { HOOKS_SECTION } from '#/features/externalHooks/configSection'; +import type { HookDef } from '#/features/externalHooks/internal/types'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IConfigService } from '#/app/config/config'; import { IPluginService } from '#/app/plugin/plugin'; diff --git a/packages/agent-core-v2/test/agent/externalHooks/runner.test.ts b/packages/agent-core-v2/test/features/externalHooks/runner.test.ts similarity index 98% rename from packages/agent-core-v2/test/agent/externalHooks/runner.test.ts rename to packages/agent-core-v2/test/features/externalHooks/runner.test.ts index 853ff1076f..58c029cff8 100644 --- a/packages/agent-core-v2/test/agent/externalHooks/runner.test.ts +++ b/packages/agent-core-v2/test/features/externalHooks/runner.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildHookSpawnOptions, runHook } from '#/agent/externalHooks/runner'; +import { buildHookSpawnOptions, runHook } from '#/features/externalHooks/internal/runHook'; import { HostProcessService } from '#/os/backends/node-local/hostProcessService'; const hostProcess = new HostProcessService(); diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index 96c26962e2..ca6e04cfc1 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -1127,6 +1127,17 @@ export class AgentTestContext { 'app', ); this.root = createAppScope({ seeds: appSeeds }); + // Feature contributions materialize through the ScopeUnits fold during + // scope creation and overwrite a same-token seed (the static-batch seed + // filter does not apply to them), so re-assert the hook-runner test + // double afterwards to keep it authoritative. + const hookRunnerSeed = appSeeds.find(([id]) => id === IExternalHooksRunnerService); + if (hookRunnerSeed !== undefined) { + this.root.instantiation.provide( + IExternalHooksRunnerService, + hookRunnerSeed[1] as IExternalHooksRunnerService, + ); + } const initialConfig = this.root.accessor.get(IConfigService); this.root.accessor diff --git a/packages/agent-core-v2/test/tool/tool.test.ts b/packages/agent-core-v2/test/tool/tool.test.ts index 14709c15a2..5bb1efec03 100644 --- a/packages/agent-core-v2/test/tool/tool.test.ts +++ b/packages/agent-core-v2/test/tool/tool.test.ts @@ -25,7 +25,7 @@ import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInj import { IAgentTaskService } from '#/agent/task/task'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import { IAgentTokenCountingService } from '#/agent/tokenCounting/tokenCounting'; -import { makeHookRunner } from '../agent/externalHooks/runner-stub'; +import { makeHookRunner } from '../features/externalHooks/runner-stub'; import { IAgentProfileService } from '#/agent/profile/profile'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { ToolAccesses, type ExecutableTool } from '#/tool/toolContract'; diff --git a/packages/agent-core-v2/test/workspace/sessionLifecycle/sessionLifecycle.test.ts b/packages/agent-core-v2/test/workspace/sessionLifecycle/sessionLifecycle.test.ts index a848c9521b..d2c77e3ac2 100644 --- a/packages/agent-core-v2/test/workspace/sessionLifecycle/sessionLifecycle.test.ts +++ b/packages/agent-core-v2/test/workspace/sessionLifecycle/sessionLifecycle.test.ts @@ -52,7 +52,7 @@ import { SessionLifecycleService } from '#/workspace/sessionLifecycle/sessionLif import { IWorkspaceToolPolicy } from '#/workspace/workspaceToolPolicy/workspaceToolPolicy'; import { WorkspaceToolPolicyService } from '#/workspace/workspaceToolPolicy/workspaceToolPolicyService'; import { IAgentActivityView } from '#/agent/activityView/activityView'; -import { ISessionExternalHooksService } from '#/session/externalHooks/externalHooks'; +import { ISessionExternalHooksService } from '#/features/externalHooks/session/sessionExternalHooks'; import { ISessionLifecycleHooks, type SessionLifecycleHookSlots, diff --git a/packages/kap-server/src/protocol/events-zod.ts b/packages/kap-server/src/protocol/events-zod.ts index a99558cfb7..f66160ea85 100644 --- a/packages/kap-server/src/protocol/events-zod.ts +++ b/packages/kap-server/src/protocol/events-zod.ts @@ -26,7 +26,7 @@ import type { UserPromptOrigin, } from '@moonshot-ai/agent-core-v2/agent/contextMemory/types'; import { messageContentSchema } from './message'; -import type { HookResultEvent } from '@moonshot-ai/agent-core-v2/agent/externalHooks/externalHooksService'; +import type { HookResultEvent } from '@moonshot-ai/agent-core-v2/features/externalHooks/agent/agentExternalHooksService'; import type { CompactionBlockedEvent, CompactionCancelledEvent, diff --git a/packages/klient/src/contract/global/plugins.ts b/packages/klient/src/contract/global/plugins.ts index 112011221f..6047073626 100644 --- a/packages/klient/src/contract/global/plugins.ts +++ b/packages/klient/src/contract/global/plugins.ts @@ -2,7 +2,7 @@ * `pluginService` — plugin management and consumption. Mirrors * `agent-core-v2/app/plugin/plugin.ts` and `agent-core-v2/app/plugin/types.ts`; * nested `McpServerConfig` mirrors `agent-core-v2/mcpCore/config-schema.ts`, - * `HookDefConfig` mirrors `agent-core-v2/agent/externalHooks/configSection.ts`. + * `HookDefConfig` mirrors `agent-core-v2/features/externalHooks/configSection.ts`. * `pluginSkillRoots`, `enabledSessionStarts`, `enabledSystemPrompts`, * `enabledMcpServers`, and `enabledHooks` are excluded (not part of the * klient wire surface).