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:
2026-02-05 18:35:23 -05:00
parent ad514a920c
commit e2cb41f8d3
11 changed files with 1826 additions and 59 deletions

View 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;

View 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,
},
};

View File

@@ -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 (

View File

@@ -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);
});
};

View 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";
};

View File

@@ -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

View File

@@ -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

View File

@@ -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";

View File

@@ -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)

View File

@@ -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,
};

View File

@@ -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[];
}