refactor(agent-core-v2): extract externalHooks into a scope-organized feature (#2805)

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
This commit is contained in:
Haozhe 2026-08-19 16:52:51 +08:00 committed by GitHub
parent cb8a7e5f81
commit 16499408d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 186 additions and 96 deletions

View file

@ -65,7 +65,7 @@ import type {
ToolCallDelta,
} from '@moonshot-ai/agent-core-v2/agent/loop/turnEvents';
import type { TurnStepRetrying } from '@moonshot-ai/agent-core-v2/agent/stepRetry/stepRetryService';
import type { HookResult } from '@moonshot-ai/agent-core-v2/agent/externalHooks/externalHooksService';
import type { HookResult } from '@moonshot-ai/agent-core-v2/features/externalHooks/agent/agentExternalHooksService';
import type {
ToolCallStarted,
ToolProgress,

View file

@ -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); event/state vocabulary — `EventStateContribution``IEventDispatcher` fold (a record bundles `events`; `registerEvent2Class` stays the static channel drained at fold time, while `defineState(...).replayable(...)` keys are explicit owner-service contributions: each replayable key's owning service contributes it via `contributeState` at construction — Agent-scope owners are eager, the session-domain todo/cron/interaction keys bridge through `agentLifecycle.onDidCreate` before `restore()` — and replaying a withdrawn domain's history lands on the unknown-type 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 `agentCommandService.list` / `run`).
`src/features/` — built-in capabilities authored as self-contained Feature units (`plan` was the first, extracted from `agent/plan` + `agent/tools/plan`; `swarm` followed, extracted from `agent/swarm` + `session/swarm` + `agent/tools/agent-swarm` into a scope-organized `agent/` + `session/` + `tools/` layout; `tower` lives here as `features/tower/` — protocol store, rate limit, tower-mode service, eleven `Tower*` tools, the `tower-worker` profile, and the `/tower` skill body). 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`; `swarm` followed, extracted from `agent/swarm` + `session/swarm` + `agent/tools/agent-swarm` into a scope-organized `agent/` + `session/` + `tools/` layout; `tower` lives here as `features/tower/` — protocol store, rate limit, tower-mode service, eleven `Tower*` tools, the `tower-worker` profile, and the `/tower` skill body). 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)

View file

@ -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
@ -135,7 +135,7 @@ extra_skill_dirs = []
# ##########################################################################
# hooks
# owner: src/agent/externalHooks/configSection.ts
# owner: src/features/externalHooks/configSection.ts
# scope: core
# hooks: custom fromToml · custom toToml
# ##########################################################################

View file

@ -69,7 +69,7 @@
// contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts
// cron src/session/cron/cronOps.ts
// dateChange.seed src/features/dateChange/dateChangeService.ts
// externalHooks.stopHookContinuationUsed src/agent/externalHooks/externalHooksService.ts
// externalHooks.stopHookContinuationUsed src/features/externalHooks/agent/agentExternalHooksService.ts
// fullCompaction src/agent/fullCompaction/compactionOps.ts
// fullCompaction.activeTurnId src/agent/fullCompaction/fullCompactionService.ts
// fullCompaction.compactionCountInTurn src/agent/fullCompaction/fullCompactionService.ts
@ -1188,8 +1188,6 @@ export interface AgentStateSnapshot {
})[];
// src/agent/contextProjector/contextProjectorService.ts
'contextProjector.lastRepairSignature': string | null;
// src/agent/externalHooks/externalHooksService.ts
'externalHooks.stopHookContinuationUsed': boolean;
// src/agent/fullCompaction/compactionOps.ts
// replayable · durable — folds: FullCompactionBegin, FullCompactionCancel, FullCompactionComplete
'fullCompaction': /* CompactionState — packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts */ {
@ -1575,6 +1573,8 @@ export interface AgentStateSnapshot {
readonly timeZone: string;
readonly renderGeneration: number;
} | undefined;
// src/features/externalHooks/agent/agentExternalHooksService.ts
'externalHooks.stopHookContinuationUsed': boolean;
// src/features/plan/injection/planModeInjection.ts
'plan.wasActive': boolean;
// src/features/plan/planOps.ts

View file

@ -1,2 +0,0 @@
export * from './externalHooksRunner';
export * from './externalHooksRunnerService';

View file

@ -3,7 +3,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';

View file

@ -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 {

View file

@ -1,6 +1,6 @@
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';

View file

@ -8,7 +8,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';

View file

@ -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';

View file

@ -1,11 +1,5 @@
import { createDecorator } from '#/_base/di/instantiation';
export interface RenderedExternalHookResult {
readonly event: string;
readonly message: string;
readonly text: string;
}
export interface IAgentExternalHooksService {
readonly _serviceBrand: undefined;
}

View file

@ -1,8 +1,6 @@
/* oxlint-disable typescript-eslint/no-unsafe-declaration-merging, eslint-plugin-import/namespace -- Event2 class+payload-interface declaration merging is the sanctioned event-declaration idiom. */
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 '#/state/state';
import { isPlainRecord } from '#/_base/utils/canonical-args';
import { IAgentStateService } from '#/agent/state/agentState';
@ -39,13 +37,13 @@ import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
import { IEventDispatcher } from '#/state/eventDispatcher';
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 HookResultPayload {
readonly turnId?: number;
@ -471,11 +469,3 @@ function toolOutputText(output: ExecutableToolResult['output']): string {
.map((part) => part.text)
.join('');
}
registerScopedService(
LifecycleScope.Agent,
IAgentExternalHooksService,
AgentExternalHooksService,
ScopeActivation.OnScopeCreated,
'externalHooks',
);

View file

@ -1,6 +1,6 @@
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;

View file

@ -1,20 +1,18 @@
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';
export class ExternalHooksRunnerService extends Disposable implements IExternalHooksRunnerService {
declare readonly _serviceBrand: undefined;
@ -120,11 +118,3 @@ export class ExternalHooksRunnerService extends Disposable implements IExternalH
this._onDidReload.fire();
}
}
registerScopedService(
LifecycleScope.App,
IExternalHooksRunnerService,
ExternalHooksRunnerService,
ScopeActivation.OnScopeCreated,
'externalHooksRunner',
);

View file

@ -3,7 +3,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';

View file

@ -0,0 +1,32 @@
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);

View file

@ -1,13 +1,14 @@
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;

View file

@ -1,8 +1,5 @@
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 { ISessionManager } from '#/app/sessionManager/sessionManager';
import { IModelService } from '#/kosong/model/model';
import {
@ -20,7 +17,8 @@ import {
type SessionCreateSource,
} from '#/workspace/sessionLifecycle/sessionLifecycle';
import { ISessionExternalHooksService } from './externalHooks';
import { IExternalHooksRunnerService } from '../app/externalHooksRunner';
import { ISessionExternalHooksService } from './sessionExternalHooks';
type SessionStartHookSource = Exclude<SessionCreateSource, 'fork'>;
@ -183,11 +181,3 @@ export class SessionExternalHooksService
});
}
}
registerScopedService(
LifecycleScope.Session,
ISessionExternalHooksService,
SessionExternalHooksService,
ScopeActivation.OnScopeCreated,
'externalHooks',
);

View file

@ -322,6 +322,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 '#/features/debugEvents/debugEvents';
export * from '#/features/debugEvents/debugEventsService';
import '#/features/debugEvents/debugEventsFeature';
@ -455,8 +463,6 @@ export * from '#/workspace/sessionLifecycle/sessionLifecycleEvents';
export * from '#/workspace/sessionLifecycle/sessionLifecycleService';
export * from '#/workspace/sessionLifecycle/coldSessionArchive';
export * from '#/workspace/sessionLifecycle/internal/addressing';
export * from '#/session/externalHooks/externalHooks';
export * from '#/session/externalHooks/externalHooksService';
import '#/app/sessionExport/errors';
export * from '#/app/sessionExport/sessionExport';
export * from '#/app/sessionExport/sessionExportService';
@ -574,8 +580,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';
@ -611,9 +615,6 @@ export * from '#/agent/contextInjector/contextInjectorService';
export * from '#/agent/plugin/agentPlugin';
export * from '#/agent/plugin/agentPluginOps';
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';

View file

@ -1,2 +0,0 @@
export * from './externalHooks';
export * from './externalHooksService';

View file

@ -18,8 +18,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';

View file

@ -19,7 +19,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';

View file

@ -0,0 +1,89 @@
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<T>(scope: Scope, token: CollectionToken<T>): CollectionView<T> {
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();
});
});

View file

@ -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, ' '))}`;

View file

@ -26,9 +26,9 @@ 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 { TurnStarted } from '#/agent/loop/turnEvents';
@ -43,9 +43,9 @@ import {
PermissionApprovalResolved,
} from '#/agent/toolApproval/toolApprovalService';
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';
@ -69,15 +69,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';

View file

@ -1,7 +1,7 @@
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';

View file

@ -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();

View file

@ -1183,6 +1183,13 @@ export class AgentTestContext {
'app',
);
this.root = createAppScope({ seeds: appSeeds });
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

View file

@ -18,7 +18,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, type ProfileData } from '#/agent/profile/profile';
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
import { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime';

View file

@ -19,7 +19,7 @@ import type {
UserPromptOrigin,
} from '@moonshot-ai/agent-core-v2/agent/contextMemory/types';
import { messageContentSchema } from './message';
import type { HookResultPayload } from '@moonshot-ai/agent-core-v2/agent/externalHooks/externalHooksService';
import type { HookResultPayload } from '@moonshot-ai/agent-core-v2/features/externalHooks/agent/agentExternalHooksService';
import type {
CompactionBlockedPayload,
CompactionCompletedPayload,

View file

@ -1,6 +1,6 @@
import type { AgentActivityUpdated } from '@moonshot-ai/agent-core-v2/agent/activityView/activityView';
import type { ContextSpliced } from '@moonshot-ai/agent-core-v2/agent/contextMemory/contextEvents';
import type { HookResult } from '@moonshot-ai/agent-core-v2/agent/externalHooks/externalHooksService';
import type { HookResult } from '@moonshot-ai/agent-core-v2/features/externalHooks/agent/agentExternalHooksService';
import type {
CompactionBlocked,
CompactionCancelled,

View file

@ -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).