refactor(coding-agent): simplify extension runtime architecture

- Replace per-extension closures with shared ExtensionRuntime
- Split context actions: ExtensionContextActions (required) + ExtensionCommandContextActions (optional)
- Rename LoadedExtension to Extension, remove setter methods
- Change runner.initialize() from options object to positional params
- Derive hasUI from uiContext presence (no separate param)
- Add warning when extensions override built-in tools
- RPC and print modes now provide full command context actions

BREAKING CHANGE: Extension system types and initialization API changed.
See CHANGELOG.md for migration details.
This commit is contained in:
Mario Zechner 2026-01-07 23:50:18 +01:00
parent faa26ffbf9
commit cb3ac0ba9e
16 changed files with 580 additions and 736 deletions

View file

@ -231,35 +231,63 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
// Set up extensions with RPC-based UI context
const extensionRunner = session.extensionRunner;
if (extensionRunner) {
extensionRunner.initialize({
getModel: () => session.agent.state.model,
sendMessageHandler: (message, options) => {
session.sendCustomMessage(message, options).catch((e) => {
output(error(undefined, "extension_send", e.message));
});
extensionRunner.initialize(
// ExtensionActions
{
sendMessage: (message, options) => {
session.sendCustomMessage(message, options).catch((e) => {
output(error(undefined, "extension_send", e.message));
});
},
sendUserMessage: (content, options) => {
session.sendUserMessage(content, options).catch((e) => {
output(error(undefined, "extension_send_user", e.message));
});
},
appendEntry: (customType, data) => {
session.sessionManager.appendCustomEntry(customType, data);
},
getActiveTools: () => session.getActiveToolNames(),
getAllTools: () => session.getAllToolNames(),
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
setModel: async (model) => {
const key = await session.modelRegistry.getApiKey(model);
if (!key) return false;
await session.setModel(model);
return true;
},
getThinkingLevel: () => session.thinkingLevel,
setThinkingLevel: (level) => session.setThinkingLevel(level),
},
sendUserMessageHandler: (content, options) => {
session.sendUserMessage(content, options).catch((e) => {
output(error(undefined, "extension_send_user", e.message));
});
// ExtensionContextActions
{
getModel: () => session.agent.state.model,
isIdle: () => !session.isStreaming,
abort: () => session.abort(),
hasPendingMessages: () => session.pendingMessageCount > 0,
},
appendEntryHandler: (customType, data) => {
session.sessionManager.appendCustomEntry(customType, data);
// ExtensionCommandContextActions - commands invokable via prompt("/command")
{
waitForIdle: () => session.agent.waitForIdle(),
newSession: async (options) => {
const success = await session.newSession({ parentSession: options?.parentSession });
// Note: setup callback runs but no UI feedback in RPC mode
if (success && options?.setup) {
await options.setup(session.sessionManager);
}
return { cancelled: !success };
},
branch: async (entryId) => {
const result = await session.branch(entryId);
return { cancelled: result.cancelled };
},
navigateTree: async (targetId, options) => {
const result = await session.navigateTree(targetId, { summarize: options?.summarize });
return { cancelled: result.cancelled };
},
},
getActiveToolsHandler: () => session.getActiveToolNames(),
getAllToolsHandler: () => session.getAllToolNames(),
setActiveToolsHandler: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
setModelHandler: async (model) => {
const key = await session.modelRegistry.getApiKey(model);
if (!key) return false;
await session.setModel(model);
return true;
},
getThinkingLevelHandler: () => session.thinkingLevel,
setThinkingLevelHandler: (level) => session.setThinkingLevel(level),
uiContext: createExtensionUIContext(),
hasUI: false,
});
createExtensionUIContext(),
);
extensionRunner.onError((err) => {
output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
});