diff --git a/.changeset/fix-lifecycle-event-plumbing.md b/.changeset/fix-lifecycle-event-plumbing.md new file mode 100644 index 00000000..e67178b1 --- /dev/null +++ b/.changeset/fix-lifecycle-event-plumbing.md @@ -0,0 +1,8 @@ +--- +"@perstack/skill-manager": patch +"@perstack/runtime": patch +--- + +fix: connect skill adapter lifecycle events to runtime event system + +Replace unused `runtimeEventListener` with `onLifecycleEvent` callback in SkillManagerOptions, thread it through all factory contexts and adapters, and bridge adapter-level lifecycle events (connecting/connected/stderr/disconnected) to runtime events (skillStarting/skillConnected/skillStderr/skillDisconnected) in CoordinatorExecutor. This enables real-time skill lifecycle notifications instead of batch post-connect events. diff --git a/packages/runtime/src/orchestration/coordinator-executor.test.ts b/packages/runtime/src/orchestration/coordinator-executor.test.ts index 03958bb3..8fa1844c 100644 --- a/packages/runtime/src/orchestration/coordinator-executor.test.ts +++ b/packages/runtime/src/orchestration/coordinator-executor.test.ts @@ -1,4 +1,5 @@ -import type { Checkpoint, Expert, RunSetting } from "@perstack/core" +import type { Checkpoint, Expert, RunSetting, RuntimeEvent } from "@perstack/core" +import type { SkillAdapterLifecycleEvent } from "@perstack/skill-manager" import { describe, expect, it, vi } from "vitest" import { CoordinatorExecutor } from "./coordinator-executor.js" @@ -47,7 +48,7 @@ vi.mock("../helpers/index.js", () => ({ })), })) -const { mockSkillManagerInstance } = vi.hoisted(() => ({ +const { mockSkillManagerInstance, capturedOnLifecycleEvent } = vi.hoisted(() => ({ mockSkillManagerInstance: { getToolDefinitions: vi.fn().mockReturnValue([]), getAdapterByToolName: vi.fn(), @@ -56,12 +57,37 @@ const { mockSkillManagerInstance } = vi.hoisted(() => ({ isClosed: false, getAdapters: vi.fn().mockReturnValue(new Map()), }, + capturedOnLifecycleEvent: { + current: undefined as ((event: SkillAdapterLifecycleEvent) => void) | undefined, + }, })) vi.mock("@perstack/skill-manager", () => ({ SkillManager: { - fromExpert: vi.fn().mockResolvedValue(mockSkillManagerInstance), - fromLockfile: vi.fn().mockResolvedValue(mockSkillManagerInstance), + fromExpert: vi + .fn() + .mockImplementation( + ( + _expert: unknown, + _experts: unknown, + options: { onLifecycleEvent?: (event: SkillAdapterLifecycleEvent) => void }, + ) => { + capturedOnLifecycleEvent.current = options?.onLifecycleEvent + return Promise.resolve(mockSkillManagerInstance) + }, + ), + fromLockfile: vi + .fn() + .mockImplementation( + ( + _expert: unknown, + _experts: unknown, + options: { onLifecycleEvent?: (event: SkillAdapterLifecycleEvent) => void }, + ) => { + capturedOnLifecycleEvent.current = options?.onLifecycleEvent + return Promise.resolve(mockSkillManagerInstance) + }, + ), }, })) @@ -254,5 +280,167 @@ describe("@perstack/runtime: coordinator-executor", () => { }), ) }) + + it("passes onLifecycleEvent bridge to SkillManager", async () => { + const { SkillManager } = await import("@perstack/skill-manager") + const eventListener = vi.fn() + const executor = new CoordinatorExecutor({ eventListener }) + const setting = createMockSetting() + + await executor.execute(setting) + + expect(SkillManager.fromExpert).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + onLifecycleEvent: expect.any(Function), + }), + ) + }) + + it("does not pass onLifecycleEvent when no eventListener", async () => { + const { SkillManager } = await import("@perstack/skill-manager") + const executor = new CoordinatorExecutor() + const setting = createMockSetting() + + await executor.execute(setting) + + expect(SkillManager.fromExpert).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + onLifecycleEvent: undefined, + }), + ) + }) + }) + + describe("lifecycle event bridge", () => { + it("bridges connecting event to skillStarting RuntimeEvent", async () => { + const events: RuntimeEvent[] = [] + const eventListener = (event: unknown) => { + const e = event as RuntimeEvent + if ("type" in e && !("stepNumber" in e)) events.push(e) + } + const executor = new CoordinatorExecutor({ eventListener }) + await executor.execute(createMockSetting()) + + const bridge = capturedOnLifecycleEvent.current + expect(bridge).toBeDefined() + + bridge!({ + type: "connecting", + adapterName: "test-skill", + data: { command: "npx", args: ["@test/pkg"] }, + }) + + const startingEvent = events.find((e) => e.type === "skillStarting") + expect(startingEvent).toBeDefined() + expect(startingEvent).toMatchObject({ + type: "skillStarting", + skillName: "test-skill", + command: "npx", + args: ["@test/pkg"], + }) + }) + + it("bridges connected event to skillConnected RuntimeEvent", async () => { + const events: RuntimeEvent[] = [] + const eventListener = (event: unknown) => { + const e = event as RuntimeEvent + if ("type" in e && !("stepNumber" in e)) events.push(e) + } + const executor = new CoordinatorExecutor({ eventListener }) + await executor.execute(createMockSetting()) + + const bridge = capturedOnLifecycleEvent.current! + + bridge({ + type: "connected", + adapterName: "test-skill", + data: { connectDurationMs: 100, spawnDurationMs: 50 }, + }) + + const connectedEvent = events.find((e) => e.type === "skillConnected") + expect(connectedEvent).toBeDefined() + expect(connectedEvent).toMatchObject({ + type: "skillConnected", + skillName: "test-skill", + connectDurationMs: 100, + spawnDurationMs: 50, + }) + }) + + it("bridges stderr event to skillStderr RuntimeEvent", async () => { + const events: RuntimeEvent[] = [] + const eventListener = (event: unknown) => { + const e = event as RuntimeEvent + if ("type" in e && !("stepNumber" in e)) events.push(e) + } + const executor = new CoordinatorExecutor({ eventListener }) + await executor.execute(createMockSetting()) + + const bridge = capturedOnLifecycleEvent.current! + + bridge({ + type: "stderr", + adapterName: "test-skill", + data: { message: "warning: something happened" }, + }) + + const stderrEvent = events.find((e) => e.type === "skillStderr") + expect(stderrEvent).toBeDefined() + expect(stderrEvent).toMatchObject({ + type: "skillStderr", + skillName: "test-skill", + message: "warning: something happened", + }) + }) + + it("bridges disconnected event to skillDisconnected RuntimeEvent", async () => { + const events: RuntimeEvent[] = [] + const eventListener = (event: unknown) => { + const e = event as RuntimeEvent + if ("type" in e && !("stepNumber" in e)) events.push(e) + } + const executor = new CoordinatorExecutor({ eventListener }) + await executor.execute(createMockSetting()) + + const bridge = capturedOnLifecycleEvent.current! + + bridge({ + type: "disconnected", + adapterName: "test-skill", + }) + + const disconnectedEvent = events.find((e) => e.type === "skillDisconnected") + expect(disconnectedEvent).toBeDefined() + expect(disconnectedEvent).toMatchObject({ + type: "skillDisconnected", + skillName: "test-skill", + }) + }) + + it("does not bridge error events to RuntimeEvent", async () => { + const events: RuntimeEvent[] = [] + const eventListener = (event: unknown) => { + const e = event as RuntimeEvent + if ("type" in e && !("stepNumber" in e)) events.push(e) + } + const executor = new CoordinatorExecutor({ eventListener }) + await executor.execute(createMockSetting()) + + const bridge = capturedOnLifecycleEvent.current! + + bridge({ + type: "error", + adapterName: "test-skill", + data: { error: "connection failed" }, + }) + + // Only initializeRuntime should be in events (emitted during execute), no error bridge + const errorEvents = events.filter((e) => e.type !== "initializeRuntime") + expect(errorEvents).toHaveLength(0) + }) }) }) diff --git a/packages/runtime/src/orchestration/coordinator-executor.ts b/packages/runtime/src/orchestration/coordinator-executor.ts index 77e65c7b..80c79c7d 100644 --- a/packages/runtime/src/orchestration/coordinator-executor.ts +++ b/packages/runtime/src/orchestration/coordinator-executor.ts @@ -9,7 +9,7 @@ import { type RuntimeEvent, type Step, } from "@perstack/core" -import { SkillManager } from "@perstack/skill-manager" +import { type SkillAdapterLifecycleEvent, SkillManager } from "@perstack/skill-manager" import pkg from "../../package.json" with { type: "json" } import { RunEventEmitter } from "../events/event-emitter.js" import { @@ -64,6 +64,8 @@ export class CoordinatorExecutor { this.emitInitEvent(setting, expertToRun, experts) + const onLifecycleEvent = this.createLifecycleEventBridge(setting) + const lockfileExpert = this.options.lockfile?.experts[setting.expertKey] const skillManager = lockfileExpert ? await SkillManager.fromLockfile(expertToRun, experts, { @@ -71,15 +73,15 @@ export class CoordinatorExecutor { perstackBaseSkillCommand: setting.perstackBaseSkillCommand, isDelegatedRun: !!checkpoint?.delegatedBy, lockfileToolDefinitions: getLockfileExpertToolDefinitions(lockfileExpert), + onLifecycleEvent, }) : await SkillManager.fromExpert(expertToRun, experts, { env: setting.env, perstackBaseSkillCommand: setting.perstackBaseSkillCommand, isDelegatedRun: !!checkpoint?.delegatedBy, + onLifecycleEvent, }) - this.emitSkillConnectedEvents(setting, skillManager) - const initialCheckpoint = checkpoint ? createNextStepCheckpoint(createId(), checkpoint, setting.runId) : createInitialCheckpoint(createId(), { @@ -120,17 +122,56 @@ export class CoordinatorExecutor { } } - private emitSkillConnectedEvents(setting: RunSetting, skillManager: SkillManager): void { - if (!this.options.eventListener) return - - for (const adapter of skillManager.getAdapters().values()) { - if (adapter.type === "delegate" || adapter.type === "interactive") continue - const event = createRuntimeEvent("skillConnected", setting.jobId, setting.runId, { - skillName: adapter.name, - spawnDurationMs: adapter.spawnDurationMs, - totalDurationMs: adapter.connectDurationMs, - }) - this.options.eventListener(event) + private createLifecycleEventBridge( + setting: RunSetting, + ): ((event: SkillAdapterLifecycleEvent) => void) | undefined { + if (!this.options.eventListener) return undefined + const listener = this.options.eventListener + return (event: SkillAdapterLifecycleEvent) => { + switch (event.type) { + case "connecting": { + const data = event.data ?? {} + listener( + createRuntimeEvent("skillStarting", setting.jobId, setting.runId, { + skillName: event.adapterName, + command: (data.command as string) ?? "", + args: (data.args as string[]) ?? [], + }), + ) + break + } + case "connected": { + const data = event.data ?? {} + listener( + createRuntimeEvent("skillConnected", setting.jobId, setting.runId, { + skillName: event.adapterName, + connectDurationMs: data.connectDurationMs as number | undefined, + totalDurationMs: data.connectDurationMs as number | undefined, + spawnDurationMs: data.spawnDurationMs as number | undefined, + }), + ) + break + } + case "stderr": { + const data = event.data ?? {} + listener( + createRuntimeEvent("skillStderr", setting.jobId, setting.runId, { + skillName: event.adapterName, + message: (data.message as string) ?? "", + }), + ) + break + } + case "disconnected": { + listener( + createRuntimeEvent("skillDisconnected", setting.jobId, setting.runId, { + skillName: event.adapterName, + }), + ) + break + } + // "error" events are handled through promise rejection, no RuntimeEvent needed + } } } diff --git a/packages/skill-manager/src/adapters/lockfile-adapter.test.ts b/packages/skill-manager/src/adapters/lockfile-adapter.test.ts index 8254fce4..e77434fc 100644 --- a/packages/skill-manager/src/adapters/lockfile-adapter.test.ts +++ b/packages/skill-manager/src/adapters/lockfile-adapter.test.ts @@ -2,6 +2,7 @@ import type { LockfileToolDefinition, McpSseSkill, McpStdioSkill } from "@persta import { describe, expect, it, vi } from "vitest" import type { SkillAdapter } from "../skill-adapter.js" import type { SkillAdapterFactory } from "../skill-adapter-factory.js" +import type { SkillAdapterLifecycleEvent } from "../types.js" import { LockfileSkillAdapter } from "./lockfile-adapter.js" function createMockSkill(overrides: Partial = {}): McpStdioSkill { @@ -279,4 +280,39 @@ describe("@perstack/skill-manager: LockfileSkillAdapter", () => { expect(adapter.skill).toBe(skill) }) }) + + describe("lifecycle events", () => { + it("does not emit spurious events from lockfile adapter itself", async () => { + const events: SkillAdapterLifecycleEvent[] = [] + const adapter = new LockfileSkillAdapter({ + skill: createMockSkill(), + toolDefinitions: createMockToolDefinitions(), + env: {}, + factory: createMockFactory(), + onLifecycleEvent: (event) => events.push(event), + }) + await adapter.connect() + // Lockfile adapter's own connect/disconnect should not emit events + expect(events).toHaveLength(0) + }) + + it("passes onLifecycleEvent to real adapter factory context", async () => { + const onLifecycleEvent = vi.fn() + const mockAdapter = createMockAdapter() + const factory = createMockFactory(mockAdapter) + const adapter = new LockfileSkillAdapter({ + skill: createMockSkill(), + toolDefinitions: createMockToolDefinitions(), + env: {}, + factory, + onLifecycleEvent, + }) + await adapter.connect() + await adapter.callTool("tool-a", {}) + expect(factory.createMcp).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ onLifecycleEvent }), + ) + }) + }) }) diff --git a/packages/skill-manager/src/adapters/lockfile-adapter.ts b/packages/skill-manager/src/adapters/lockfile-adapter.ts index 2673b610..c3d25de8 100644 --- a/packages/skill-manager/src/adapters/lockfile-adapter.ts +++ b/packages/skill-manager/src/adapters/lockfile-adapter.ts @@ -33,14 +33,16 @@ export class LockfileSkillAdapter extends SkillAdapter { private _env: Record private _perstackBaseSkillCommand?: string[] private _factory: SkillAdapterFactory + private _realAdapterOnLifecycleEvent?: (event: SkillAdapterLifecycleEvent) => void constructor(options: LockfileSkillAdapterOptions) { - super(options.onLifecycleEvent) + super() this.name = options.skill.name this.skill = options.skill this._env = options.env this._perstackBaseSkillCommand = options.perstackBaseSkillCommand this._factory = options.factory + this._realAdapterOnLifecycleEvent = options.onLifecycleEvent this._cachedToolDefinitions = options.toolDefinitions.map((def) => ({ skillName: def.skillName, name: def.name, @@ -92,7 +94,7 @@ export class LockfileSkillAdapter extends SkillAdapter { const factoryCtx: SkillAdapterFactoryContext = { env: this._env, - onLifecycleEvent: this._onLifecycleEvent, + onLifecycleEvent: this._realAdapterOnLifecycleEvent, } let adapter: SkillAdapter diff --git a/packages/skill-manager/src/adapters/mcp-adapter.test.ts b/packages/skill-manager/src/adapters/mcp-adapter.test.ts index 0b68c814..ff1b6d65 100644 --- a/packages/skill-manager/src/adapters/mcp-adapter.test.ts +++ b/packages/skill-manager/src/adapters/mcp-adapter.test.ts @@ -215,6 +215,46 @@ describe("@perstack/skill-manager: McpSkillAdapter", () => { await adapter.disconnect() expect(events.map((e) => e.type)).toEqual(["disconnected"]) }) + + it("connecting event includes command and args for stdio skill", async () => { + const events: Array<{ type: string; data?: Record }> = [] + const skill = createStdioSkill({ packageName: undefined, args: ["@example/pkg"] }) + const adapter = new McpSkillAdapter(skill, {}, (event) => events.push(event), { + transportFactory: createMockTransportFactory(), + }) + await adapter.connect() + const connectingEvent = events.find((e) => e.type === "connecting") + expect(connectingEvent?.data).toMatchObject({ + command: "npx", + args: ["-y", "@example/pkg"], + }) + }) + + it("connecting event includes endpoint for SSE skill", async () => { + const events: Array<{ type: string; data?: Record }> = [] + const skill = createSseSkill({ endpoint: "https://api.example.com/sse" }) + const adapter = new McpSkillAdapter(skill, {}, (event) => events.push(event), { + transportFactory: createMockTransportFactory(), + }) + await adapter.connect() + const connectingEvent = events.find((e) => e.type === "connecting") + expect(connectingEvent?.data).toMatchObject({ + endpoint: "https://api.example.com/sse", + }) + }) + + it("connected event includes timing data", async () => { + const events: Array<{ type: string; data?: Record }> = [] + const skill = createStdioSkill({ packageName: undefined, args: ["@example/pkg"] }) + const adapter = new McpSkillAdapter(skill, {}, (event) => events.push(event), { + transportFactory: createMockTransportFactory(), + }) + await adapter.connect() + const connectedEvent = events.find((e) => e.type === "connected") + expect(connectedEvent?.data).toHaveProperty("connectDurationMs") + expect(connectedEvent?.data).toHaveProperty("spawnDurationMs") + expect(typeof connectedEvent?.data?.connectDurationMs).toBe("number") + }) }) describe("properties", () => { diff --git a/packages/skill-manager/src/adapters/mcp-adapter.ts b/packages/skill-manager/src/adapters/mcp-adapter.ts index 66d27cf7..0fb87b91 100644 --- a/packages/skill-manager/src/adapters/mcp-adapter.ts +++ b/packages/skill-manager/src/adapters/mcp-adapter.ts @@ -106,6 +106,14 @@ export class McpSkillAdapter extends SkillAdapter { await this._mcpClient!.connect(transport) } + protected override _connectingEventData(): Record | undefined { + if (this.skill.type === "mcpStdioSkill") { + const { command, args } = getCommandArgs(this.skill) + return { command, args } + } + return { endpoint: this.skill.endpoint } + } + protected override async _doDisconnect(): Promise { if (this._mcpClient) { await this._mcpClient.close() diff --git a/packages/skill-manager/src/index.ts b/packages/skill-manager/src/index.ts index 2e13db36..689c414d 100644 --- a/packages/skill-manager/src/index.ts +++ b/packages/skill-manager/src/index.ts @@ -1,3 +1,8 @@ export type { SkillAdapter } from "./skill-adapter.js" export { SkillManager } from "./skill-manager.js" -export type { CollectedToolDefinition, LockfileInitOptions, SkillManagerOptions } from "./types.js" +export type { + CollectedToolDefinition, + LockfileInitOptions, + SkillAdapterLifecycleEvent, + SkillManagerOptions, +} from "./types.js" diff --git a/packages/skill-manager/src/skill-adapter.ts b/packages/skill-manager/src/skill-adapter.ts index 0caf4bf0..4e2553c9 100644 --- a/packages/skill-manager/src/skill-adapter.ts +++ b/packages/skill-manager/src/skill-adapter.ts @@ -56,14 +56,17 @@ export abstract class SkillAdapter { throw new Error(`Adapter ${this.name} is closed`) } this._state = "initializing" - this._emitLifecycleEvent("connecting") + this._emitLifecycleEvent("connecting", this._connectingEventData()) const startTime = Date.now() this._connectPromise = this._doConnect() try { await this._connectPromise this._connectDurationMs = Date.now() - startTime this._state = "ready" - this._emitLifecycleEvent("connected") + this._emitLifecycleEvent("connected", { + connectDurationMs: this._connectDurationMs, + spawnDurationMs: this._spawnDurationMs, + }) } catch (error) { this._state = "error" this._emitLifecycleEvent("error", { @@ -103,6 +106,10 @@ export abstract class SkillAdapter { return tools } + protected _connectingEventData(): Record | undefined { + return undefined + } + protected _emitLifecycleEvent( type: SkillAdapterLifecycleEvent["type"], data?: Record, diff --git a/packages/skill-manager/src/skill-manager.test.ts b/packages/skill-manager/src/skill-manager.test.ts index c7ead79e..c5a55063 100644 --- a/packages/skill-manager/src/skill-manager.test.ts +++ b/packages/skill-manager/src/skill-manager.test.ts @@ -313,6 +313,22 @@ describe("@perstack/skill-manager: SkillManager", () => { expect(factory.createMcp).toHaveBeenCalledOnce() await manager.close() }) + + it("passes onLifecycleEvent to factory context", async () => { + const factory = createMockFactory() + const onLifecycleEvent = vi.fn() + const expert = createExpert() + const manager = await SkillManager.fromExpert( + expert, + {}, + { env: {}, factory, onLifecycleEvent }, + ) + expect(factory.createInMemoryBase).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ onLifecycleEvent }), + ) + await manager.close() + }) }) describe("fromLockfile", () => { @@ -508,6 +524,32 @@ describe("@perstack/skill-manager: SkillManager", () => { await manager.close() }) + it("addSkill passes onLifecycleEvent to factory context", async () => { + const factory = createMockFactory() + const onLifecycleEvent = vi.fn() + const manager = await SkillManager.fromExpert( + createExpert(), + {}, + { env: {}, factory, onLifecycleEvent }, + ) + const skill: McpStdioSkill = { + type: "mcpStdioSkill", + name: "new-skill", + command: "npx", + args: ["@new/pkg"], + requiredEnv: [], + pick: [], + omit: [], + lazyInit: false, + } as McpStdioSkill + await manager.addSkill(skill) + expect(factory.createMcp).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ onLifecycleEvent }), + ) + await manager.close() + }) + it("addSkill throws for duplicate name", async () => { const manager = await SkillManager.fromExpert( createExpert(), diff --git a/packages/skill-manager/src/skill-manager.ts b/packages/skill-manager/src/skill-manager.ts index eb968499..fdd5fc99 100644 --- a/packages/skill-manager/src/skill-manager.ts +++ b/packages/skill-manager/src/skill-manager.ts @@ -13,6 +13,7 @@ import { defaultSkillAdapterFactory } from "./skill-adapter-factory.js" import type { CollectedToolDefinition, LockfileInitOptions, + SkillAdapterLifecycleEvent, SkillManagerOptions, ToolCallResult, } from "./types.js" @@ -125,15 +126,18 @@ export class SkillManager { private _closed = false private _factory: SkillAdapterFactory private _env: Record + private _onLifecycleEvent?: (event: SkillAdapterLifecycleEvent) => void private constructor( adapters: Map, factory: SkillAdapterFactory, env: Record, + onLifecycleEvent?: (event: SkillAdapterLifecycleEvent) => void, ) { this._adapters = adapters this._factory = factory this._env = env + this._onLifecycleEvent = onLifecycleEvent } // ---- Static factories ---- @@ -148,6 +152,7 @@ export class SkillManager { perstackBaseSkillCommand, isDelegatedRun, factory = defaultSkillAdapterFactory, + onLifecycleEvent, } = options const { skills } = expert @@ -157,6 +162,7 @@ export class SkillManager { const factoryCtx: SkillAdapterFactoryContext = { env, + onLifecycleEvent, } const allAdapters: SkillAdapter[] = [] @@ -227,7 +233,7 @@ export class SkillManager { for (const adapter of allAdapters) { adapterMap.set(adapter.name, adapter) } - const sm = new SkillManager(adapterMap, factory, env) + const sm = new SkillManager(adapterMap, factory, env, onLifecycleEvent) // Bind skill management callbacks to the in-memory base adapter for (const adapter of allAdapters) { @@ -268,6 +274,7 @@ export class SkillManager { isDelegatedRun, lockfileToolDefinitions, factory = defaultSkillAdapterFactory, + onLifecycleEvent, } = options const { skills } = expert @@ -277,6 +284,7 @@ export class SkillManager { const factoryCtx: SkillAdapterFactoryContext = { env, + onLifecycleEvent, } const allAdapters: SkillAdapter[] = [] @@ -296,6 +304,7 @@ export class SkillManager { env, perstackBaseSkillCommand, factory, + onLifecycleEvent, }) await adapter.connect() allAdapters.push(adapter) @@ -332,7 +341,7 @@ export class SkillManager { for (const adapter of allAdapters) { adapterMap.set(adapter.name, adapter) } - return new SkillManager(adapterMap, factory, env) + return new SkillManager(adapterMap, factory, env, onLifecycleEvent) } static async collectToolDefinitions( @@ -415,7 +424,10 @@ export class SkillManager { if (this._adapters.has(skill.name)) { throw new Error(`Adapter "${skill.name}" already exists`) } - const adapter = this._factory.createMcp(skill, { env: this._env }) + const adapter = this._factory.createMcp(skill, { + env: this._env, + onLifecycleEvent: this._onLifecycleEvent, + }) await adapter.connect() this._adapters.set(adapter.name, adapter) } @@ -435,7 +447,10 @@ export class SkillManager { if (this._adapters.has(expert.name)) { throw new Error(`Adapter "${expert.name}" already exists`) } - const adapter = this._factory.createDelegate(expert, { env: this._env }) + const adapter = this._factory.createDelegate(expert, { + env: this._env, + onLifecycleEvent: this._onLifecycleEvent, + }) await adapter.connect() this._adapters.set(adapter.name, adapter) } diff --git a/packages/skill-manager/src/types.ts b/packages/skill-manager/src/types.ts index 4d20e577..5feba022 100644 --- a/packages/skill-manager/src/types.ts +++ b/packages/skill-manager/src/types.ts @@ -2,8 +2,6 @@ import type { FileInlinePart, ImageInlinePart, LockfileToolDefinition, - RunEvent, - RuntimeEvent, TextPart, } from "@perstack/core" import type { SkillAdapterFactory } from "./skill-adapter-factory.js" @@ -27,7 +25,7 @@ export interface SkillManagerOptions { perstackBaseSkillCommand?: string[] isDelegatedRun?: boolean factory?: SkillAdapterFactory - runtimeEventListener?: (event: RunEvent | RuntimeEvent) => void + onLifecycleEvent?: (event: SkillAdapterLifecycleEvent) => void } /** Options for SkillManager.fromLockfile() */