diff --git a/core/config/markdown/loadMarkdownRules.ts b/core/config/markdown/loadMarkdownRules.ts index bdeddedaaae..91ed7216485 100644 --- a/core/config/markdown/loadMarkdownRules.ts +++ b/core/config/markdown/loadMarkdownRules.ts @@ -37,7 +37,7 @@ export async function loadMarkdownRules(ide: IDE): Promise<{ }); rules.push({ ...rule, - source: "agent-file", + source: "agentFile", sourceFile: agentFileUri, alwaysApply: true, }); diff --git a/core/index.d.ts b/core/index.d.ts index fb59d97bdc8..685b8ade1bd 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1852,7 +1852,7 @@ export type RuleSource = | "colocated-markdown" | "json-systemMessage" | ".continuerules" - | "agent-file"; + | "agentFile"; export interface RuleWithSource { name?: string; diff --git a/core/llm/rules/rules-utils.ts b/core/llm/rules/rules-utils.ts index 41afc7cc078..bafa3fa5daf 100644 --- a/core/llm/rules/rules-utils.ts +++ b/core/llm/rules/rules-utils.ts @@ -26,7 +26,7 @@ export function getRuleSourceDisplayName(rule: RuleWithSource): string { return "Base System Plan Message"; case "model-options-chat": return "Base System Chat Message"; - case "agent-file": + case "agentFile": if (rule.sourceFile) { return getLastNPathParts(rule.sourceFile, 2); } else { diff --git a/extensions/cli/src/__mocks__/services/index.ts b/extensions/cli/src/__mocks__/services/index.ts index a062ef2a4a2..5df13605a63 100644 --- a/extensions/cli/src/__mocks__/services/index.ts +++ b/extensions/cli/src/__mocks__/services/index.ts @@ -26,5 +26,5 @@ export const SERVICE_NAMES = { CHAT_HISTORY: "chatHistory", UPDATE: "update", STORAGE_SYNC: "storageSync", - WORKFLOW: "workflow", + AGENT_FILE: "agentFile", } as const; diff --git a/extensions/cli/src/commands/BaseCommandOptions.ts b/extensions/cli/src/commands/BaseCommandOptions.ts index bb32e9a9031..9c457d066d9 100644 --- a/extensions/cli/src/commands/BaseCommandOptions.ts +++ b/extensions/cli/src/commands/BaseCommandOptions.ts @@ -21,8 +21,8 @@ export interface BaseCommandOptions { ask?: string[]; /** Array of tools to exclude from use (--exclude) */ exclude?: string[]; - /** Workflow slug from the hub (--workflow) */ - workflow?: string; + /** Agent file slug from the hub (--agent) */ + agent?: string; } /** diff --git a/extensions/cli/src/commands/chat.ts b/extensions/cli/src/commands/chat.ts index 05332d80ead..c2401283d10 100644 --- a/extensions/cli/src/commands/chat.ts +++ b/extensions/cli/src/commands/chat.ts @@ -13,9 +13,9 @@ import { sentryService } from "../sentry.js"; import { initializeServices, services } from "../services/index.js"; import { serviceContainer } from "../services/ServiceContainer.js"; import { + AgentFileServiceState, ModelServiceState, SERVICE_NAMES, - WorkflowServiceState, } from "../services/types.js"; import { loadSession, @@ -478,11 +478,11 @@ async function runHeadlessMode( const { processAndCombinePrompts } = await import( "../util/promptProcessor.js" ); - const workflowState = await serviceContainer.get( - SERVICE_NAMES.WORKFLOW, + const agentFileState = await serviceContainer.get( + SERVICE_NAMES.AGENT_FILE, ); const initialPrompt = - `${workflowState?.workflowFile?.prompt ?? ""}\n\n${prompt ?? ""}`.trim() || + `${agentFileState?.agentFile?.prompt ?? ""}\n\n${prompt ?? ""}`.trim() || undefined; const initialUserInput = await processAndCombinePrompts( options.prompt, @@ -549,12 +549,12 @@ export async function chat(prompt?: string, options: ChatOptions = {}) { toolPermissionOverrides: permissionOverrides, }); - const workflowState = await serviceContainer.get( - SERVICE_NAMES.WORKFLOW, + const agentFileState = await serviceContainer.get( + SERVICE_NAMES.AGENT_FILE, ); const initialPrompt = - `${workflowState?.workflowFile?.prompt ?? ""}\n\n${prompt ?? ""}`.trim() || + `${agentFileState?.agentFile?.prompt ?? ""}\n\n${prompt ?? ""}`.trim() || undefined; // Start TUI with skipOnboarding since we already handled it diff --git a/extensions/cli/src/configEnhancer.test.ts b/extensions/cli/src/configEnhancer.test.ts index 4b37951d568..75fc33a854e 100644 --- a/extensions/cli/src/configEnhancer.test.ts +++ b/extensions/cli/src/configEnhancer.test.ts @@ -21,15 +21,15 @@ vi.mock("./hubLoader.js", () => ({ modelProcessor: {}, })); -// Mock the service container to provide empty workflow state +// Mock the service container to provide empty agent file state vi.mock("./services/ServiceContainer.js", () => ({ serviceContainer: { get: vi.fn(() => Promise.resolve({ - workflowService: null, - workflowModelName: null, - workflowFile: null, - workflow: null, + agentFileService: null, + agentFileModelName: null, + agentFile: null, + slug: null, }), ), }, @@ -37,7 +37,7 @@ vi.mock("./services/ServiceContainer.js", () => ({ vi.mock("./services/types.js", () => ({ SERVICE_NAMES: { - WORKFLOW: "workflow", + AGENT_FILE: "agentFile", }, })); @@ -288,41 +288,41 @@ describe("ConfigEnhancer", () => { }); }); - it("should handle workflow integration gracefully when no workflow", async () => { - // The mocked service container returns null workflow state + it("should handle agent file integration gracefully when no agent file", async () => { + // The mocked service container returns null agent file state const options: BaseCommandOptions = { rule: ["test-rule"], }; const config = await enhancer.enhanceConfig(mockConfig, options); - // Should work normally when no workflow is active + // Should work normally when no agent file is active expect(config.rules).toEqual(["test-rule"]); }); - it("should handle workflow integration when workflow is active", async () => { - // Mock service container to return active workflow + it("should handle agent file integration when agent file is active", async () => { + // Mock service container to return active agent file const options: BaseCommandOptions = { rule: ["user-rule"], prompt: ["user-prompt"], }; const config = await enhancer.enhanceConfig(mockConfig, options, { - workflowFile: { - name: "Test Workflow", + agentFile: { + name: "Test Agent", prompt: "You are a test assistant", rules: "Always be helpful", model: "gpt-4", tools: "bash,read", }, - slug: "owner/test-workflow", - workflowModelName: null, - workflowService: null, + slug: "owner/test-agent", + agentFileModelName: null, + agentFileService: null, }); - // Should have both workflow and user rules + // Should have both agent file and user rules expect(config.rules).toHaveLength(2); - expect(config.rules?.[0]).toBe("Always be helpful"); // Workflow rule first + expect(config.rules?.[0]).toBe("Always be helpful"); // Agent file rule first expect(config.rules?.[1]).toBe("user-rule"); // User rule second }); }); diff --git a/extensions/cli/src/configEnhancer.ts b/extensions/cli/src/configEnhancer.ts index b299e871b49..106023b75df 100644 --- a/extensions/cli/src/configEnhancer.ts +++ b/extensions/cli/src/configEnhancer.ts @@ -1,6 +1,6 @@ import { AssistantUnrolled, - parseWorkflowTools, + parseAgentFileTools, Rule, } from "@continuedev/config-yaml"; @@ -12,7 +12,7 @@ import { modelProcessor, processRule, } from "./hubLoader.js"; -import { WorkflowServiceState } from "./services/types.js"; +import { AgentFileServiceState } from "./services/types.js"; import { logger } from "./util/logger.js"; /** @@ -20,16 +20,16 @@ import { logger } from "./util/logger.js"; */ export class ConfigEnhancer { // added this for lint complexity rule - private async enhanceConfigFromWorkflow( + private async enhanceConfigFromAgentFile( config: AssistantUnrolled, _options: BaseCommandOptions | undefined, - workflowState?: WorkflowServiceState, + agentFileState?: AgentFileServiceState, ) { const enhancedConfig = { ...config }; const options = { ..._options }; - if (workflowState?.workflowFile) { - const { rules, model, tools, prompt } = workflowState?.workflowFile; + if (agentFileState?.agentFile) { + const { rules, model, tools, prompt } = agentFileState?.agentFile; if (rules) { options.rule = [ ...rules @@ -42,38 +42,41 @@ export class ConfigEnhancer { if (tools) { try { - const parsedTools = parseWorkflowTools(tools); + const parsedTools = parseAgentFileTools(tools); if (parsedTools.mcpServers.length > 0) { options.mcp = [...parsedTools.mcpServers, ...(options.mcp || [])]; } } catch (e) { - logger.error("Failed to parse workflow tools", e); + logger.error("Failed to parse agent file tools", e); } } - // --model takes precedence over workflow model + // --model takes precedence over agent file model if (model) { try { - const workflowModel = await loadPackageFromHub(model, modelProcessor); + const agentFileModel = await loadPackageFromHub( + model, + modelProcessor, + ); enhancedConfig.models = [ - workflowModel, + agentFileModel, ...(enhancedConfig.models ?? []), ]; - workflowState?.workflowService?.setWorkflowModelName( - workflowModel.name, + agentFileState?.agentFileService?.setagentFileModelName( + agentFileModel.name, ); } catch (e) { - logger.error("Failed to load workflow model", e); + logger.error("Failed to load agent model", e); } } - // Workflow prompt is included as a slash command, initial kickoff is handled elsewhere + // Agent file prompt is included as a slash command, initial kickoff is handled elsewhere if (prompt) { enhancedConfig.prompts = [ { - name: `Workflow prompt (${workflowState.workflowFile.name})`, + name: `Agent prompt (${agentFileState.agentFile.name})`, prompt, - description: workflowState.workflowFile.description, + description: agentFileState.agentFile.description, }, ...(enhancedConfig.prompts ?? []), ]; @@ -87,12 +90,12 @@ export class ConfigEnhancer { async enhanceConfig( config: AssistantUnrolled, _options?: BaseCommandOptions, - workflowState?: WorkflowServiceState, + agentFileState?: AgentFileServiceState, ): Promise { - const enhanced = await this.enhanceConfigFromWorkflow( + const enhanced = await this.enhanceConfigFromAgentFile( config, _options, - workflowState, + agentFileState, ); let { enhancedConfig } = enhanced; const { options } = enhanced; diff --git a/extensions/cli/src/hubLoader.ts b/extensions/cli/src/hubLoader.ts index 006cda7a884..3807daf74e0 100644 --- a/extensions/cli/src/hubLoader.ts +++ b/extensions/cli/src/hubLoader.ts @@ -1,4 +1,4 @@ -import { parseWorkflowFile, WorkflowFile } from "@continuedev/config-yaml"; +import { AgentFile, parseAgentFile } from "@continuedev/config-yaml"; import JSZip from "jszip"; import { env } from "./env.js"; @@ -12,7 +12,7 @@ const HUB_SLUG_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; /** * Hub package type definitions */ -export type HubPackageType = "rule" | "mcp" | "model" | "prompt" | "workflow"; +export type HubPackageType = "rule" | "mcp" | "model" | "prompt" | "agentFile"; /** * Hub package processor interface @@ -100,12 +100,12 @@ export const promptProcessor: HubPackageProcessor = { parseContent: (content: string) => content, }; -export const workflowProcessor: HubPackageProcessor = { - type: "workflow", +export const agentFileProcessor: HubPackageProcessor = { + type: "agentFile", expectedFileExtensions: [".md"], - parseContent: (content: string) => parseWorkflowFile(content), - validateContent: (workflowFile: WorkflowFile) => { - return !!workflowFile.name; + parseContent: (content: string) => parseAgentFile(content), + validateContent: (agentFile: AgentFile) => { + return !!agentFile.name; }, }; diff --git a/extensions/cli/src/integration/rule-duplication.test.ts b/extensions/cli/src/integration/rule-duplication.test.ts index 83143dedec1..d58d11df3c2 100644 --- a/extensions/cli/src/integration/rule-duplication.test.ts +++ b/extensions/cli/src/integration/rule-duplication.test.ts @@ -19,12 +19,12 @@ vi.mock("../hubLoader.js", () => ({ modelProcessor: {}, })); -// Mock the service container to provide empty workflow state +// Mock the service container to provide empty agent file state vi.mock("../services/ServiceContainer.js", () => ({ serviceContainer: { get: vi.fn(() => Promise.resolve({ - workflowFile: null, + agentFile: null, slug: null, }), ), @@ -33,7 +33,7 @@ vi.mock("../services/ServiceContainer.js", () => ({ vi.mock("../services/types.js", () => ({ SERVICE_NAMES: { - WORKFLOW: "workflow", + AGENT_FILE: "agentFile", }, })); diff --git a/extensions/cli/src/services/AgentFileService.test.ts b/extensions/cli/src/services/AgentFileService.test.ts new file mode 100644 index 00000000000..13e1eb9bd22 --- /dev/null +++ b/extensions/cli/src/services/AgentFileService.test.ts @@ -0,0 +1,292 @@ +import { Mock, vi } from "vitest"; + +import { agentFileProcessor } from "../hubLoader.js"; + +import { AgentFileService } from "./AgentFileService.js"; + +// Mock the hubLoader module +vi.mock("../hubLoader.js", () => ({ + loadPackageFromHub: vi.fn(), + HubPackageProcessor: vi.fn(), + agentFileProcessor: { + type: "agentFile", + expectedFileExtensions: [".md"], + parseContent: vi.fn(), + validateContent: vi.fn(), + }, +})); + +// Mock the logger +vi.mock("../util/logger.js", () => ({ + logger: { + debug: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + }, +})); + +describe("AgentFileService", () => { + let service: AgentFileService; + let mockLoadPackageFromHub: any; + + beforeEach(async () => { + vi.clearAllMocks(); + service = new AgentFileService(); + + // Get the mock function + const hubLoaderModule = await import("../hubLoader.js"); + mockLoadPackageFromHub = hubLoaderModule.loadPackageFromHub as any; + }); + + describe("initialization", () => { + it("should initialize with inactive state when no agent file provided", async () => { + const state = await service.initialize(); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); + }); + + it("should initialize with inactive state when agent slug is empty string", async () => { + const state = await service.initialize(""); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); + }); + + it("should reject invalid agent slug format", async () => { + const state = await service.initialize("invalid-slug"); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); + + // Should not call loadPackageFromHub with invalid slug + expect(mockLoadPackageFromHub).not.toHaveBeenCalled(); + }); + + it("should reject agent slug with too many parts", async () => { + const state = await service.initialize("owner/package/extra"); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); + + expect(mockLoadPackageFromHub).not.toHaveBeenCalled(); + }); + + it("should load valid agent file successfully", async () => { + const mockAgentFile = { + name: "Test Agent", + description: "A test agent", + model: "gpt-4", + tools: "bash,read,write", + rules: "Be helpful", + prompt: "You are a helpful assistant.", + }; + + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + + const state = await service.initialize("owner/package"); + + expect(state).toEqual({ + agentFile: mockAgentFile, + slug: "owner/package", + agentFileModelName: null, + agentFileService: service, + }); + + expect(mockLoadPackageFromHub).toHaveBeenCalledWith( + "owner/package", + agentFileProcessor, + ); + }); + + it("should handle loading errors gracefully", async () => { + mockLoadPackageFromHub.mockRejectedValue(new Error("Network error")); + + const state = await service.initialize("owner/package"); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); + + expect(mockLoadPackageFromHub).toHaveBeenCalledWith( + "owner/package", + agentFileProcessor, + ); + }); + + it("should handle minimal agent file", async () => { + const mockAgentFile = { + name: "Minimal Agent File", + prompt: "", + }; + + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + + const state = await service.initialize("owner/minimal"); + + expect(state).toEqual({ + agentFile: mockAgentFile, + slug: "owner/minimal", + agentFileModelName: null, + agentFileService: service, + }); + }); + }); + + describe("state getters", () => { + beforeEach(async () => { + const mockAgentFile = { + name: "Test Agent", + description: "A test Agent", + model: "gpt-4", + tools: "bash,read,write", + rules: "Be helpful", + prompt: "You are a helpful assistant.", + }; + + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await service.initialize("owner/package"); + }); + + it("should return agent file state", () => { + const state = service.getState(); + expect(state.agentFile?.name).toBe("Test Agent"); + expect(state.slug).toBe("owner/package"); + expect(state.agentFile?.model).toBe("gpt-4"); + expect(state.agentFile?.tools).toBe("bash,read,write"); + expect(state.agentFile?.rules).toBe("Be helpful"); + expect(state.agentFile?.prompt).toBe("You are a helpful assistant."); + }); + }); + + describe("inactive agent file state", () => { + beforeEach(() => { + service.initialize(); + }); + + it("should return inactive state when no agent file", () => { + const state = service.getState(); + expect(state.agentFile).toBeNull(); + expect(state.slug).toBeNull(); + }); + }); + + describe("partial agent file data", () => { + it("should handle agent file with only name and prompt", async () => { + const mockAgentFile = { + name: "Simple Agent", + prompt: "Simple prompt", + }; + + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await service.initialize("owner/simple"); + + const state = service.getState(); + expect(state.agentFile?.model).toBeUndefined(); + expect(state.agentFile?.tools).toBeUndefined(); + expect(state.agentFile?.rules).toBeUndefined(); + expect(state.agentFile?.prompt).toBe("Simple prompt"); + }); + + it("should handle agent file with empty prompt", async () => { + const mockAgentFile = { + name: "Empty Prompt Agent", + model: "gpt-3.5-turbo", + prompt: "", + }; + + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await service.initialize("owner/empty"); + + const state = service.getState(); + expect(state.agentFile?.model).toBe("gpt-3.5-turbo"); + expect(state.agentFile?.prompt).toBe(""); + }); + }); +}); + +describe("agentFileProcessor", () => { + it("should have correct type and extensions", () => { + expect(agentFileProcessor.type).toBe("agentFile"); + expect(agentFileProcessor.expectedFileExtensions).toEqual([".md"]); + }); + + it("should parse agent file content correctly", () => { + const content = `--- +name: Test Agent +model: gpt-4 +tools: bash,read +--- +You are a helpful assistant.`; + + // Set up the mock to return expected result + const expectedResult = { + name: "Test Agent", + model: "gpt-4", + tools: "bash,read", + prompt: "You are a helpful assistant.", + }; + (agentFileProcessor.parseContent as Mock).mockReturnValue(expectedResult); + + const result = agentFileProcessor.parseContent(content, "test.md"); + expect(result).toEqual({ + name: "Test Agent", + model: "gpt-4", + tools: "bash,read", + prompt: "You are a helpful assistant.", + }); + }); + + it("should validate agent file content", () => { + const validAgentFile = { + name: "Valid Agent", + prompt: "Test prompt", + }; + + const invalidAgentFile = { + prompt: "Test prompt", + // Missing name + }; + + // Set up mock validation responses + (agentFileProcessor.validateContent as Mock) + .mockReturnValueOnce(true) // For valid agent file + .mockReturnValueOnce(false); // For invalid agent file + + expect(agentFileProcessor.validateContent?.(validAgentFile)).toBe(true); + expect(agentFileProcessor.validateContent?.(invalidAgentFile as any)).toBe( + false, + ); + }); + + it("should validate agent file with empty name as invalid", () => { + const invalidAgentFile = { + name: "", + prompt: "Test prompt", + }; + + // Mock validation to return false for empty name + (agentFileProcessor.validateContent as Mock).mockReturnValue(false); + + expect(agentFileProcessor.validateContent?.(invalidAgentFile)).toBe(false); + }); +}); diff --git a/extensions/cli/src/services/AgentFileService.ts b/extensions/cli/src/services/AgentFileService.ts new file mode 100644 index 00000000000..8220e34f56f --- /dev/null +++ b/extensions/cli/src/services/AgentFileService.ts @@ -0,0 +1,78 @@ +import { agentFileProcessor, loadPackageFromHub } from "../hubLoader.js"; +import { logger } from "../util/logger.js"; + +import { BaseService } from "./BaseService.js"; +import { serviceContainer } from "./ServiceContainer.js"; +import { AgentFileServiceState } from "./types.js"; + +/** + * Service for managing agent file state + * Loads agent files from the hub and extracts model, tools, and prompt information + */ +export class AgentFileService extends BaseService { + /** + * Set the resolved agent file model name after it's been processed + * Called by ConfigEnhancer after resolving the model slug + */ + setagentFileModelName(modelName: string): void { + this.setState({ + agentFileModelName: modelName, + }); + } + constructor() { + super("AgentFileService", { + agentFileService: null, + agentFile: null, + slug: null, + agentFileModelName: null, + }); + } + + /** + * Initialize the agent file service with a hub slug + */ + async doInitialize(agentFileSlug?: string): Promise { + if (!agentFileSlug) { + return { + agentFileService: this, + agentFile: null, + slug: null, + agentFileModelName: null, + }; + } + + try { + const parts = agentFileSlug.split("/"); + if (parts.length !== 2) { + throw new Error( + `Invalid agent slug format. Expected "owner/package", got: ${agentFileSlug}`, + ); + } + + const agentFile = await loadPackageFromHub( + agentFileSlug, + agentFileProcessor, + ); + + return { + agentFileService: this, + agentFile, + slug: agentFileSlug, + agentFileModelName: null, // Will be set by ConfigEnhancer after model resolution + }; + } catch (error: any) { + logger.error("Failed to initialize AgentFileService:", error); + return { + agentFileService: this, + agentFile: null, + slug: null, + agentFileModelName: null, + }; + } + } + + protected override setState(newState: Partial): void { + super.setState(newState); + serviceContainer.set("update", this.currentState); + } +} diff --git a/extensions/cli/src/services/ConfigService.test.ts b/extensions/cli/src/services/ConfigService.test.ts index dbb313fb728..2ca7b9146d6 100644 --- a/extensions/cli/src/services/ConfigService.test.ts +++ b/extensions/cli/src/services/ConfigService.test.ts @@ -12,7 +12,7 @@ import * as configLoader from "../configLoader.js"; import { ConfigService } from "./ConfigService.js"; import { serviceContainer } from "./ServiceContainer.js"; -import { SERVICE_NAMES, WorkflowServiceState } from "./types.js"; +import { AgentFileServiceState, SERVICE_NAMES } from "./types.js"; describe("ConfigService", () => { let service: ConfigService; @@ -23,11 +23,11 @@ describe("ConfigService", () => { systemMessage: "Test system message", } as any; const mockApiClient = { get: vi.fn(), post: vi.fn() }; - const mockWorkflowState: WorkflowServiceState = { + const mockAgentFileState: AgentFileServiceState = { slug: null, - workflowFile: null, - workflowModelName: null, - workflowService: null, + agentFile: null, + agentFileModelName: null, + agentFileService: null, }; beforeEach(() => { vi.clearAllMocks(); @@ -46,7 +46,7 @@ describe("ConfigService", () => { configPath: "/path/to/config.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); expect(state).toEqual({ @@ -66,7 +66,7 @@ describe("ConfigService", () => { configPath: undefined, _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); expect(state).toEqual({ @@ -94,7 +94,7 @@ describe("ConfigService", () => { configPath: "/config.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, injectedConfigOptions: { rule: ["rule1", "rule2"] }, }); @@ -102,7 +102,7 @@ describe("ConfigService", () => { expect(vi.mocked(configEnhancer.enhanceConfig)).toHaveBeenCalledWith( mockConfig, { rule: ["rule1", "rule2"] }, - mockWorkflowState, + mockAgentFileState, ); expect(state.config).toEqual(expectedConfig); @@ -121,7 +121,7 @@ describe("ConfigService", () => { configPath: "/old.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); // Switch to new config @@ -151,7 +151,7 @@ describe("ConfigService", () => { configPath: "/old.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); vi.mocked(configLoader.loadConfiguration).mockRejectedValue( @@ -181,7 +181,7 @@ describe("ConfigService", () => { configPath: "/config.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); // Modify mock to return updated config @@ -214,7 +214,7 @@ describe("ConfigService", () => { configPath: undefined, _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); await expect( @@ -239,7 +239,7 @@ describe("ConfigService", () => { configPath: "/old.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); // Mock service container @@ -282,7 +282,7 @@ describe("ConfigService", () => { configPath: "/old.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); vi.mocked(workos.loadAuthConfig).mockReturnValue({ @@ -304,7 +304,7 @@ describe("ConfigService", () => { expect(service.getDependencies()).toEqual([ "auth", "apiClient", - "workflow", + "agentFile", ]); }); }); @@ -320,7 +320,7 @@ describe("ConfigService", () => { configPath: "/old.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); const listener = vi.fn(); @@ -354,7 +354,7 @@ describe("ConfigService", () => { configPath: "/old.yaml", _organizationId: "org-123", apiClient: mockApiClient as any, - workflowState: mockWorkflowState, + agentFileState: mockAgentFileState, }); const errorListener = vi.fn(); diff --git a/extensions/cli/src/services/ConfigService.ts b/extensions/cli/src/services/ConfigService.ts index 72f88540f9a..391215399ef 100644 --- a/extensions/cli/src/services/ConfigService.ts +++ b/extensions/cli/src/services/ConfigService.ts @@ -8,10 +8,10 @@ import { logger } from "../util/logger.js"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; import { + AgentFileServiceState, ApiClientServiceState, ConfigServiceState, SERVICE_NAMES, - WorkflowServiceState, } from "./types.js"; interface ConfigServiceInit { @@ -19,7 +19,7 @@ interface ConfigServiceInit { configPath: string | undefined; _organizationId: string | null; apiClient: DefaultApiInterface; - workflowState: WorkflowServiceState; + agentFileState: AgentFileServiceState; injectedConfigOptions?: BaseCommandOptions; } /** @@ -44,7 +44,7 @@ export class ConfigService return [ SERVICE_NAMES.AUTH, SERVICE_NAMES.API_CLIENT, - SERVICE_NAMES.WORKFLOW, + SERVICE_NAMES.AGENT_FILE, ]; } @@ -55,7 +55,7 @@ export class ConfigService apiClient, authConfig, configPath, - workflowState, + agentFileState, injectedConfigOptions, }: ConfigServiceInit): Promise { // Use the new streamlined config loader @@ -66,13 +66,13 @@ export class ConfigService // Apply injected config if provided if ( - workflowState?.workflowFile || + agentFileState?.agentFile || (injectedConfigOptions && this.hasInjectedConfig(injectedConfigOptions)) ) { config = await configEnhancer.enhanceConfig( config, injectedConfigOptions, - workflowState, + agentFileState, ); logger.debug("Applied injected configuration"); diff --git a/extensions/cli/src/services/ModelService.test.ts b/extensions/cli/src/services/ModelService.test.ts index b5241455884..bcc79778003 100644 --- a/extensions/cli/src/services/ModelService.test.ts +++ b/extensions/cli/src/services/ModelService.test.ts @@ -357,8 +357,12 @@ describe("ModelService", () => { }); describe("getDependencies()", () => { - test("should declare auth, config, and workflow dependencies", () => { - expect(service.getDependencies()).toEqual(["auth", "config", "workflow"]); + test("should declare auth, config, and agent-file dependencies", () => { + expect(service.getDependencies()).toEqual([ + "auth", + "config", + "agentFile", + ]); }); }); diff --git a/extensions/cli/src/services/ModelService.ts b/extensions/cli/src/services/ModelService.ts index e09c65d49a4..f4e0a18b8e6 100644 --- a/extensions/cli/src/services/ModelService.ts +++ b/extensions/cli/src/services/ModelService.ts @@ -5,7 +5,7 @@ import { createLlmApi, getLlmApi } from "../config.js"; import { logger } from "../util/logger.js"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; -import { ModelServiceState, WorkflowServiceState } from "./types.js"; +import { AgentFileServiceState, ModelServiceState } from "./types.js"; /** * Service for managing LLM and model state @@ -32,7 +32,7 @@ export class ModelService * Declare dependencies on other services */ getDependencies(): string[] { - return ["auth", "config", "workflow"]; + return ["auth", "config", "agentFile"]; } /** @@ -41,7 +41,7 @@ export class ModelService async doInitialize( assistant: AssistantUnrolled, authConfig: AuthConfig, - workflowServiceState?: WorkflowServiceState, + agentFileServiceState?: AgentFileServiceState, ): Promise { logger.debug("ModelService.doInitialize called", { hasAssistant: !!assistant, @@ -59,10 +59,10 @@ export class ModelService let preferredModelName: string | null | undefined = null; let modelSource = "default"; - // Priority = workflow -> last selected model - if (workflowServiceState?.workflowModelName) { - preferredModelName = workflowServiceState.workflowModelName; - modelSource = "workflow"; + // Priority = agentFile -> last selected model + if (agentFileServiceState?.agentFileModelName) { + preferredModelName = agentFileServiceState.agentFileModelName; + modelSource = "agentFile"; } else { preferredModelName = getModelName(authConfig); if (preferredModelName) { @@ -70,7 +70,7 @@ export class ModelService } } - // Try to use the preferred model (workflow or persisted) + // Try to use the preferred model (agent file or persisted) if (preferredModelName) { // During initialization, we need to check against availableModels directly const modelIndex = this.availableModels.findIndex((model) => { diff --git a/extensions/cli/src/services/ModelService.workflow-priority.test.ts b/extensions/cli/src/services/ModelService.workflow-priority.test.ts index 2d04c58abcd..e683a86d26a 100644 --- a/extensions/cli/src/services/ModelService.workflow-priority.test.ts +++ b/extensions/cli/src/services/ModelService.workflow-priority.test.ts @@ -5,7 +5,7 @@ import { AuthConfig } from "../auth/workos.js"; import * as config from "../config.js"; import { ModelService } from "./ModelService.js"; -import { WorkflowServiceState } from "./types.js"; +import { AgentFileServiceState } from "./types.js"; // Mock the dependencies vi.mock("../auth/workos.js", () => ({ @@ -27,7 +27,7 @@ vi.mock("../util/logger.js", () => ({ }, })); -describe("ModelService workflow model prioritization", () => { +describe("ModelService agent file model prioritization", () => { let modelService: ModelService; let mockAssistant: AssistantUnrolled; let mockAuthConfig: AuthConfig; @@ -52,16 +52,16 @@ describe("ModelService workflow model prioritization", () => { vi.clearAllMocks(); }); - it("should prioritize workflow model over default when workflow file has model specified", async () => { - const workflowServiceState: WorkflowServiceState = { - workflowFile: { - name: "test-workflow", + it("should prioritize agent file model over default when agent file has model specified", async () => { + const agentFileServiceState: AgentFileServiceState = { + agentFile: { + name: "test-agent", model: "gpt-4", - prompt: "Test workflow", + prompt: "Test agent", }, - slug: "test/workflow", - workflowModelName: "gpt-4", - workflowService: null, + slug: "test/agent", + agentFileModelName: "gpt-4", + agentFileService: null, }; // Mock createLlmApi to return different models based on the selected model @@ -77,7 +77,7 @@ describe("ModelService workflow model prioritization", () => { const result = await modelService.doInitialize( mockAssistant, mockAuthConfig, - workflowServiceState, + agentFileServiceState, ); expect(result.model).toEqual( @@ -87,23 +87,23 @@ describe("ModelService workflow model prioritization", () => { }), ); - // Should have called createLlmApi with the workflow-specified model + // Should have called createLlmApi with the agent-file-specified model expect(createLlmApiMock).toHaveBeenCalledWith( expect.objectContaining({ name: "gpt-4" }), mockAuthConfig, ); }); - it("should fall back to persisted model when no workflow model is specified", async () => { - const workflowServiceState: WorkflowServiceState = { - workflowFile: { - name: "test-workflow", - prompt: "Test workflow", + it("should fall back to persisted model when no agent file model is specified", async () => { + const agentFileServiceState: AgentFileServiceState = { + agentFile: { + name: "test-agent", + prompt: "Test agent file", // No model specified }, - slug: "test/workflow", - workflowModelName: null, - workflowService: null, + slug: "test/agent", + agentFileModelName: null, + agentFileService: null, }; // Mock getModelName to return a persisted model @@ -122,7 +122,7 @@ describe("ModelService workflow model prioritization", () => { const result = await modelService.doInitialize( mockAssistant, mockAuthConfig, - workflowServiceState, + agentFileServiceState, ); expect(result.model).toEqual( @@ -138,16 +138,16 @@ describe("ModelService workflow model prioritization", () => { ); }); - it("should fall back to default model when workflow model is not available", async () => { - const workflowServiceState: WorkflowServiceState = { - workflowFile: { - name: "test-workflow", + it("should fall back to default model when agent file model is not available", async () => { + const agentFileServiceState: AgentFileServiceState = { + agentFile: { + name: "test-agent", model: "non-existent-model", // Model not in available models - prompt: "Test workflow", + prompt: "Test agent", }, - slug: "test/workflow", - workflowModelName: "non-existent-model", - workflowService: null, + slug: "test/agent", + agentFileModelName: "non-existent-model", + agentFileService: null, }; const getLlmApiMock = vi.mocked(config.getLlmApi); @@ -159,7 +159,7 @@ describe("ModelService workflow model prioritization", () => { const result = await modelService.doInitialize( mockAssistant, mockAuthConfig, - workflowServiceState, + agentFileServiceState, ); expect(result.model).toEqual( @@ -173,12 +173,12 @@ describe("ModelService workflow model prioritization", () => { expect(getLlmApiMock).toHaveBeenCalledWith(mockAssistant, mockAuthConfig); }); - it("should use default model when no workflow file exists", async () => { - const workflowServiceState: WorkflowServiceState = { - workflowFile: null, + it("should use default model when no agent file exists", async () => { + const agentFileServiceState: AgentFileServiceState = { + agentFile: null, slug: null, - workflowModelName: null, - workflowService: null, + agentFileModelName: null, + agentFileService: null, }; // Make sure getModelName returns null (no persisted model) @@ -194,7 +194,7 @@ describe("ModelService workflow model prioritization", () => { const result = await modelService.doInitialize( mockAssistant, mockAuthConfig, - workflowServiceState, + agentFileServiceState, ); expect(result.model).toEqual( @@ -207,16 +207,16 @@ describe("ModelService workflow model prioritization", () => { expect(getLlmApiMock).toHaveBeenCalledWith(mockAssistant, mockAuthConfig); }); - it("should prioritize workflow model over persisted model", async () => { - const workflowServiceState: WorkflowServiceState = { - workflowFile: { - name: "test-workflow", - model: "gpt-4", // Workflow specifies gpt-4 - prompt: "Test workflow", + it("should prioritize agent file model over persisted model", async () => { + const agentFileServiceState: AgentFileServiceState = { + agentFile: { + name: "test-agent", + model: "gpt-4", // Agent file specifies gpt-4 + prompt: "Test agent", }, - slug: "test/workflow", - workflowModelName: "gpt-4", - workflowService: null, + slug: "test/agent", + agentFileModelName: "gpt-4", + agentFileService: null, }; // Mock getModelName to return a different persisted model @@ -235,10 +235,10 @@ describe("ModelService workflow model prioritization", () => { const result = await modelService.doInitialize( mockAssistant, mockAuthConfig, - workflowServiceState, + agentFileServiceState, ); - // Should use workflow model (gpt-4), not persisted model (claude-3-haiku) + // Should use agent file model (gpt-4), not persisted model (claude-3-haiku) expect(result.model).toEqual( expect.objectContaining({ name: "gpt-4", diff --git a/extensions/cli/src/services/ToolPermissionService.test.ts b/extensions/cli/src/services/ToolPermissionService.test.ts index b11e506b210..1cd898ae9fa 100644 --- a/extensions/cli/src/services/ToolPermissionService.test.ts +++ b/extensions/cli/src/services/ToolPermissionService.test.ts @@ -30,7 +30,7 @@ describe("ToolPermissionService", () => { currentMode: "normal", isHeadless: false, modePolicyCount: 0, - workflowPolicyCount: 0, + agentFilePolicyCount: 0, }); }); diff --git a/extensions/cli/src/services/ToolPermissionService.ts b/extensions/cli/src/services/ToolPermissionService.ts index a6fa5db1e7a..fb22f5a0792 100644 --- a/extensions/cli/src/services/ToolPermissionService.ts +++ b/extensions/cli/src/services/ToolPermissionService.ts @@ -1,4 +1,4 @@ -import { parseWorkflowTools } from "@continuedev/config-yaml"; +import { parseAgentFileTools } from "@continuedev/config-yaml"; import { ensurePermissionsYamlExists } from "../permissions/permissionsYamlLoader.js"; import { resolvePermissionPrecedence } from "../permissions/precedenceResolver.js"; @@ -11,7 +11,7 @@ import { logger } from "../util/logger.js"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; -import { SERVICE_NAMES, WorkflowServiceState } from "./types.js"; +import { AgentFileServiceState, SERVICE_NAMES } from "./types.js"; export interface InitializeToolServiceOverrides { allow?: string[]; @@ -26,7 +26,7 @@ export interface ToolPermissionServiceState { currentMode: PermissionMode; isHeadless: boolean; modePolicyCount?: number; // Track how many policies are from mode vs other sources - workflowPolicyCount?: number; + agentFilePolicyCount?: number; originalPolicies?: ToolPermissions; // Store original policies when switching modes } @@ -61,21 +61,21 @@ export class ToolPermissionService * Declare dependencies on other services */ getDependencies(): string[] { - return [SERVICE_NAMES.WORKFLOW]; + return [SERVICE_NAMES.AGENT_FILE]; } /** - * Generate workflow-specific policies if a workflow is active + * Generate agent-file-specific policies if an agent file is active */ - private generateWorkflowPolicies( - workflowServiceState?: WorkflowServiceState, + private generateAgentFilePolicies( + agentFileServiceState?: AgentFileServiceState, ): undefined | ToolPermissionPolicy[] { - if (!workflowServiceState?.workflowFile?.tools) { + if (!agentFileServiceState?.agentFile?.tools) { return undefined; } - const parsedTools = parseWorkflowTools( - workflowServiceState.workflowFile.tools, + const parsedTools = parseAgentFileTools( + agentFileServiceState.agentFile.tools, ); if (parsedTools.tools.length === 0) { return undefined; @@ -101,7 +101,7 @@ export class ToolPermissionService } if (policies.length > 0) { - logger.debug(`Generated ${policies.length} workflow tool policies`); + logger.debug(`Generated ${policies.length} agent file tool policies`); } if (parsedTools.allBuiltIn) { @@ -164,7 +164,7 @@ export class ToolPermissionService */ initializeSync( runtimeOverrides?: InitializeToolServiceOverrides, - workflowServiceState?: WorkflowServiceState, + agentFileServiceState?: AgentFileServiceState, ): ToolPermissionServiceState { logger.debug("Synchronously initializing ToolPermissionService"); @@ -178,16 +178,17 @@ export class ToolPermissionService this.setState({ isHeadless: runtimeOverrides.isHeadless }); } - const workflowPolicies = - this.generateWorkflowPolicies(workflowServiceState); + const agentFilePolicies = this.generateAgentFilePolicies( + agentFileServiceState, + ); const modePolicies = this.generateModePolicies(); // For plan and auto modes, use ONLY mode policies (absolute override) // For normal mode, combine with user configuration let allPolicies: ToolPermissionPolicy[]; - if (workflowPolicies) { - // Workflow policies take full precedence on init - allPolicies = workflowPolicies; + if (agentFilePolicies) { + // Agent file policies take full precedence on init + allPolicies = agentFilePolicies; } else if ( this.currentState.currentMode === "plan" || this.currentState.currentMode === "auto" @@ -209,7 +210,7 @@ export class ToolPermissionService currentMode: this.currentState.currentMode, isHeadless: this.currentState.isHeadless, modePolicyCount: modePolicies.length, - workflowPolicyCount: (workflowPolicies ?? []).length, + agentFilePolicyCount: (agentFilePolicies ?? []).length, }); (this as any).isInitialized = true; @@ -222,12 +223,12 @@ export class ToolPermissionService */ async doInitialize( runtimeOverrides?: InitializeToolServiceOverrides, - workflowServiceState?: WorkflowServiceState, + agentFileServiceState?: AgentFileServiceState, ): Promise { await ensurePermissionsYamlExists(); // Use the synchronous version after ensuring the file exists - return this.initializeSync(runtimeOverrides, workflowServiceState); + return this.initializeSync(runtimeOverrides, agentFileServiceState); } /** diff --git a/extensions/cli/src/services/WorkflowService.test.ts b/extensions/cli/src/services/WorkflowService.test.ts deleted file mode 100644 index f88e6ed9c52..00000000000 --- a/extensions/cli/src/services/WorkflowService.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { Mock, vi } from "vitest"; - -import { workflowProcessor } from "../hubLoader.js"; - -import { WorkflowService } from "./WorkflowService.js"; - -// Mock the hubLoader module -vi.mock("../hubLoader.js", () => ({ - loadPackageFromHub: vi.fn(), - HubPackageProcessor: vi.fn(), - workflowProcessor: { - type: "workflow", - expectedFileExtensions: [".md"], - parseContent: vi.fn(), - validateContent: vi.fn(), - }, -})); - -// Mock the logger -vi.mock("../util/logger.js", () => ({ - logger: { - debug: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - }, -})); - -describe("WorkflowService", () => { - let service: WorkflowService; - let mockLoadPackageFromHub: any; - - beforeEach(async () => { - vi.clearAllMocks(); - service = new WorkflowService(); - - // Get the mock function - const hubLoaderModule = await import("../hubLoader.js"); - mockLoadPackageFromHub = hubLoaderModule.loadPackageFromHub as any; - }); - - describe("initialization", () => { - it("should initialize with inactive state when no workflow provided", async () => { - const state = await service.initialize(); - - expect(state).toEqual({ - workflowFile: null, - slug: null, - workflowModelName: null, - workflowService: service, - }); - }); - - it("should initialize with inactive state when workflow is empty string", async () => { - const state = await service.initialize(""); - - expect(state).toEqual({ - workflowFile: null, - slug: null, - workflowModelName: null, - workflowService: service, - }); - }); - - it("should reject invalid workflow slug format", async () => { - const state = await service.initialize("invalid-slug"); - - expect(state).toEqual({ - workflowFile: null, - slug: null, - workflowModelName: null, - workflowService: service, - }); - - // Should not call loadPackageFromHub with invalid slug - expect(mockLoadPackageFromHub).not.toHaveBeenCalled(); - }); - - it("should reject workflow slug with too many parts", async () => { - const state = await service.initialize("owner/package/extra"); - - expect(state).toEqual({ - workflowFile: null, - slug: null, - workflowModelName: null, - workflowService: service, - }); - - expect(mockLoadPackageFromHub).not.toHaveBeenCalled(); - }); - - it("should load valid workflow successfully", async () => { - const mockWorkflowFile = { - name: "Test Workflow", - description: "A test workflow", - model: "gpt-4", - tools: "bash,read,write", - rules: "Be helpful", - prompt: "You are a helpful assistant.", - }; - - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - - const state = await service.initialize("owner/package"); - - expect(state).toEqual({ - workflowFile: mockWorkflowFile, - slug: "owner/package", - workflowModelName: null, - workflowService: service, - }); - - expect(mockLoadPackageFromHub).toHaveBeenCalledWith( - "owner/package", - workflowProcessor, - ); - }); - - it("should handle loading errors gracefully", async () => { - mockLoadPackageFromHub.mockRejectedValue(new Error("Network error")); - - const state = await service.initialize("owner/package"); - - expect(state).toEqual({ - workflowFile: null, - slug: null, - workflowModelName: null, - workflowService: service, - }); - - expect(mockLoadPackageFromHub).toHaveBeenCalledWith( - "owner/package", - workflowProcessor, - ); - }); - - it("should handle minimal workflow file", async () => { - const mockWorkflowFile = { - name: "Minimal Workflow", - prompt: "", - }; - - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - - const state = await service.initialize("owner/minimal"); - - expect(state).toEqual({ - workflowFile: mockWorkflowFile, - slug: "owner/minimal", - workflowModelName: null, - workflowService: service, - }); - }); - }); - - describe("state getters", () => { - beforeEach(async () => { - const mockWorkflowFile = { - name: "Test Workflow", - description: "A test workflow", - model: "gpt-4", - tools: "bash,read,write", - rules: "Be helpful", - prompt: "You are a helpful assistant.", - }; - - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await service.initialize("owner/package"); - }); - - it("should return workflow state", () => { - const state = service.getState(); - expect(state.workflowFile?.name).toBe("Test Workflow"); - expect(state.slug).toBe("owner/package"); - expect(state.workflowFile?.model).toBe("gpt-4"); - expect(state.workflowFile?.tools).toBe("bash,read,write"); - expect(state.workflowFile?.rules).toBe("Be helpful"); - expect(state.workflowFile?.prompt).toBe("You are a helpful assistant."); - }); - }); - - describe("inactive workflow state", () => { - beforeEach(() => { - service.initialize(); - }); - - it("should return inactive state when no workflow", () => { - const state = service.getState(); - expect(state.workflowFile).toBeNull(); - expect(state.slug).toBeNull(); - }); - }); - - describe("partial workflow data", () => { - it("should handle workflow with only name and prompt", async () => { - const mockWorkflowFile = { - name: "Simple Workflow", - prompt: "Simple prompt", - }; - - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await service.initialize("owner/simple"); - - const state = service.getState(); - expect(state.workflowFile?.model).toBeUndefined(); - expect(state.workflowFile?.tools).toBeUndefined(); - expect(state.workflowFile?.rules).toBeUndefined(); - expect(state.workflowFile?.prompt).toBe("Simple prompt"); - }); - - it("should handle workflow with empty prompt", async () => { - const mockWorkflowFile = { - name: "Empty Prompt Workflow", - model: "gpt-3.5-turbo", - prompt: "", - }; - - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await service.initialize("owner/empty"); - - const state = service.getState(); - expect(state.workflowFile?.model).toBe("gpt-3.5-turbo"); - expect(state.workflowFile?.prompt).toBe(""); - }); - }); -}); - -describe("workflowProcessor", () => { - it("should have correct type and extensions", () => { - expect(workflowProcessor.type).toBe("workflow"); - expect(workflowProcessor.expectedFileExtensions).toEqual([".md"]); - }); - - it("should parse workflow content correctly", () => { - const content = `--- -name: Test Workflow -model: gpt-4 -tools: bash,read ---- -You are a helpful assistant.`; - - // Set up the mock to return expected result - const expectedResult = { - name: "Test Workflow", - model: "gpt-4", - tools: "bash,read", - prompt: "You are a helpful assistant.", - }; - (workflowProcessor.parseContent as Mock).mockReturnValue(expectedResult); - - const result = workflowProcessor.parseContent(content, "test.md"); - expect(result).toEqual({ - name: "Test Workflow", - model: "gpt-4", - tools: "bash,read", - prompt: "You are a helpful assistant.", - }); - }); - - it("should validate workflow content", () => { - const validWorkflow = { - name: "Valid Workflow", - prompt: "Test prompt", - }; - - const invalidWorkflow = { - prompt: "Test prompt", - // Missing name - }; - - // Set up mock validation responses - (workflowProcessor.validateContent as Mock) - .mockReturnValueOnce(true) // For valid workflow - .mockReturnValueOnce(false); // For invalid workflow - - expect(workflowProcessor.validateContent?.(validWorkflow)).toBe(true); - expect(workflowProcessor.validateContent?.(invalidWorkflow as any)).toBe( - false, - ); - }); - - it("should validate workflow with empty name as invalid", () => { - const invalidWorkflow = { - name: "", - prompt: "Test prompt", - }; - - // Mock validation to return false for empty name - (workflowProcessor.validateContent as Mock).mockReturnValue(false); - - expect(workflowProcessor.validateContent?.(invalidWorkflow)).toBe(false); - }); -}); diff --git a/extensions/cli/src/services/WorkflowService.ts b/extensions/cli/src/services/WorkflowService.ts deleted file mode 100644 index cc0096af57b..00000000000 --- a/extensions/cli/src/services/WorkflowService.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { loadPackageFromHub, workflowProcessor } from "../hubLoader.js"; -import { logger } from "../util/logger.js"; - -import { BaseService } from "./BaseService.js"; -import { serviceContainer } from "./ServiceContainer.js"; -import { WorkflowServiceState } from "./types.js"; - -/** - * Service for managing workflow state - * Loads workflows from the hub and extracts model, tools, and prompt information - */ -export class WorkflowService extends BaseService { - /** - * Set the resolved workflow model name after it's been processed - * Called by ConfigEnhancer after resolving the model slug - */ - setWorkflowModelName(modelName: string): void { - this.setState({ - workflowModelName: modelName, - }); - } - constructor() { - super("WorkflowService", { - workflowService: null, - workflowFile: null, - slug: null, - workflowModelName: null, - }); - } - - /** - * Initialize the workflow service with a workflow slug - */ - async doInitialize(workflowSlug?: string): Promise { - if (!workflowSlug) { - return { - workflowService: this, - workflowFile: null, - slug: null, - workflowModelName: null, - }; - } - - try { - const parts = workflowSlug.split("/"); - if (parts.length !== 2) { - throw new Error( - `Invalid workflow slug format. Expected "owner/package", got: ${workflowSlug}`, - ); - } - - const workflowFile = await loadPackageFromHub( - workflowSlug, - workflowProcessor, - ); - - return { - workflowService: this, - workflowFile, - slug: workflowSlug, - workflowModelName: null, // Will be set by ConfigEnhancer after model resolution - }; - } catch (error: any) { - logger.error("Failed to initialize WorkflowService:", error); - return { - workflowService: this, - workflowFile: null, - slug: null, - workflowModelName: null, - }; - } - } - - protected override setState(newState: Partial): void { - super.setState(newState); - serviceContainer.set("update", this.currentState); - } -} diff --git a/extensions/cli/src/services/workflow-integration.test.ts b/extensions/cli/src/services/agent-file-integration.test.ts similarity index 56% rename from extensions/cli/src/services/workflow-integration.test.ts rename to extensions/cli/src/services/agent-file-integration.test.ts index 6d68563af67..905f7a68340 100644 --- a/extensions/cli/src/services/workflow-integration.test.ts +++ b/extensions/cli/src/services/agent-file-integration.test.ts @@ -2,8 +2,8 @@ import { vi } from "vitest"; import { ConfigEnhancer } from "../configEnhancer.js"; +import { AgentFileService } from "./AgentFileService.js"; import { ModelService } from "./ModelService.js"; -import { WorkflowService } from "./WorkflowService.js"; // Mock the hubLoader module vi.mock("../hubLoader.js", () => ({ @@ -12,8 +12,8 @@ vi.mock("../hubLoader.js", () => ({ mcpProcessor: {}, modelProcessor: {}, processRule: vi.fn(), - workflowProcessor: { - type: "workflow", + agentFileProcessor: { + type: "agentFile", expectedFileExtensions: [".md"], parseContent: vi.fn(), validateContent: vi.fn(), @@ -40,8 +40,8 @@ vi.mock("../auth/workos.js", () => ({ getModelName: vi.fn(), })); -describe("Workflow Integration Tests", () => { - let workflowService: WorkflowService; +describe("Agent file Integration Tests", () => { + let agentFileService: AgentFileService; let modelService: ModelService; let configEnhancer: ConfigEnhancer; let mockLoadPackageFromHub: any; @@ -51,13 +51,13 @@ describe("Workflow Integration Tests", () => { let mockGetLlmApi: any; let mockModelProcessor: any; - const mockWorkflowFile = { - name: "Test Workflow", - description: "A test workflow for integration testing", - model: "gpt-4-workflow", + const mockAgentFile = { + name: "Test Agent File", + description: "A test agent for integration testing", + model: "gpt-4-agent", tools: "bash,read,write", rules: "Always be helpful and concise", - prompt: "You are a workflow assistant.", + prompt: "You are an assistant.", }; const mockAssistant = { @@ -94,7 +94,7 @@ describe("Workflow Integration Tests", () => { mockGetLlmApi = configModule.getLlmApi as any; // Create service instances - workflowService = new WorkflowService(); + agentFileService = new AgentFileService(); modelService = new ModelService(); configEnhancer = new ConfigEnhancer(); @@ -112,74 +112,74 @@ describe("Workflow Integration Tests", () => { ]); }); - describe("Workflow models are injected via ConfigEnhancer", () => { - it("should add workflow model to options when workflow is active", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + describe("Agent file models are injected via ConfigEnhancer", () => { + it("should add agent file model to options when agent file active", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - const workflowState = workflowService.getState(); - expect(workflowState.workflowFile?.model).toBe("gpt-4-workflow"); + const agentFileState = agentFileService.getState(); + expect(agentFileState.agentFile?.model).toBe("gpt-4-agent"); - // Mock loadPackageFromHub to return a model for the workflow model + // Mock loadPackageFromHub to return a model for the agent file model mockLoadPackageFromHub.mockResolvedValueOnce({ - name: "gpt-4-workflow", + name: "gpt-4-agent", provider: "openai", }); - // Test that ConfigEnhancer adds the workflow model to options + // Test that ConfigEnhancer adds the agent file model to options const baseConfig = { models: [] }; const baseOptions = {}; // No --model flag const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowState, + agentFileState, ); - // Should have loaded the workflow model directly via loadPackageFromHub + // Should have loaded the agent file model directly via loadPackageFromHub expect(mockLoadPackageFromHub).toHaveBeenCalledWith( - "gpt-4-workflow", + "gpt-4-agent", mockModelProcessor, ); - // The workflow model should be prepended to the models array + // The agent file model should be prepended to the models array expect(enhancedConfig.models).toHaveLength(1); expect(enhancedConfig.models?.[0]).toEqual({ - name: "gpt-4-workflow", + name: "gpt-4-agent", provider: "openai", }); }); - it("should not add workflow model when no workflow is active", async () => { - // Initialize workflow service without workflow - await workflowService.initialize(); + it("should not add agent file model when no agent file active", async () => { + // Initialize agent file service without agent + await agentFileService.initialize(); - const workflowState = workflowService.getState(); - expect(workflowState.workflowFile).toBeNull(); + const agentFileState = agentFileService.getState(); + expect(agentFileState.agentFile).toBeNull(); - // Test that ConfigEnhancer doesn't add any workflow models + // Test that ConfigEnhancer doesn't add any agent file models const baseConfig = { models: [] }; const baseOptions = {}; const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowState, + agentFileState, ); // Should not have enhanced with any models expect(enhancedConfig.models).toEqual([]); }); - it("should respect --model flag priority over workflow model", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + it("should respect --model flag priority over agent file model", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - // Mock loadPackageFromHub for workflow model and loadPackagesFromHub for user models + // Mock loadPackageFromHub for agent file model and loadPackagesFromHub for user models mockLoadPackageFromHub.mockResolvedValueOnce({ - name: "gpt-4-workflow", + name: "gpt-4-agent", provider: "openai", }); mockLoadPackagesFromHub.mockResolvedValueOnce([ @@ -196,7 +196,7 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowService.getState(), + agentFileService.getState(), ); // Should process the user model via loadPackagesFromHub @@ -205,9 +205,9 @@ describe("Workflow Integration Tests", () => { mockModelProcessor, ); - // Should also load the workflow model + // Should also load the agent file model expect(mockLoadPackageFromHub).toHaveBeenCalledWith( - "gpt-4-workflow", + "gpt-4-agent", mockModelProcessor, ); @@ -218,17 +218,17 @@ describe("Workflow Integration Tests", () => { provider: "anthropic", }); expect(enhancedConfig.models?.[1]).toEqual({ - name: "gpt-4-workflow", + name: "gpt-4-agent", provider: "openai", }); }); }); - describe("WorkflowService affects ConfigEnhancer", () => { - it("should inject workflow rules when workflow is active", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + describe("AgentFileService affects ConfigEnhancer", () => { + it("should inject agent file rules when agent file active", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); const baseConfig = { rules: ["existing rule"], @@ -237,22 +237,19 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); - // Rules should be processed normally since workflow rules are now added to options.rule - expect(mockProcessRule).toHaveBeenCalledWith(mockWorkflowFile.rules); + // Rules should be processed normally since agent file rules are now added to options.rule + expect(mockProcessRule).toHaveBeenCalledWith(mockAgentFile.rules); expect(enhancedConfig.rules).toHaveLength(2); - // The workflow rule is processed first, then existing rules - expect(mockProcessRule).toHaveBeenNthCalledWith( - 1, - mockWorkflowFile.rules, - ); + // The agent file rule is processed first, then existing rules + expect(mockProcessRule).toHaveBeenNthCalledWith(1, mockAgentFile.rules); }); - it("should not inject workflow rules when workflow is inactive", async () => { - // Initialize workflow service without workflow - await workflowService.initialize(); + it("should not inject agent file rules when agent file inactive", async () => { + // Initialize agent file service without agent file + await agentFileService.initialize(); const baseConfig = { rules: ["existing rule"], @@ -261,7 +258,7 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); expect(mockProcessRule).not.toHaveBeenCalled(); @@ -269,14 +266,14 @@ describe("Workflow Integration Tests", () => { expect(enhancedConfig?.rules?.[0]).toBe("existing rule"); }); - it("should not inject workflow rules when workflow has no rules", async () => { - const workflowWithoutRules = { - ...mockWorkflowFile, + it("should not inject agent file rules when agent file has no rules", async () => { + const agentFileWithoutRules = { + ...mockAgentFile, rules: undefined, }; - mockLoadPackageFromHub.mockResolvedValue(workflowWithoutRules); - await workflowService.initialize("owner/workflow"); + mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutRules); + await agentFileService.initialize("owner/agent"); const baseConfig = { rules: ["existing rule"], @@ -285,7 +282,7 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); expect(mockProcessRule).not.toHaveBeenCalled(); @@ -294,10 +291,10 @@ describe("Workflow Integration Tests", () => { }); }); - describe("Workflow model constraints", () => { - it("should filter available models to only workflow model when specified", async () => { - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + describe("Agent file model constraints", () => { + it("should filter available models to only agent file model when specified", async () => { + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); await modelService.initialize( mockAssistant as any, @@ -314,8 +311,8 @@ describe("Workflow Integration Tests", () => { ]); }); - it("should allow all models when no workflow is active", async () => { - await workflowService.initialize(); + it("should allow all models when no agent file active", async () => { + await agentFileService.initialize(); await modelService.initialize( mockAssistant as any, @@ -332,13 +329,13 @@ describe("Workflow Integration Tests", () => { }); describe("Error handling", () => { - it("should handle workflow loading errors gracefully", async () => { + it("should handle agent loading errors gracefully", async () => { mockLoadPackageFromHub.mockRejectedValue(new Error("Network error")); - await workflowService.initialize("owner/workflow"); + await agentFileService.initialize("owner/agent"); - const workflowState = workflowService.getState(); - expect(workflowState.workflowFile).toBeNull(); + const agentFileState = agentFileService.getState(); + expect(agentFileState.agentFile).toBeNull(); // Model service should work normally await modelService.initialize( @@ -348,11 +345,11 @@ describe("Workflow Integration Tests", () => { expect(mockGetLlmApi).toHaveBeenCalled(); }); - it("should handle workflow rule processing errors gracefully", async () => { - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); + it("should handle agent rule processing errors gracefully", async () => { + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); mockProcessRule.mockRejectedValue(new Error("Rule processing failed")); - await workflowService.initialize("owner/workflow"); + await agentFileService.initialize("owner/agent"); const baseConfig = { rules: ["existing rule"], @@ -361,21 +358,21 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); - // Should not inject workflow rule but should preserve existing rules + // Should not inject agent file rule but should preserve existing rules expect(enhancedConfig.rules).toHaveLength(1); expect(enhancedConfig.rules?.[0]).toBe("existing rule"); }); - // Removed test for missing service container since workflow service + // Removed test for missing service container since agent file service // should always be initialized before ConfigEnhancer is called - it("should inject workflow prompt when workflow is active", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + it("should inject agent file prompt when agent file active", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); const baseConfig = { rules: ["existing rule"], @@ -385,23 +382,23 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); - // Workflow prompt should be added to config.prompts + // Agent file prompt should be added to config.prompts expect(enhancedConfig.prompts).toBeDefined(); expect(enhancedConfig.prompts?.length).toBeGreaterThan(0); expect(enhancedConfig.prompts?.[0]).toMatchObject({ - prompt: "You are a workflow assistant.", - name: expect.stringContaining("Test Workflow"), + prompt: "You are an assistant.", + name: expect.stringContaining("Test Agent"), }); expect(enhancedConfig.rules).toHaveLength(2); }); - it("should add workflow prompt to config alongside other prompts", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + it("should add agent file prompt to config alongside other prompts", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); const baseConfig = { prompts: [{ name: "Existing", prompt: "existing-prompt" }], @@ -411,14 +408,14 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowService.getState(), + agentFileService.getState(), ); - // Workflow prompt should be prepended to existing prompts + // Agent file prompt should be prepended to existing prompts expect(enhancedConfig.prompts).toHaveLength(2); expect(enhancedConfig.prompts?.[0]).toMatchObject({ - name: expect.stringContaining("Test Workflow"), - prompt: "You are a workflow assistant.", + name: expect.stringContaining("Test Agent"), + prompt: "You are an assistant.", }); expect(enhancedConfig.prompts?.[1]).toMatchObject({ name: "Existing", @@ -426,14 +423,14 @@ describe("Workflow Integration Tests", () => { }); }); - it("should not add workflow prompt when workflow has no prompt", async () => { - const workflowWithoutPrompt = { - ...mockWorkflowFile, + it("should not add agent file prompt when agent file has no prompt", async () => { + const agentFileWithoutPrompt = { + ...mockAgentFile, prompt: undefined, }; - mockLoadPackageFromHub.mockResolvedValue(workflowWithoutPrompt); - await workflowService.initialize("owner/workflow"); + mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutPrompt); + await agentFileService.initialize("owner/agent"); const baseConfig = { prompts: [{ name: "Existing", prompt: "existing-prompt" }], @@ -443,10 +440,10 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowService.getState(), + agentFileService.getState(), ); - // Should only have the existing prompt, no workflow prompt added + // Should only have the existing prompt, no agent file prompt added expect(enhancedConfig.prompts).toHaveLength(1); expect(enhancedConfig.prompts?.[0]).toMatchObject({ name: "Existing", @@ -456,111 +453,105 @@ describe("Workflow Integration Tests", () => { }); describe("ConfigEnhancer prompt integration", () => { - it("should add workflow prompt to config.prompts when workflow is active", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + it("should add agent file prompt to config.prompts when agent file active", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); const baseOptions = { prompt: ["user-prompt"] }; const baseConfig = { prompts: [] }; - // Enhance config with workflow state + // Enhance config with agent file state const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowService.getState(), + agentFileService.getState(), ); - // Verify that the workflow prompt was added to config.prompts + // Verify that the agent file prompt was added to config.prompts expect(enhancedConfig.prompts).toBeDefined(); expect(enhancedConfig.prompts).toHaveLength(1); expect(enhancedConfig.prompts?.[0]).toMatchObject({ - name: expect.stringContaining("Test Workflow"), - prompt: "You are a workflow assistant.", - description: "A test workflow for integration testing", + name: expect.stringContaining("Test Agent"), + prompt: "You are an assistant.", + description: "A test agent for integration testing", }); }); - it("should work end-to-end with workflow prompt in config", async () => { - // Setup workflow service with active workflow - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + it("should work end-to-end with agent file prompt in config", async () => { + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - const workflowState = workflowService.getState(); - expect(workflowState.workflowFile?.prompt).toBe( - "You are a workflow assistant.", - ); + const agentFileState = agentFileService.getState(); + expect(agentFileState.agentFile?.prompt).toBe("You are an assistant."); const baseConfig = { prompts: [] }; const baseOptions = { prompt: ["Tell me about TypeScript"] }; - // Enhance config with workflow + // Enhance config with agent file const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, baseOptions, - workflowState, + agentFileState, ); - // Verify workflow prompt is added to config.prompts + // Verify agent file prompt is added to config.prompts expect(enhancedConfig.prompts).toBeDefined(); expect(enhancedConfig.prompts?.length).toBeGreaterThan(0); - expect(enhancedConfig.prompts?.[0]?.prompt).toBe( - "You are a workflow assistant.", - ); - expect(enhancedConfig.prompts?.[0]?.name).toContain("Test Workflow"); + expect(enhancedConfig.prompts?.[0]?.prompt).toBe("You are an assistant."); + expect(enhancedConfig.prompts?.[0]?.name).toContain("Test Agent"); }); }); - describe("Workflow data extraction", () => { - it("should correctly extract all workflow properties", async () => { - mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile); - await workflowService.initialize("owner/workflow"); + describe("Agent file data extraction", () => { + it("should correctly extract all agent file properties", async () => { + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - const workflowState = workflowService.getState(); - expect(workflowState.workflowFile?.model).toBe("gpt-4-workflow"); - expect(workflowState.workflowFile?.tools).toBe("bash,read,write"); - expect(workflowState.workflowFile?.rules).toBe( + const agentFileState = agentFileService.getState(); + expect(agentFileState.agentFile?.model).toBe("gpt-4-agent"); + expect(agentFileState.agentFile?.tools).toBe("bash,read,write"); + expect(agentFileState.agentFile?.rules).toBe( "Always be helpful and concise", ); - expect(workflowState.workflowFile?.prompt).toBe( - "You are a workflow assistant.", - ); - expect(workflowState.slug).toBe("owner/workflow"); + expect(agentFileState.agentFile?.prompt).toBe("You are an assistant."); + expect(agentFileState.slug).toBe("owner/agent"); }); - it("should handle partial workflow data", async () => { - const partialWorkflow = { - name: "Partial Workflow", + it("should handle partial agent file data", async () => { + const partialAgentFile = { + name: "Partial Agent File", model: "gpt-3.5-turbo", prompt: "Partial prompt", // No tools or rules }; - mockLoadPackageFromHub.mockResolvedValue(partialWorkflow); - await workflowService.initialize("owner/partial"); + mockLoadPackageFromHub.mockResolvedValue(partialAgentFile); + await agentFileService.initialize("owner/partial"); - const workflowState = workflowService.getState(); - expect(workflowState.workflowFile?.model).toBe("gpt-3.5-turbo"); - expect(workflowState.workflowFile?.tools).toBeUndefined(); - expect(workflowState.workflowFile?.rules).toBeUndefined(); - expect(workflowState.workflowFile?.prompt).toBe("Partial prompt"); + const agentFileState = agentFileService.getState(); + expect(agentFileState.agentFile?.model).toBe("gpt-3.5-turbo"); + expect(agentFileState.agentFile?.tools).toBeUndefined(); + expect(agentFileState.agentFile?.rules).toBeUndefined(); + expect(agentFileState.agentFile?.prompt).toBe("Partial prompt"); }); }); - describe("Workflow tools integration", () => { - it("should inject MCP servers from workflow tools", async () => { - const workflowWithTools = { - ...mockWorkflowFile, + describe("Agent file tools integration", () => { + it("should inject MCP servers from agent file tools", async () => { + const agentFileWithTools = { + ...mockAgentFile, tools: "owner/mcp1, another/mcp2:specific_tool", }; // Clear the default mock and setup specific mocks mockLoadPackageFromHub.mockReset(); - // First call loads the workflow file - mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithTools); - // Second call loads the workflow model + // First call loads the agent file + mockLoadPackageFromHub.mockResolvedValueOnce(agentFileWithTools); + // Second call loads the agent file model mockLoadPackageFromHub.mockResolvedValueOnce({ - name: "gpt-4-workflow", + name: "gpt-4-agent", provider: "openai", }); // Third call loads mcp1 @@ -568,7 +559,7 @@ describe("Workflow Integration Tests", () => { // Fourth call loads mcp2 mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp2" }); - await workflowService.initialize("owner/workflow"); + await agentFileService.initialize("owner/agent"); const baseConfig = { mcpServers: [{ name: "existing-mcp" }], @@ -577,7 +568,7 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); expect(enhancedConfig.mcpServers).toHaveLength(3); @@ -587,15 +578,15 @@ describe("Workflow Integration Tests", () => { expect(enhancedConfig.mcpServers?.[2]).toEqual({ name: "existing-mcp" }); }); - it("should not inject MCP servers when workflow has no tools", async () => { - const workflowWithoutTools = { - ...mockWorkflowFile, + it("should not inject MCP servers when agent file has no tools", async () => { + const agentWithoutTools = { + ...mockAgentFile, tools: undefined, }; mockLoadPackageFromHub.mockReset(); - mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithoutTools); - await workflowService.initialize("owner/workflow"); + mockLoadPackageFromHub.mockResolvedValueOnce(agentWithoutTools); + await agentFileService.initialize("owner/agent"); const baseConfig = { mcpServers: [{ name: "existing-mcp" }], @@ -604,7 +595,7 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); expect(enhancedConfig.mcpServers).toHaveLength(1); @@ -612,24 +603,24 @@ describe("Workflow Integration Tests", () => { }); it("should deduplicate MCP servers", async () => { - const workflowWithDuplicateTools = { - ...mockWorkflowFile, + const agentFileWithDuplicateTools = { + ...mockAgentFile, tools: "owner/mcp1, owner/mcp1:tool1, owner/mcp1:tool2", }; // Clear the default mock and setup specific mocks mockLoadPackageFromHub.mockReset(); - // First call loads the workflow file - mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithDuplicateTools); - // Second call loads the workflow model + // First call loads the agent file + mockLoadPackageFromHub.mockResolvedValueOnce(agentFileWithDuplicateTools); + // Second call loads the agent file model mockLoadPackageFromHub.mockResolvedValueOnce({ - name: "gpt-4-workflow", + name: "gpt-4-agent", provider: "openai", }); - // Third call: The parseWorkflowTools will extract only unique MCP servers, so only one loadPackageFromHub call + // Third call: The parseAgentFileTools will extract only unique MCP servers, so only one loadPackageFromHub call mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" }); - await workflowService.initialize("owner/workflow"); + await agentFileService.initialize("owner/agent"); const baseConfig = { mcpServers: [{ name: "existing-mcp" }], // Changed to avoid confusion @@ -638,10 +629,10 @@ describe("Workflow Integration Tests", () => { const enhancedConfig = await configEnhancer.enhanceConfig( baseConfig as any, {}, - workflowService.getState(), + agentFileService.getState(), ); - // parseWorkflowTools deduplicates, so we only get mcp1 once + // parseAgentFileTools deduplicates, so we only get mcp1 once expect(enhancedConfig.mcpServers).toHaveLength(2); expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "mcp1" }); expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "existing-mcp" }); diff --git a/extensions/cli/src/services/index.test.ts b/extensions/cli/src/services/index.test.ts index 3e0109f1902..07145f70549 100644 --- a/extensions/cli/src/services/index.test.ts +++ b/extensions/cli/src/services/index.test.ts @@ -1,6 +1,6 @@ import { vi } from "vitest"; -import { WorkflowService } from "./WorkflowService.js"; +import { AgentFileService } from "./AgentFileService.js"; import { initializeServices, services } from "./index.js"; @@ -50,9 +50,9 @@ describe("initializeServices", () => { }, { slug: null, - workflowFile: null, - workflowModelName: null, - workflowService: expect.any(WorkflowService), + agentFile: null, + agentFileModelName: null, + agentFileService: expect.any(AgentFileService), }, ); }); diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index 9ff58f36530..79b0b42697a 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -2,6 +2,7 @@ import { loadAuthConfig } from "../auth/workos.js"; import { initializeWithOnboarding } from "../onboarding.js"; import { logger } from "../util/logger.js"; +import { AgentFileService } from "./AgentFileService.js"; import { ApiClientService } from "./ApiClientService.js"; import { AuthService } from "./AuthService.js"; import { ChatHistoryService } from "./ChatHistoryService.js"; @@ -18,15 +19,14 @@ import { ToolPermissionService, } from "./ToolPermissionService.js"; import { + AgentFileServiceState, ApiClientServiceState, AuthServiceState, ConfigServiceState, SERVICE_NAMES, ServiceInitOptions, - WorkflowServiceState, } from "./types.js"; import { UpdateService } from "./UpdateService.js"; -import { WorkflowService } from "./WorkflowService.js"; // Service instances const authService = new AuthService(); @@ -39,7 +39,7 @@ const resourceMonitoringService = new ResourceMonitoringService(); const chatHistoryService = new ChatHistoryService(); const updateService = new UpdateService(); const storageSyncService = new StorageSyncService(); -const workflowService = new WorkflowService(); +const agentFileService = new AgentFileService(); const toolPermissionService = new ToolPermissionService(); const systemMessageService = new SystemMessageService(); @@ -76,16 +76,16 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { } serviceContainer.register( - SERVICE_NAMES.WORKFLOW, - () => workflowService.initialize(commandOptions.workflow), + SERVICE_NAMES.AGENT_FILE, + () => agentFileService.initialize(commandOptions.agent), [], ); serviceContainer.register( SERVICE_NAMES.TOOL_PERMISSIONS, async () => { - const workflowState = await serviceContainer.get( - SERVICE_NAMES.WORKFLOW, + const agentFileState = await serviceContainer.get( + SERVICE_NAMES.AGENT_FILE, ); // Initialize mode service with tool permission overrides @@ -105,18 +105,18 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { initArgs.mode = overrides.mode; } // If mode is "normal" or undefined, no flags are set - return await toolPermissionService.initialize(initArgs, workflowState); + return await toolPermissionService.initialize(initArgs, agentFileState); } else { // Even if no overrides, we need to initialize with defaults return await toolPermissionService.initialize( { isHeadless: initOptions.headless, }, - workflowState, + agentFileState, ); } }, - [SERVICE_NAMES.WORKFLOW], + [SERVICE_NAMES.AGENT_FILE], ); // Initialize SystemMessageService with command options @@ -157,10 +157,10 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { serviceContainer.register( SERVICE_NAMES.CONFIG, async () => { - const [authState, apiClientState, workflowState] = await Promise.all([ + const [authState, apiClientState, agentFileState] = await Promise.all([ serviceContainer.get(SERVICE_NAMES.AUTH), serviceContainer.get(SERVICE_NAMES.API_CLIENT), - serviceContainer.get(SERVICE_NAMES.WORKFLOW), + serviceContainer.get(SERVICE_NAMES.AGENT_FILE), ]); // Ensure organization is selected if authenticated @@ -205,20 +205,20 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { configPath, // organizationId: finalAuthState.organizationId || null, apiClient: apiClientState.apiClient, - workflowState, + agentFileState, injectedConfigOptions: commandOptions, }); }, - [SERVICE_NAMES.AUTH, SERVICE_NAMES.API_CLIENT, SERVICE_NAMES.WORKFLOW], // Dependencies + [SERVICE_NAMES.AUTH, SERVICE_NAMES.API_CLIENT, SERVICE_NAMES.AGENT_FILE], // Dependencies ); serviceContainer.register( SERVICE_NAMES.MODEL, async () => { - const [configState, authState, workflowState] = await Promise.all([ + const [configState, authState, agentFileState] = await Promise.all([ serviceContainer.get(SERVICE_NAMES.CONFIG), serviceContainer.get(SERVICE_NAMES.AUTH), - serviceContainer.get(SERVICE_NAMES.WORKFLOW), + serviceContainer.get(SERVICE_NAMES.AGENT_FILE), ]); if (!configState.config) { @@ -228,10 +228,10 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { return modelService.initialize( configState.config, authState.authConfig, - workflowState, + agentFileState, ); }, - [SERVICE_NAMES.CONFIG, SERVICE_NAMES.AUTH, SERVICE_NAMES.WORKFLOW], // Depends on config, auth, and workflow + [SERVICE_NAMES.CONFIG, SERVICE_NAMES.AUTH, SERVICE_NAMES.AGENT_FILE], // Depends on config, auth, and agentFile ); serviceContainer.register( @@ -332,7 +332,7 @@ export const services = { chatHistory: chatHistoryService, updateService: updateService, storageSync: storageSyncService, - workflow: workflowService, + agentFile: agentFileService, toolPermissions: toolPermissionService, } as const; diff --git a/extensions/cli/src/services/types.ts b/extensions/cli/src/services/types.ts index 9ce7b355b9c..cbe4121d482 100644 --- a/extensions/cli/src/services/types.ts +++ b/extensions/cli/src/services/types.ts @@ -1,7 +1,7 @@ import { + AgentFile, AssistantUnrolled, ModelConfig, - WorkflowFile, } from "@continuedev/config-yaml"; import { BaseLlmApi } from "@continuedev/openai-adapters"; import { AssistantConfig } from "@continuedev/sdk"; @@ -12,8 +12,8 @@ import { AuthConfig } from "../auth/workos.js"; import { BaseCommandOptions } from "../commands/BaseCommandOptions.js"; import { PermissionMode } from "../permissions/types.js"; +import { AgentFileService } from "./AgentFileService.js"; import { type MCPService } from "./MCPService.js"; -import { WorkflowService } from "./WorkflowService.js"; /** * Service lifecycle states @@ -116,11 +116,11 @@ export interface StorageSyncServiceState { lastError?: string | null; } -export interface WorkflowServiceState { - workflowFile: WorkflowFile | null; +export interface AgentFileServiceState { + agentFile: AgentFile | null; slug: string | null; - workflowModelName: string | null; - workflowService: WorkflowService | null; + agentFileModelName: string | null; + agentFileService: AgentFileService | null; } export type { ChatHistoryState } from "./ChatHistoryService.js"; @@ -142,7 +142,7 @@ export const SERVICE_NAMES = { CHAT_HISTORY: "chatHistory", UPDATE: "update", STORAGE_SYNC: "storageSync", - WORKFLOW: "workflow", + AGENT_FILE: "agentFile", } as const; /** diff --git a/extensions/cli/src/shared-options.ts b/extensions/cli/src/shared-options.ts index 54a2a415403..43f00b2dbbf 100644 --- a/extensions/cli/src/shared-options.ts +++ b/extensions/cli/src/shared-options.ts @@ -86,8 +86,8 @@ export function addCommonOptions(command: Command): Command { [] as string[], ) .option( - "--workflow ", - "Load workflow from the hub (slug in format 'owner/package')", + "--agent ", + "Load agent file from the hub (slug in format 'owner/package')", ); } @@ -111,7 +111,7 @@ export function mergeParentOptions(parentCommand: Command, options: any): any { "allow", "ask", "exclude", - "workflow", + "agent", ]; for (const optName of inheritableOptions) { diff --git a/packages/config-yaml/src/markdown/workflowFiles.test.ts b/packages/config-yaml/src/markdown/agentFiles.test.ts similarity index 70% rename from packages/config-yaml/src/markdown/workflowFiles.test.ts rename to packages/config-yaml/src/markdown/agentFiles.test.ts index 50e802418b6..5f1a951af1c 100644 --- a/packages/config-yaml/src/markdown/workflowFiles.test.ts +++ b/packages/config-yaml/src/markdown/agentFiles.test.ts @@ -1,13 +1,13 @@ import { - parseWorkflowFile, - parseWorkflowTools, - serializeWorkflowFile, - type WorkflowFile, -} from "./workflowFiles.js"; + AgentFile, + parseAgentFile, + parseAgentFileTools, + serializeAgentFile, +} from "./agentFiles.js"; const example = ` --- -name: Example Agent / Workflow +name: Example Agent / Agent File description: Trying to wrap my head around what are these files model: anthropic/claude-sonnet-4 tools: linear-mcp, sentry-mcp:read-alerts, Read, Glob, Bash(git diff:*) @@ -18,7 +18,7 @@ This is the prompt const minimalExample = ` --- -name: Minimal Workflow +name: Minimal Agent File --- Just a simple prompt `.trim(); @@ -39,12 +39,12 @@ name: [invalid: yaml: syntax This should fail `.trim(); -describe("parseWorkflowFile", () => { - test("parses complete workflow file correctly", () => { - const result = parseWorkflowFile(example); +describe("parseAgentFile", () => { + test("parses complete agent file correctly", () => { + const result = parseAgentFile(example); expect(result).toEqual({ - name: "Example Agent / Workflow", + name: "Example Agent / Agent File", description: "Trying to wrap my head around what are these files", model: "anthropic/claude-sonnet-4", tools: "linear-mcp, sentry-mcp:read-alerts, Read, Glob, Bash(git diff:*)", @@ -53,11 +53,11 @@ describe("parseWorkflowFile", () => { }); }); - test("parses minimal workflow file with only name", () => { - const result = parseWorkflowFile(minimalExample); + test("parses minimal agent file with only name", () => { + const result = parseAgentFile(minimalExample); expect(result).toEqual({ - name: "Minimal Workflow", + name: "Minimal Agent File", prompt: "Just a simple prompt", }); @@ -69,19 +69,19 @@ describe("parseWorkflowFile", () => { }); test("throws error when name field is missing", () => { - expect(() => parseWorkflowFile(invalidExample)).toThrow( - "Workflow file must contain YAML frontmatter with a 'name' field", + expect(() => parseAgentFile(invalidExample)).toThrow( + "Agent file must contain YAML frontmatter with a 'name' field", ); }); test("throws error when no frontmatter is present", () => { - expect(() => parseWorkflowFile(noFrontmatterExample)).toThrow( - "Workflow file must contain YAML frontmatter with a 'name' field", + expect(() => parseAgentFile(noFrontmatterExample)).toThrow( + "Agent file must contain YAML frontmatter with a 'name' field", ); }); test("throws error with invalid YAML syntax", () => { - expect(() => parseWorkflowFile(invalidYamlExample)).toThrow(); + expect(() => parseAgentFile(invalidYamlExample)).toThrow(); }); test("handles empty frontmatter with name", () => { @@ -91,7 +91,7 @@ name: Empty Test --- `.trim(); - const result = parseWorkflowFile(emptyFrontmatter); + const result = parseAgentFile(emptyFrontmatter); expect(result).toEqual({ name: "Empty Test", prompt: "", @@ -109,7 +109,7 @@ Line 2 Line 4 with gap `.trim(); - const result = parseWorkflowFile(multilineExample); + const result = parseAgentFile(multilineExample); expect(result.prompt).toBe("Line 1\nLine 2\n\nLine 4 with gap"); }); @@ -122,7 +122,7 @@ This has --- in the middle And more content after `.trim(); - const result = parseWorkflowFile(dashedContentExample); + const result = parseAgentFile(dashedContentExample); expect(result.prompt).toBe( "This has --- in the middle\nAnd more content after", ); @@ -137,29 +137,29 @@ extraField: should be ignored by schema Prompt content `.trim(); - const result = parseWorkflowFile(extraFieldsExample); + const result = parseAgentFile(extraFieldsExample); expect(result.name).toBe("Extra Fields Test"); expect(result.prompt).toBe("Prompt content"); // Extra fields should not be in the result expect("extraField" in result).toBe(false); }); - test("parses workflow with tools but no rules", () => { + test("parses agent file with tools but no rules", () => { const toolsOnlyExample = ` --- -name: Tools Only Workflow -description: A workflow that uses tools but no rules +name: Tools Only Agent File +description: An agent file that uses tools but no rules tools: git, filesystem, search --- -This workflow uses tools but doesn't define any rules. +This agent file uses tools but doesn't define any rules. `.trim(); - const result = parseWorkflowFile(toolsOnlyExample); + const result = parseAgentFile(toolsOnlyExample); expect(result).toEqual({ - name: "Tools Only Workflow", - description: "A workflow that uses tools but no rules", + name: "Tools Only Agent File", + description: "An agent file that uses tools but no rules", tools: "git, filesystem, search", - prompt: "This workflow uses tools but doesn't define any rules.", + prompt: "This agent file uses tools but doesn't define any rules.", }); // Rules should be undefined @@ -174,43 +174,43 @@ name: 123 Prompt `.trim(); - expect(() => parseWorkflowFile(invalidNameType)).toThrow( - "Invalid workflow file frontmatter", + expect(() => parseAgentFile(invalidNameType)).toThrow( + "Invalid agent file frontmatter", ); }); }); -describe("serializeWorkflowFile", () => { - test("serializes complete workflow file correctly", () => { - const workflowFile: WorkflowFile = { - name: "Test Workflow", - description: "A test workflow", +describe("serializeAgentFile", () => { + test("serializes complete agent file correctly", () => { + const agentFile: AgentFile = { + name: "Test Agent File", + description: "A test agent file", model: "anthropic/claude-3-sonnet", tools: "tool1, tool2", rules: "rule1, rule2", prompt: "This is the test prompt", }; - const result = serializeWorkflowFile(workflowFile); + const result = serializeAgentFile(agentFile); // Parse it back to verify round-trip consistency - const parsed = parseWorkflowFile(result); - expect(parsed).toEqual(workflowFile); + const parsed = parseAgentFile(result); + expect(parsed).toEqual(agentFile); }); - test("serializes minimal workflow file", () => { - const workflowFile: WorkflowFile = { + test("serializes minimal agent file", () => { + const agentFile: AgentFile = { name: "Minimal", prompt: "Simple prompt", }; - const result = serializeWorkflowFile(workflowFile); - const parsed = parseWorkflowFile(result); - expect(parsed).toEqual(workflowFile); + const result = serializeAgentFile(agentFile); + const parsed = parseAgentFile(result); + expect(parsed).toEqual(agentFile); }); test("filters out undefined values from frontmatter", () => { - const workflowFile: WorkflowFile = { + const agentFile: AgentFile = { name: "Test", description: undefined, model: "gpt-4", @@ -219,7 +219,7 @@ describe("serializeWorkflowFile", () => { prompt: "Test prompt", }; - const result = serializeWorkflowFile(workflowFile); + const result = serializeAgentFile(agentFile); // Should not contain undefined fields in YAML expect(result).not.toContain("description"); @@ -228,7 +228,7 @@ describe("serializeWorkflowFile", () => { expect(result).toContain("model: gpt-4"); // Verify round-trip - const parsed = parseWorkflowFile(result); + const parsed = parseAgentFile(result); expect(parsed.name).toBe("Test"); expect(parsed.model).toBe("gpt-4"); expect(parsed.description).toBeUndefined(); @@ -237,41 +237,41 @@ describe("serializeWorkflowFile", () => { }); test("handles empty prompt", () => { - const workflowFile: WorkflowFile = { + const agentFile: AgentFile = { name: "Empty Prompt", prompt: "", }; - const result = serializeWorkflowFile(workflowFile); - const parsed = parseWorkflowFile(result); + const result = serializeAgentFile(agentFile); + const parsed = parseAgentFile(result); expect(parsed.prompt).toBe(""); }); test("preserves multiline prompts", () => { - const workflowFile: WorkflowFile = { + const agentFile: AgentFile = { name: "Multiline", prompt: "Line 1\nLine 2\n\nLine 4", }; - const result = serializeWorkflowFile(workflowFile); - const parsed = parseWorkflowFile(result); - expect(parsed.prompt).toBe(workflowFile.prompt); + const result = serializeAgentFile(agentFile); + const parsed = parseAgentFile(result); + expect(parsed.prompt).toBe(agentFile.prompt); }); }); describe("round-trip consistency", () => { - test("example workflow maintains consistency", () => { - const parsed = parseWorkflowFile(example); - const serialized = serializeWorkflowFile(parsed); - const reparsed = parseWorkflowFile(serialized); + test("example agent file maintains consistency", () => { + const parsed = parseAgentFile(example); + const serialized = serializeAgentFile(parsed); + const reparsed = parseAgentFile(serialized); expect(reparsed).toEqual(parsed); }); - test("minimal workflow maintains consistency", () => { - const parsed = parseWorkflowFile(minimalExample); - const serialized = serializeWorkflowFile(parsed); - const reparsed = parseWorkflowFile(serialized); + test("minimal agent file maintains consistency", () => { + const parsed = parseAgentFile(minimalExample); + const serialized = serializeAgentFile(parsed); + const reparsed = parseAgentFile(serialized); expect(reparsed).toEqual(parsed); }); @@ -280,15 +280,15 @@ describe("round-trip consistency", () => { describe("edge cases", () => { test("handles Windows line endings", () => { const windowsExample = example.replace(/\n/g, "\r\n"); - const result = parseWorkflowFile(windowsExample); + const result = parseAgentFile(windowsExample); - expect(result.name).toBe("Example Agent / Workflow"); + expect(result.name).toBe("Example Agent / Agent File"); expect(result.prompt).toBe("This is the prompt"); }); test("handles mixed line endings", () => { const mixedExample = `---\r\nname: Mixed\n---\r\nPrompt content\n`; - const result = parseWorkflowFile(mixedExample); + const result = parseAgentFile(mixedExample); expect(result.name).toBe("Mixed"); expect(result.prompt).toBe("Prompt content"); @@ -302,8 +302,8 @@ name: "" Prompt `.trim(); - expect(() => parseWorkflowFile(emptyNameExample)).toThrow( - "Workflow file must contain YAML frontmatter with a 'name' field", + expect(() => parseAgentFile(emptyNameExample)).toThrow( + "Agent file must contain YAML frontmatter with a 'name' field", ); }); @@ -315,28 +315,28 @@ name: null Prompt `.trim(); - expect(() => parseWorkflowFile(nullNameExample)).toThrow( - "Workflow file must contain YAML frontmatter with a 'name' field", + expect(() => parseAgentFile(nullNameExample)).toThrow( + "Agent file must contain YAML frontmatter with a 'name' field", ); }); }); -describe("parseWorkflowTools", () => { +describe("parseAgentFileTools", () => { it("should return empty arrays for undefined tools", () => { - const result = parseWorkflowTools(undefined); + const result = parseAgentFileTools(undefined); expect(result).toEqual({ tools: [], mcpServers: [], allBuiltIn: false }); }); it("should return empty arrays for empty tools string", () => { - const result = parseWorkflowTools(""); + const result = parseAgentFileTools(""); expect(result).toEqual({ tools: [], mcpServers: [], allBuiltIn: false }); - const result2 = parseWorkflowTools(" "); + const result2 = parseAgentFileTools(" "); expect(result2).toEqual({ tools: [], mcpServers: [], allBuiltIn: false }); }); it("should parse built-in tools", () => { - const result = parseWorkflowTools("bash, read, edit"); + const result = parseAgentFileTools("bash, read, edit"); expect(result).toEqual({ tools: [{ toolName: "bash" }, { toolName: "read" }, { toolName: "edit" }], mcpServers: [], @@ -345,7 +345,7 @@ describe("parseWorkflowTools", () => { }); it("should parse built_in keyword", () => { - const result = parseWorkflowTools("built_in"); + const result = parseAgentFileTools("built_in"); expect(result).toEqual({ tools: [], mcpServers: [], @@ -354,7 +354,7 @@ describe("parseWorkflowTools", () => { }); it("should parse built_in with other tools", () => { - const result = parseWorkflowTools("built_in, owner/package"); + const result = parseAgentFileTools("built_in, owner/package"); expect(result).toEqual({ tools: [{ mcpServer: "owner/package" }], mcpServers: ["owner/package"], @@ -363,7 +363,7 @@ describe("parseWorkflowTools", () => { }); it("should parse MCP server (all tools)", () => { - const result = parseWorkflowTools("owner/package, another/server"); + const result = parseAgentFileTools("owner/package, another/server"); expect(result).toEqual({ tools: [{ mcpServer: "owner/package" }, { mcpServer: "another/server" }], mcpServers: ["owner/package", "another/server"], @@ -372,7 +372,7 @@ describe("parseWorkflowTools", () => { }); it("should parse specific MCP tools", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "owner/package:tool1, owner/package:tool2", ); expect(result).toEqual({ @@ -386,7 +386,7 @@ describe("parseWorkflowTools", () => { }); it("should parse mixed tool types", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "anmcp/serverslug:a_tool, anmcp/serverslug:another_tool, asecond/mcpserver, bash, read, edit", ); expect(result).toEqual({ @@ -404,7 +404,7 @@ describe("parseWorkflowTools", () => { }); it("should parse mixed tools with built_in keyword", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "built_in, anmcp/serverslug:a_tool, asecond/mcpserver", ); expect(result).toEqual({ @@ -418,7 +418,7 @@ describe("parseWorkflowTools", () => { }); it("should deduplicate MCP servers", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "owner/package:tool1, owner/package:tool2, owner/package, other/server", ); expect(result).toEqual({ @@ -434,7 +434,7 @@ describe("parseWorkflowTools", () => { }); it("should handle extra whitespace", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( " owner/package:tool1 , bash , other/server ", ); expect(result).toEqual({ @@ -449,10 +449,10 @@ describe("parseWorkflowTools", () => { }); it("should handle any MCP server slug format", () => { - expect(() => parseWorkflowTools("invalid-slug")).not.toThrow(); - expect(() => parseWorkflowTools("invalid/slug/extra")).not.toThrow(); + expect(() => parseAgentFileTools("invalid-slug")).not.toThrow(); + expect(() => parseAgentFileTools("invalid/slug/extra")).not.toThrow(); expect(() => - parseWorkflowTools("owner/package:tool, invalid/slug/extra:tool"), + parseAgentFileTools("owner/package:tool, invalid/slug/extra:tool"), ).not.toThrow(); }); @@ -466,13 +466,13 @@ describe("parseWorkflowTools", () => { ]; for (const slug of validSlugs) { - expect(() => parseWorkflowTools(slug)).not.toThrow(); - expect(() => parseWorkflowTools(`${slug}:tool`)).not.toThrow(); + expect(() => parseAgentFileTools(slug)).not.toThrow(); + expect(() => parseAgentFileTools(`${slug}:tool`)).not.toThrow(); } }); it("should handle empty tool names and empty entries", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "owner/package:tool1, , owner/package:, bash", ); expect(result).toEqual({ @@ -488,61 +488,61 @@ describe("parseWorkflowTools", () => { describe("whitespace validation in colon-separated MCP tool references", () => { it("should reject MCP tool reference with space after colon", () => { - expect(() => parseWorkflowTools("owner/slug: tool")).toThrow( + expect(() => parseAgentFileTools("owner/slug: tool")).toThrow( 'Invalid MCP tool reference "owner/slug: tool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with space before colon", () => { - expect(() => parseWorkflowTools("owner/slug :tool")).toThrow( + expect(() => parseAgentFileTools("owner/slug :tool")).toThrow( 'Invalid MCP tool reference "owner/slug :tool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with spaces around colon", () => { - expect(() => parseWorkflowTools("owner/slug : tool")).toThrow( + expect(() => parseAgentFileTools("owner/slug : tool")).toThrow( 'Invalid MCP tool reference "owner/slug : tool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with space in tool name", () => { - expect(() => parseWorkflowTools("owner/slug:my tool")).toThrow( + expect(() => parseAgentFileTools("owner/slug:my tool")).toThrow( 'Invalid MCP tool reference "owner/slug:my tool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with space in server slug", () => { - expect(() => parseWorkflowTools("owner /slug:tool")).toThrow( + expect(() => parseAgentFileTools("owner /slug:tool")).toThrow( 'Invalid MCP tool reference "owner /slug:tool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with tab character", () => { - expect(() => parseWorkflowTools("owner/slug:\ttool")).toThrow( + expect(() => parseAgentFileTools("owner/slug:\ttool")).toThrow( 'Invalid MCP tool reference "owner/slug:\ttool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with newline character", () => { - expect(() => parseWorkflowTools("owner/slug:\ntool")).toThrow( + expect(() => parseAgentFileTools("owner/slug:\ntool")).toThrow( 'Invalid MCP tool reference "owner/slug:\ntool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with multiple spaces", () => { - expect(() => parseWorkflowTools("owner/slug: tool ")).toThrow( + expect(() => parseAgentFileTools("owner/slug: tool ")).toThrow( 'Invalid MCP tool reference "owner/slug: tool": colon-separated tool references cannot contain whitespace', ); }); it("should reject MCP tool reference with leading spaces in tool name", () => { - expect(() => parseWorkflowTools("owner/slug: tool")).toThrow( + expect(() => parseAgentFileTools("owner/slug: tool")).toThrow( 'Invalid MCP tool reference "owner/slug: tool": colon-separated tool references cannot contain whitespace', ); }); it("should accept valid colon-separated MCP tool reference without whitespace", () => { - const result = parseWorkflowTools("owner/package:tool_name"); + const result = parseAgentFileTools("owner/package:tool_name"); expect(result).toEqual({ tools: [{ mcpServer: "owner/package", toolName: "tool_name" }], mcpServers: ["owner/package"], @@ -551,7 +551,7 @@ describe("parseWorkflowTools", () => { }); it("should accept valid MCP tool references with hyphens and underscores", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "owner-name/package_name:tool-name_123", ); expect(result).toEqual({ @@ -567,7 +567,7 @@ describe("parseWorkflowTools", () => { }); it("should allow whitespace between comma-separated items", () => { - const result = parseWorkflowTools( + const result = parseAgentFileTools( "owner/package:tool1, owner/package:tool2 , bash", ); expect(result).toEqual({ @@ -583,7 +583,7 @@ describe("parseWorkflowTools", () => { it("should reject whitespace in one reference but accept valid references in same string", () => { expect(() => - parseWorkflowTools("owner/package:tool1, owner/package: tool2, bash"), + parseAgentFileTools("owner/package:tool1, owner/package: tool2, bash"), ).toThrow( 'Invalid MCP tool reference "owner/package: tool2": colon-separated tool references cannot contain whitespace', ); @@ -592,13 +592,13 @@ describe("parseWorkflowTools", () => { it("should not reject whitespace in MCP server references without colon", () => { // Note: This behavior may or may not be desired, but documents current behavior // Server-only references (no colon) don't trigger the whitespace check - expect(() => parseWorkflowTools("owner /package")).not.toThrow(); + expect(() => parseAgentFileTools("owner /package")).not.toThrow(); }); it("should not reject whitespace in built-in tool names", () => { // Note: This behavior may or may not be desired, but documents current behavior // Built-in tools (no slash) don't trigger the whitespace check - expect(() => parseWorkflowTools("my tool")).not.toThrow(); + expect(() => parseAgentFileTools("my tool")).not.toThrow(); }); }); }); diff --git a/packages/config-yaml/src/markdown/workflowFiles.ts b/packages/config-yaml/src/markdown/agentFiles.ts similarity index 72% rename from packages/config-yaml/src/markdown/workflowFiles.ts rename to packages/config-yaml/src/markdown/agentFiles.ts index 0e32c794d7f..5f072c4acc5 100644 --- a/packages/config-yaml/src/markdown/workflowFiles.ts +++ b/packages/config-yaml/src/markdown/agentFiles.ts @@ -3,28 +3,26 @@ import z from "zod"; import { parseMarkdownRule } from "./markdownToRule.js"; /* - Experimental/internal config format for workflows + Experimental/internal config format for agents */ -const workflowFileFrontmatterSchema = z.object({ +const agentFileFrontmatterSchema = z.object({ name: z.string().min(1, "Name cannot be empty"), description: z.string().optional(), model: z.string().optional(), tools: z.string().optional(), // TODO also accept yaml array rules: z.string().optional(), // TODO also accept yaml array }); -export type WorkflowFileFrontmatter = z.infer< - typeof workflowFileFrontmatterSchema ->; +export type AgentFileFrontmatter = z.infer; -const workflowFileSchema = workflowFileFrontmatterSchema.extend({ +const agentFileSchema = agentFileFrontmatterSchema.extend({ prompt: z.string(), }); -export type WorkflowFile = z.infer; +export type AgentFile = z.infer; /** - * Parsed workflow tool reference + * Parsed agent tool reference */ -export interface WorkflowToolReference { +export interface AgentToolReference { /** MCP server slug (owner/package) if this is an MCP tool */ mcpServer?: string; /** Specific tool name - either MCP tool name or built-in tool name */ @@ -32,11 +30,11 @@ export interface WorkflowToolReference { } /** - * Parsed workflow tools configuration + * Parsed agent tools configuration */ -export interface ParsedWorkflowTools { +export interface ParsedAgentTools { /** All tool references */ - tools: WorkflowToolReference[]; + tools: AgentToolReference[]; /** Unique MCP server slugs that need to be added to config */ mcpServers: string[]; /** Whether all built-in tools are allowed */ @@ -44,25 +42,25 @@ export interface ParsedWorkflowTools { } /** - * Parses and validates a workflow file from markdown content - * Workflow files must have frontmatter with at least a name + * Parses and validates an agent file from markdown content + * Agent files must have frontmatter with at least a name */ -export function parseWorkflowFile(content: string): WorkflowFile { +export function parseAgentFile(content: string): AgentFile { const { frontmatter, markdown } = parseMarkdownRule(content); if (!frontmatter.name) { throw new Error( - "Workflow file must contain YAML frontmatter with a 'name' field", + "Agent file must contain YAML frontmatter with a 'name' field", ); } - const validationResult = workflowFileFrontmatterSchema.safeParse(frontmatter); + const validationResult = agentFileFrontmatterSchema.safeParse(frontmatter); if (!validationResult.success) { const errorDetails = validationResult.error.issues .map((issue) => `${issue.path.join(".")}: ${issue.message}`) .join(", "); - throw new Error(`Invalid workflow file frontmatter: ${errorDetails}`); + throw new Error(`Invalid agent file frontmatter: ${errorDetails}`); } return { @@ -72,10 +70,10 @@ export function parseWorkflowFile(content: string): WorkflowFile { } /** - * Serializes a Workflow file back to markdown with YAML frontmatter + * Serializes an Agent file back to markdown with YAML frontmatter */ -export function serializeWorkflowFile(workflowFile: WorkflowFile): string { - const { prompt, ...frontmatter } = workflowFile; +export function serializeAgentFile(agentFile: AgentFile): string { + const { prompt, ...frontmatter } = agentFile; // Filter out undefined values from frontmatter const cleanFrontmatter = Object.fromEntries( @@ -88,7 +86,7 @@ export function serializeWorkflowFile(workflowFile: WorkflowFile): string { } /** - * Parse workflow tools string into structured format + * Parse agent tools string into structured format * * Supports formats: * - owner/package - all tools from MCP server @@ -99,12 +97,12 @@ export function serializeWorkflowFile(workflowFile: WorkflowFile): string { * @param toolsString Comma-separated tools string * @returns Parsed tools configuration */ -export function parseWorkflowTools(toolsString?: string): ParsedWorkflowTools { +export function parseAgentFileTools(toolsString?: string): ParsedAgentTools { if (!toolsString?.trim()) { return { tools: [], mcpServers: [], allBuiltIn: false }; } - const tools: WorkflowToolReference[] = []; + const tools: AgentToolReference[] = []; const mcpServerSet = new Set(); let allBuiltIn = false; diff --git a/packages/config-yaml/src/markdown/index.ts b/packages/config-yaml/src/markdown/index.ts index 08ea04dfa77..6414936f598 100644 --- a/packages/config-yaml/src/markdown/index.ts +++ b/packages/config-yaml/src/markdown/index.ts @@ -2,4 +2,4 @@ export * from "./createMarkdownPrompt.js"; export * from "./createMarkdownRule.js"; export * from "./getRuleType.js"; export * from "./markdownToRule.js"; -export * from "./workflowFiles.js"; +export * from "./agentFiles.js";