mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-31 10:37:12 +00:00
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
This commit is contained in:
parent
f54ebe0499
commit
f851cc4776
20 changed files with 203 additions and 190 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<IAgentContextMemoryService>('agentContextMemoryService');
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CompactionStartedEvent, 'type'>;
|
||||
'compaction.blocked': Omit<CompactionBlockedEvent, 'type'>;
|
||||
'compaction.cancelled': Omit<CompactionCancelledEvent, 'type'>;
|
||||
'compaction.completed': Omit<CompactionCompletedEvent, 'type'>;
|
||||
'compaction.completed': Omit<CompactionCompletedEvent, 'type'> & { readonly trigger: CompactionSource };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ export interface IAgentFullCompactionService {
|
|||
|
||||
readonly hooks: Hooks<{
|
||||
onWillCompact: FullCompactionWillCompactContext;
|
||||
onDidCompact: FullCompactionDidCompactContext;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,6 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
declare readonly _serviceBrand: undefined;
|
||||
readonly hooks: IAgentFullCompactionService['hooks'] = {
|
||||
onWillCompact: new OrderedHookSlot<FullCompactionWillCompactContext>(),
|
||||
onDidCompact: new OrderedHookSlot<FullCompactionDidCompactContext>(),
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
const turnId = ctx.turn.id;
|
||||
private async handleTurnEnded(
|
||||
turnId: number,
|
||||
result: { reason: TurnResult['reason']; error?: TurnResult['error'] },
|
||||
): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,6 @@ export interface IAgentPermissionGate {
|
|||
authorize(
|
||||
context: ResolvedToolExecutionHookContext,
|
||||
): Promise<AuthorizeToolExecutionResult | undefined>;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onDidRequestApproval: PermissionApprovalRequestContext;
|
||||
onDidResolveApproval: PermissionApprovalResultContext;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const IAgentPermissionGate =
|
||||
|
|
|
|||
|
|
@ -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<PermissionApprovalRequestContext>(),
|
||||
onDidResolveApproval: new OrderedHookSlot<PermissionApprovalResultContext>(),
|
||||
};
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,10 +118,6 @@ export interface IAgentTaskService {
|
|||
waitForForegroundRelease(
|
||||
taskId: string,
|
||||
): Promise<ForegroundTaskReleaseReason | undefined>;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onDidNotify: AgentTaskNotificationContext;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const IAgentTaskService =
|
||||
|
|
|
|||
|
|
@ -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<ForegroundTaskReleaseReason>;
|
||||
|
|
@ -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<AgentTaskNotificationContext>(),
|
||||
};
|
||||
|
||||
private readonly tasks = new Map<string, ManagedTask>();
|
||||
private readonly ghosts = new Map<string, AgentTaskInfo>();
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<IAgentToolRegistryService>('agentToolRegistryService');
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ export class AgentToolRegistryService extends Disposable implements IAgentToolRe
|
|||
declare readonly _serviceBrand: undefined;
|
||||
private readonly tools = new Map<string, ToolEntry>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,21 +15,11 @@ export interface Turn {
|
|||
readonly result: Promise<TurnResult>;
|
||||
}
|
||||
|
||||
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<IAgentTurnService>('agentTurnService');
|
||||
|
|
|
|||
|
|
@ -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<TurnEndedContext>(),
|
||||
};
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue