Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/agent-core-v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core-v2/docs/config-manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
# ##########################################################################
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
9 changes: 0 additions & 9 deletions packages/agent-core-v2/src/app/externalHooksRunner/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/plugin/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/plugin/manifest.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/plugin/pluginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/plugin/types.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -482,11 +481,3 @@ function toolOutputText(output: ExecutableToolResult['output']): string {
.map((part) => part.text)
.join('');
}

registerScopedService(
LifecycleScope.Agent,
IAgentExternalHooksService,
AgentExternalHooksService,
ScopeActivation.OnScopeCreated,
'externalHooks',
);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -137,11 +136,3 @@ export class ExternalHooksRunnerService extends Disposable implements IExternalH
this._onDidReload.fire();
}
}

registerScopedService(
LifecycleScope.App,
IExternalHooksRunnerService,
ExternalHooksRunnerService,
ScopeActivation.OnScopeCreated,
'externalHooksRunner',
);
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
);
Comment on lines +34 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve seeded hook-runner overrides

Previously IExternalHooksRunnerService was a registerScopedService, so bootstrap(..., extraSeeds) / createAppScope({ extra }) callers could seed a noop or custom runner and the static registration was skipped. This feature contribution materializes through ScopeUnits during app creation and unconditionally provides the production ExternalHooksRunnerService, so those callers silently lose their override and configured shell hooks can run anyway; preserve the seed check before contributing/providing the hook services.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L28-L28

Useful? React with 👍 / 👎.

this.contributeService(
LifecycleScope.Session,
ISessionExternalHooksService,
SessionExternalHooksService,
);
this.contributeAgentService(IAgentExternalHooksService, AgentExternalHooksService);
}
}

registerFeature(ExternalHooksFeature);
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* `externalHooks` domain — `UserPromptSubmit` hook-result rendering.
*
* Owns folding `UserPromptSubmit` hook results into the display message and
* the `<hook_result>` 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<SessionCreateSource, 'fork'>;

Expand Down Expand Up @@ -209,11 +207,3 @@ export class SessionExternalHooksService
});
}
}

registerScopedService(
LifecycleScope.Session,
ISessionExternalHooksService,
SessionExternalHooksService,
ScopeActivation.OnScopeCreated,
'externalHooks',
);
Loading
Loading