From 53cdf9de1563cd5041f4867fcf42437a3084159a Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 23 Mar 2026 21:13:55 +0000 Subject: [PATCH] feat(backends): add archetype field to AgentEngineDefinition --- src/backends/catalog.ts | 4 +++ src/backends/index.ts | 2 ++ src/backends/registry.ts | 16 +++++++++ src/backends/secretOrchestrator.ts | 5 +-- src/backends/types.ts | 1 + tests/unit/agents/registry.test.ts | 1 + tests/unit/api/routers/agentConfigs.test.ts | 2 ++ tests/unit/backends/adapter.test.ts | 2 ++ tests/unit/backends/catalog.test.ts | 32 +++++++++++++++++ tests/unit/backends/engine-contract.test.ts | 9 +++++ tests/unit/backends/postProcess.test.ts | 1 + tests/unit/backends/registry.test.ts | 39 +++++++++++++++++++-- 12 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/backends/catalog.ts b/src/backends/catalog.ts index 725f763b..52224d24 100644 --- a/src/backends/catalog.ts +++ b/src/backends/catalog.ts @@ -6,6 +6,7 @@ export const LLMIST_ENGINE_DEFINITION: AgentEngineDefinition = { id: 'llmist', label: 'LLMist', description: 'LLMist SDK with synthetic tool context and CASCADE gadget support.', + archetype: 'sdk', capabilities: [ 'synthetic_tool_context', 'streaming_text_events', @@ -21,6 +22,7 @@ export const CLAUDE_CODE_ENGINE_DEFINITION: AgentEngineDefinition = { id: 'claude-code', label: 'Claude Code', description: 'Anthropic Claude Code SDK with built-in file tools and Bash-driven CASCADE tools.', + archetype: 'native-tool', capabilities: [ 'inline_prompt_context', 'offloaded_context_files', @@ -80,6 +82,7 @@ export const CODEX_ENGINE_DEFINITION: AgentEngineDefinition = { id: 'codex', label: 'Codex', description: 'OpenAI Codex CLI in headless automation mode with CASCADE tool guidance.', + archetype: 'native-tool', capabilities: [ 'inline_prompt_context', 'offloaded_context_files', @@ -145,6 +148,7 @@ export const OPENCODE_ENGINE_DEFINITION: AgentEngineDefinition = { id: 'opencode', label: 'OpenCode', description: 'OpenCode headless agent server with scoped permissions and CASCADE tool guidance.', + archetype: 'native-tool', capabilities: [ 'inline_prompt_context', 'offloaded_context_files', diff --git a/src/backends/index.ts b/src/backends/index.ts index 2a1121a6..a26bc367 100644 --- a/src/backends/index.ts +++ b/src/backends/index.ts @@ -15,6 +15,8 @@ export { getEngine, getEngineCatalog, getRegisteredEngines, + isNativeToolEngine, + isNativeToolEngineDefinition, registerEngine, } from './registry.js'; export { registerBuiltInEngines } from './bootstrap.js'; diff --git a/src/backends/registry.ts b/src/backends/registry.ts index 80fc8eed..727e4454 100644 --- a/src/backends/registry.ts +++ b/src/backends/registry.ts @@ -20,3 +20,19 @@ export function getRegisteredEngines(): string[] { export function getEngineCatalog(): AgentEngineDefinition[] { return Array.from(engines.values()).map((engine) => engine.definition); } + +/** + * Returns true if the given engine definition has the 'native-tool' archetype. + */ +export function isNativeToolEngineDefinition(def: AgentEngineDefinition): boolean { + return def.archetype === 'native-tool'; +} + +/** + * Returns true if the engine with the given ID is registered and has the 'native-tool' archetype. + */ +export function isNativeToolEngine(engineId: string): boolean { + const engine = engines.get(engineId); + if (!engine) return false; + return isNativeToolEngineDefinition(engine.definition); +} diff --git a/src/backends/secretOrchestrator.ts b/src/backends/secretOrchestrator.ts index d14beb6d..c0b004d9 100644 --- a/src/backends/secretOrchestrator.ts +++ b/src/backends/secretOrchestrator.ts @@ -13,6 +13,7 @@ import type { AgentInput, CascadeConfig, ProjectConfig } from '../types/index.js import { getDashboardUrl } from '../utils/runLink.js'; import { createNativeToolRuntimeArtifacts } from './nativeToolRuntime.js'; import { isGitHubAckComment } from './progressLifecycle.js'; +import { isNativeToolEngineDefinition } from './registry.js'; import { augmentProjectSecrets, injectGitHubAckCommentId, @@ -34,7 +35,7 @@ export async function buildExecutionPlan( _log: ReturnType, gitHubToken: string | undefined, isGitHubAck: boolean, - engineId: string, + _engineId: string, engine: AgentEngine, ): Promise< Omit & { @@ -106,7 +107,7 @@ export async function buildExecutionPlan( }); const cliToolsDir = new URL('../../bin', import.meta.url).pathname; - const needsNativeToolRuntime = ['claude-code', 'codex', 'opencode'].includes(engineId); + const needsNativeToolRuntime = isNativeToolEngineDefinition(engine.definition); const nativeToolRuntime = needsNativeToolRuntime ? createNativeToolRuntimeArtifacts() : undefined; // Build per-project secrets with CASCADE env var injections diff --git a/src/backends/types.ts b/src/backends/types.ts index d445be3b..d2f29c81 100644 --- a/src/backends/types.ts +++ b/src/backends/types.ts @@ -146,6 +146,7 @@ export interface AgentEngineDefinition { readonly id: string; readonly label: string; readonly description: string; + readonly archetype: 'sdk' | 'native-tool'; readonly capabilities: string[]; readonly modelSelection: | { type: 'free-text' } diff --git a/tests/unit/agents/registry.test.ts b/tests/unit/agents/registry.test.ts index c8994ada..9f3068d2 100644 --- a/tests/unit/agents/registry.test.ts +++ b/tests/unit/agents/registry.test.ts @@ -62,6 +62,7 @@ function makeMockEngine(id: string, supportsAll = true): AgentEngine { id, label: id, description: `${id} description`, + archetype: 'sdk', capabilities: [], modelSelection: { type: 'free-text' }, logLabel: 'Engine Log', diff --git a/tests/unit/api/routers/agentConfigs.test.ts b/tests/unit/api/routers/agentConfigs.test.ts index 77f0f9ca..983c7cb0 100644 --- a/tests/unit/api/routers/agentConfigs.test.ts +++ b/tests/unit/api/routers/agentConfigs.test.ts @@ -93,6 +93,7 @@ describe('agentConfigsRouter', () => { id: 'llmist', label: 'LLMist', description: 'LLMist', + archetype: 'sdk', capabilities: [], modelSelection: { type: 'free-text' }, logLabel: 'LLMist Log', @@ -101,6 +102,7 @@ describe('agentConfigsRouter', () => { id: 'claude-code', label: 'Claude Code', description: 'Claude Code', + archetype: 'native-tool', capabilities: [], modelSelection: { type: 'select', diff --git a/tests/unit/backends/adapter.test.ts b/tests/unit/backends/adapter.test.ts index 5690a499..99e3e524 100644 --- a/tests/unit/backends/adapter.test.ts +++ b/tests/unit/backends/adapter.test.ts @@ -171,11 +171,13 @@ function makeInput( } function makeMockBackend(id = 'test-engine'): AgentEngine { + const nativeToolIds = ['claude-code', 'codex', 'opencode']; return { definition: { id, label: 'Test Engine', description: 'Test engine', + archetype: nativeToolIds.includes(id) ? 'native-tool' : 'sdk', capabilities: [], modelSelection: { type: 'free-text' }, logLabel: 'Engine Log', diff --git a/tests/unit/backends/catalog.test.ts b/tests/unit/backends/catalog.test.ts index b71e0e6a..b929389b 100644 --- a/tests/unit/backends/catalog.test.ts +++ b/tests/unit/backends/catalog.test.ts @@ -57,6 +57,10 @@ describe('LLMIST_ENGINE_DEFINITION', () => { expect(LLMIST_ENGINE_DEFINITION.label).toBe('LLMist'); }); + it('has sdk archetype', () => { + expect(LLMIST_ENGINE_DEFINITION.archetype).toBe('sdk'); + }); + it('has free-text model selection', () => { expect(LLMIST_ENGINE_DEFINITION.modelSelection.type).toBe('free-text'); }); @@ -78,6 +82,10 @@ describe('CLAUDE_CODE_ENGINE_DEFINITION', () => { expect(CLAUDE_CODE_ENGINE_DEFINITION.label).toBe('Claude Code'); }); + it('has native-tool archetype', () => { + expect(CLAUDE_CODE_ENGINE_DEFINITION.archetype).toBe('native-tool'); + }); + it('has select model selection with default label', () => { expect(CLAUDE_CODE_ENGINE_DEFINITION.modelSelection.type).toBe('select'); if (CLAUDE_CODE_ENGINE_DEFINITION.modelSelection.type === 'select') { @@ -130,6 +138,10 @@ describe('CODEX_ENGINE_DEFINITION', () => { expect(CODEX_ENGINE_DEFINITION.label).toBe('Codex'); }); + it('has native-tool archetype', () => { + expect(CODEX_ENGINE_DEFINITION.archetype).toBe('native-tool'); + }); + it('has select model selection with default label', () => { expect(CODEX_ENGINE_DEFINITION.modelSelection.type).toBe('select'); if (CODEX_ENGINE_DEFINITION.modelSelection.type === 'select') { @@ -173,6 +185,10 @@ describe('OPENCODE_ENGINE_DEFINITION', () => { expect(OPENCODE_ENGINE_DEFINITION.label).toBe('OpenCode'); }); + it('has native-tool archetype', () => { + expect(OPENCODE_ENGINE_DEFINITION.archetype).toBe('native-tool'); + }); + it('has free-text model selection', () => { expect(OPENCODE_ENGINE_DEFINITION.modelSelection.type).toBe('free-text'); }); @@ -202,6 +218,22 @@ describe('OPENCODE_ENGINE_DEFINITION', () => { // ─── Cross-cutting properties ───────────────────────────────────────────────── describe('Engine definitions cross-cutting properties', () => { + it('all engines have a valid archetype value', () => { + for (const engine of DEFAULT_ENGINE_CATALOG) { + expect(['sdk', 'native-tool']).toContain(engine.archetype); + } + }); + + it('only llmist has sdk archetype', () => { + for (const engine of DEFAULT_ENGINE_CATALOG) { + if (engine.id === 'llmist') { + expect(engine.archetype).toBe('sdk'); + } else { + expect(engine.archetype).toBe('native-tool'); + } + } + }); + it('all engines have scoped_env_secrets capability', () => { for (const engine of DEFAULT_ENGINE_CATALOG) { expect(engine.capabilities).toContain('scoped_env_secrets'); diff --git a/tests/unit/backends/engine-contract.test.ts b/tests/unit/backends/engine-contract.test.ts index c30c1a72..630d97e6 100644 --- a/tests/unit/backends/engine-contract.test.ts +++ b/tests/unit/backends/engine-contract.test.ts @@ -51,6 +51,8 @@ describe.each(EXPECTED_ENGINE_IDS)('engine: %s', (engineId) => { expect(typeof definition.description).toBe('string'); expect(definition.description.length).toBeGreaterThan(0); + expect(['sdk', 'native-tool']).toContain(definition.archetype); + expect(Array.isArray(definition.capabilities)).toBe(true); expect(definition.modelSelection).toBeDefined(); @@ -60,6 +62,13 @@ describe.each(EXPECTED_ENGINE_IDS)('engine: %s', (engineId) => { expect(definition.logLabel.length).toBeGreaterThan(0); }); + it('has archetype set to sdk or native-tool', () => { + const engine = getEngine(engineId); + expect(engine).toBeDefined(); + if (!engine) return; + expect(['sdk', 'native-tool']).toContain(engine.definition.archetype); + }); + it("definition.id matches the engine's registry key", () => { const engine = getEngine(engineId); expect(engine).toBeDefined(); diff --git a/tests/unit/backends/postProcess.test.ts b/tests/unit/backends/postProcess.test.ts index 6af47c26..5310f3b4 100644 --- a/tests/unit/backends/postProcess.test.ts +++ b/tests/unit/backends/postProcess.test.ts @@ -19,6 +19,7 @@ function makeEngine(id = 'test-engine'): AgentEngine { id, label: id, description: `${id} description`, + archetype: 'sdk', capabilities: [], modelSelection: { type: 'free-text' }, logLabel: 'Engine Log', diff --git a/tests/unit/backends/registry.test.ts b/tests/unit/backends/registry.test.ts index 3cabb480..6a229a87 100644 --- a/tests/unit/backends/registry.test.ts +++ b/tests/unit/backends/registry.test.ts @@ -1,13 +1,20 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { getEngine, getRegisteredEngines, registerEngine } from '../../../src/backends/registry.js'; +import { + getEngine, + getRegisteredEngines, + isNativeToolEngine, + isNativeToolEngineDefinition, + registerEngine, +} from '../../../src/backends/registry.js'; import type { AgentEngine } from '../../../src/backends/types.js'; -function createMockEngine(id: string): AgentEngine { +function createMockEngine(id: string, archetype: 'sdk' | 'native-tool' = 'sdk'): AgentEngine { return { definition: { id, label: id, description: `${id} description`, + archetype, capabilities: [], modelSelection: { type: 'free-text' }, logLabel: 'Engine Log', @@ -57,3 +64,31 @@ describe('getRegisteredEngines', () => { expect(names).toContain('test-list-b'); }); }); + +describe('isNativeToolEngineDefinition', () => { + it('returns true for native-tool archetype', () => { + const engine = createMockEngine('test-native-def', 'native-tool'); + expect(isNativeToolEngineDefinition(engine.definition)).toBe(true); + }); + + it('returns false for sdk archetype', () => { + const engine = createMockEngine('test-sdk-def', 'sdk'); + expect(isNativeToolEngineDefinition(engine.definition)).toBe(false); + }); +}); + +describe('isNativeToolEngine', () => { + it('returns true for a registered native-tool engine', () => { + registerEngine(createMockEngine('test-native-reg', 'native-tool')); + expect(isNativeToolEngine('test-native-reg')).toBe(true); + }); + + it('returns false for a registered sdk engine', () => { + registerEngine(createMockEngine('test-sdk-reg', 'sdk')); + expect(isNativeToolEngine('test-sdk-reg')).toBe(false); + }); + + it('returns false for an unknown engine id', () => { + expect(isNativeToolEngine('nonexistent-engine-abc')).toBe(false); + }); +});