kimi-code / packages /agent-core-v2 /src /session /sessionActivity /sessionActivityService.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw
History Blame Contribute Delete
10.2 kB
import { Disposable, DisposableStore, toDisposable, type IDisposable } from '#/_base/di/lifecycle';
import { LifecycleScope } from '#/app/scopes';
import {
ScopeActivation,
registerScopedService,
type IAgentScopeHandle,
} from '#/_base/di/scope';
import { Emitter, type Event } from '#/_base/event';
import { defineState } from '#/state/state';
import { IEventBus } from '#/app/event/eventBus';
import { IEventDispatcher } from '#/state/eventDispatcher';
import { IAgentLoopService } from '#/agent/loop/loop';
import { TurnStarted, type TurnEndReason } from '#/agent/loop/turnEvents';
import { TurnEnded, turnKey } from '#/agent/loop/turnOps';
import { IAgentTaskService } from '#/agent/task/task';
import { TaskStarted, TaskTerminatedNotice } from '#/agent/task/taskOps';
import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction';
import {
CompactionCancelled,
CompactionCompleted,
CompactionStarted,
} from '#/agent/fullCompaction/compactionOps';
import { IAgentStateService } from '#/agent/state/agentState';
import { IAgentLifecycleService, MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
import {
INTERACTION_TAG_SESSION_ID,
type Interaction,
} from '#/human/interaction/interaction';
import { interactions } from '#/human/interaction/facade';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { ISessionStateService } from '#/session/state/sessionState';
import {
ISessionActivityView,
type SessionActivityCause,
type SessionActivityChangedEvent,
type SessionActivityState,
type SessionPendingInteraction,
type SessionTurnOutcome,
} from './sessionActivity';
interface AgentWorkFold {
turnActive: boolean;
background: ReadonlySet<string>;
compacting: boolean;
lastTurnReason?: SessionTurnOutcome;
}
export const sessionActivityFoldsKey = defineState<Map<string, AgentWorkFold>>(
'sessionActivity.folds',
() => new Map(),
);
export const sessionActivityCurrentKey = defineState<SessionActivityState>('sessionActivity.current', () => ({
busy: false,
mainTurnActive: false,
pendingInteraction: 'none',
lastTurnReason: undefined,
}));
export class SessionActivityView extends Disposable implements ISessionActivityView {
declare readonly _serviceBrand: undefined;
private readonly _onDidChange = this._register(new Emitter<SessionActivityChangedEvent>());
readonly onDidChange: Event<SessionActivityChangedEvent> = this._onDidChange.event;
private readonly agentSubscriptions = new Map<string, IDisposable>();
constructor(
@ISessionStateService private readonly states: ISessionStateService,
@IAgentLifecycleService private readonly agents: IAgentLifecycleService,
@ISessionContext private readonly ctx: ISessionContext,
) {
super();
this.states.contributeState(sessionActivityFoldsKey);
this.states.contributeState(sessionActivityCurrentKey);
for (const agent of this.agents.list()) {
const handle = this.agents.handleOf(agent.agentId);
if (handle !== undefined) this.attachAgent(handle);
}
this.current = this.aggregate();
this._register(
this.agents.onDidCreateScope(({ handle }) => {
this.attachAgent(handle);
this.recompute('agent_lifecycle');
}),
);
this._register(
this.agents.onDidClose((agent) => {
this.agentSubscriptions.get(agent.agentId)?.dispose();
this.agentSubscriptions.delete(agent.agentId);
if (this.folds.delete(agent.agentId)) this.recompute('agent_lifecycle');
}),
);
this._register(
toDisposable(
interactions.onDidChangePending(() => this.recompute('interaction')),
),
);
this._register(
toDisposable(() => {
for (const subscription of this.agentSubscriptions.values()) subscription.dispose();
this.agentSubscriptions.clear();
}),
);
}
private get folds(): Map<string, AgentWorkFold> {
return this.states.get(sessionActivityFoldsKey);
}
private get current(): SessionActivityState {
return this.states.get(sessionActivityCurrentKey);
}
private set current(value: SessionActivityState) {
this.states.set(sessionActivityCurrentKey, value);
}
state(): SessionActivityState {
return this.current;
}
private attachAgent(handle: IAgentScopeHandle): void {
if (this.folds.has(handle.id)) return;
this.folds.set(handle.id, seedFold(handle));
const bus = handle.accessor.get(IEventBus) as IEventBus | undefined;
if (bus === undefined) return;
const subscriptions = new DisposableStore();
subscriptions.add(
bus.subscribe(TurnStarted, () =>
this.patchFold(handle.id, (fold) => ({
...fold,
turnActive: true,
lastTurnReason: handle.id === MAIN_AGENT_ID ? undefined : fold.lastTurnReason,
})),
),
);
subscriptions.add(
bus.subscribe(TurnEnded, (event) =>
this.patchFold(handle.id, (fold) => ({
...fold,
turnActive: false,
lastTurnReason: handle.id === MAIN_AGENT_ID ? mapTurnReason(event.reason) : fold.lastTurnReason,
})),
),
);
subscriptions.add(
bus.subscribe(TaskStarted, (event) =>
this.patchFold(handle.id, (fold) => ({
...fold,
background: new Set(fold.background).add(event.info.taskId),
})),
),
);
subscriptions.add(
bus.subscribe(TaskTerminatedNotice, (event) =>
this.patchFold(handle.id, (fold) => {
if (!fold.background.has(event.info.taskId)) return fold;
const background = new Set(fold.background);
background.delete(event.info.taskId);
return { ...fold, background };
}),
),
);
subscriptions.add(
bus.subscribe(CompactionStarted, () =>
this.patchFold(handle.id, (fold) => ({ ...fold, compacting: true })),
),
);
subscriptions.add(
bus.subscribe(CompactionCompleted, () =>
this.patchFold(handle.id, (fold) => ({ ...fold, compacting: false })),
),
);
subscriptions.add(
bus.subscribe(CompactionCancelled, () =>
this.patchFold(handle.id, (fold) => ({ ...fold, compacting: false })),
),
);
const dispatcher = handle.accessor.get(IEventDispatcher) as IEventDispatcher | undefined;
if (dispatcher !== undefined) {
subscriptions.add(
dispatcher.hooks.onDidRestore.register('sessionActivity', async (_ctx, next) => {
this.folds.set(handle.id, seedFold(handle));
this.recompute('agent_lifecycle');
await next();
}),
);
}
this.agentSubscriptions.set(handle.id, subscriptions);
}
private patchFold(agentId: string, patch: (fold: AgentWorkFold) => AgentWorkFold): void {
const previous = this.folds.get(agentId);
if (previous === undefined) return;
const next = patch(previous);
this.folds.set(agentId, next);
let cause: SessionActivityCause | undefined;
if (!previous.turnActive && next.turnActive) cause = 'turn_started';
else if (previous.turnActive && !next.turnActive) cause = 'turn_ended';
else if (previous.background.size !== next.background.size || previous.compacting !== next.compacting) {
cause = 'background';
}
else if (agentId === MAIN_AGENT_ID && previous.lastTurnReason !== next.lastTurnReason) {
cause = 'turn_ended';
}
if (cause !== undefined) this.recompute(cause);
}
private recompute(cause: SessionActivityCause): void {
const next = this.aggregate();
if (activityEquals(this.current, next)) return;
this.current = next;
this._onDidChange.fire({ state: next, cause });
}
private aggregate(): SessionActivityState {
let busy = false;
for (const fold of this.folds.values()) {
if (fold.turnActive || fold.background.size > 0 || fold.compacting) {
busy = true;
break;
}
}
return {
busy,
mainTurnActive: this.folds.get(MAIN_AGENT_ID)?.turnActive ?? false,
pendingInteraction: resolvePendingInteraction(
interactions.findAll({
resolved: false,
tags: { [INTERACTION_TAG_SESSION_ID]: this.ctx.sessionId },
}),
),
lastTurnReason: this.folds.get(MAIN_AGENT_ID)?.lastTurnReason,
};
}
}
function seedFold(handle: IAgentScopeHandle): AgentWorkFold {
const loop = handle.accessor.get(IAgentLoopService) as IAgentLoopService | undefined;
const tasks = handle.accessor.get(IAgentTaskService) as IAgentTaskService | undefined;
const compaction = handle.accessor.get(IAgentFullCompactionService) as
| IAgentFullCompactionService
| undefined;
const states = handle.accessor.get(IAgentStateService) as IAgentStateService | undefined;
const lastEnded =
handle.id === MAIN_AGENT_ID && states?.has(turnKey) === true
? states.get(turnKey).lastEnded
: undefined;
return {
turnActive: loop?.snapshot().state === 'running',
background: new Set(tasks?.list(true).map((task) => task.taskId) ?? []),
compacting: (compaction?.compacting ?? null) !== null,
lastTurnReason:
loop?.snapshot().state === 'running' ? undefined : mapTurnReason(lastEnded?.reason),
};
}
function mapTurnReason(reason: TurnEndReason | undefined): SessionTurnOutcome | undefined {
if (reason === undefined) return undefined;
return reason === 'completed' ? 'completed' : reason === 'cancelled' ? 'cancelled' : 'failed';
}
function resolvePendingInteraction(pending: readonly Interaction[]): SessionPendingInteraction {
if (pending.some((interaction) => interaction.kind === 'approval')) return 'approval';
if (pending.some((interaction) => interaction.kind === 'question')) return 'question';
return 'none';
}
function activityEquals(a: SessionActivityState, b: SessionActivityState): boolean {
return (
a.busy === b.busy &&
a.mainTurnActive === b.mainTurnActive &&
a.pendingInteraction === b.pendingInteraction &&
a.lastTurnReason === b.lastTurnReason
);
}
registerScopedService(
LifecycleScope.Session,
ISessionActivityView,
SessionActivityView,
ScopeActivation.OnScopeCreated,
'sessionActivity',
);