mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 18:01:22 +00:00
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:
parent
faa26ffbf9
commit
cb3ac0ba9e
16 changed files with 580 additions and 736 deletions
|
|
@ -11,8 +11,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|||
import { AgentSession } from "../src/core/agent-session.js";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import {
|
||||
createExtensionRuntime,
|
||||
type Extension,
|
||||
ExtensionRunner,
|
||||
type LoadedExtension,
|
||||
type SessionBeforeCompactEvent,
|
||||
type SessionCompactEvent,
|
||||
type SessionEvent,
|
||||
|
|
@ -21,7 +22,6 @@ import { ModelRegistry } from "../src/core/model-registry.js";
|
|||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
import { codingTools } from "../src/core/tools/index.js";
|
||||
import { theme } from "../src/modes/interactive/theme/theme.js";
|
||||
|
||||
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
function createExtension(
|
||||
onBeforeCompact?: (event: SessionBeforeCompactEvent) => { cancel?: boolean; compaction?: any } | undefined,
|
||||
onCompact?: (event: SessionCompactEvent) => void,
|
||||
): LoadedExtension {
|
||||
): Extension {
|
||||
const handlers = new Map<string, ((event: any, ctx: any) => Promise<any>)[]>();
|
||||
|
||||
handlers.set("session_before_compact", [
|
||||
|
|
@ -80,22 +80,11 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setSendUserMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetActiveToolsHandler: () => {},
|
||||
setGetAllToolsHandler: () => {},
|
||||
setSetActiveToolsHandler: () => {},
|
||||
setSetModelHandler: () => {},
|
||||
setGetThinkingLevelHandler: () => {},
|
||||
setSetThinkingLevelHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(extensions: LoadedExtension[]) {
|
||||
function createSession(extensions: Extension[]) {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||
const agent = new Agent({
|
||||
getApiKey: () => API_KEY,
|
||||
|
|
@ -111,39 +100,29 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
const authStorage = new AuthStorage(join(tempDir, "auth.json"));
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
extensionRunner = new ExtensionRunner(extensions, tempDir, sessionManager, modelRegistry);
|
||||
extensionRunner.initialize({
|
||||
getModel: () => session.model,
|
||||
sendMessageHandler: async () => {},
|
||||
sendUserMessageHandler: async () => {},
|
||||
appendEntryHandler: async () => {},
|
||||
getActiveToolsHandler: () => [],
|
||||
getAllToolsHandler: () => [],
|
||||
setActiveToolsHandler: () => {},
|
||||
setModelHandler: async () => false,
|
||||
getThinkingLevelHandler: () => "off",
|
||||
setThinkingLevelHandler: () => {},
|
||||
uiContext: {
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
setStatus: () => {},
|
||||
setWidget: () => {},
|
||||
setFooter: () => {},
|
||||
setHeader: () => {},
|
||||
setTitle: () => {},
|
||||
custom: async () => undefined as never,
|
||||
setEditorText: () => {},
|
||||
getEditorText: () => "",
|
||||
editor: async () => undefined,
|
||||
setEditorComponent: () => {},
|
||||
get theme() {
|
||||
return theme;
|
||||
},
|
||||
const runtime = createExtensionRuntime();
|
||||
extensionRunner = new ExtensionRunner(extensions, runtime, tempDir, sessionManager, modelRegistry);
|
||||
extensionRunner.initialize(
|
||||
// ExtensionActions
|
||||
{
|
||||
sendMessage: async () => {},
|
||||
sendUserMessage: async () => {},
|
||||
appendEntry: async () => {},
|
||||
getActiveTools: () => [],
|
||||
getAllTools: () => [],
|
||||
setActiveTools: () => {},
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => "off",
|
||||
setThinkingLevel: () => {},
|
||||
},
|
||||
hasUI: false,
|
||||
});
|
||||
// ExtensionContextActions
|
||||
{
|
||||
getModel: () => session.model,
|
||||
isIdle: () => !session.isStreaming,
|
||||
abort: () => session.abort(),
|
||||
hasPendingMessages: () => session.pendingMessageCount > 0,
|
||||
},
|
||||
);
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
|
|
@ -264,7 +243,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
}, 120000);
|
||||
|
||||
it("should continue with default compaction if extension throws error", async () => {
|
||||
const throwingExtension: LoadedExtension = {
|
||||
const throwingExtension: Extension = {
|
||||
path: "throwing-extension",
|
||||
resolvedPath: "/test/throwing-extension.ts",
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
|
|
@ -291,18 +270,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setSendUserMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetActiveToolsHandler: () => {},
|
||||
setGetAllToolsHandler: () => {},
|
||||
setSetActiveToolsHandler: () => {},
|
||||
setSetModelHandler: () => {},
|
||||
setGetThinkingLevelHandler: () => {},
|
||||
setSetThinkingLevelHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
|
||||
createSession([throwingExtension]);
|
||||
|
|
@ -323,7 +291,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
it("should call multiple extensions in order", async () => {
|
||||
const callOrder: string[] = [];
|
||||
|
||||
const extension1: LoadedExtension = {
|
||||
const extension1: Extension = {
|
||||
path: "extension1",
|
||||
resolvedPath: "/test/extension1.ts",
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
|
|
@ -350,21 +318,10 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setSendUserMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetActiveToolsHandler: () => {},
|
||||
setGetAllToolsHandler: () => {},
|
||||
setSetActiveToolsHandler: () => {},
|
||||
setSetModelHandler: () => {},
|
||||
setGetThinkingLevelHandler: () => {},
|
||||
setSetThinkingLevelHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
|
||||
const extension2: LoadedExtension = {
|
||||
const extension2: Extension = {
|
||||
path: "extension2",
|
||||
resolvedPath: "/test/extension2.ts",
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
|
|
@ -391,18 +348,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setSendUserMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetActiveToolsHandler: () => {},
|
||||
setGetAllToolsHandler: () => {},
|
||||
setSetActiveToolsHandler: () => {},
|
||||
setSetModelHandler: () => {},
|
||||
setGetThinkingLevelHandler: () => {},
|
||||
setSetThinkingLevelHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
|
||||
createSession([extension1, extension2]);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ describe("ExtensionRunner", () => {
|
|||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const shortcuts = runner.getShortcuts();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("conflicts with built-in"));
|
||||
|
|
@ -79,7 +79,7 @@ describe("ExtensionRunner", () => {
|
|||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const shortcuts = runner.getShortcuts();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("shortcut conflict"));
|
||||
|
|
@ -108,7 +108,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "tool-b.ts"), toolCode("tool_b"));
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const tools = runner.getAllRegisteredTools();
|
||||
|
||||
expect(tools.length).toBe(2);
|
||||
|
|
@ -130,7 +130,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "cmd-b.ts"), cmdCode("cmd-b"));
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const commands = runner.getRegisteredCommands();
|
||||
|
||||
expect(commands.length).toBe(2);
|
||||
|
|
@ -149,7 +149,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "cmd.ts"), cmdCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
const cmd = runner.getCommand("my-cmd");
|
||||
expect(cmd).toBeDefined();
|
||||
|
|
@ -173,7 +173,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "throws.ts"), extCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
const errors: Array<{ extensionPath: string; event: string; error: string }> = [];
|
||||
runner.onError((err) => {
|
||||
|
|
@ -199,7 +199,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "renderer.ts"), extCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
const renderer = runner.getMessageRenderer("my-type");
|
||||
expect(renderer).toBeDefined();
|
||||
|
|
@ -222,7 +222,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "with-flag.ts"), extCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const flags = runner.getFlags();
|
||||
|
||||
expect(flags.has("--my-flag")).toBe(true);
|
||||
|
|
@ -240,14 +240,13 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "flag.ts"), extCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
// Setting a flag value should not throw
|
||||
runner.setFlagValue("--test-flag", true);
|
||||
|
||||
// The flag values are stored in the extension's flagValues map
|
||||
const ext = result.extensions[0];
|
||||
expect(ext.flagValues.get("--test-flag")).toBe(true);
|
||||
// The flag values are stored in the shared runtime
|
||||
expect(result.runtime.flagValues.get("--test-flag")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -261,7 +260,7 @@ describe("ExtensionRunner", () => {
|
|||
fs.writeFileSync(path.join(extensionsDir, "handler.ts"), extCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
expect(runner.hasHandlers("tool_call")).toBe(true);
|
||||
expect(runner.hasHandlers("agent_end")).toBe(false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue