feat: implement parallel agent execution and unified agent registry
- Fix streaming JSON parsing issues (buffer processing on stream end) - Increase max_tokens for tool-enabled requests (16384) - Add parallel tool execution for task_agent, read, glob, grep (up to 3 concurrent) - Register task_agent tool with queue system and concurrency control - Add session-based isolation with parentSessionId tracking - Create unified agent registry mapping agents from: - OpenCode (7 agents: build, plan, explore, general, etc.) - Claude Code (12 agents: code-explorer, code-architect, etc.) - Cursor (3 agents: pair-programmer, cli, chat) - CodeTyper native (6 agents) - Add agent/skill creation system with LLM-based generation - Store custom agents in .codetyper/agents/*.md - Store custom skills in .codetyper/skills/*/SKILL.md
This commit is contained in:
286
src/constants/agent-templates.ts
Normal file
286
src/constants/agent-templates.ts
Normal file
@@ -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 <example> 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. <example>Context: User wants code reviewed\nuser: "Review this function for bugs"\nassistant: "I'll use the code-reviewer agent to analyze the code."\n</example>
|
||||
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;
|
||||
665
src/constants/unified-agent-registry.ts
Normal file
665
src/constants/unified-agent-registry.ts
Normal file
@@ -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<AgentTier, "fast" | "balanced" | "thorough"> = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
255
src/services/agent-creator-service.ts
Normal file
255
src/services/agent-creator-service.ts
Normal file
@@ -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<void> => {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new agent from description using LLM
|
||||
*/
|
||||
export const createAgentFromDescription = async (
|
||||
options: CreateAgentOptions,
|
||||
): Promise<CreationResult> => {
|
||||
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<CreationResult> => {
|
||||
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<string[]> => {
|
||||
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<string[]> => {
|
||||
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<string, string[]> = {
|
||||
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";
|
||||
};
|
||||
@@ -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<ToolResult> => {
|
||||
// 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<Array<{ toolCall: ToolCall; result: ToolResult }>> => {
|
||||
// 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<Array<{ toolCall: ToolCall; result: ToolResult }>> => {
|
||||
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
|
||||
|
||||
@@ -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<Array<{ toolCall: ToolCall; result: ToolResult }>> => {
|
||||
// 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<Array<{ toolCall: ToolCall; result: ToolResult }>> => {
|
||||
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
|
||||
|
||||
@@ -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<void> => {
|
||||
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<SubagentChatSession> => {
|
||||
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<SubagentChatSession[]> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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";
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<typeof taskAgentSchema>;
|
||||
|
||||
/**
|
||||
* Active background agents
|
||||
* Active background agents with their results
|
||||
*/
|
||||
const backgroundAgents = new Map<string, {
|
||||
interface BackgroundAgent {
|
||||
id: string;
|
||||
type: AgentType;
|
||||
task: string;
|
||||
startTime: number;
|
||||
promise: Promise<ToolResult>;
|
||||
}>();
|
||||
result?: ToolResult;
|
||||
}
|
||||
|
||||
const backgroundAgents = new Map<string, BackgroundAgent>();
|
||||
|
||||
/**
|
||||
* 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<void> => {
|
||||
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<ToolResult> => {
|
||||
// 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,
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user