From f851cc47767de9e35914db90f0cd555b1c38bb4e Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Mon, 6 Jul 2026 20:17:46 +0800 Subject: [PATCH] refactor(agent-core-v2): migrate service hooks to IEventBus events - replace OrderedHookSlot-based hooks (turn onLaunched/onEnded, context onSpliced, permission approval request/resolve, task onDidNotify, fullCompaction onDidCompact, toolRegistry register/unregister) with IEventBus domain events - add contextMemory named primitives (append/clear/undo/applyCompaction) on wire-protocol 1.4 ops, retaining splice for replay and rare single-deletes - update externalHooks, goal, swarm, compaction, context-injector, micro-compaction, and task-notification to subscribe to the new events - drop removed hook slots from the turn and task test stubs --- .../contextInjector/contextInjectorService.ts | 10 +- .../src/agent/contextMemory/contextMemory.ts | 40 +++++-- .../contextMemory/contextMemoryService.ts | 107 ++++++++++++++---- .../externalHooks/externalHooksService.ts | 60 +++++----- .../src/agent/fullCompaction/compactionOps.ts | 4 +- .../agent/fullCompaction/fullCompaction.ts | 1 - .../fullCompaction/fullCompactionService.ts | 12 +- .../src/agent/goal/goalService.ts | 31 +++-- .../microCompaction/microCompactionService.ts | 7 +- .../agent/permissionGate/permissionGate.ts | 5 - .../permissionGate/permissionGateService.ts | 27 +++-- .../src/agent/swarm/swarmService.ts | 6 +- packages/agent-core-v2/src/agent/task/task.ts | 4 - .../src/agent/task/taskService.ts | 22 ++-- .../src/agent/toolRegistry/toolRegistry.ts | 5 - .../agent/toolRegistry/toolRegistryService.ts | 8 -- packages/agent-core-v2/src/agent/turn/turn.ts | 10 -- .../src/agent/turn/turnService.ts | 13 +-- .../test/shellTools/bash.test.ts | 3 - packages/agent-core-v2/test/turn/stubs.ts | 18 +-- 20 files changed, 203 insertions(+), 190 deletions(-) diff --git a/packages/agent-core-v2/src/agent/contextInjector/contextInjectorService.ts b/packages/agent-core-v2/src/agent/contextInjector/contextInjectorService.ts index a578bed6f..faf542447 100644 --- a/packages/agent-core-v2/src/agent/contextInjector/contextInjectorService.ts +++ b/packages/agent-core-v2/src/agent/contextInjector/contextInjectorService.ts @@ -9,6 +9,7 @@ import { IAgentContextMemoryService } from '#/agent/contextMemory'; import { IAgentLoopService } from '#/agent/loop'; import { IAgentSystemReminderService } from '#/agent/systemReminder'; import { IAgentTurnService } from '#/agent/turn'; +import { IEventBus } from '#/app/event'; import type { ContextMessage } from '#/agent/contextMemory'; import { IAgentContextInjectorService, @@ -34,6 +35,7 @@ export class AgentContextInjectorService extends Disposable implements IAgentCon @IAgentTurnService turnService: IAgentTurnService, @IAgentLoopService loopService: IAgentLoopService, @IAgentSystemReminderService private readonly reminders: IAgentSystemReminderService, + @IEventBus private readonly eventBus: IEventBus, ) { super(); this._register( @@ -43,17 +45,13 @@ export class AgentContextInjectorService extends Disposable implements IAgentCon }), ); this._register( - turnService.hooks.onLaunched.register('context-injector', (_ctx, next) => { + this.eventBus.subscribe('turn.started', () => { for (const entry of this.entries) { entry.turnConsumed = false; } - return next(); }), ); - context.hooks.onSpliced.register('context-injector', (ctx, next) => { - this.handleSplice(ctx); - return next(); - }); + this.eventBus.subscribe('context.spliced', (e) => this.handleSplice(e)); } register( diff --git a/packages/agent-core-v2/src/agent/contextMemory/contextMemory.ts b/packages/agent-core-v2/src/agent/contextMemory/contextMemory.ts index 525537164..0217e7839 100644 --- a/packages/agent-core-v2/src/agent/contextMemory/contextMemory.ts +++ b/packages/agent-core-v2/src/agent/contextMemory/contextMemory.ts @@ -1,27 +1,47 @@ import { createDecorator } from "#/_base/di"; -import type { Hooks } from '#/hooks'; +import type { UndoCut } from './contextOps'; import type { ContextMessage } from './types'; +export interface ContextCompactionInput { + readonly count: number; + readonly summary: ContextMessage; + readonly tokens?: number; +} + export interface IAgentContextMemoryService { readonly _serviceBrand: undefined; get(): readonly ContextMessage[]; + + /** Append one or more already-folded messages (`context.append_message`). */ + append(...messages: readonly ContextMessage[]): void; + + /** Drop the entire history (`context.clear`). No-op when already empty. */ + clear(): void; + + /** + * Remove the trailing `count` real-user prompts and the exchange that follows + * them (`context.undo`). Returns the computed cut so the caller can surface a + * `request.invalid` when fewer than `count` prompts were undoable; the model is + * left untouched in that case. + */ + undo(count: number): UndoCut; + + /** Replace the leading `count` messages with a compaction summary (`context.apply_compaction`). */ + applyCompaction(input: ContextCompactionInput): void; + + /** + * Arbitrary splice (`context.splice`). Retained for replay of protocol 1.5 + * sessions and the few internal single-delete mutations with no 1.4 spelling; + * new code should prefer the named primitives above. + */ splice( start: number, deleteCount: number, messages: readonly ContextMessage[], tokens?: number, ): void; - - readonly hooks: Hooks<{ - onSpliced: { - start: number; - deleteCount: number; - messages: ContextMessage[]; - tokens?: number; - }; - }>; } export const IAgentContextMemoryService = createDecorator('agentContextMemoryService'); diff --git a/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts b/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts index ec00114c1..d01ea715b 100644 --- a/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts +++ b/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts @@ -2,27 +2,35 @@ * `contextMemory` domain (L4) — `IAgentContextMemoryService` implementation. * * Owns the per-agent conversation history in the wire `ContextModel` - * (`ContextMessage[]`): reads through `wire.getModel`, writes through - * `wire.dispatch(contextSplice(...))` (splice is the single primitive). The - * `context.splice` record still rides the shared wire log read by `getRecords()` - * and replayed into the Model, so its shape stays declared in `WireRecordMap`; - * blob offload now lives in the `WireService` hook (seeded with - * `contextBlobSelector`) rather than a `record.define(..., { blobs })` facet. - * Message ids are stamped at the dispatch call site so `apply` stays pure. - * `onSpliced` fires from the live `splice` path only — replay rebuilds the Model - * silently and never invokes the service method, so the hook is quiet on - * restore. The legacy replay read model (`IAgentRecordService`) is no longer - * mirrored here. Bound at Agent scope. + * (`ContextMessage[]`): reads through `wire.getModel`, writes through the + * wire-protocol 1.4 Ops (`append` / `clear` / `undo` / `applyCompaction`), with + * `splice` retained for protocol 1.5 replay and the rare internal single-delete. + * Every mutation still fires `onSpliced` from the live path only (replay rebuilds + * the Model silently and never invokes these methods), so existing subscribers + * (micro-compaction, context-injector, task-notification) observe the same + * splice-shaped change events regardless of which 1.4 Op was persisted. Message + * ids are stamped at the dispatch call site so `apply` stays pure. Blob offload + * lives in the `WireService` hook seeded with `contextBlobSelector`. Bound at + * Agent scope. */ import { Disposable } from '#/_base/di'; import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { OrderedHookSlot } from '#/hooks'; +import { IEventBus } from '#/app/event'; import { IAgentWireService, type IWireService } from '#/wire'; -import { IAgentContextMemoryService } from './contextMemory'; -import { ContextModel, contextSplice } from './contextOps'; +import { IAgentContextMemoryService, type ContextCompactionInput } from './contextMemory'; +import { + computeUndoCut, + ContextModel, + contextAppendMessage, + contextApplyCompaction, + contextClear, + contextSplice, + contextUndo, + type UndoCut, +} from './contextOps'; import { ensureMessageId } from './messageId'; import type { ContextMessage } from './types'; @@ -37,19 +45,24 @@ declare module '#/agent/wireRecord' { } } +declare module '#/app/event/eventBus' { + interface DomainEventMap { + 'context.spliced': { + start: number; + deleteCount: number; + messages: readonly ContextMessage[]; + tokens?: number; + }; + } +} + export class AgentContextMemoryService extends Disposable implements IAgentContextMemoryService { declare readonly _serviceBrand: undefined; - readonly hooks = { - onSpliced: new OrderedHookSlot<{ - start: number; - deleteCount: number; - messages: ContextMessage[]; - tokens?: number; - }>(), - }; - - constructor(@IAgentWireService private readonly wire: IWireService) { + constructor( + @IAgentWireService private readonly wire: IWireService, + @IEventBus private readonly eventBus: IEventBus, + ) { super(); } @@ -57,6 +70,50 @@ export class AgentContextMemoryService extends Disposable implements IAgentConte return this.wire.getModel(ContextModel) as readonly ContextMessage[]; } + append(...messages: readonly ContextMessage[]): void { + if (messages.length === 0) return; + const stamped = messages.map(ensureMessageId); + const start = this.get().length; + this.wire.dispatch(...stamped.map((message) => contextAppendMessage({ message }))); + this.eventBus.publish({ type: 'context.spliced', + start, + deleteCount: 0, + messages: [...stamped], + }); + } + + clear(): void { + const deleteCount = this.get().length; + if (deleteCount === 0) return; + this.wire.dispatch(contextClear({})); + this.eventBus.publish({ type: 'context.spliced', start: 0, deleteCount, messages: [] }); + } + + undo(count: number): UndoCut { + const history = this.get(); + const cut = computeUndoCut(history, count); + if (cut.cutIndex >= 0 && cut.removedCount >= count) { + this.wire.dispatch(contextUndo({ count })); + this.eventBus.publish({ type: 'context.spliced', + start: cut.cutIndex, + deleteCount: history.length - cut.cutIndex, + messages: [], + }); + } + return cut; + } + + applyCompaction(input: ContextCompactionInput): void { + const summary = ensureMessageId(input.summary); + this.wire.dispatch(contextApplyCompaction({ count: input.count, summary })); + this.eventBus.publish({ type: 'context.spliced', + start: 0, + deleteCount: input.count, + messages: [summary], + tokens: input.tokens, + }); + } + splice( start: number, deleteCount: number, @@ -65,7 +122,7 @@ export class AgentContextMemoryService extends Disposable implements IAgentConte ): void { const stamped = messages.map(ensureMessageId); this.wire.dispatch(contextSplice({ start, deleteCount, messages: stamped, tokens })); - void this.hooks.onSpliced.run({ + this.eventBus.publish({ type: 'context.spliced', start, deleteCount, messages: [...stamped], diff --git a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts index 7afcc3691..c1a6c9a43 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts +++ b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts @@ -22,9 +22,9 @@ import { IAgentTaskService, type AgentTaskNotificationContext } from '#/agent/ta import { IAgentContextMemoryService, USER_PROMPT_ORIGIN } from '#/agent/contextMemory'; import { IAgentFullCompactionService, - type FullCompactionDidCompactContext, type FullCompactionWillCompactContext, } from '#/agent/fullCompaction'; +import type { CompactionResult, CompactionSource } from '#/agent/fullCompaction/types'; import { IAgentLoopService, type TurnAfterStepContext } from '#/agent/loop'; import { IAgentPermissionGate, @@ -33,7 +33,7 @@ import { IAgentPromptService, type PromptSubmitContext, } from '#/agent/prompt'; -import type { HookResultEvent } from '@moonshot-ai/protocol'; +import type { HookResultEvent, TurnEndReason } from '@moonshot-ai/protocol'; import { IEventBus } from '#/app/event'; import type { ExecutableToolResult, @@ -43,7 +43,6 @@ import type { import { IAgentToolExecutorService } from '#/agent/toolExecutor'; import { IAgentTurnService, - type TurnEndedContext, } from '#/agent/turn'; import { IBootstrapService } from '#/app/bootstrap'; import { IConfigService } from '#/app/config'; @@ -169,21 +168,21 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter private registerPermissionHooks(permission: IAgentPermissionGate): void { this._register( - permission.hooks.onDidRequestApproval.register('externalHooks', async (ctx, next) => { + this.eventBus.subscribe('permission.approval.requested', (e) => { + const { type: _type, ...inputData } = e; void this.engine()?.fireAndForgetTrigger('PermissionRequest', { - matcherValue: ctx.toolName, - inputData: { ...ctx }, + matcherValue: e.toolName, + inputData, }); - await next(); }), ); this._register( - permission.hooks.onDidResolveApproval.register('externalHooks', async (ctx, next) => { + this.eventBus.subscribe('permission.approval.resolved', (e) => { + const { type: _type, ...inputData } = e; void this.engine()?.fireAndForgetTrigger('PermissionResult', { - matcherValue: ctx.toolName, - inputData: { ...ctx }, + matcherValue: e.toolName, + inputData, }); - await next(); }), ); } @@ -202,10 +201,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter private registerTurnHooks(turn: IAgentTurnService): void { this._register( - turn.hooks.onEnded.register('externalHooks', async (ctx, next) => { - this.notifyTurnEnded(ctx); - await next(); - }), + this.eventBus.subscribe('turn.ended', (e) => this.notifyTurnEnded(e)), ); } @@ -244,18 +240,15 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter }), ); this._register( - fullCompaction.hooks.onDidCompact.register('externalHooks', async (ctx, next) => { - this.notifyPostCompact(ctx); - await next(); - }), + this.eventBus.subscribe('compaction.completed', (e) => this.notifyPostCompact(e)), ); } private registerTaskHooks(tasks: IAgentTaskService): void { this._register( - tasks.hooks.onDidNotify.register('externalHooks', async (ctx, next) => { + this.eventBus.subscribe('task.notified', (e) => { + const { type: _type, ...ctx } = e; this.notifyTaskNotification(ctx); - await next(); }), ); } @@ -352,17 +345,18 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter return false; } - private notifyTurnEnded(ctx: TurnEndedContext): void { + private notifyTurnEnded(event: { + turnId: number; + reason: TurnEndReason; + error?: unknown; + }): void { this.stopHookContinuationUsed = false; - if (ctx.result.reason === 'failed' && ctx.result.error !== undefined) { - this.notifyStopFailure(ctx.result.error, ctx.turn.abortController.signal); + if (event.reason === 'failed' && event.error !== undefined) { + this.notifyStopFailure(event.error, new AbortController().signal); } - if ( - ctx.result.reason === 'cancelled' && - isUserCancellation(ctx.turn.abortController.signal.reason) - ) { + if (event.reason === 'cancelled') { void this.engine()?.fireAndForgetTrigger('Interrupt', { - inputData: { turnId: ctx.turn.id, reason: 'cancelled' }, + inputData: { turnId: event.turnId, reason: 'cancelled' }, }); } } @@ -406,12 +400,12 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter ctx.signal.throwIfAborted(); } - private notifyPostCompact(ctx: FullCompactionDidCompactContext): void { + private notifyPostCompact(event: { trigger: CompactionSource; result: CompactionResult }): void { void this.engine()?.fireAndForgetTrigger('PostCompact', { - matcherValue: ctx.trigger, + matcherValue: event.trigger, inputData: { - trigger: ctx.trigger, - estimatedTokenCount: ctx.estimatedTokenCount, + trigger: event.trigger, + estimatedTokenCount: event.result.tokensAfter, }, }); } diff --git a/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts b/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts index 08df1b739..7bc845c07 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts @@ -47,7 +47,7 @@ import type { import { defineModel, defineOp } from '#/wire'; import type { FullCompactionCompleteData } from './fullCompaction'; -import type { CompactionBeginData } from './types'; +import type { CompactionBeginData, CompactionSource } from './types'; export type CompactionPhase = 'idle' | 'running' | 'cancelled' | 'completed'; @@ -64,7 +64,7 @@ declare module '#/app/event/eventBus' { 'compaction.started': Omit; 'compaction.blocked': Omit; 'compaction.cancelled': Omit; - 'compaction.completed': Omit; + 'compaction.completed': Omit & { readonly trigger: CompactionSource }; } } diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts index 2c6599c50..217253758 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts @@ -32,7 +32,6 @@ export interface IAgentFullCompactionService { readonly hooks: Hooks<{ onWillCompact: FullCompactionWillCompactContext; - onDidCompact: FullCompactionDidCompactContext; }>; } diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index a30dd3bd1..81aeb70e0 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -102,7 +102,6 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull declare readonly _serviceBrand: undefined; readonly hooks: IAgentFullCompactionService['hooks'] = { onWillCompact: new OrderedHookSlot(), - onDidCompact: new OrderedHookSlot(), }; private readonly strategy: CompactionStrategy; @@ -138,10 +137,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull new RuntimeCompactionStrategy(() => this.profile.resolveModelContext()); this._register(this.wire.onRestored(() => this.normalizeAfterReplay())); this._register( - turnService.hooks.onLaunched.register('full-compaction-reset', async (_ctx, next) => { - this.resetForTurn(); - await next(); - }), + this.eventBus.subscribe('turn.started', () => this.resetForTurn()), ); this._register( loopService.hooks.beforeStep.register('full-compaction', async (ctx, next) => { @@ -342,11 +338,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull if (this.compacting !== active) return; this.lastCompactedTokenCount = finalResult.tokensAfter; this.markCompleted(completeData(finalResult)); - this.eventBus.publish({ type: 'compaction.completed', result: finalResult }); - void this.hooks.onDidCompact.run({ - trigger: data.source, - estimatedTokenCount: finalResult.tokensAfter, - }).catch(() => undefined); + this.eventBus.publish({ type: 'compaction.completed', result: finalResult, trigger: data.source }); } catch (error) { if (isAbortError(error)) return; const blockedByTurn = this.compacting === active && active.blockedByTurn; diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts index 4ab89d68c..cefe50274 100644 --- a/packages/agent-core-v2/src/agent/goal/goalService.ts +++ b/packages/agent-core-v2/src/agent/goal/goalService.ts @@ -42,7 +42,7 @@ import { type TurnBeforeStepContext, } from '#/agent/loop'; import { IAgentSystemReminderService } from '#/agent/systemReminder'; -import { IAgentTurnService, type Turn, type TurnEndedContext } from '#/agent/turn'; +import { IAgentTurnService, type TurnResult } from '#/agent/turn'; import type { TokenUsage } from '#/app/llmProtocol'; import type { TelemetryProperties } from '#/app/telemetry'; import { ITelemetryService } from '#/app/telemetry'; @@ -161,10 +161,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { // fork clear handled by wire forkGoal op; reminder intentionally dropped (reversible). this._register(this.wire.onRestored(() => this.normalizeAfterReplay())); this._register( - turnService.hooks.onLaunched.register('goal-track-launched-turn', (ctx, next) => { - this.handleTurnLaunched(ctx.turn); - return next(); - }), + this.eventBus.subscribe('turn.started', (e) => this.handleTurnLaunched(e.turnId)), ); this._register( loopService.hooks.beforeStep.register('goal-count-turn', async (ctx, next) => { @@ -179,9 +176,10 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { }), ); this._register( - turnService.hooks.onEnded.register('goal-drive-continuation', async (ctx, next) => { - await next(); - await this.handleTurnEnded(ctx); + this.eventBus.subscribe('turn.ended', (e) => { + void this.handleTurnEnded(e.turnId, { reason: e.reason, error: e.error }).catch( + () => undefined, + ); }), ); } @@ -379,8 +377,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { return this.blockIfBudgetReached(next) ?? this.toSnapshot(next); } - private handleTurnLaunched(turn: Turn): void { - const turnId = turn.id; + private handleTurnLaunched(turnId: number): void { if (this.goalState?.status === 'active') this.goalDrivenTurns.add(turnId); this.goalOutcomeContinuationTurns.delete(turnId); } @@ -409,23 +406,25 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { ctx.continue = true; } - private async handleTurnEnded(ctx: TurnEndedContext): Promise { - const turnId = ctx.turn.id; + private async handleTurnEnded( + turnId: number, + result: { reason: TurnResult['reason']; error?: TurnResult['error'] }, + ): Promise { this.goalDrivenTurns.delete(turnId); this.countedGoalTurns.delete(turnId); this.goalOutcomeContinuationTurns.delete(turnId); - if (ctx.result.reason === 'blocked') { + if (result.reason === 'blocked') { await this.markBlocked({ reason: 'Blocked by UserPromptSubmit hook' }); return; } - if (ctx.result.reason === 'cancelled') { + if (result.reason === 'cancelled') { await this.pauseOnInterrupt({ reason: 'Paused after interruption' }); return; } - if (ctx.result.reason === 'failed') { - await this.pauseActiveGoal({ reason: goalFailurePauseReason(ctx.result.error) }); + if (result.reason === 'failed') { + await this.pauseActiveGoal({ reason: goalFailurePauseReason(result.error) }); return; } diff --git a/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts b/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts index ce1827709..92ebda56f 100644 --- a/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts +++ b/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts @@ -28,6 +28,7 @@ import { } from "#/_base/utils/tokens"; import type { TelemetryProperties } from '#/app/telemetry'; import { IConfigService } from '#/app/config'; +import { IEventBus } from '#/app/event'; import { IAgentContextMemoryService } from '#/agent/contextMemory'; import { IAgentContextSizeService } from '#/agent/contextSize'; import { IFlagService } from '#/app/flag'; @@ -72,6 +73,7 @@ export class AgentMicroCompactionService @ITelemetryService private readonly telemetry: ITelemetryService, @IAgentLoopService loop: IAgentLoopService, @IConfigService private readonly config: IConfigService, + @IEventBus private readonly eventBus: IEventBus, ) { super(); this.microConfig = this.readConfig(); @@ -92,10 +94,7 @@ export class AgentMicroCompactionService ), ); this._register( - this.context.hooks.onSpliced.register('micro-compaction', async (ctx, next) => { - this.observeSplice(ctx); - await next(); - }), + this.eventBus.subscribe('context.spliced', (e) => this.observeSplice(e)), ); } diff --git a/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts b/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts index 1ea5af9c6..4b744faf6 100644 --- a/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts +++ b/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts @@ -37,11 +37,6 @@ export interface IAgentPermissionGate { authorize( context: ResolvedToolExecutionHookContext, ): Promise; - - readonly hooks: Hooks<{ - onDidRequestApproval: PermissionApprovalRequestContext; - onDidResolveApproval: PermissionApprovalResultContext; - }>; } export const IAgentPermissionGate = diff --git a/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts b/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts index f6c0ad89c..9dcbfbd61 100644 --- a/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts +++ b/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts @@ -29,15 +29,17 @@ import { } from './permissionGate'; import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { OrderedHookSlot } from '#/hooks'; +import { IEventBus } from '#/app/event'; + +declare module '#/app/event/eventBus' { + interface DomainEventMap { + 'permission.approval.requested': PermissionApprovalRequestContext; + 'permission.approval.resolved': PermissionApprovalResultContext; + } +} export class AgentPermissionGate extends Disposable implements IAgentPermissionGate { declare readonly _serviceBrand: undefined; - readonly hooks: IAgentPermissionGate['hooks'] = { - onDidRequestApproval: new OrderedHookSlot(), - onDidResolveApproval: new OrderedHookSlot(), - }; - constructor( private readonly options: PermissionGateOptions = {}, @IAgentPermissionModeService private readonly modeService: IAgentPermissionModeService, @@ -46,6 +48,7 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG @ISessionContext private readonly session: ISessionContext, @IInstantiationService private readonly instantiation: IInstantiationService, @ITelemetryService private readonly telemetry: ITelemetryService, + @IEventBus private readonly eventBus: IEventBus, @IAgentToolExecutorService toolExecutor: IAgentToolExecutorService, ) { super(); @@ -147,7 +150,7 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG if (approvalService === undefined) { response = { decision: 'approved' }; } else { - void this.hooks.onDidRequestApproval.run(approvalContext).catch(() => undefined); + this.eventBus.publish({ type: 'permission.approval.requested', ...approvalContext }); try { response = await abortable( approvalService.request(approvalRequest), @@ -166,11 +169,12 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG session_cache_written: false, has_feedback: false, }); - void this.hooks.onDidResolveApproval.run({ + this.eventBus.publish({ + type: 'permission.approval.resolved', ...approvalContext, decision: 'error', error: error instanceof Error ? error.message : String(error), - }).catch(() => undefined); + }); const resolved = result.resolveError?.(error); if (resolved !== undefined) { return this.permissionPolicyResolutionToAuthorize(resolved, context, policyName); @@ -184,10 +188,11 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG ? context.execution.approvalRule : undefined; if (approvalService !== undefined) { - void this.hooks.onDidResolveApproval.run({ + this.eventBus.publish({ + type: 'permission.approval.resolved', ...approvalContext, ...response, - }).catch(() => undefined); + }); } this.rulesService.recordApprovalResult({ turnId: context.turnId, diff --git a/packages/agent-core-v2/src/agent/swarm/swarmService.ts b/packages/agent-core-v2/src/agent/swarm/swarmService.ts index 8290af31b..5dafd8688 100644 --- a/packages/agent-core-v2/src/agent/swarm/swarmService.ts +++ b/packages/agent-core-v2/src/agent/swarm/swarmService.ts @@ -17,6 +17,7 @@ import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { IAgentSystemReminderService } from '#/agent/systemReminder'; import { IAgentTurnService } from '#/agent/turn'; +import { IEventBus } from '#/app/event'; import { IAgentWireService, type IWireService } from '#/wire'; import SWARM_MODE_ENTER_REMINDER from './enter-reminder.md?raw'; import SWARM_MODE_EXIT_REMINDER from './exit-reminder.md?raw'; @@ -30,15 +31,14 @@ export class AgentSwarmService extends Disposable implements IAgentSwarmService @IAgentWireService private readonly wire: IWireService, @IAgentSystemReminderService private readonly reminders: IAgentSystemReminderService, @IAgentTurnService turnService: IAgentTurnService, + @IEventBus private readonly eventBus: IEventBus, ) { super(); this._register( - turnService.hooks.onEnded.register('swarm-mode-auto-exit', (_ctx, next) => { - const done = next(); + this.eventBus.subscribe('turn.ended', () => { if (this.shouldAutoExit) { this.exit(); } - return done; }), ); } diff --git a/packages/agent-core-v2/src/agent/task/task.ts b/packages/agent-core-v2/src/agent/task/task.ts index d93078d16..2d537f395 100644 --- a/packages/agent-core-v2/src/agent/task/task.ts +++ b/packages/agent-core-v2/src/agent/task/task.ts @@ -118,10 +118,6 @@ export interface IAgentTaskService { waitForForegroundRelease( taskId: string, ): Promise; - - readonly hooks: Hooks<{ - onDidNotify: AgentTaskNotificationContext; - }>; } export const IAgentTaskService = diff --git a/packages/agent-core-v2/src/agent/task/taskService.ts b/packages/agent-core-v2/src/agent/task/taskService.ts index ac98f9a82..aae8068c3 100644 --- a/packages/agent-core-v2/src/agent/task/taskService.ts +++ b/packages/agent-core-v2/src/agent/task/taskService.ts @@ -19,6 +19,7 @@ import type { ContentPart } from '#/app/llmProtocol'; import { Disposable } from '#/_base/di'; import { escapeXml, escapeXmlAttr } from '#/_base/utils/xml-escape'; +import { IEventBus } from '#/app/event'; import type { TaskOrigin } from '#/agent/contextMemory'; import { ITaskService, type ITaskHandle, TERMINAL_TASK_STATES } from '#/app/task'; import { @@ -55,7 +56,6 @@ import { TaskModel, taskStarted, taskTerminated } from './taskOps'; import { TaskListTool } from '#/agent/task/tools/task-list'; import { TaskOutputTool } from '#/agent/task/tools/task-output'; import { TaskStopTool } from '#/agent/task/tools/task-stop'; -import { OrderedHookSlot } from '#/hooks'; interface ForegroundRelease { readonly promise: Promise; @@ -123,11 +123,14 @@ export function isAgentTaskTerminal(status: AgentTaskStatus): boolean { return TERMINAL_STATUSES.has(status); } +declare module '#/app/event/eventBus' { + interface DomainEventMap { + 'task.notified': AgentTaskNotificationContext; + } +} + export class AgentTaskService extends Disposable implements IAgentTaskService { declare readonly _serviceBrand: undefined; - readonly hooks: IAgentTaskService['hooks'] = { - onDidNotify: new OrderedHookSlot(), - }; private readonly tasks = new Map(); private readonly ghosts = new Map(); @@ -145,6 +148,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService { @ISessionContext session: ISessionContext, @ITaskService private readonly taskService: ITaskService, @IAgentWireService private readonly wire: IWireService, + @IEventBus private readonly eventBus: IEventBus, ) { super(); this.persistence = new AgentTaskPersistence( @@ -155,9 +159,8 @@ export class AgentTaskService extends Disposable implements IAgentTaskService { ); this._register(this.wire.onRestored(() => this.restoreAfterReplay())); this._register( - context.hooks.onSpliced.register('task-notification-delivery', async (ctx, next) => { - await next(); - for (const message of ctx.messages) { + this.eventBus.subscribe('context.spliced', (e) => { + for (const message of e.messages) { if (isTaskOrigin(message.origin)) { this.markDeliveredNotification(message.origin); } @@ -865,14 +868,15 @@ export class AgentTaskService extends Disposable implements IAgentTaskService { } private fireNotificationHook(notification: AgentTaskNotification): void { - void this.hooks.onDidNotify.run({ + this.eventBus.publish({ + type: 'task.notified', notificationType: notification.type, title: notification.title, body: notification.body, severity: notification.severity, sourceKind: notification.source_kind, sourceId: notification.source_id, - }).catch(() => undefined); + }); } private isTerminalNotificationSuppressed(taskId: string): boolean { diff --git a/packages/agent-core-v2/src/agent/toolRegistry/toolRegistry.ts b/packages/agent-core-v2/src/agent/toolRegistry/toolRegistry.ts index 337b23ff6..e0046da29 100644 --- a/packages/agent-core-v2/src/agent/toolRegistry/toolRegistry.ts +++ b/packages/agent-core-v2/src/agent/toolRegistry/toolRegistry.ts @@ -22,11 +22,6 @@ export interface IAgentToolRegistryService { register(tool: ExecutableTool, options?: ToolRegistrationOptions): IDisposable; list(): readonly ToolInfo[]; resolve(name: string): ExecutableTool | undefined; - - readonly hooks: Hooks<{ - onRegistered: { tool: ExecutableTool }; - onUnregistered: { tool: ExecutableTool }; - }>; } export const IAgentToolRegistryService = createDecorator('agentToolRegistryService'); diff --git a/packages/agent-core-v2/src/agent/toolRegistry/toolRegistryService.ts b/packages/agent-core-v2/src/agent/toolRegistry/toolRegistryService.ts index fc731814d..f3c0233f9 100644 --- a/packages/agent-core-v2/src/agent/toolRegistry/toolRegistryService.ts +++ b/packages/agent-core-v2/src/agent/toolRegistry/toolRegistryService.ts @@ -18,11 +18,6 @@ export class AgentToolRegistryService extends Disposable implements IAgentToolRe declare readonly _serviceBrand: undefined; private readonly tools = new Map(); - readonly hooks = { - onRegistered: new OrderedHookSlot<{ tool: ExecutableTool }>(), - onUnregistered: new OrderedHookSlot<{ tool: ExecutableTool }>(), - }; - constructor() { super(); } @@ -33,8 +28,6 @@ export class AgentToolRegistryService extends Disposable implements IAgentToolRe this.unregisterTool(tool.name); this.tools.set(tool.name, entry); - void this.hooks.onRegistered.run({ tool: entry.tool }); - return toDisposable(() => { const current = this.tools.get(tool.name); if (current !== entry) return; @@ -61,7 +54,6 @@ export class AgentToolRegistryService extends Disposable implements IAgentToolRe const entry = this.tools.get(name); if (entry === undefined) return undefined; this.tools.delete(name); - void this.hooks.onUnregistered.run({ tool: entry.tool }); return entry; } } diff --git a/packages/agent-core-v2/src/agent/turn/turn.ts b/packages/agent-core-v2/src/agent/turn/turn.ts index 1521c9e0e..1c39489c0 100644 --- a/packages/agent-core-v2/src/agent/turn/turn.ts +++ b/packages/agent-core-v2/src/agent/turn/turn.ts @@ -15,21 +15,11 @@ export interface Turn { readonly result: Promise; } -export interface TurnEndedContext { - readonly turn: Turn; - readonly result: TurnResult; -} - export interface IAgentTurnService { readonly _serviceBrand: undefined; launch(): Turn; getActiveTurn(): Turn | undefined; - - readonly hooks: Hooks<{ - onLaunched: { turn: Turn }; - onEnded: TurnEndedContext; - }>; } export const IAgentTurnService = createDecorator('agentTurnService'); diff --git a/packages/agent-core-v2/src/agent/turn/turnService.ts b/packages/agent-core-v2/src/agent/turn/turnService.ts index 9c264854e..fde53e464 100644 --- a/packages/agent-core-v2/src/agent/turn/turnService.ts +++ b/packages/agent-core-v2/src/agent/turn/turnService.ts @@ -25,7 +25,7 @@ import { IAgentLoopService } from '#/agent/loop'; import { IEventBus } from '#/app/event'; import { IAgentTelemetryContextService, ITelemetryService } from '#/app/telemetry'; import { IAgentWireService, type IWireService } from '#/wire'; -import type { Turn, TurnEndedContext, TurnResult } from './turn'; +import type { Turn, TurnResult } from './turn'; import { IAgentTurnService } from './turn'; import { promptTurn, TurnModel } from './turnOps'; @@ -42,11 +42,6 @@ export class AgentTurnService implements IAgentTurnService { declare readonly _serviceBrand: undefined; private activeTurn: Turn | undefined; - readonly hooks = { - onLaunched: new OrderedHookSlot<{ turn: Turn }>(), - onEnded: new OrderedHookSlot(), - }; - constructor( @IAgentLoopService private readonly loop: IAgentLoopService, @IAgentWireService private readonly wire: IWireService, @@ -77,7 +72,6 @@ export class AgentTurnService implements IAgentTurnService { void ready.catch(() => undefined); this.activeTurn = turn; turn.result = this.runTurn(turn, ready); - void this.hooks.onLaunched.run({ turn }); return turn; } @@ -128,9 +122,8 @@ export class AgentTurnService implements IAgentTurnService { turnTelemetry.track('turn_interrupted', { at_step: result.steps ?? null }); } } - if (result !== undefined) { - await this.hooks.onEnded.run({ turn, result }); - } + // `turn.ended` is published to `IEventBus` above; subscribers (swarm / + // goal / externalHooks) react there — no hook slot to run here. } } } diff --git a/packages/agent-core-v2/test/shellTools/bash.test.ts b/packages/agent-core-v2/test/shellTools/bash.test.ts index 044bbe376..0fb974710 100644 --- a/packages/agent-core-v2/test/shellTools/bash.test.ts +++ b/packages/agent-core-v2/test/shellTools/bash.test.ts @@ -38,7 +38,6 @@ import { type ISessionContext, makeSessionContext } from '#/session/sessionConte import type { IProcess, ISessionProcessRunner } from '#/session/process'; import { type BashInput, BashInputSchema, BashTool } from '#/os/backends/node-local/tools/bash'; import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool'; -import { createHooks } from '#/hooks'; const posixEnv: IHostEnvironment = { _serviceBrand: undefined, @@ -451,8 +450,6 @@ function createFakeTaskService(options: { maxRunningTasks?: number } = {}): { const service: IAgentTaskService = { _serviceBrand: undefined, - hooks: createHooks(['onDidNotify']) as IAgentTaskService['hooks'], - track(): never { throw new Error('fake IAgentTaskService.track is not implemented'); }, diff --git a/packages/agent-core-v2/test/turn/stubs.ts b/packages/agent-core-v2/test/turn/stubs.ts index b016548bd..3171989e1 100644 --- a/packages/agent-core-v2/test/turn/stubs.ts +++ b/packages/agent-core-v2/test/turn/stubs.ts @@ -39,13 +39,6 @@ function makeTurn(id: number): Turn { }; } -function makeHooks(): IAgentTurnService['hooks'] { - return createHooks([ - 'onLaunched', - 'onEnded', - ]) as IAgentTurnService['hooks']; -} - function makeAgentLoopHookSlots(): IAgentLoopService['hooks'] { return createHooks([ 'beforeStep', @@ -61,7 +54,6 @@ export function stubTurn(options: StubTurnOptions = {}): StubTurn { let nextId = typeof options.currentId === 'number' ? options.currentId : 0; return { _serviceBrand: undefined, - hooks: makeHooks(), launch() { const turn = makeTurn(nextId++); launches.push(turn.id); @@ -84,13 +76,9 @@ export function stubTurn(options: StubTurnOptions = {}): StubTurn { * returns a minimal {@link Turn}; `getActiveTurn` is a no-op. */ export function stubTurnWithHooks(): IAgentTurnService { - const turn = makeTurn(0); - return { - _serviceBrand: undefined, - hooks: makeHooks(), - launch: () => turn, - getActiveTurn: () => undefined, - }; + // Turn-lifecycle hooks moved to `IEventBus`; no service registers turn hooks + // anymore, so this is now equivalent to `stubTurn()`. + return stubTurn(); } /** An `IAgentLoopService` stub backed by real loop lifecycle hook slots. */