Skip to content
Merged
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
1 change: 0 additions & 1 deletion packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,6 @@ export * from '#/workspace/workspaceContext/workspaceContext';
export * from '#/workspace/sessionLifecycle/sessionLifecycle';
export * from '#/workspace/sessionLifecycle/sessionLifecycleService';
export * from '#/workspace/sessionLifecycle/internal/addressing';
export * from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
export * from '#/session/externalHooks/externalHooks';
export * from '#/session/externalHooks/externalHooksService';
import '#/app/sessionExport/errors';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,22 @@ 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 {
ISessionAgentProfileCatalog,
} from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import {
ISessionLifecycleHooks,
type SessionCloseReason,
type SessionCreateSource,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
import {
type AgentTaskStartHookContext,
type AgentTaskStopHookContext,
ISessionSubagentService,
} from '#/session/subagent/subagent';
import {
ISessionLifecycleService,
type SessionCloseReason,
type SessionCreateSource,
} from '#/workspace/sessionLifecycle/sessionLifecycle';

import { ISessionExternalHooksService } from './externalHooks';

Expand All @@ -64,7 +62,7 @@ export class SessionExternalHooksService

constructor(
@ISessionContext private readonly context: ISessionContext,
@ISessionLifecycleHooks lifecycleHooks: Hooks<SessionLifecycleHookSlots>,
@ISessionLifecycleService lifecycle: ISessionLifecycleService,
@ISessionSubagentService subagents: ISessionSubagentService,
@ISessionMetadata private readonly metadata: ISessionMetadata,
@ISessionAgentProfileCatalog private readonly profiles: ISessionAgentProfileCatalog,
Expand All @@ -90,17 +88,17 @@ export class SessionExternalHooksService
}),
);
this._register(
lifecycleHooks.onDidCreateSession.register('externalHooks', async (event, next) => {
lifecycle.onDidCreateSession((event) => {
if (event.sessionId !== this.context.sessionId) return;
if (event.source !== 'fork') {
await this.triggerSessionStart(event.source);
event.waitUntil(this.triggerSessionStart(event.source));
}
await next();
}),
);
this._register(
lifecycleHooks.onWillCloseSession.register('externalHooks', async (event, next) => {
await this.triggerSessionEnd(event.reason);
await next();
lifecycle.onWillCloseSession((event) => {
if (event.sessionId !== this.context.sessionId) return;
event.waitUntil(this.triggerSessionEnd(event.reason));
}),
);
this._register(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@

import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { ISessionScopeHandle } from '#/_base/di/scope';
import type { Event } from '#/_base/event';
import { type Event, type IWaitUntil } from '#/_base/event';
import type { BindAgentInput } from '#/agent/profile/profile';
import type { McpServerConfig } from '#/mcpCore/config-schema';
import type {
SessionCloseReason,
SessionCreateSource,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';

export type { SessionCloseReason, SessionCreateSource };
export type SessionCreateSource = 'startup' | 'resume' | 'fork';

export type SessionCloseReason = 'exit' | 'archive';

export interface CreateSessionOptions {
readonly sessionId?: string;
Expand Down Expand Up @@ -124,7 +122,8 @@ export interface ISessionLifecycleService {
readonly _serviceBrand: undefined;

readonly onWillCreateSession: Event<SessionWillCreateEvent>;
readonly onDidCreateSession: Event<SessionCreatedEvent>;
readonly onDidCreateSession: Event<SessionCreatedEvent & IWaitUntil>;
readonly onWillCloseSession: Event<SessionWillCloseEvent & IWaitUntil>;
readonly onDidCloseSession: Event<SessionClosedEvent>;
readonly onDidArchiveSession: Event<SessionArchivedEvent>;
readonly onDidForkSession: Event<SessionForkedEvent>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import {
registerScopedService,
} from '#/_base/di/scope';
import { unwrapErrorCause } from '#/_base/errors/errors';
import { Emitter, type Event } from '#/_base/event';
import { AsyncEmitter, Emitter, type Event, type IWaitUntil } from '#/_base/event';
import { DEFAULT_PLAN_MODE_SECTION } from '#/features/plan/configSection';
import { IAgentPlanService } from '#/features/plan/plan';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
Expand All @@ -118,7 +118,6 @@ import {
} from '#/app/sessionIndex/sessionIndex';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { ErrorCodes, Error2, isError2 } from '#/errors';
import { createHooks } from '#/hooks';
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
import { IHostFileSystem, type HostDirEntry } from '#/os/interface/hostFileSystem';
import { IAppendLogStore } from '#/persistence/interface/appendLogStore';
Expand All @@ -130,11 +129,6 @@ import { ISessionContext, sessionContextSeed } from '#/session/sessionContext/se
import { sessionEphemeralMcpServersSeed } from '#/session/mcp/ephemeralMcpServers';
import { sessionAgentProfileCatalogSeed } from '#/session/sessionAgentProfileCatalog/agentProfileCatalogSeed';
import { installSessionSeedAdapters } from '#/session/sessionSeed/sessionSeedAdapters';
import {
ISessionLifecycleHooks,
sessionLifecycleHooksSeed,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { ISessionMetadata, type SessionMeta } from '#/session/sessionMetadata/sessionMetadata';
import { drainSessionMetadataWrites, toEpochMs } from '#/session/sessionMetadata/sessionMetadataService';
import { ISessionProcessRunner } from '#/session/process/processRunner';
Expand Down Expand Up @@ -183,6 +177,8 @@ type MaterializeSessionOptions = Omit<CreateSessionOptions, 'sessionId'> & {
readonly sessionId: string;
};

const NO_ABORT = new AbortController().signal;

// NOTE: stays Disposable — its own 'get' and 'config' collide with the Fiber
export class SessionLifecycleService extends Disposable implements ISessionLifecycleService {
declare readonly _serviceBrand: undefined;
Expand All @@ -192,8 +188,16 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
);
readonly onWillCreateSession: Event<SessionWillCreateEvent> =
this._onWillCreateSession.event;
private readonly _onDidCreateSession = this._register(new Emitter<SessionCreatedEvent>());
readonly onDidCreateSession: Event<SessionCreatedEvent> = this._onDidCreateSession.event;
private readonly _onDidCreateSession = this._register(
new AsyncEmitter<SessionCreatedEvent & IWaitUntil>(),
);
readonly onDidCreateSession: Event<SessionCreatedEvent & IWaitUntil> =
this._onDidCreateSession.event;
private readonly _onWillCloseSession = this._register(
new AsyncEmitter<SessionWillCloseEvent & IWaitUntil>(),
);
readonly onWillCloseSession: Event<SessionWillCloseEvent & IWaitUntil> =
this._onWillCloseSession.event;
private readonly _onDidCloseSession = this._register(new Emitter<SessionClosedEvent>());
readonly onDidCloseSession: Event<SessionClosedEvent> = this._onDidCloseSession.event;
private readonly _onDidArchiveSession = this._register(new Emitter<SessionArchivedEvent>());
Expand Down Expand Up @@ -295,10 +299,6 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
scope: (subKey?: string): string =>
subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`,
};
const hooks = createHooks<SessionLifecycleHookSlots, keyof SessionLifecycleHookSlots>([
'onDidCreateSession',
'onWillCloseSession',
]);
await this.hostEnv.ready;
const handle = createScopedChildHandle(
this.instantiation,
Expand All @@ -307,7 +307,6 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
{
seeds: [
...sessionContextSeed(ctx),
...sessionLifecycleHooksSeed(hooks),
[ITelemetryService, this.telemetry.withContext({ sessionId: opts.sessionId })],
...sessionAgentProfileCatalogSeed({
_serviceBrand: undefined,
Expand Down Expand Up @@ -364,10 +363,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
}

private async announceCreated(event: SessionCreatedEvent): Promise<void> {
await event.handle.accessor
.get(ISessionLifecycleHooks)
.onDidCreateSession.run({ source: event.source });
this._onDidCreateSession.fire(event);
await this._onDidCreateSession.fireAsync(event, NO_ABORT);
event.handle.accessor
.get(ITelemetryService)
.track2('session_started', { resumed: event.source === 'resume' });
Expand Down Expand Up @@ -491,9 +487,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
}

private async announceWillClose(event: SessionWillCloseEvent): Promise<void> {
await event.handle.accessor
.get(ISessionLifecycleHooks)
.onWillCloseSession.run({ reason: event.reason });
await this._onWillCloseSession.fireAsync(event, NO_ABORT);
}

private async drainAgents(handle: ISessionScopeHandle): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
createServices,
type TestInstantiationService,
} from '#/_base/di/test';
import { Emitter, Event } from '#/_base/event';
import { AsyncEmitter, Emitter, Event, type IWaitUntil } from '#/_base/event';
import { emptyUsage } from '#/kosong/contract/usage';
import { buildContextCompactionShape } from '#/agent/contextMemory/compactionHandoff';
import {
Expand Down Expand Up @@ -47,10 +47,13 @@ import { IPluginService } from '#/app/plugin/plugin';
import { IHostProcessService } from '#/os/interface/hostProcess';
import { HostProcessService } from '#/os/backends/node-local/hostProcessService';
import {
ISessionLifecycleHooks,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { createHooks, type Hooks } from '#/hooks';
ISessionLifecycleService,
type SessionCloseReason,
type SessionCreatedEvent,
type SessionCreateSource,
type SessionWillCloseEvent,
} from '#/workspace/sessionLifecycle/sessionLifecycle';
import { createHooks } from '#/hooks';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import {
type AgentTaskHooks,
Expand Down Expand Up @@ -229,11 +232,21 @@ function stubSessionContext(): ISessionContext {
};
}

function stubSessionLifecycleHooks(): Hooks<SessionLifecycleHookSlots> {
return createHooks<SessionLifecycleHookSlots, keyof SessionLifecycleHookSlots>([
'onDidCreateSession',
'onWillCloseSession',
]);
function stubSessionLifecycle() {
const didCreate = new AsyncEmitter<SessionCreatedEvent & IWaitUntil>();
const willClose = new AsyncEmitter<SessionWillCloseEvent & IWaitUntil>();
const noAbort = new AbortController().signal;
const handle = {} as ISessionScopeHandle;
return {
service: {
onDidCreateSession: didCreate.event,
onWillCloseSession: willClose.event,
},
fireDidCreate: (source: SessionCreateSource): Promise<void> =>
didCreate.fireAsync({ sessionId: 'session-1', handle, source }, noAbort),
fireWillClose: (reason: SessionCloseReason): Promise<void> =>
willClose.fireAsync({ sessionId: 'session-1', handle, reason }, noAbort),
};
}

describe('IExternalHooksRunnerService integration', () => {
Expand Down Expand Up @@ -539,7 +552,7 @@ describe('IExternalHooksRunnerService integration', () => {
? 'sessions/workspace-1/session-1'
: `sessions/workspace-1/session-1/${subKey}`,
});
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
Expand Down Expand Up @@ -856,7 +869,7 @@ describe('IExternalHooksRunnerService integration', () => {
const disposables = new DisposableStore();
let ix: TestInstantiationService | undefined;
try {
const lifecycleHooks = stubSessionLifecycleHooks();
const lifecycle = stubSessionLifecycle();
const path = hookLogPath();
const command = appendHookLogCommand(path);
const cwd = mkdtempSync(join(tmpdir(), 'session-external-hooks-cwd-'));
Expand All @@ -877,7 +890,7 @@ describe('IExternalHooksRunnerService integration', () => {
? 'sessions/workspace-1/session-1'
: `sessions/workspace-1/session-1/${subKey}`,
});
reg.defineInstance(ISessionLifecycleHooks, lifecycleHooks);
reg.definePartialInstance(ISessionLifecycleService, lifecycle.service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
Expand Down Expand Up @@ -907,11 +920,11 @@ describe('IExternalHooksRunnerService integration', () => {
ix.set(ISessionExternalHooksService, new SyncDescriptor(SessionExternalHooksService));
ix.get(ISessionExternalHooksService);

await lifecycleHooks.onDidCreateSession.run({ source: 'startup' });
await lifecycleHooks.onDidCreateSession.run({ source: 'resume' });
await lifecycleHooks.onDidCreateSession.run({ source: 'fork' });
await lifecycleHooks.onWillCloseSession.run({ reason: 'exit' });
await lifecycleHooks.onWillCloseSession.run({ reason: 'archive' });
await lifecycle.fireDidCreate('startup');
await lifecycle.fireDidCreate('resume');
await lifecycle.fireDidCreate('fork');
await lifecycle.fireWillClose('exit');
await lifecycle.fireWillClose('archive');

expect(readHookLog(path)).toEqual([
{
Expand Down Expand Up @@ -1053,7 +1066,7 @@ describe('IExternalHooksRunnerService integration', () => {
const disposables = new DisposableStore();
let ix: TestInstantiationService | undefined;
try {
const lifecycleHooks = stubSessionLifecycleHooks();
const lifecycle = stubSessionLifecycle();
const path = hookLogPath();
const command = stdinScript([
'const fs = require("node:fs");',
Expand All @@ -1074,7 +1087,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, lifecycleHooks);
reg.definePartialInstance(ISessionLifecycleService, lifecycle.service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata('My Session'));
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog('coder'));
reg.defineInstance(IModelService, stubModelService('kimi-k2'));
Expand Down Expand Up @@ -1102,7 +1115,7 @@ describe('IExternalHooksRunnerService integration', () => {
ix.get(ISessionExternalHooksService);
await flushMicrotasks();

await lifecycleHooks.onDidCreateSession.run({ source: 'startup' });
await lifecycle.fireDidCreate('startup');

expect(readHookLog(path)).toEqual([
{
Expand Down Expand Up @@ -1261,7 +1274,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
Expand Down Expand Up @@ -1306,7 +1319,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
Expand Down Expand Up @@ -1353,7 +1366,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/test/app/gateway/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe('RestGateway', () => {
_serviceBrand: undefined,
onWillCreateSession: () => ({ dispose: () => {} }),
onDidCreateSession: () => ({ dispose: () => {} }),
onWillCloseSession: () => ({ dispose: () => {} }),
onDidCloseSession: () => ({ dispose: () => {} }),
onDidArchiveSession: () => ({ dispose: () => {} }),
onDidForkSession: () => ({ dispose: () => {} }),
Expand Down
Loading
Loading