diff --git a/src/constants/agent-templates.ts b/src/constants/agent-templates.ts new file mode 100644 index 0000000..e785afe --- /dev/null +++ b/src/constants/agent-templates.ts @@ -0,0 +1,286 @@ +/** + * Agent and Skill Templates + * + * Templates for creating new agents and skills in .codetyper/ directories. + * Based on patterns from claude-code and opencode. + */ + +/** + * Valid agent colors with semantic meaning + */ +export const AGENT_COLORS = { + blue: "Analysis, review, code inspection", + cyan: "Analysis, review, code inspection", + green: "Success, generation, creation", + yellow: "Validation, caution, verification", + red: "Security, critical issues", + magenta: "Transformation, creative tasks", + white: "Neutral, general purpose", + gray: "Internal, utility", +} as const; + +/** + * Valid model options + */ +export const AGENT_MODELS = { + inherit: "Uses parent model (recommended)", + sonnet: "Claude Sonnet (balanced, default)", + opus: "Claude Opus (most capable)", + haiku: "Claude Haiku (fast, cheap)", + "gpt-4o": "GPT-4o (OpenAI balanced)", + "gpt-4o-mini": "GPT-4o Mini (OpenAI fast)", +} as const; + +/** + * Agent frontmatter template + */ +export const AGENT_FRONTMATTER_TEMPLATE = `--- +name: {{name}} +description: {{description}} +model: {{model}} +color: {{color}} +tools: [{{tools}}] +--- +`; + +/** + * Agent markdown template + */ +export const AGENT_MARKDOWN_TEMPLATE = `--- +name: {{name}} +description: Use this agent when {{trigger_description}}. Examples: {{examples}} +model: {{model}} +color: {{color}} +{{#if tools}} +tools: [{{tools}}] +{{/if}} +--- + +# {{display_name}} + +{{system_prompt}} + +## Core Process + +{{process_steps}} + +## Output Format + +{{output_format}} + +## Rules + +{{rules}} +`; + +/** + * Skill frontmatter template + */ +export const SKILL_FRONTMATTER_TEMPLATE = `--- +name: {{name}} +description: {{description}} +version: 0.1.0 +--- +`; + +/** + * Skill markdown template + */ +export const SKILL_MARKDOWN_TEMPLATE = `--- +name: {{name}} +description: This skill should be used when {{trigger_description}}. +version: 0.1.0 +--- + +# {{display_name}} + +## Overview + +{{overview}} + +## When to Use + +{{when_to_use}} + +## How to Use + +{{how_to_use}} + +## Examples + +{{examples}} + +## Best Practices + +{{best_practices}} +`; + +/** + * Default tools by agent type + */ +export const DEFAULT_TOOLS_BY_TYPE = { + explore: ["glob", "grep", "read"], + review: ["glob", "grep", "read"], + implement: ["glob", "grep", "read", "write", "edit", "bash"], + test: ["glob", "grep", "read", "write", "edit", "bash"], + refactor: ["glob", "grep", "read", "write", "edit"], + plan: ["glob", "grep", "read"], + security: ["glob", "grep", "read"], + documentation: ["glob", "grep", "read", "write"], + general: ["glob", "grep", "read", "write", "edit", "bash", "web_search", "web_fetch"], +} as const; + +/** + * Agent creation prompt for LLM + */ +export const AGENT_CREATION_PROMPT = `You are an agent configuration generator. Create a high-quality agent definition based on the user's description. + +## Output Format + +Generate a complete agent markdown file with: + +1. **Frontmatter** (YAML between ---): + - name: lowercase, hyphens only, 3-50 chars (e.g., "code-reviewer", "api-docs-writer") + - description: Must start with "Use this agent when..." and include 2-4 blocks + - model: inherit (recommended) | sonnet | opus | haiku | gpt-4o | gpt-4o-mini + - color: blue | cyan | green | yellow | magenta | red (match semantic meaning) + - tools: array of tool names (optional, omit for full access) + +2. **System Prompt** (markdown body): + - Clear role definition + - Core process steps + - Output format specification + - Rules and constraints + +## Example + +\`\`\`markdown +--- +name: code-reviewer +description: Use this agent when the user asks to "review code", "check for bugs", "find issues", or wants code quality feedback. Context: User wants code reviewed\nuser: "Review this function for bugs"\nassistant: "I'll use the code-reviewer agent to analyze the code."\n +model: inherit +color: red +tools: ["glob", "grep", "read"] +--- + +# Code Reviewer + +You are a senior code reviewer who identifies bugs, logic errors, and quality issues. + +## Core Process + +1. Read the code thoroughly +2. Check for common bug patterns +3. Analyze logic flow +4. Review error handling +5. Check for security issues + +## Output Format + +- **Issues Found:** List with severity, location, description +- **Suggestions:** Improvement recommendations +- **Summary:** Overall assessment + +## Rules + +- Never modify files +- Focus on actionable feedback +- Prioritize by severity +\`\`\` + +## User's Description + +{{description}} + +Generate the complete agent markdown file:`; + +/** + * Skill creation prompt for LLM + */ +export const SKILL_CREATION_PROMPT = `You are a skill documentation generator. Create educational skill documentation based on the user's description. + +## Output Format + +Generate a complete skill SKILL.md file with: + +1. **Frontmatter** (YAML between ---): + - name: Display name with spaces + - description: Trigger phrases when this skill should be used + - version: 0.1.0 + +2. **Content** (markdown body): + - Overview section + - When to Use section + - How to Use section with examples + - Best Practices section + +## Example + +\`\`\`markdown +--- +name: Git Workflow +description: This skill should be used when the user asks about "git commands", "branching strategy", "commit messages", "pull requests", or needs guidance on git workflows and best practices. +version: 0.1.0 +--- + +# Git Workflow + +## Overview + +This skill covers git best practices for development workflows. + +## When to Use + +- Setting up a new git repository +- Creating feature branches +- Writing commit messages +- Managing pull requests + +## How to Use + +### Creating a Feature Branch + +\`\`\`bash +git checkout -b feature/my-feature +\`\`\` + +### Commit Message Format + +\`\`\` +type(scope): description + +[optional body] + +[optional footer] +\`\`\` + +## Best Practices + +1. Use descriptive branch names +2. Keep commits atomic +3. Write clear commit messages +4. Review before merging +\`\`\` + +## User's Description + +{{description}} + +Generate the complete skill SKILL.md file:`; + +/** + * Directories for agents and skills + */ +export const CODETYPER_DIRS = { + agents: ".codetyper/agents", + skills: ".codetyper/skills", + plugins: ".codetyper/plugins", +} as const; + +/** + * File patterns for discovery + */ +export const DISCOVERY_PATTERNS = { + agents: "**/*.md", + skills: "**/SKILL.md", +} as const; diff --git a/src/constants/unified-agent-registry.ts b/src/constants/unified-agent-registry.ts new file mode 100644 index 0000000..ee1eded --- /dev/null +++ b/src/constants/unified-agent-registry.ts @@ -0,0 +1,665 @@ +/** + * Unified Agent Registry + * + * Maps agents/skills from different AI coding tools: + * - Claude Code + * - OpenCode + * - Cursor + * - CodeTyper native + * + * This registry enables CodeTyper to use agent patterns from all tools. + */ + +import type { AgentDefinition } from "@/types/agent-definition"; + +/** + * Agent tier determines model selection and capabilities + */ +export type AgentTier = "fast" | "balanced" | "thorough" | "reasoning"; + +/** + * Agent mode determines when/how the agent can be invoked + */ +export type AgentMode = "primary" | "subagent" | "all"; + +/** + * Source tool that defined this agent pattern + */ +export type AgentSource = + | "claude-code" + | "opencode" + | "cursor" + | "codetyper" + | "github-copilot"; + +/** + * Unified agent definition that can represent agents from any tool + */ +export interface UnifiedAgentDefinition { + /** Unique agent identifier */ + id: string; + /** Display name */ + name: string; + /** Description of agent capabilities */ + description: string; + /** Source tool this agent pattern comes from */ + source: AgentSource; + /** Agent tier for model selection */ + tier: AgentTier; + /** Agent mode (primary, subagent, or both) */ + mode: AgentMode; + /** Tools this agent has access to */ + tools: string[]; + /** Tools explicitly denied */ + deniedTools?: string[]; + /** Maximum iterations/turns */ + maxTurns: number; + /** Custom system prompt */ + systemPrompt?: string; + /** Temperature for model (0.0-1.0) */ + temperature?: number; + /** Whether agent is visible in UI */ + hidden?: boolean; + /** Color for UI display */ + color?: string; + /** Tags for categorization */ + tags: string[]; +} + +// ============================================================================= +// OpenCode Agents +// ============================================================================= + +export const OPENCODE_AGENTS: UnifiedAgentDefinition[] = [ + { + id: "opencode-build", + name: "Build", + description: + "The default agent. Executes tools based on configured permissions. Full code generation and modification capabilities.", + source: "opencode", + tier: "balanced", + mode: "primary", + tools: [ + "bash", + "read", + "glob", + "grep", + "edit", + "write", + "web_fetch", + "web_search", + "task_agent", + "todo_read", + "todo_write", + "apply_patch", + ], + maxTurns: 50, + tags: ["code-generation", "editing", "general-purpose"], + }, + { + id: "opencode-plan", + name: "Plan", + description: + "Plan mode. Disallows all edit tools. Use for planning and research without file modifications.", + source: "opencode", + tier: "balanced", + mode: "primary", + tools: ["bash", "read", "glob", "grep", "web_fetch", "web_search"], + deniedTools: ["edit", "write", "apply_patch"], + maxTurns: 30, + tags: ["planning", "research", "read-only"], + }, + { + id: "opencode-explore", + name: "Explore", + description: + "Fast agent specialized for exploring codebases. Use for finding files by patterns, searching code for keywords, or answering questions about the codebase. Supports thoroughness levels: quick, medium, very thorough.", + source: "opencode", + tier: "fast", + mode: "subagent", + tools: ["grep", "glob", "read", "bash", "web_fetch", "web_search"], + deniedTools: ["edit", "write", "apply_patch", "task_agent"], + maxTurns: 15, + tags: ["exploration", "search", "codebase-analysis"], + }, + { + id: "opencode-general", + name: "General", + description: + "General-purpose agent for researching complex questions and executing multi-step tasks. Use to execute multiple units of work in parallel.", + source: "opencode", + tier: "balanced", + mode: "subagent", + tools: [ + "bash", + "read", + "glob", + "grep", + "edit", + "write", + "web_fetch", + "web_search", + "task_agent", + "apply_patch", + ], + deniedTools: ["todo_read", "todo_write"], + maxTurns: 25, + tags: ["general-purpose", "parallel", "research"], + }, + { + id: "opencode-title", + name: "Title Generator", + description: "Session title generator. Internal use only.", + source: "opencode", + tier: "fast", + mode: "primary", + tools: [], + maxTurns: 1, + temperature: 0.5, + hidden: true, + tags: ["internal", "utility"], + }, + { + id: "opencode-summary", + name: "Summary", + description: "Conversation summarizer. Internal use only.", + source: "opencode", + tier: "fast", + mode: "primary", + tools: [], + maxTurns: 1, + hidden: true, + tags: ["internal", "utility"], + }, + { + id: "opencode-compaction", + name: "Compaction", + description: "Internal message compaction agent.", + source: "opencode", + tier: "fast", + mode: "primary", + tools: [], + maxTurns: 1, + hidden: true, + tags: ["internal", "utility"], + }, +]; + +// ============================================================================= +// Claude Code Agents +// ============================================================================= + +export const CLAUDE_CODE_AGENTS: UnifiedAgentDefinition[] = [ + // Feature-Dev Plugin Agents + { + id: "claude-code-explorer", + name: "Code Explorer", + description: + "Deeply analyzes existing codebase features by tracing execution paths, mapping architecture layers, understanding patterns and abstractions, and documenting dependencies.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: [ + "glob", + "grep", + "read", + "web_fetch", + "web_search", + "todo_write", + ], + deniedTools: ["edit", "write", "bash"], + maxTurns: 15, + color: "yellow", + tags: ["exploration", "analysis", "feature-dev"], + }, + { + id: "claude-code-architect", + name: "Code Architect", + description: + "Designs feature architectures by analyzing existing codebase patterns and conventions, providing comprehensive implementation blueprints with specific files to create/modify.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: [ + "glob", + "grep", + "read", + "web_fetch", + "web_search", + "todo_write", + ], + deniedTools: ["edit", "write", "bash"], + maxTurns: 15, + color: "green", + tags: ["architecture", "design", "feature-dev"], + }, + { + id: "claude-code-reviewer", + name: "Code Reviewer", + description: + "Reviews code for bugs, logic errors, security vulnerabilities, code quality issues, and adherence to project conventions using confidence-based filtering.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read", "web_fetch", "web_search"], + deniedTools: ["edit", "write", "bash"], + maxTurns: 10, + color: "red", + tags: ["review", "quality", "security"], + }, + // PR Review Toolkit Agents + { + id: "claude-pr-reviewer", + name: "PR Reviewer", + description: + "Reviews code for adherence to project guidelines, style guides, and best practices using confidence-based scoring.", + source: "claude-code", + tier: "thorough", + mode: "subagent", + tools: ["glob", "grep", "read"], + maxTurns: 10, + color: "green", + tags: ["pr-review", "quality"], + }, + { + id: "claude-code-simplifier", + name: "Code Simplifier", + description: + "Simplifies code for clarity, consistency, and maintainability while preserving all functionality.", + source: "claude-code", + tier: "thorough", + mode: "subagent", + tools: ["glob", "grep", "read", "edit", "write"], + maxTurns: 15, + tags: ["refactoring", "simplification"], + }, + { + id: "claude-comment-analyzer", + name: "Comment Analyzer", + description: + "Analyzes code comments for accuracy, completeness, and long-term maintainability.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read"], + maxTurns: 10, + color: "green", + tags: ["documentation", "comments", "quality"], + }, + { + id: "claude-test-analyzer", + name: "Test Analyzer", + description: + "Reviews code for test coverage quality and completeness, focusing on behavioral coverage.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read"], + maxTurns: 10, + color: "cyan", + tags: ["testing", "coverage", "quality"], + }, + { + id: "claude-silent-failure-hunter", + name: "Silent Failure Hunter", + description: + "Identifies silent failures, inadequate error handling, and inappropriate fallback behavior.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read"], + maxTurns: 10, + color: "yellow", + tags: ["error-handling", "reliability", "quality"], + }, + { + id: "claude-type-analyzer", + name: "Type Design Analyzer", + description: + "Analyzes type design for encapsulation quality, invariant expression, and practical usefulness.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read"], + maxTurns: 10, + color: "pink", + tags: ["types", "design", "quality"], + }, + // Plugin Dev Agents + { + id: "claude-agent-creator", + name: "Agent Creator", + description: + "Creates high-performance agent configurations from user requirements.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["write", "read"], + maxTurns: 10, + color: "magenta", + tags: ["meta", "agent-creation"], + }, + { + id: "claude-plugin-validator", + name: "Plugin Validator", + description: + "Validates plugin structure, configuration, and components including manifests and hooks.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["read", "grep", "glob", "bash"], + maxTurns: 10, + color: "yellow", + tags: ["validation", "plugins"], + }, + { + id: "claude-skill-reviewer", + name: "Skill Reviewer", + description: + "Reviews and improves skills for maximum effectiveness and reliability.", + source: "claude-code", + tier: "balanced", + mode: "subagent", + tools: ["read", "grep", "glob"], + maxTurns: 10, + color: "cyan", + tags: ["review", "skills"], + }, +]; + +// ============================================================================= +// Cursor Agents +// ============================================================================= + +export const CURSOR_AGENTS: UnifiedAgentDefinition[] = [ + { + id: "cursor-pair-programmer", + name: "Pair Programmer", + description: + "Default Cursor agent with pair-level context awareness. Automatic context attachment for open files, cursor position, edit history, and linter errors.", + source: "cursor", + tier: "balanced", + mode: "primary", + tools: [ + "glob", + "grep", + "read", + "edit", + "write", + "bash", + "web_search", + ], + maxTurns: 50, + tags: ["general-purpose", "pair-programming"], + systemPrompt: `You are a pair programmer. Keep going until the query is completely resolved. +Use codebase_search (semantic) as the primary exploration tool. +Maximize parallel tool calls where possible. +Never output code unless requested - use edit tools instead.`, + }, + { + id: "cursor-cli", + name: "CLI Agent", + description: + "Interactive CLI agent for software engineering tasks. Uses Grep as main exploration tool with status updates and flow-based execution.", + source: "cursor", + tier: "balanced", + mode: "primary", + tools: [ + "glob", + "grep", + "read", + "edit", + "write", + "bash", + "web_search", + ], + maxTurns: 50, + tags: ["cli", "terminal", "interactive"], + systemPrompt: `You are an interactive CLI tool for software engineering tasks. +Flow: Goal detection → Discovery → Tool batching → Summary +Primary Tool: Grep (fast exact matching) +Provide status updates and concise summaries.`, + }, + { + id: "cursor-chat", + name: "Chat", + description: + "Conversational coding assistant focused on question answering. Only edits if certain user wants edits.", + source: "cursor", + tier: "balanced", + mode: "primary", + tools: ["glob", "grep", "read", "web_search"], + deniedTools: ["edit", "write"], + maxTurns: 20, + tags: ["chat", "q&a", "conversational"], + }, +]; + +// ============================================================================= +// CodeTyper Native Agents +// ============================================================================= + +export const CODETYPER_AGENTS: UnifiedAgentDefinition[] = [ + { + id: "codetyper-explore", + name: "Explore", + description: "Fast codebase exploration agent (read-only).", + source: "codetyper", + tier: "fast", + mode: "subagent", + tools: ["glob", "grep", "read"], + deniedTools: ["edit", "write", "bash"], + maxTurns: 10, + tags: ["exploration", "search"], + }, + { + id: "codetyper-implement", + name: "Implement", + description: "Code writing and modification agent.", + source: "codetyper", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read", "write", "edit", "bash"], + maxTurns: 20, + tags: ["implementation", "coding"], + }, + { + id: "codetyper-test", + name: "Test", + description: "Test creation and execution agent.", + source: "codetyper", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read", "write", "edit", "bash"], + maxTurns: 15, + tags: ["testing", "quality"], + }, + { + id: "codetyper-review", + name: "Review", + description: "Code review and suggestions agent.", + source: "codetyper", + tier: "balanced", + mode: "subagent", + tools: ["glob", "grep", "read"], + deniedTools: ["edit", "write", "bash"], + maxTurns: 10, + tags: ["review", "quality"], + }, + { + id: "codetyper-refactor", + name: "Refactor", + description: "Code refactoring and improvement agent.", + source: "codetyper", + tier: "thorough", + mode: "subagent", + tools: ["glob", "grep", "read", "write", "edit"], + maxTurns: 25, + tags: ["refactoring", "improvement"], + }, + { + id: "codetyper-plan", + name: "Plan", + description: "Planning and architecture design agent.", + source: "codetyper", + tier: "thorough", + mode: "subagent", + tools: ["glob", "grep", "read"], + deniedTools: ["edit", "write", "bash"], + maxTurns: 15, + tags: ["planning", "architecture"], + }, +]; + +// ============================================================================= +// Combined Registry +// ============================================================================= + +/** + * All agents from all sources + */ +export const UNIFIED_AGENT_REGISTRY: UnifiedAgentDefinition[] = [ + ...OPENCODE_AGENTS, + ...CLAUDE_CODE_AGENTS, + ...CURSOR_AGENTS, + ...CODETYPER_AGENTS, +]; + +/** + * Get agent by ID + */ +export const getAgentById = ( + id: string, +): UnifiedAgentDefinition | undefined => { + return UNIFIED_AGENT_REGISTRY.find((a) => a.id === id); +}; + +/** + * Get agents by source + */ +export const getAgentsBySource = ( + source: AgentSource, +): UnifiedAgentDefinition[] => { + return UNIFIED_AGENT_REGISTRY.filter((a) => a.source === source); +}; + +/** + * Get agents by tier + */ +export const getAgentsByTier = ( + tier: AgentTier, +): UnifiedAgentDefinition[] => { + return UNIFIED_AGENT_REGISTRY.filter((a) => a.tier === tier); +}; + +/** + * Get agents by mode + */ +export const getAgentsByMode = ( + mode: AgentMode, +): UnifiedAgentDefinition[] => { + return UNIFIED_AGENT_REGISTRY.filter( + (a) => a.mode === mode || a.mode === "all", + ); +}; + +/** + * Get agents by tag + */ +export const getAgentsByTag = (tag: string): UnifiedAgentDefinition[] => { + return UNIFIED_AGENT_REGISTRY.filter((a) => a.tags.includes(tag)); +}; + +/** + * Get visible agents only + */ +export const getVisibleAgents = (): UnifiedAgentDefinition[] => { + return UNIFIED_AGENT_REGISTRY.filter((a) => !a.hidden); +}; + +/** + * Get subagents only (for task_agent tool) + */ +export const getSubagents = (): UnifiedAgentDefinition[] => { + return UNIFIED_AGENT_REGISTRY.filter( + (a) => (a.mode === "subagent" || a.mode === "all") && !a.hidden, + ); +}; + +/** + * Map unified tier to internal tier (internal doesn't have "reasoning") + */ +const mapTier = (tier: AgentTier): "fast" | "balanced" | "thorough" => { + const tierMap: Record = { + fast: "fast", + balanced: "balanced", + thorough: "thorough", + reasoning: "thorough", // Map reasoning to thorough + }; + return tierMap[tier]; +}; + +/** + * Map unified color to internal color + */ +const mapColor = ( + color?: string, +): "red" | "green" | "blue" | "yellow" | "cyan" | "magenta" | "white" | "gray" => { + const validColors = new Set([ + "red", + "green", + "blue", + "yellow", + "cyan", + "magenta", + "white", + "gray", + ]); + if (color && validColors.has(color)) { + return color as "red" | "green" | "blue" | "yellow" | "cyan" | "magenta" | "white" | "gray"; + } + return "cyan"; +}; + +/** + * Convert unified agent to internal AgentDefinition format + */ +export const toAgentDefinition = ( + agent: UnifiedAgentDefinition, +): AgentDefinition => ({ + name: agent.id, + description: agent.description, + tools: agent.tools, + tier: mapTier(agent.tier), + color: mapColor(agent.color), + maxTurns: agent.maxTurns, + systemPrompt: agent.systemPrompt, +}); + +/** + * Summary statistics + */ +export const REGISTRY_STATS = { + total: UNIFIED_AGENT_REGISTRY.length, + bySource: { + opencode: OPENCODE_AGENTS.length, + "claude-code": CLAUDE_CODE_AGENTS.length, + cursor: CURSOR_AGENTS.length, + codetyper: CODETYPER_AGENTS.length, + }, + byTier: { + fast: UNIFIED_AGENT_REGISTRY.filter((a) => a.tier === "fast").length, + balanced: UNIFIED_AGENT_REGISTRY.filter((a) => a.tier === "balanced") + .length, + thorough: UNIFIED_AGENT_REGISTRY.filter((a) => a.tier === "thorough") + .length, + reasoning: UNIFIED_AGENT_REGISTRY.filter((a) => a.tier === "reasoning") + .length, + }, + byMode: { + primary: UNIFIED_AGENT_REGISTRY.filter((a) => a.mode === "primary").length, + subagent: UNIFIED_AGENT_REGISTRY.filter((a) => a.mode === "subagent") + .length, + all: UNIFIED_AGENT_REGISTRY.filter((a) => a.mode === "all").length, + }, +}; diff --git a/src/providers/copilot/core/chat.ts b/src/providers/copilot/core/chat.ts index c442b2b..b64d649 100644 --- a/src/providers/copilot/core/chat.ts +++ b/src/providers/copilot/core/chat.ts @@ -63,6 +63,11 @@ interface ChatRequestBody { tool_choice?: string; } +// Default max tokens for requests without tools +const DEFAULT_MAX_TOKENS = 4096; +// Higher max tokens when tools are enabled (tool calls often write large content) +const DEFAULT_MAX_TOKENS_WITH_TOOLS = 16384; + const buildRequestBody = ( messages: Message[], options: ChatCompletionOptions | undefined, @@ -76,15 +81,21 @@ const buildRequestBody = ( ? options.model : getDefaultModel()); + // Use higher max_tokens when tools are enabled to prevent truncation + const hasTools = options?.tools && options.tools.length > 0; + const defaultTokens = hasTools + ? DEFAULT_MAX_TOKENS_WITH_TOOLS + : DEFAULT_MAX_TOKENS; + const body: ChatRequestBody = { model, messages: formatMessages(messages), - max_tokens: options?.maxTokens ?? 4096, + max_tokens: options?.maxTokens ?? defaultTokens, temperature: options?.temperature ?? 0.3, stream, }; - if (options?.tools && options.tools.length > 0) { + if (hasTools) { body.tools = options.tools; body.tool_choice = "auto"; } @@ -211,6 +222,54 @@ export const chat = async ( throw lastError; }; +/** + * Process a single SSE line and emit appropriate chunks + */ +const processStreamLine = ( + line: string, + onChunk: (chunk: StreamChunk) => void, +): boolean => { + if (!line.startsWith("data: ")) { + return false; + } + + const jsonStr = line.slice(6).trim(); + if (jsonStr === "[DONE]") { + onChunk({ type: "done" }); + return true; + } + + try { + const parsed = JSON.parse(jsonStr); + const delta = parsed.choices?.[0]?.delta; + const finishReason = parsed.choices?.[0]?.finish_reason; + + if (delta?.content) { + onChunk({ type: "content", content: delta.content }); + } + + if (delta?.tool_calls) { + for (const tc of delta.tool_calls) { + addDebugLog("api", `Tool call chunk: ${JSON.stringify(tc)}`); + onChunk({ type: "tool_call", toolCall: tc }); + } + } + + // Handle truncation: if finish_reason is "length", content was cut off + if (finishReason === "length") { + addDebugLog("api", "Stream truncated due to max_tokens limit"); + onChunk({ + type: "error", + error: "Response truncated: max_tokens limit reached", + }); + } + } catch { + // Ignore parse errors in stream + } + + return false; +}; + const executeStream = ( endpoint: string, token: CopilotToken, @@ -224,6 +283,7 @@ const executeStream = ( }); let buffer = ""; + let doneReceived = false; stream.on("data", (data: Buffer) => { buffer += data.toString(); @@ -231,34 +291,9 @@ const executeStream = ( buffer = lines.pop() ?? ""; for (const line of lines) { - if (line.startsWith("data: ")) { - const jsonStr = line.slice(6).trim(); - if (jsonStr === "[DONE]") { - onChunk({ type: "done" }); - return; - } - - try { - const parsed = JSON.parse(jsonStr); - const delta = parsed.choices?.[0]?.delta; - - if (delta?.content) { - onChunk({ type: "content", content: delta.content }); - } - - if (delta?.tool_calls) { - for (const tc of delta.tool_calls) { - addDebugLog("api", `Tool call chunk: ${JSON.stringify(tc)}`); - console.log( - "Debug: Tool call chunk received:", - JSON.stringify(tc), - ); - onChunk({ type: "tool_call", toolCall: tc }); - } - } - } catch { - // Ignore parse errors in stream - } + if (processStreamLine(line, onChunk)) { + doneReceived = true; + return; } } }); @@ -268,7 +303,23 @@ const executeStream = ( reject(error); }); - stream.on("end", resolve); + stream.on("end", () => { + // Process any remaining data in buffer that didn't have trailing newline + if (buffer.trim()) { + processStreamLine(buffer, onChunk); + } + + // Ensure done is sent even if stream ended without [DONE] message + if (!doneReceived) { + addDebugLog( + "api", + "Stream ended without [DONE] message, sending done chunk", + ); + onChunk({ type: "done" }); + } + + resolve(); + }); }); export const chatStream = async ( diff --git a/src/providers/ollama/stream.ts b/src/providers/ollama/stream.ts index 232575b..2514987 100644 --- a/src/providers/ollama/stream.ts +++ b/src/providers/ollama/stream.ts @@ -79,9 +79,15 @@ export const ollamaChatStream = async ( }); let buffer = ""; + let doneReceived = false; stream.on("data", (data: Buffer) => { - buffer = processStreamData(data, buffer, onChunk); + buffer = processStreamData(data, buffer, (chunk) => { + if (chunk.type === "done") { + doneReceived = true; + } + onChunk(chunk); + }); }); stream.on("error", (error: Error) => { @@ -89,7 +95,28 @@ export const ollamaChatStream = async ( }); return new Promise((resolve, reject) => { - stream.on("end", resolve); + stream.on("end", () => { + // Process any remaining data in buffer that didn't have trailing newline + if (buffer.trim()) { + parseStreamLine(buffer, (chunk) => { + if (chunk.type === "done") { + doneReceived = true; + } + onChunk(chunk); + }); + } + + // Ensure done is sent even if stream ended without done message + if (!doneReceived) { + addDebugLog( + "api", + "Ollama stream ended without done, sending done chunk", + ); + onChunk({ type: "done" }); + } + + resolve(); + }); stream.on("error", reject); }); }; diff --git a/src/services/agent-creator-service.ts b/src/services/agent-creator-service.ts new file mode 100644 index 0000000..cd6ff25 --- /dev/null +++ b/src/services/agent-creator-service.ts @@ -0,0 +1,255 @@ +/** + * Agent Creator Service + * + * Creates new agents and skills from user descriptions using LLM. + * Stores them in .codetyper/agents/ and .codetyper/skills/. + */ + +import { mkdir, writeFile, readdir } from "fs/promises"; +import { join } from "path"; +import { chat } from "@providers/core/chat"; +import { + AGENT_CREATION_PROMPT, + SKILL_CREATION_PROMPT, + CODETYPER_DIRS, +} from "@constants/agent-templates"; +import type { ProviderName } from "@/types/providers"; + +/** + * Result of agent/skill creation + */ +export interface CreationResult { + success: boolean; + filePath?: string; + name?: string; + error?: string; +} + +/** + * Options for creating an agent + */ +export interface CreateAgentOptions { + description: string; + workingDir: string; + provider?: ProviderName; + model?: string; +} + +/** + * Options for creating a skill + */ +export interface CreateSkillOptions { + description: string; + workingDir: string; + provider?: ProviderName; + model?: string; +} + +/** + * Extract name from generated markdown + */ +const extractName = (markdown: string): string | null => { + const match = markdown.match(/^name:\s*(.+)$/m); + return match ? match[1].trim() : null; +}; + +/** + * Clean markdown content (remove code fences if present) + */ +const cleanMarkdown = (content: string): string => { + // Remove ```markdown and ``` wrappers if present + let cleaned = content.trim(); + if (cleaned.startsWith("```markdown")) { + cleaned = cleaned.slice("```markdown".length); + } else if (cleaned.startsWith("```")) { + cleaned = cleaned.slice(3); + } + if (cleaned.endsWith("```")) { + cleaned = cleaned.slice(0, -3); + } + return cleaned.trim(); +}; + +/** + * Ensure directory exists + */ +const ensureDir = async (dirPath: string): Promise => { + await mkdir(dirPath, { recursive: true }); +}; + +/** + * Create a new agent from description using LLM + */ +export const createAgentFromDescription = async ( + options: CreateAgentOptions, +): Promise => { + const { description, workingDir, provider = "copilot", model } = options; + + try { + // Generate agent using LLM + const prompt = AGENT_CREATION_PROMPT.replace("{{description}}", description); + + const response = await chat( + provider, + [ + { role: "system", content: "You are an expert agent configuration generator." }, + { role: "user", content: prompt }, + ], + { model, temperature: 0.7 }, + ); + + if (!response.content) { + return { success: false, error: "No response from LLM" }; + } + + // Clean and parse the generated markdown + const markdown = cleanMarkdown(response.content); + const name = extractName(markdown); + + if (!name) { + return { success: false, error: "Could not extract agent name from generated content" }; + } + + // Validate name format + if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || name.length < 3 || name.length > 50) { + return { + success: false, + error: `Invalid agent name: ${name}. Must be 3-50 chars, lowercase, hyphens only.`, + }; + } + + // Create agents directory and write file + const agentsDir = join(workingDir, CODETYPER_DIRS.agents); + await ensureDir(agentsDir); + + const filePath = join(agentsDir, `${name}.md`); + await writeFile(filePath, markdown, "utf-8"); + + return { + success: true, + filePath, + name, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +}; + +/** + * Create a new skill from description using LLM + */ +export const createSkillFromDescription = async ( + options: CreateSkillOptions, +): Promise => { + const { description, workingDir, provider = "copilot", model } = options; + + try { + // Generate skill using LLM + const prompt = SKILL_CREATION_PROMPT.replace("{{description}}", description); + + const response = await chat( + provider, + [ + { role: "system", content: "You are an expert skill documentation generator." }, + { role: "user", content: prompt }, + ], + { model, temperature: 0.7 }, + ); + + if (!response.content) { + return { success: false, error: "No response from LLM" }; + } + + // Clean and parse the generated markdown + const markdown = cleanMarkdown(response.content); + const name = extractName(markdown); + + if (!name) { + return { success: false, error: "Could not extract skill name from generated content" }; + } + + // Convert name to directory-safe format + const dirName = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + + // Create skills directory structure and write file + const skillDir = join(workingDir, CODETYPER_DIRS.skills, dirName); + await ensureDir(skillDir); + + const filePath = join(skillDir, "SKILL.md"); + await writeFile(filePath, markdown, "utf-8"); + + return { + success: true, + filePath, + name, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +}; + +/** + * List existing agents in .codetyper/agents/ + */ +export const listCustomAgents = async ( + workingDir: string, +): Promise => { + const agentsDir = join(workingDir, CODETYPER_DIRS.agents); + + try { + const files = await readdir(agentsDir); + return files + .filter((f) => f.endsWith(".md")) + .map((f) => f.replace(".md", "")); + } catch { + return []; + } +}; + +/** + * List existing skills in .codetyper/skills/ + */ +export const listCustomSkills = async ( + workingDir: string, +): Promise => { + const skillsDir = join(workingDir, CODETYPER_DIRS.skills); + + try { + const dirs = await readdir(skillsDir, { withFileTypes: true }); + return dirs.filter((d) => d.isDirectory()).map((d) => d.name); + } catch { + return []; + } +}; + +/** + * Get suggested agent types based on description + */ +export const suggestAgentType = (description: string): string => { + const lower = description.toLowerCase(); + + const patterns: Record = { + explore: ["search", "find", "explore", "understand", "analyze codebase"], + review: ["review", "check", "audit", "inspect", "quality"], + implement: ["implement", "create", "build", "write code", "develop"], + test: ["test", "testing", "unit test", "coverage"], + refactor: ["refactor", "improve", "clean up", "optimize"], + security: ["security", "vulnerability", "secure", "audit"], + documentation: ["document", "docs", "readme", "api docs"], + plan: ["plan", "design", "architect", "strategy"], + }; + + for (const [type, keywords] of Object.entries(patterns)) { + if (keywords.some((kw) => lower.includes(kw))) { + return type; + } + } + + return "general"; +}; diff --git a/src/services/agent-stream.ts b/src/services/agent-stream.ts index e221403..a15e48e 100644 --- a/src/services/agent-stream.ts +++ b/src/services/agent-stream.ts @@ -175,6 +175,44 @@ const getToolCallIndex = ( return accumulator.toolCalls.size; }; +/** + * Check if JSON appears to be truncated (incomplete) + */ +const isLikelyTruncatedJson = (jsonStr: string): boolean => { + const trimmed = jsonStr.trim(); + if (!trimmed) return false; + + // Count braces and brackets + let braceCount = 0; + let bracketCount = 0; + let inString = false; + let escaped = false; + + for (const char of trimmed) { + if (escaped) { + escaped = false; + continue; + } + if (char === "\\") { + escaped = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } + if (!inString) { + if (char === "{") braceCount++; + if (char === "}") braceCount--; + if (char === "[") bracketCount++; + if (char === "]") bracketCount--; + } + } + + // If counts are unbalanced, JSON is truncated + return braceCount !== 0 || bracketCount !== 0 || inString; +}; + /** * Convert partial tool call to complete tool call */ @@ -188,10 +226,16 @@ const finalizeToolCall = (partial: PartialToolCall): ToolCall => { try { args = JSON.parse(rawBuffer); } catch (e) { + const isTruncated = isLikelyTruncatedJson(rawBuffer); + const errorType = isTruncated + ? "JSON truncated (likely max_tokens exceeded)" + : "JSON parse failed"; + args = { - __debug_error: "JSON parse failed", + __debug_error: errorType, __debug_buffer: rawBuffer.substring(0, 200), __debug_parseError: e instanceof Error ? e.message : String(e), + __debug_truncated: isTruncated, }; } } @@ -211,6 +255,23 @@ const executeTool = async ( state: StreamAgentState, toolCall: ToolCall, ): Promise => { + // Check for debug error markers from truncated/malformed JSON + const debugError = toolCall.arguments.__debug_error as string | undefined; + if (debugError) { + const isTruncated = toolCall.arguments.__debug_truncated === true; + const title = isTruncated ? "Tool call truncated" : "Tool validation error"; + const hint = isTruncated + ? "\nHint: The model's response was cut off. Try a simpler request or increase max_tokens." + : ""; + + return { + success: false, + title, + output: "", + error: `Tool validation error: ${toolCall.name}: ${debugError}${hint}\nReceived: ${JSON.stringify(toolCall.arguments)}`, + }; + } + const tool = getTool(toolCall.name); if (!tool) { @@ -246,6 +307,103 @@ const executeTool = async ( } }; +// ============================================================================= +// Parallel Tool Execution +// ============================================================================= + +/** + * Tools that are safe to execute in parallel (read-only or isolated) + */ +const PARALLEL_SAFE_TOOLS = new Set([ + "task_agent", // Subagent spawning - designed for parallel execution + "read", // Read-only + "glob", // Read-only + "grep", // Read-only + "web_search", // External API, no local state + "web_fetch", // External API, no local state + "todo_read", // Read-only + "lsp", // Read-only queries +]); + +/** + * Maximum number of parallel tool executions + */ +const MAX_PARALLEL_TOOLS = 3; + +/** + * Execute tool calls with intelligent parallelism + * - Parallel-safe tools (task_agent, read, glob, grep) run concurrently + * - File-modifying tools (write, edit, bash) run sequentially + */ +const executeToolCallsWithParallelism = async ( + state: StreamAgentState, + toolCalls: ToolCall[], +): Promise> => { + // Separate into parallel-safe and sequential groups + const parallelCalls: ToolCall[] = []; + const sequentialCalls: ToolCall[] = []; + + for (const tc of toolCalls) { + if (PARALLEL_SAFE_TOOLS.has(tc.name)) { + parallelCalls.push(tc); + } else { + sequentialCalls.push(tc); + } + } + + const results: Array<{ toolCall: ToolCall; result: ToolResult }> = []; + + // Execute parallel-safe tools in parallel (up to MAX_PARALLEL_TOOLS at a time) + if (parallelCalls.length > 0) { + const parallelResults = await executeInParallelChunks( + state, + parallelCalls, + MAX_PARALLEL_TOOLS, + ); + results.push(...parallelResults); + } + + // Execute sequential tools one at a time + for (const toolCall of sequentialCalls) { + const result = await executeTool(state, toolCall); + results.push({ toolCall, result }); + } + + // Return results in original order + return toolCalls.map((tc) => { + const found = results.find((r) => r.toolCall.id === tc.id); + return found ?? { toolCall: tc, result: { success: false, title: "Error", output: "", error: "Tool result not found" } }; + }); +}; + +/** + * Execute tools in parallel chunks + */ +const executeInParallelChunks = async ( + state: StreamAgentState, + toolCalls: ToolCall[], + chunkSize: number, +): Promise> => { + const results: Array<{ toolCall: ToolCall; result: ToolResult }> = []; + + // Process in chunks of chunkSize + for (let i = 0; i < toolCalls.length; i += chunkSize) { + const chunk = toolCalls.slice(i, i + chunkSize); + + // Execute chunk in parallel + const chunkResults = await Promise.all( + chunk.map(async (toolCall) => { + const result = await executeTool(state, toolCall); + return { toolCall, result }; + }), + ); + + results.push(...chunkResults); + } + + return results; +}; + // ============================================================================= // Streaming LLM Call // ============================================================================= @@ -368,13 +526,16 @@ export const runAgentLoopStream = async ( // Track if all tool calls in this iteration failed let allFailed = true; - // Execute each tool call - for (const toolCall of response.toolCalls) { + // Execute tool calls with parallel execution for safe tools + const toolResults = await executeToolCallsWithParallelism( + state, + response.toolCalls, + ); + + // Process results in order + for (const { toolCall, result } of toolResults) { state.options.onToolCall?.(toolCall); - - const result = await executeTool(state, toolCall); allToolCalls.push({ call: toolCall, result }); - state.options.onToolResult?.(toolCall.id, result); // Track success/failure diff --git a/src/services/core/agent.ts b/src/services/core/agent.ts index a78e50f..3ce34d3 100644 --- a/src/services/core/agent.ts +++ b/src/services/core/agent.ts @@ -31,6 +31,25 @@ import { import { MAX_ITERATIONS } from "@constants/agent"; import { usageStore } from "@stores/core/usage-store"; +/** + * Tools that are safe to execute in parallel (read-only or isolated) + */ +const PARALLEL_SAFE_TOOLS = new Set([ + "task_agent", // Subagent spawning - designed for parallel execution + "read", // Read-only + "glob", // Read-only + "grep", // Read-only + "web_search", // External API, no local state + "web_fetch", // External API, no local state + "todo_read", // Read-only + "lsp", // Read-only queries +]); + +/** + * Maximum number of parallel tool executions + */ +const MAX_PARALLEL_TOOLS = 3; + /** * Agent state interface */ @@ -225,6 +244,80 @@ const executeTool = async ( return result; }; +/** + * Execute tool calls with intelligent parallelism + * - Parallel-safe tools (task_agent, read, glob, grep) run concurrently + * - File-modifying tools (write, edit, bash) run sequentially + */ +const executeToolCallsWithParallelism = async ( + state: AgentState, + toolCalls: ToolCall[], +): Promise> => { + // Separate into parallel-safe and sequential groups + const parallelCalls: ToolCall[] = []; + const sequentialCalls: ToolCall[] = []; + + for (const tc of toolCalls) { + if (PARALLEL_SAFE_TOOLS.has(tc.name)) { + parallelCalls.push(tc); + } else { + sequentialCalls.push(tc); + } + } + + const results: Array<{ toolCall: ToolCall; result: ToolResult }> = []; + + // Execute parallel-safe tools in parallel (up to MAX_PARALLEL_TOOLS at a time) + if (parallelCalls.length > 0) { + const parallelResults = await executeInParallelChunks( + state, + parallelCalls, + MAX_PARALLEL_TOOLS, + ); + results.push(...parallelResults); + } + + // Execute sequential tools one at a time + for (const toolCall of sequentialCalls) { + const result = await executeTool(state, toolCall); + results.push({ toolCall, result }); + } + + // Return results in original order + return toolCalls.map((tc) => { + const found = results.find((r) => r.toolCall.id === tc.id); + return found ?? { toolCall: tc, result: { success: false, title: "Error", output: "", error: "Tool result not found" } }; + }); +}; + +/** + * Execute tools in parallel chunks + */ +const executeInParallelChunks = async ( + state: AgentState, + toolCalls: ToolCall[], + chunkSize: number, +): Promise> => { + const results: Array<{ toolCall: ToolCall; result: ToolResult }> = []; + + // Process in chunks of chunkSize + for (let i = 0; i < toolCalls.length; i += chunkSize) { + const chunk = toolCalls.slice(i, i + chunkSize); + + // Execute chunk in parallel + const chunkResults = await Promise.all( + chunk.map(async (toolCall) => { + const result = await executeTool(state, toolCall); + return { toolCall, result }; + }), + ); + + results.push(...chunkResults); + } + + return results; +}; + /** * Run the agent with the given messages */ @@ -282,8 +375,14 @@ export const runAgentLoop = async ( state.options.onText?.(response.content); } - // Execute each tool call - for (const toolCall of response.toolCalls) { + // Execute tool calls with parallel execution for safe tools + const toolResults = await executeToolCallsWithParallelism( + state, + response.toolCalls, + ); + + // Process results in order + for (const { toolCall, result } of toolResults) { state.options.onToolCall?.(toolCall); if (state.options.verbose) { @@ -293,9 +392,7 @@ export const runAgentLoop = async ( ); } - const result = await executeTool(state, toolCall); allToolCalls.push({ call: toolCall, result }); - state.options.onToolResult?.(toolCall.id, result); // Add tool result message diff --git a/src/services/core/session.ts b/src/services/core/session.ts index fcbfe84..8e1034c 100644 --- a/src/services/core/session.ts +++ b/src/services/core/session.ts @@ -1,9 +1,19 @@ import fs from "fs/promises"; import path from "path"; import type { AgentType, ChatSession, ChatMessage } from "@/types/common"; -import type { SessionInfo } from "@/types/session"; +import type { SessionInfo, SubagentSessionConfig } from "@/types/session"; import { DIRS } from "@constants/paths"; +/** + * Extended ChatSession with subagent support + */ +interface SubagentChatSession extends ChatSession { + parentSessionId?: string; + isSubagent?: boolean; + subagentType?: string; + task?: string; +} + /** * Current session state */ @@ -252,5 +262,87 @@ export const setWorkingDirectory = async (dir: string): Promise => { await saveSession(); }; +/** + * Create a subagent session (child of a parent session) + * Used by task_agent for proper session-based isolation like opencode + */ +export const createSubagentSession = async ( + config: SubagentSessionConfig, +): Promise => { + const session: SubagentChatSession = { + id: generateId(), + agent: "subagent" as AgentType, + messages: [], + contextFiles: config.contextFiles ?? [], + createdAt: Date.now(), + updatedAt: Date.now(), + parentSessionId: config.parentSessionId, + isSubagent: true, + subagentType: config.subagentType, + task: config.task, + }; + + // Set working directory + (session as SubagentChatSession & { workingDirectory?: string }).workingDirectory = + config.workingDirectory; + + // Save but don't set as current (subagents run independently) + await saveSession(session); + return session; +}; + +/** + * Get all subagent sessions for a parent session + */ +export const getSubagentSessions = async ( + parentSessionId: string, +): Promise => { + const sessions = await listSessions(); + return sessions.filter( + (s) => (s as SubagentChatSession).parentSessionId === parentSessionId, + ) as SubagentChatSession[]; +}; + +/** + * Add message to a specific session (for subagents) + */ +export const addMessageToSession = async ( + sessionId: string, + role: "user" | "assistant" | "system" | "tool", + content: string, +): Promise => { + const session = await loadSession(sessionId); + if (!session) { + throw new Error(`Session not found: ${sessionId}`); + } + + const message: ChatMessage = { + role: role as "user" | "assistant" | "system", + content, + timestamp: Date.now(), + }; + + session.messages.push(message); + await saveSession(session); +}; + +/** + * Update subagent session with result + */ +export const completeSubagentSession = async ( + sessionId: string, + result: { success: boolean; output: string; error?: string }, +): Promise => { + const session = await loadSession(sessionId); + if (!session) return; + + // Add final result as assistant message + const resultContent = result.success + ? `## Subagent Result\n\n${result.output}` + : `## Subagent Error\n\n${result.error}\n\n${result.output}`; + + await addMessageToSession(sessionId, "assistant", resultContent); +}; + // Re-export types -export type { SessionInfo } from "@/types/session"; +export type { SessionInfo, SubagentSessionConfig } from "@/types/session"; diff --git a/src/tools/index.ts b/src/tools/index.ts index 8e782db..e5d46a2 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -13,6 +13,7 @@ import { webFetchTool } from "@tools/web-fetch/execute"; import { multiEditTool } from "@tools/multi-edit/execute"; import { lspTool } from "@tools/lsp"; import { applyPatchTool } from "@tools/apply-patch"; +import { taskAgentTool } from "@tools/task-agent/execute"; import { isMCPTool, executeMCPTool, @@ -40,6 +41,7 @@ export const tools: ToolDefinition[] = [ webFetchTool, lspTool, applyPatchTool, + taskAgentTool, ]; // Tools that are read-only (allowed in chat mode) diff --git a/src/tools/task-agent/execute.ts b/src/tools/task-agent/execute.ts index cb673cc..8bebd6b 100644 --- a/src/tools/task-agent/execute.ts +++ b/src/tools/task-agent/execute.ts @@ -2,12 +2,14 @@ * Task Agent Tool * * Allows spawning specialized sub-agents for complex tasks. - * Implements the agent delegation pattern from claude-code. + * Implements the agent delegation pattern from claude-code and opencode. + * Supports parallel execution of up to 3 agents simultaneously. */ import { z } from "zod"; import { v4 as uuidv4 } from "uuid"; import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools"; +import { MULTI_AGENT_DEFAULTS } from "@/constants/multi-agent"; /** * Agent types available for delegation @@ -85,14 +87,58 @@ const taskAgentSchema = z.object({ type TaskAgentParams = z.infer; /** - * Active background agents + * Active background agents with their results */ -const backgroundAgents = new Map; -}>(); + result?: ToolResult; +} + +const backgroundAgents = new Map(); + +/** + * Currently running foreground agents (for parallel execution limit) + */ +let runningForegroundAgents = 0; +const MAX_CONCURRENT_AGENTS = MULTI_AGENT_DEFAULTS.maxConcurrent; // 3 + +/** + * Queue for agents waiting to run + */ +interface QueuedAgent { + params: TaskAgentParams; + systemPrompt: string; + taskPrompt: string; + ctx: ToolContext; + resolve: (result: ToolResult) => void; + reject: (error: Error) => void; +} + +const agentQueue: QueuedAgent[] = []; + +/** + * Process the agent queue + */ +const processQueue = async (): Promise => { + while (agentQueue.length > 0 && runningForegroundAgents < MAX_CONCURRENT_AGENTS) { + const queued = agentQueue.shift(); + if (!queued) break; + + runningForegroundAgents++; + + executeAgentInternal(queued.params, queued.systemPrompt, queued.taskPrompt, queued.ctx) + .then(queued.resolve) + .catch(queued.reject) + .finally(() => { + runningForegroundAgents--; + processQueue(); + }); + } +}; /** * Execute the task agent tool @@ -228,9 +274,43 @@ const buildTaskPrompt = (params: TaskAgentParams): string => { }; /** - * Run agent in foreground (blocking) + * Run agent in foreground with concurrency control */ const runAgentInForeground = async ( + params: TaskAgentParams, + systemPrompt: string, + taskPrompt: string, + ctx: ToolContext, +): Promise => { + // Check if we can run immediately + if (runningForegroundAgents < MAX_CONCURRENT_AGENTS) { + runningForegroundAgents++; + + try { + return await executeAgentInternal(params, systemPrompt, taskPrompt, ctx); + } finally { + runningForegroundAgents--; + processQueue(); + } + } + + // Queue the agent if at capacity + return new Promise((resolve, reject) => { + agentQueue.push({ + params, + systemPrompt, + taskPrompt, + ctx, + resolve, + reject, + }); + }); +}; + +/** + * Execute agent internal logic + */ +const executeAgentInternal = async ( params: TaskAgentParams, systemPrompt: string, taskPrompt: string, @@ -296,15 +376,19 @@ const runAgentInBackground = async ( const promise = runAgentInForeground(params, systemPrompt, taskPrompt, ctx); - backgroundAgents.set(agentId, { + const agent: BackgroundAgent = { + id: agentId, type: params.agent_type, task: params.task, startTime: Date.now(), promise, - }); + }; - // Cleanup after completion - promise.then(() => { + backgroundAgents.set(agentId, agent); + + // Store result and cleanup after completion + promise.then((result) => { + agent.result = result; // Keep result for 5 minutes setTimeout(() => backgroundAgents.delete(agentId), 5 * 60 * 1000); }); @@ -312,7 +396,7 @@ const runAgentInBackground = async ( return { success: true, title: `${params.agent_type} agent started in background`, - output: `Agent ID: ${agentId}\n\nThe ${params.agent_type} agent is now running in the background.\nUse the agent ID to check status or retrieve results.`, + output: `Agent ID: ${agentId}\n\nThe ${params.agent_type} agent is now running in the background.\nUse the agent ID to check status or retrieve results.\n\nCurrent running agents: ${runningForegroundAgents}/${MAX_CONCURRENT_AGENTS}`, metadata: { agentId, agentType: params.agent_type, @@ -338,10 +422,15 @@ export const getBackgroundAgentStatus = async ( }; } - // Check if completed + // Return cached result if available + if (agent.result) { + return agent.result; + } + + // Check if completed (with short timeout) const result = await Promise.race([ - agent.promise.then(r => ({ completed: true, result: r })), - new Promise<{ completed: false }>(resolve => + agent.promise.then(r => ({ completed: true as const, result: r })), + new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), 100), ), ]); @@ -365,6 +454,21 @@ export const getBackgroundAgentStatus = async ( }; }; +/** + * Get current agent execution status + */ +export const getAgentExecutionStatus = (): { + running: number; + queued: number; + background: number; + maxConcurrent: number; +} => ({ + running: runningForegroundAgents, + queued: agentQueue.length, + background: backgroundAgents.size, + maxConcurrent: MAX_CONCURRENT_AGENTS, +}); + /** * Tool definition for task agent */ @@ -380,12 +484,17 @@ Available agent types: - refactor: Code refactoring and improvement - plan: Planning and architecture design +PARALLEL EXECUTION: +- Up to ${MAX_CONCURRENT_AGENTS} agents can run simultaneously +- Launch multiple agents in a single message for parallel execution +- Agents exceeding the limit will queue automatically + Use agents when: - Task requires specialized focus - Multiple parallel investigations needed - Complex implementation that benefits from isolation -Agents run with their own context and tools, returning results when complete.`, +Example: To explore 3 areas of the codebase in parallel, call task_agent 3 times in the same message.`, parameters: taskAgentSchema, execute: executeTaskAgent, }; diff --git a/src/types/session.ts b/src/types/session.ts index 07131d4..30e8ddd 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -8,4 +8,26 @@ export interface SessionInfo { workingDirectory?: string; createdAt: number; updatedAt: number; + /** Parent session ID for subagent sessions */ + parentSessionId?: string; + /** Whether this is a subagent session */ + isSubagent?: boolean; + /** Subagent type if this is a subagent session */ + subagentType?: string; +} + +/** + * Configuration for creating a subagent session + */ +export interface SubagentSessionConfig { + /** Parent session ID */ + parentSessionId: string; + /** Type of subagent (explore, implement, test, etc.) */ + subagentType: string; + /** Task description */ + task: string; + /** Working directory */ + workingDirectory: string; + /** Context files to include */ + contextFiles?: string[]; }