fix(coding-agent): add replacement-session callbacks closes #2860
This commit is contained in:
@@ -2,7 +2,7 @@ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import type { AgentSession } from "./agent-session.js";
|
||||
import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.js";
|
||||
import type { SessionShutdownEvent, SessionStartEvent } from "./extensions/index.js";
|
||||
import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.js";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||
import type { CreateAgentSessionResult } from "./sdk.js";
|
||||
import { assertSessionCwdExists } from "./session-cwd.js";
|
||||
@@ -65,6 +65,8 @@ function extractUserMessageText(content: string | Array<{ type: string; text?: s
|
||||
* caller. The caller is responsible for user-facing error handling.
|
||||
*/
|
||||
export class AgentSessionRuntime {
|
||||
private rebindSession?: (session: AgentSession) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
private _session: AgentSession,
|
||||
private _services: AgentSessionServices,
|
||||
@@ -93,6 +95,10 @@ export class AgentSessionRuntime {
|
||||
return this._modelFallbackMessage;
|
||||
}
|
||||
|
||||
setRebindSession(rebindSession?: (session: AgentSession) => Promise<void>): void {
|
||||
this.rebindSession = rebindSession;
|
||||
}
|
||||
|
||||
private async emitBeforeSwitch(
|
||||
reason: "new" | "resume",
|
||||
targetSessionFile?: string,
|
||||
@@ -143,14 +149,26 @@ export class AgentSessionRuntime {
|
||||
this._modelFallbackMessage = result.modelFallbackMessage;
|
||||
}
|
||||
|
||||
async switchSession(sessionPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
|
||||
private async finishSessionReplacement(withSession?: (ctx: ReplacedSessionContext) => Promise<void>): Promise<void> {
|
||||
if (this.rebindSession) {
|
||||
await this.rebindSession(this.session);
|
||||
}
|
||||
if (withSession) {
|
||||
await withSession(this.session.createReplacedSessionContext());
|
||||
}
|
||||
}
|
||||
|
||||
async switchSession(
|
||||
sessionPath: string,
|
||||
options?: { cwdOverride?: string; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);
|
||||
if (beforeResult.cancelled) {
|
||||
return beforeResult;
|
||||
}
|
||||
|
||||
const previousSessionFile = this.session.sessionFile;
|
||||
const sessionManager = SessionManager.open(sessionPath, undefined, cwdOverride);
|
||||
const sessionManager = SessionManager.open(sessionPath, undefined, options?.cwdOverride);
|
||||
assertSessionCwdExists(sessionManager, this.cwd);
|
||||
await this.teardownCurrent("resume", sessionManager.getSessionFile());
|
||||
this.apply(
|
||||
@@ -161,12 +179,14 @@ export class AgentSessionRuntime {
|
||||
sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile },
|
||||
}),
|
||||
);
|
||||
await this.finishSessionReplacement(options?.withSession);
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
async newSession(options?: {
|
||||
parentSession?: string;
|
||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||
}): Promise<{ cancelled: boolean }> {
|
||||
const beforeResult = await this.emitBeforeSwitch("new");
|
||||
if (beforeResult.cancelled) {
|
||||
@@ -193,12 +213,13 @@ export class AgentSessionRuntime {
|
||||
await options.setup(this.session.sessionManager);
|
||||
this.session.agent.state.messages = this.session.sessionManager.buildSessionContext().messages;
|
||||
}
|
||||
await this.finishSessionReplacement(options?.withSession);
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
async fork(
|
||||
entryId: string,
|
||||
options?: { position?: "before" | "at" },
|
||||
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
): Promise<{ cancelled: boolean; selectedText?: string }> {
|
||||
const position = options?.position ?? "before";
|
||||
const beforeResult = await this.emitBeforeFork(entryId, { position });
|
||||
@@ -242,6 +263,7 @@ export class AgentSessionRuntime {
|
||||
sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile },
|
||||
}),
|
||||
);
|
||||
await this.finishSessionReplacement(options?.withSession);
|
||||
return { cancelled: false, selectedText };
|
||||
}
|
||||
|
||||
@@ -260,6 +282,7 @@ export class AgentSessionRuntime {
|
||||
sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile },
|
||||
}),
|
||||
);
|
||||
await this.finishSessionReplacement(options?.withSession);
|
||||
return { cancelled: false, selectedText };
|
||||
}
|
||||
|
||||
@@ -278,6 +301,7 @@ export class AgentSessionRuntime {
|
||||
sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile },
|
||||
}),
|
||||
);
|
||||
await this.finishSessionReplacement(options?.withSession);
|
||||
return { cancelled: false, selectedText };
|
||||
}
|
||||
|
||||
@@ -321,6 +345,7 @@ export class AgentSessionRuntime {
|
||||
sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile },
|
||||
}),
|
||||
);
|
||||
await this.finishSessionReplacement();
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
type MessageEndEvent,
|
||||
type MessageStartEvent,
|
||||
type MessageUpdateEvent,
|
||||
type ReplacedSessionContext,
|
||||
type SessionBeforeCompactResult,
|
||||
type SessionBeforeTreeResult,
|
||||
type SessionStartEvent,
|
||||
@@ -722,6 +723,9 @@ export class AgentSession {
|
||||
* Call this when completely done with the session.
|
||||
*/
|
||||
dispose(): void {
|
||||
this._extensionRunner.invalidate(
|
||||
"This extension instance is stale after session replacement or reload. Use the provided replacement-session context instead.",
|
||||
);
|
||||
this._disconnectFromAgent();
|
||||
this._eventListeners = [];
|
||||
}
|
||||
@@ -3066,6 +3070,16 @@ export class AgentSession {
|
||||
// Extension System
|
||||
// =========================================================================
|
||||
|
||||
createReplacedSessionContext(): ReplacedSessionContext {
|
||||
const context = Object.defineProperties(
|
||||
{},
|
||||
Object.getOwnPropertyDescriptors(this._extensionRunner.createCommandContext()),
|
||||
) as ReplacedSessionContext;
|
||||
context.sendMessage = (message, options) => this.sendCustomMessage(message, options);
|
||||
context.sendUserMessage = (content, options) => this.sendUserMessage(content, options);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if extensions have handlers for a specific event type.
|
||||
*/
|
||||
|
||||
@@ -103,6 +103,7 @@ export type {
|
||||
// Commands
|
||||
RegisteredCommand,
|
||||
RegisteredTool,
|
||||
ReplacedSessionContext,
|
||||
ResolvedCommand,
|
||||
// Events - Resources
|
||||
ResourcesDiscoverEvent,
|
||||
|
||||
@@ -121,6 +121,12 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
||||
const notInitialized = () => {
|
||||
throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
|
||||
};
|
||||
const state: { staleMessage?: string } = {};
|
||||
const assertActive = () => {
|
||||
if (state.staleMessage) {
|
||||
throw new Error(state.staleMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const runtime: ExtensionRuntime = {
|
||||
sendMessage: notInitialized,
|
||||
@@ -140,6 +146,12 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
||||
setThinkingLevel: notInitialized,
|
||||
flagValues: new Map(),
|
||||
pendingProviderRegistrations: [],
|
||||
assertActive,
|
||||
invalidate: (message) => {
|
||||
state.staleMessage ??=
|
||||
message ??
|
||||
"This extension instance is stale after session replacement or reload. Use the provided replacement-session context instead.";
|
||||
},
|
||||
// Pre-bind: queue registrations so bindCore() can flush them once the
|
||||
// model registry is available. bindCore() replaces both with direct calls.
|
||||
registerProvider: (name, config, extensionPath = "<unknown>") => {
|
||||
@@ -167,12 +179,14 @@ function createExtensionAPI(
|
||||
const api = {
|
||||
// Registration methods - write to extension
|
||||
on(event: string, handler: HandlerFn): void {
|
||||
runtime.assertActive();
|
||||
const list = extension.handlers.get(event) ?? [];
|
||||
list.push(handler);
|
||||
extension.handlers.set(event, list);
|
||||
},
|
||||
|
||||
registerTool(tool: ToolDefinition): void {
|
||||
runtime.assertActive();
|
||||
extension.tools.set(tool.name, {
|
||||
definition: tool,
|
||||
sourceInfo: extension.sourceInfo,
|
||||
@@ -181,6 +195,7 @@ function createExtensionAPI(
|
||||
},
|
||||
|
||||
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void {
|
||||
runtime.assertActive();
|
||||
extension.commands.set(name, {
|
||||
name,
|
||||
sourceInfo: extension.sourceInfo,
|
||||
@@ -195,6 +210,7 @@ function createExtensionAPI(
|
||||
handler: (ctx: import("./types.js").ExtensionContext) => Promise<void> | void;
|
||||
},
|
||||
): void {
|
||||
runtime.assertActive();
|
||||
extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
|
||||
},
|
||||
|
||||
@@ -202,6 +218,7 @@ function createExtensionAPI(
|
||||
name: string,
|
||||
options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
|
||||
): void {
|
||||
runtime.assertActive();
|
||||
extension.flags.set(name, { name, extensionPath: extension.path, ...options });
|
||||
if (options.default !== undefined && !runtime.flagValues.has(name)) {
|
||||
runtime.flagValues.set(name, options.default);
|
||||
@@ -209,77 +226,95 @@ function createExtensionAPI(
|
||||
},
|
||||
|
||||
registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
|
||||
runtime.assertActive();
|
||||
extension.messageRenderers.set(customType, renderer as MessageRenderer);
|
||||
},
|
||||
|
||||
// Flag access - checks extension registered it, reads from runtime
|
||||
getFlag(name: string): boolean | string | undefined {
|
||||
runtime.assertActive();
|
||||
if (!extension.flags.has(name)) return undefined;
|
||||
return runtime.flagValues.get(name);
|
||||
},
|
||||
|
||||
// Action methods - delegate to shared runtime
|
||||
sendMessage(message, options): void {
|
||||
runtime.assertActive();
|
||||
runtime.sendMessage(message, options);
|
||||
},
|
||||
|
||||
sendUserMessage(content, options): void {
|
||||
runtime.assertActive();
|
||||
runtime.sendUserMessage(content, options);
|
||||
},
|
||||
|
||||
appendEntry(customType: string, data?: unknown): void {
|
||||
runtime.assertActive();
|
||||
runtime.appendEntry(customType, data);
|
||||
},
|
||||
|
||||
setSessionName(name: string): void {
|
||||
runtime.assertActive();
|
||||
runtime.setSessionName(name);
|
||||
},
|
||||
|
||||
getSessionName(): string | undefined {
|
||||
runtime.assertActive();
|
||||
return runtime.getSessionName();
|
||||
},
|
||||
|
||||
setLabel(entryId: string, label: string | undefined): void {
|
||||
runtime.assertActive();
|
||||
runtime.setLabel(entryId, label);
|
||||
},
|
||||
|
||||
exec(command: string, args: string[], options?: ExecOptions) {
|
||||
runtime.assertActive();
|
||||
return execCommand(command, args, options?.cwd ?? cwd, options);
|
||||
},
|
||||
|
||||
getActiveTools(): string[] {
|
||||
runtime.assertActive();
|
||||
return runtime.getActiveTools();
|
||||
},
|
||||
|
||||
getAllTools() {
|
||||
runtime.assertActive();
|
||||
return runtime.getAllTools();
|
||||
},
|
||||
|
||||
setActiveTools(toolNames: string[]): void {
|
||||
runtime.assertActive();
|
||||
runtime.setActiveTools(toolNames);
|
||||
},
|
||||
|
||||
getCommands() {
|
||||
runtime.assertActive();
|
||||
return runtime.getCommands();
|
||||
},
|
||||
|
||||
setModel(model) {
|
||||
runtime.assertActive();
|
||||
return runtime.setModel(model);
|
||||
},
|
||||
|
||||
getThinkingLevel() {
|
||||
runtime.assertActive();
|
||||
return runtime.getThinkingLevel();
|
||||
},
|
||||
|
||||
setThinkingLevel(level) {
|
||||
runtime.assertActive();
|
||||
runtime.setThinkingLevel(level);
|
||||
},
|
||||
|
||||
registerProvider(name: string, config: ProviderConfig) {
|
||||
runtime.assertActive();
|
||||
runtime.registerProvider(name, config, extension.path);
|
||||
},
|
||||
|
||||
unregisterProvider(name: string) {
|
||||
runtime.assertActive();
|
||||
runtime.unregisterProvider(name, extension.path);
|
||||
},
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
ProviderConfig,
|
||||
RegisteredCommand,
|
||||
RegisteredTool,
|
||||
ReplacedSessionContext,
|
||||
ResolvedCommand,
|
||||
ResourcesDiscoverEvent,
|
||||
ResourcesDiscoverResult,
|
||||
@@ -147,11 +148,12 @@ export type ExtensionErrorListener = (error: ExtensionError) => void;
|
||||
export type NewSessionHandler = (options?: {
|
||||
parentSession?: string;
|
||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||
}) => Promise<{ cancelled: boolean }>;
|
||||
|
||||
export type ForkHandler = (
|
||||
entryId: string,
|
||||
options?: { position?: "before" | "at" },
|
||||
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
) => Promise<{ cancelled: boolean }>;
|
||||
|
||||
export type NavigateTreeHandler = (
|
||||
@@ -159,7 +161,10 @@ export type NavigateTreeHandler = (
|
||||
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
||||
) => Promise<{ cancelled: boolean }>;
|
||||
|
||||
export type SwitchSessionHandler = (sessionPath: string) => Promise<{ cancelled: boolean }>;
|
||||
export type SwitchSessionHandler = (
|
||||
sessionPath: string,
|
||||
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
) => Promise<{ cancelled: boolean }>;
|
||||
|
||||
export type ReloadHandler = () => Promise<void>;
|
||||
|
||||
@@ -235,6 +240,7 @@ export class ExtensionRunner {
|
||||
private shutdownHandler: ShutdownHandler = () => {};
|
||||
private shortcutDiagnostics: ResourceDiagnostic[] = [];
|
||||
private commandDiagnostics: ResourceDiagnostic[] = [];
|
||||
private staleMessage: string | undefined;
|
||||
|
||||
constructor(
|
||||
extensions: Extension[],
|
||||
@@ -451,6 +457,19 @@ export class ExtensionRunner {
|
||||
return this.shortcutDiagnostics;
|
||||
}
|
||||
|
||||
invalidate(message = "This extension instance is stale after session replacement or reload."): void {
|
||||
if (!this.staleMessage) {
|
||||
this.staleMessage = message;
|
||||
this.runtime.invalidate(message);
|
||||
}
|
||||
}
|
||||
|
||||
private assertActive(): void {
|
||||
if (this.staleMessage) {
|
||||
throw new Error(this.staleMessage);
|
||||
}
|
||||
}
|
||||
|
||||
onError(listener: ExtensionErrorListener): () => void {
|
||||
this.errorListeners.add(listener);
|
||||
return () => this.errorListeners.delete(listener);
|
||||
@@ -544,37 +563,101 @@ export class ExtensionRunner {
|
||||
* Context values are resolved at call time, so changes via bindCore/bindUI are reflected.
|
||||
*/
|
||||
createContext(): ExtensionContext {
|
||||
const runner = this;
|
||||
const getModel = this.getModel;
|
||||
return {
|
||||
ui: this.uiContext,
|
||||
hasUI: this.hasUI(),
|
||||
cwd: this.cwd,
|
||||
sessionManager: this.sessionManager,
|
||||
modelRegistry: this.modelRegistry,
|
||||
get ui() {
|
||||
runner.assertActive();
|
||||
return runner.uiContext;
|
||||
},
|
||||
get hasUI() {
|
||||
runner.assertActive();
|
||||
return runner.hasUI();
|
||||
},
|
||||
get cwd() {
|
||||
runner.assertActive();
|
||||
return runner.cwd;
|
||||
},
|
||||
get sessionManager() {
|
||||
runner.assertActive();
|
||||
return runner.sessionManager;
|
||||
},
|
||||
get modelRegistry() {
|
||||
runner.assertActive();
|
||||
return runner.modelRegistry;
|
||||
},
|
||||
get model() {
|
||||
runner.assertActive();
|
||||
return getModel();
|
||||
},
|
||||
isIdle: () => this.isIdleFn(),
|
||||
signal: this.getSignalFn(),
|
||||
abort: () => this.abortFn(),
|
||||
hasPendingMessages: () => this.hasPendingMessagesFn(),
|
||||
shutdown: () => this.shutdownHandler(),
|
||||
getContextUsage: () => this.getContextUsageFn(),
|
||||
compact: (options) => this.compactFn(options),
|
||||
getSystemPrompt: () => this.getSystemPromptFn(),
|
||||
isIdle: () => {
|
||||
runner.assertActive();
|
||||
return runner.isIdleFn();
|
||||
},
|
||||
get signal() {
|
||||
runner.assertActive();
|
||||
return runner.getSignalFn();
|
||||
},
|
||||
abort: () => {
|
||||
runner.assertActive();
|
||||
runner.abortFn();
|
||||
},
|
||||
hasPendingMessages: () => {
|
||||
runner.assertActive();
|
||||
return runner.hasPendingMessagesFn();
|
||||
},
|
||||
shutdown: () => {
|
||||
runner.assertActive();
|
||||
runner.shutdownHandler();
|
||||
},
|
||||
getContextUsage: () => {
|
||||
runner.assertActive();
|
||||
return runner.getContextUsageFn();
|
||||
},
|
||||
compact: (options) => {
|
||||
runner.assertActive();
|
||||
runner.compactFn(options);
|
||||
},
|
||||
getSystemPrompt: () => {
|
||||
runner.assertActive();
|
||||
return runner.getSystemPromptFn();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
createCommandContext(): ExtensionCommandContext {
|
||||
return {
|
||||
...this.createContext(),
|
||||
waitForIdle: () => this.waitForIdleFn(),
|
||||
newSession: (options) => this.newSessionHandler(options),
|
||||
fork: (entryId, options) => this.forkHandler(entryId, options),
|
||||
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
||||
switchSession: (sessionPath) => this.switchSessionHandler(sessionPath),
|
||||
reload: () => this.reloadHandler(),
|
||||
// Use property descriptors instead of object spread so the guarded getters from
|
||||
// createContext() stay lazy. A spread would eagerly read them once and freeze the
|
||||
// old values into the returned object, bypassing stale-instance checks.
|
||||
const context = Object.defineProperties(
|
||||
{},
|
||||
Object.getOwnPropertyDescriptors(this.createContext()),
|
||||
) as ExtensionCommandContext;
|
||||
context.waitForIdle = () => {
|
||||
this.assertActive();
|
||||
return this.waitForIdleFn();
|
||||
};
|
||||
context.newSession = (options) => {
|
||||
this.assertActive();
|
||||
return this.newSessionHandler(options);
|
||||
};
|
||||
context.fork = (entryId, options) => {
|
||||
this.assertActive();
|
||||
return this.forkHandler(entryId, options);
|
||||
};
|
||||
context.navigateTree = (targetId, options) => {
|
||||
this.assertActive();
|
||||
return this.navigateTreeHandler(targetId, options);
|
||||
};
|
||||
context.switchSession = (sessionPath, options) => {
|
||||
this.assertActive();
|
||||
return this.switchSessionHandler(sessionPath, options);
|
||||
};
|
||||
context.reload = () => {
|
||||
this.assertActive();
|
||||
return this.reloadHandler();
|
||||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
private isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
|
||||
|
||||
@@ -326,10 +326,14 @@ export interface ExtensionCommandContext extends ExtensionContext {
|
||||
newSession(options?: {
|
||||
parentSession?: string;
|
||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||
}): Promise<{ cancelled: boolean }>;
|
||||
|
||||
/** Fork from a specific entry, creating a new session file. */
|
||||
fork(entryId: string, options?: { position?: "before" | "at" }): Promise<{ cancelled: boolean }>;
|
||||
fork(
|
||||
entryId: string,
|
||||
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
): Promise<{ cancelled: boolean }>;
|
||||
|
||||
/** Navigate to a different point in the session tree. */
|
||||
navigateTree(
|
||||
@@ -338,12 +342,32 @@ export interface ExtensionCommandContext extends ExtensionContext {
|
||||
): Promise<{ cancelled: boolean }>;
|
||||
|
||||
/** Switch to a different session file. */
|
||||
switchSession(sessionPath: string): Promise<{ cancelled: boolean }>;
|
||||
switchSession(
|
||||
sessionPath: string,
|
||||
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
): Promise<{ cancelled: boolean }>;
|
||||
|
||||
/** Reload extensions, skills, prompts, and themes. */
|
||||
reload(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fresh command-capable context bound to the replacement session after a session switch.
|
||||
*
|
||||
* This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`.
|
||||
*/
|
||||
export interface ReplacedSessionContext extends ExtensionCommandContext {
|
||||
sendMessage<T = unknown>(
|
||||
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
||||
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
||||
): Promise<void>;
|
||||
|
||||
sendUserMessage(
|
||||
content: string | (TextContent | ImageContent)[],
|
||||
options?: { deliverAs?: "steer" | "followUp" },
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tool Types
|
||||
// ============================================================================
|
||||
@@ -1395,6 +1419,10 @@ export interface ExtensionRuntimeState {
|
||||
flagValues: Map<string, boolean | string>;
|
||||
/** Provider registrations queued during extension loading, processed when runner binds */
|
||||
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; extensionPath: string }>;
|
||||
/** Throws when this extension instance is stale after runtime replacement. */
|
||||
assertActive: () => void;
|
||||
/** Marks this extension instance as stale after runtime replacement or reload. */
|
||||
invalidate: (message?: string) => void;
|
||||
/**
|
||||
* Register or unregister a provider.
|
||||
*
|
||||
@@ -1451,13 +1479,20 @@ export interface ExtensionCommandContextActions {
|
||||
newSession: (options?: {
|
||||
parentSession?: string;
|
||||
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||
}) => Promise<{ cancelled: boolean }>;
|
||||
fork: (entryId: string, options?: { position?: "before" | "at" }) => Promise<{ cancelled: boolean }>;
|
||||
fork: (
|
||||
entryId: string,
|
||||
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
) => Promise<{ cancelled: boolean }>;
|
||||
navigateTree: (
|
||||
targetId: string,
|
||||
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
||||
) => Promise<{ cancelled: boolean }>;
|
||||
switchSession: (sessionPath: string) => Promise<{ cancelled: boolean }>;
|
||||
switchSession: (
|
||||
sessionPath: string,
|
||||
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
||||
) => Promise<{ cancelled: boolean }>;
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user