mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add A2A (Agent-to-Agent) protocol support - Phase 1
Implements Phase 1 of A2A protocol support as discussed in #12007: - A2A types (agent config, task, message, artifact types) - A2aHub service for managing agent connections - delegate_to_agent tool for delegating tasks to external agents - Manual configuration via a2a_settings.json (global/project) - Configurable headers for authentication (JSON key-value pairs) - Integration with system prompt, tool parser, and ClineProvider - A2A tool group added to code mode - Tests for A2aHub service
This commit is contained in:
parent
137d3f4fd8
commit
5c1123a9bf
17 changed files with 1053 additions and 5 deletions
192
packages/types/src/a2a.ts
Normal file
192
packages/types/src/a2a.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import { z } from "zod"
|
||||
|
||||
// ============================================================================
|
||||
// A2A Agent Configuration (stored in a2a_settings.json)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration for an A2A agent, stored in settings files.
|
||||
* Follows the same pattern as MCP server configuration.
|
||||
*/
|
||||
export const a2aAgentConfigSchema = z.object({
|
||||
/** The URL endpoint for the A2A agent */
|
||||
url: z.string().url(),
|
||||
/** Optional custom headers for authentication (JSON key-value pairs) */
|
||||
headers: z.record(z.string()).optional(),
|
||||
/** Whether this agent is disabled */
|
||||
disabled: z.boolean().optional(),
|
||||
/** Timeout in seconds for task operations (default: 300 for long-running tasks) */
|
||||
timeout: z.number().min(1).max(7200).optional().default(300),
|
||||
/** Human-readable description of what this agent does */
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type A2aAgentConfig = z.infer<typeof a2aAgentConfigSchema>
|
||||
|
||||
/**
|
||||
* The full A2A settings file schema (maps agent names to configs).
|
||||
*/
|
||||
export const a2aSettingsSchema = z.object({
|
||||
a2aAgents: z.record(z.string(), a2aAgentConfigSchema).default({}),
|
||||
})
|
||||
|
||||
export type A2aSettings = z.infer<typeof a2aSettingsSchema>
|
||||
|
||||
// ============================================================================
|
||||
// A2A Agent Card (fetched from the agent's /.well-known/agent.json)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Represents an A2A Agent Card as defined in the A2A protocol spec.
|
||||
* Contains metadata about an agent's capabilities.
|
||||
*/
|
||||
export type A2aAgentCard = {
|
||||
name: string
|
||||
description?: string
|
||||
url: string
|
||||
version?: string
|
||||
capabilities?: {
|
||||
streaming?: boolean
|
||||
pushNotifications?: boolean
|
||||
stateTransitionHistory?: boolean
|
||||
}
|
||||
skills?: A2aSkill[]
|
||||
}
|
||||
|
||||
export type A2aSkill = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
examples?: string[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// A2A Task Types (JSON-RPC based protocol)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A2A message part - text, file, or data.
|
||||
*/
|
||||
export type A2aTextPart = {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type A2aFilePart = {
|
||||
type: "file"
|
||||
file: {
|
||||
name?: string
|
||||
mimeType?: string
|
||||
bytes?: string // base64 encoded
|
||||
uri?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type A2aDataPart = {
|
||||
type: "data"
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type A2aPart = A2aTextPart | A2aFilePart | A2aDataPart
|
||||
|
||||
/**
|
||||
* A2A Message - a message in a task conversation.
|
||||
*/
|
||||
export type A2aMessage = {
|
||||
role: "user" | "agent"
|
||||
parts: A2aPart[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* A2A Artifact - output produced by an agent.
|
||||
*/
|
||||
export type A2aArtifact = {
|
||||
name?: string
|
||||
description?: string
|
||||
parts: A2aPart[]
|
||||
index?: number
|
||||
append?: boolean
|
||||
lastChunk?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* A2A Task state as defined in the protocol.
|
||||
*/
|
||||
export const a2aTaskStates = ["submitted", "working", "input-required", "completed", "canceled", "failed"] as const
|
||||
|
||||
export type A2aTaskState = (typeof a2aTaskStates)[number]
|
||||
|
||||
/**
|
||||
* A2A Task Status
|
||||
*/
|
||||
export type A2aTaskStatus = {
|
||||
state: A2aTaskState
|
||||
message?: A2aMessage
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A2A Task - represents a delegated unit of work.
|
||||
*/
|
||||
export type A2aTask = {
|
||||
id: string
|
||||
sessionId?: string
|
||||
status: A2aTaskStatus
|
||||
history?: A2aMessage[]
|
||||
artifacts?: A2aArtifact[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// A2A JSON-RPC Types
|
||||
// ============================================================================
|
||||
|
||||
export type A2aJsonRpcRequest = {
|
||||
jsonrpc: "2.0"
|
||||
id: string | number
|
||||
method: string
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type A2aJsonRpcResponse = {
|
||||
jsonrpc: "2.0"
|
||||
id: string | number
|
||||
result?: A2aTask
|
||||
error?: {
|
||||
code: number
|
||||
message: string
|
||||
data?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// A2A Agent Runtime State (for UI display)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Runtime state of an A2A agent, used for display in the UI.
|
||||
* Mirrors the pattern of McpServer type.
|
||||
*/
|
||||
export type A2aAgent = {
|
||||
name: string
|
||||
config: string // JSON stringified config
|
||||
status: "connected" | "connecting" | "disconnected"
|
||||
error?: string
|
||||
disabled?: boolean
|
||||
agentCard?: A2aAgentCard
|
||||
activeTasks?: A2aTask[]
|
||||
source?: "global" | "project"
|
||||
projectPath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ClineAsk type for A2A agent delegation approval.
|
||||
*/
|
||||
export type ClineAskDelegateToAgent = {
|
||||
type: "delegate_to_agent"
|
||||
agentName: string
|
||||
message: string
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./a2a.js"
|
||||
export * from "./api.js"
|
||||
export * from "./cli.js"
|
||||
export * from "./cloud.js"
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
|
|||
whenToUse:
|
||||
"Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.",
|
||||
description: "Write, modify, and refactor code",
|
||||
groups: ["read", "edit", "command", "mcp"],
|
||||
groups: ["read", "edit", "command", "mcp", "a2a"],
|
||||
},
|
||||
{
|
||||
slug: "ask",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { z } from "zod"
|
|||
* ToolGroup
|
||||
*/
|
||||
|
||||
export const toolGroups = ["read", "edit", "command", "mcp", "modes"] as const
|
||||
export const toolGroups = ["read", "edit", "command", "mcp", "a2a", "modes"] as const
|
||||
|
||||
export const toolGroupsSchema = z.enum(toolGroups)
|
||||
|
||||
|
|
@ -46,6 +46,7 @@ export const toolNames = [
|
|||
"skill",
|
||||
"generate_image",
|
||||
"custom_tool",
|
||||
"delegate_to_agent",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
|
|
|||
|
|
@ -565,6 +565,15 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
case "delegate_to_agent":
|
||||
if (partialArgs.agent_name !== undefined || partialArgs.message !== undefined) {
|
||||
nativeArgs = {
|
||||
agent_name: partialArgs.agent_name,
|
||||
message: partialArgs.message,
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "apply_patch":
|
||||
if (partialArgs.patch !== undefined) {
|
||||
nativeArgs = {
|
||||
|
|
@ -921,6 +930,15 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
case "delegate_to_agent":
|
||||
if (args.agent_name !== undefined && args.message !== undefined) {
|
||||
nativeArgs = {
|
||||
agent_name: args.agent_name,
|
||||
message: args.message,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "access_mcp_resource":
|
||||
if (args.server_name !== undefined && args.uri !== undefined) {
|
||||
nativeArgs = {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { updateTodoListTool } from "../tools/UpdateTodoListTool"
|
|||
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
|
||||
import { skillTool } from "../tools/SkillTool"
|
||||
import { generateImageTool } from "../tools/GenerateImageTool"
|
||||
import { delegateToAgentTool } from "../tools/DelegateToAgentTool"
|
||||
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
|
||||
import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
|
||||
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
|
||||
|
|
@ -789,6 +790,13 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
pushToolResult,
|
||||
})
|
||||
break
|
||||
case "delegate_to_agent":
|
||||
await delegateToAgentTool.handle(cline, block as ToolUse<"delegate_to_agent">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
})
|
||||
break
|
||||
case "ask_followup_question":
|
||||
await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
|
||||
askApproval,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { A2aHub } from "../../../services/a2a/A2aHub"
|
||||
|
||||
export function getCapabilitiesSection(cwd: string, mcpHub?: McpHub): string {
|
||||
export function getCapabilitiesSection(cwd: string, mcpHub?: McpHub, a2aHub?: A2aHub): string {
|
||||
return `====
|
||||
|
||||
CAPABILITIES
|
||||
|
|
@ -11,6 +12,18 @@ CAPABILITIES
|
|||
mcpHub
|
||||
? `
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
`
|
||||
: ""
|
||||
}${
|
||||
a2aHub && a2aHub.getAgents().length > 0
|
||||
? `
|
||||
- You have access to A2A (Agent-to-Agent) agents that you can delegate tasks to. These are external agents that can handle specialized work. Use the delegate_to_agent tool to send tasks to them. Available agents: ${a2aHub
|
||||
.getAgents()
|
||||
.map((a) => {
|
||||
const desc = a.agentCard?.description ?? ""
|
||||
return `"${a.name}"${desc ? ` (${desc})` : ""}`
|
||||
})
|
||||
.join(", ")}
|
||||
`
|
||||
: ""
|
||||
}`
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { formatLanguage } from "../../shared/language"
|
|||
import { isEmpty } from "../../utils/object"
|
||||
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { A2aHub } from "../../services/a2a/A2aHub"
|
||||
import { CodeIndexManager } from "../../services/code-index/manager"
|
||||
import { SkillsManager } from "../../services/skills/SkillsManager"
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ async function generatePrompt(
|
|||
todoList?: TodoItem[],
|
||||
modelId?: string,
|
||||
skillsManager?: SkillsManager,
|
||||
a2aHub?: A2aHub,
|
||||
): Promise<string> {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
|
|
@ -69,6 +71,11 @@ async function generatePrompt(
|
|||
const hasMcpServers = mcpHub && mcpHub.getServers().length > 0
|
||||
const shouldIncludeMcp = hasMcpGroup && hasMcpServers
|
||||
|
||||
// Check if A2A functionality should be included
|
||||
const hasA2aGroup = modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "a2a")
|
||||
const hasA2aAgents = a2aHub && a2aHub.getAgents().length > 0
|
||||
const shouldIncludeA2a = hasA2aGroup && hasA2aAgents
|
||||
|
||||
const codeIndexManager = CodeIndexManager.getInstance(context, cwd)
|
||||
|
||||
// Tool calling is native-only.
|
||||
|
|
@ -90,7 +97,7 @@ ${getSharedToolUseSection()}${toolsCatalog}
|
|||
|
||||
${getToolUseGuidelinesSection()}
|
||||
|
||||
${getCapabilitiesSection(cwd, shouldIncludeMcp ? mcpHub : undefined)}
|
||||
${getCapabilitiesSection(cwd, shouldIncludeMcp ? mcpHub : undefined, shouldIncludeA2a ? a2aHub : undefined)}
|
||||
|
||||
${modesSection}
|
||||
${skillsSection ? `\n${skillsSection}` : ""}
|
||||
|
|
@ -126,6 +133,7 @@ export const SYSTEM_PROMPT = async (
|
|||
todoList?: TodoItem[],
|
||||
modelId?: string,
|
||||
skillsManager?: SkillsManager,
|
||||
a2aHub?: A2aHub,
|
||||
): Promise<string> => {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
|
|
@ -154,5 +162,6 @@ export const SYSTEM_PROMPT = async (
|
|||
todoList,
|
||||
modelId,
|
||||
skillsManager,
|
||||
a2aHub,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
33
src/core/prompts/tools/native-tools/delegate_to_agent.ts
Normal file
33
src/core/prompts/tools/native-tools/delegate_to_agent.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type OpenAI from "openai"
|
||||
|
||||
const DELEGATE_TO_AGENT_DESCRIPTION = `Delegate a task to an external A2A (Agent-to-Agent) agent. Use this tool when you need to send work to a specialized agent that can handle a specific task. The agent will process the request and return results. This runs as a background operation - you can continue with other work while waiting for the agent to respond.
|
||||
|
||||
Parameters:
|
||||
- agent_name: (required) The name of the A2A agent to delegate to, as configured in the A2A settings.
|
||||
- message: (required) The task description or message to send to the agent. Be specific about what you need the agent to do.`
|
||||
|
||||
const delegate_to_agent: OpenAI.Chat.ChatCompletionTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delegate_to_agent",
|
||||
description: DELEGATE_TO_AGENT_DESCRIPTION,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
agent_name: {
|
||||
type: "string",
|
||||
description: "The name of the A2A agent to delegate to, as configured in the A2A settings.",
|
||||
},
|
||||
message: {
|
||||
type: "string",
|
||||
description:
|
||||
"The task description or message to send to the agent. Be specific about what you need the agent to do.",
|
||||
},
|
||||
},
|
||||
required: ["agent_name", "message"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default delegate_to_agent
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type OpenAI from "openai"
|
||||
import accessMcpResource from "./access_mcp_resource"
|
||||
import delegateToAgent from "./delegate_to_agent"
|
||||
import { apply_diff } from "./apply_diff"
|
||||
import applyPatch from "./apply_patch"
|
||||
import askFollowupQuestion from "./ask_followup_question"
|
||||
|
|
@ -53,6 +54,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
|
|||
askFollowupQuestion,
|
||||
attemptCompletion,
|
||||
codebaseSearch,
|
||||
delegateToAgent,
|
||||
executeCommand,
|
||||
generateImage,
|
||||
listFiles,
|
||||
|
|
|
|||
162
src/core/tools/DelegateToAgentTool.ts
Normal file
162
src/core/tools/DelegateToAgentTool.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import type { ClineAskDelegateToAgent } from "@roo-code/types"
|
||||
|
||||
import { Task } from "../task/Task"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { t } from "../../i18n"
|
||||
import type { ToolUse } from "../../shared/tools"
|
||||
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
|
||||
interface DelegateToAgentParams {
|
||||
agent_name: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export class DelegateToAgentTool extends BaseTool<"delegate_to_agent"> {
|
||||
readonly name = "delegate_to_agent" as const
|
||||
|
||||
async execute(params: DelegateToAgentParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
|
||||
const { askApproval, handleError, pushToolResult } = callbacks
|
||||
|
||||
try {
|
||||
// Validate required parameters
|
||||
if (!params.agent_name) {
|
||||
task.consecutiveMistakeCount++
|
||||
task.recordToolError("delegate_to_agent")
|
||||
task.didToolFailInCurrentTurn = true
|
||||
pushToolResult(await task.sayAndCreateMissingParamError("delegate_to_agent", "agent_name"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!params.message) {
|
||||
task.consecutiveMistakeCount++
|
||||
task.recordToolError("delegate_to_agent")
|
||||
task.didToolFailInCurrentTurn = true
|
||||
pushToolResult(await task.sayAndCreateMissingParamError("delegate_to_agent", "message"))
|
||||
return
|
||||
}
|
||||
|
||||
const agentName = params.agent_name
|
||||
const message = params.message
|
||||
|
||||
// Validate that the agent exists
|
||||
const provider = task.providerRef.deref()
|
||||
if (!provider) {
|
||||
pushToolResult(formatResponse.toolError("Provider reference lost"))
|
||||
return
|
||||
}
|
||||
|
||||
const a2aHub = provider.getA2aHub?.()
|
||||
if (!a2aHub) {
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
"A2A is not available. No A2A agents are configured. Please configure agents in a2a_settings.json.",
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const agent = a2aHub.getAgent(agentName)
|
||||
if (!agent) {
|
||||
const availableAgents = a2aHub
|
||||
.getAgents()
|
||||
.map((a) => a.name)
|
||||
.join(", ")
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`A2A agent "${agentName}" not found or is disabled. Available agents: ${availableAgents || "none"}`,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
task.consecutiveMistakeCount = 0
|
||||
|
||||
// Get user approval
|
||||
const completeMessage = JSON.stringify({
|
||||
type: "delegate_to_agent",
|
||||
agentName,
|
||||
message,
|
||||
} satisfies ClineAskDelegateToAgent)
|
||||
|
||||
const didApprove = await askApproval("delegate_to_agent" as any, completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the A2A task
|
||||
try {
|
||||
const result = await a2aHub.sendTask(agentName, message)
|
||||
|
||||
// Format the result
|
||||
const responseText = this.formatTaskResult(result)
|
||||
pushToolResult(responseText)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
pushToolResult(formatResponse.toolError(`Failed to delegate to agent "${agentName}": ${errorMessage}`))
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("delegating to A2A agent", error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
override async handlePartial(task: Task, block: ToolUse<"delegate_to_agent">): Promise<void> {
|
||||
const params = block.params
|
||||
const partialMessage = JSON.stringify({
|
||||
type: "delegate_to_agent",
|
||||
agentName: params.agent_name ?? "",
|
||||
message: params.message ?? "",
|
||||
} satisfies ClineAskDelegateToAgent)
|
||||
|
||||
await task.ask("delegate_to_agent" as any, partialMessage, true).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an A2A task result into a human-readable string.
|
||||
*/
|
||||
private formatTaskResult(task: import("@roo-code/types").A2aTask): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push(`Task ID: ${task.id}`)
|
||||
parts.push(`Status: ${task.status.state}`)
|
||||
|
||||
// Include the agent's response message if present
|
||||
if (task.status.message) {
|
||||
const textParts = task.status.message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => (p as import("@roo-code/types").A2aTextPart).text)
|
||||
|
||||
if (textParts.length > 0) {
|
||||
parts.push(`\nAgent Response:\n${textParts.join("\n")}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Include artifacts if present
|
||||
if (task.artifacts && task.artifacts.length > 0) {
|
||||
parts.push("\nArtifacts:")
|
||||
for (const artifact of task.artifacts) {
|
||||
if (artifact.name) {
|
||||
parts.push(` - ${artifact.name}${artifact.description ? `: ${artifact.description}` : ""}`)
|
||||
}
|
||||
const textParts = artifact.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => (p as import("@roo-code/types").A2aTextPart).text)
|
||||
if (textParts.length > 0) {
|
||||
parts.push(` ${textParts.join("\n ")}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the task requires input, note that
|
||||
if (task.status.state === "input-required") {
|
||||
parts.push(
|
||||
"\nNote: The agent is requesting additional input. You can send another delegate_to_agent call with the same agent to continue the conversation.",
|
||||
)
|
||||
}
|
||||
|
||||
return parts.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
export const delegateToAgentTool = new DelegateToAgentTool()
|
||||
|
|
@ -70,6 +70,7 @@ import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
|||
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { McpServerManager } from "../../services/mcp/McpServerManager"
|
||||
import { A2aHub } from "../../services/a2a/A2aHub"
|
||||
import { MarketplaceManager } from "../../services/marketplace"
|
||||
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
|
||||
import { CodeIndexManager } from "../../services/code-index/manager"
|
||||
|
|
@ -141,6 +142,7 @@ export class ClineProvider
|
|||
private codeIndexManager?: CodeIndexManager
|
||||
private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
|
||||
protected mcpHub?: McpHub // Change from private to protected
|
||||
protected a2aHub?: A2aHub
|
||||
protected skillsManager?: SkillsManager
|
||||
private marketplaceManager: MarketplaceManager
|
||||
private mdmService?: MdmService
|
||||
|
|
@ -225,6 +227,18 @@ export class ClineProvider
|
|||
this.log(`Failed to initialize MCP Hub: ${error}`)
|
||||
})
|
||||
|
||||
// Initialize A2A Hub for agent-to-agent communication
|
||||
const a2aHub = new A2aHub(this)
|
||||
a2aHub
|
||||
.initialize()
|
||||
.then(() => {
|
||||
this.a2aHub = a2aHub
|
||||
this.a2aHub.registerClient()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.log(`Failed to initialize A2A Hub: ${error}`)
|
||||
})
|
||||
|
||||
// Initialize Skills Manager for skill discovery
|
||||
this.skillsManager = new SkillsManager(this)
|
||||
this.skillsManager.initialize().catch((error) => {
|
||||
|
|
@ -707,6 +721,8 @@ export class ClineProvider
|
|||
this._workspaceTracker = undefined
|
||||
await this.mcpHub?.unregisterClient()
|
||||
this.mcpHub = undefined
|
||||
await this.a2aHub?.unregisterClient()
|
||||
this.a2aHub = undefined
|
||||
await this.skillsManager?.dispose()
|
||||
this.skillsManager = undefined
|
||||
this.marketplaceManager?.cleanup()
|
||||
|
|
@ -2747,6 +2763,10 @@ export class ClineProvider
|
|||
return this.mcpHub
|
||||
}
|
||||
|
||||
public getA2aHub(): A2aHub | undefined {
|
||||
return this.a2aHub
|
||||
}
|
||||
|
||||
public getSkillsManager(): SkillsManager | undefined {
|
||||
return this.skillsManager
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
|
|||
undefined, // todoList
|
||||
undefined, // modelId
|
||||
provider.getSkillsManager(),
|
||||
provider.getA2aHub(),
|
||||
)
|
||||
|
||||
return systemPrompt
|
||||
|
|
|
|||
451
src/services/a2a/A2aHub.ts
Normal file
451
src/services/a2a/A2aHub.ts
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
import type {
|
||||
A2aAgent,
|
||||
A2aAgentCard,
|
||||
A2aAgentConfig,
|
||||
A2aJsonRpcRequest,
|
||||
A2aJsonRpcResponse,
|
||||
A2aMessage,
|
||||
A2aTask,
|
||||
A2aTaskState,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { safeWriteJson } from "../../utils/safeWriteJson"
|
||||
|
||||
// ============================================================================
|
||||
// A2A Settings Types
|
||||
// ============================================================================
|
||||
|
||||
interface A2aSettingsFile {
|
||||
a2aAgents?: Record<string, A2aAgentConfig>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// A2aHub - Manages A2A agent connections
|
||||
// ============================================================================
|
||||
|
||||
export class A2aHub {
|
||||
private providerRef: WeakRef<ClineProvider>
|
||||
private agents: A2aAgent[] = []
|
||||
private fileWatchers: FSWatcher[] = []
|
||||
private settingsFilePath: string = ""
|
||||
private projectSettingsFilePath: string = ""
|
||||
private isDisposed = false
|
||||
private clientCount = 0
|
||||
|
||||
constructor(provider: ClineProvider) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set up settings file paths
|
||||
const globalStoragePath = provider.context.globalStorageUri.fsPath
|
||||
this.settingsFilePath = path.join(globalStoragePath, GlobalFileNames.a2aSettings)
|
||||
this.projectSettingsFilePath = path.join(getWorkspacePath() ?? "", ".roo", "a2a.json")
|
||||
|
||||
// Ensure default settings file exists
|
||||
if (!(await fileExistsAtPath(this.settingsFilePath))) {
|
||||
await safeWriteJson(this.settingsFilePath, { a2aAgents: {} })
|
||||
}
|
||||
|
||||
// Load agents from settings
|
||||
await this.loadAgents()
|
||||
|
||||
// Watch settings files for changes
|
||||
this.watchSettingsFiles()
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.isDisposed = true
|
||||
for (const watcher of this.fileWatchers) {
|
||||
await watcher.close()
|
||||
}
|
||||
this.fileWatchers = []
|
||||
this.agents = []
|
||||
}
|
||||
|
||||
registerClient(): void {
|
||||
this.clientCount++
|
||||
}
|
||||
|
||||
async unregisterClient(): Promise<void> {
|
||||
this.clientCount--
|
||||
if (this.clientCount <= 0) {
|
||||
await this.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Agent Management
|
||||
// ========================================================================
|
||||
|
||||
getAgents(): A2aAgent[] {
|
||||
return this.agents.filter((a) => !a.disabled)
|
||||
}
|
||||
|
||||
getAllAgents(): A2aAgent[] {
|
||||
return [...this.agents]
|
||||
}
|
||||
|
||||
getAgent(name: string): A2aAgent | undefined {
|
||||
return this.agents.find((a) => a.name === name && !a.disabled)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// A2A Protocol Operations
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Send a task to an A2A agent using the tasks/send method.
|
||||
* Returns the task result with status and any artifacts.
|
||||
*/
|
||||
async sendTask(agentName: string, message: string, taskId?: string): Promise<A2aTask> {
|
||||
const agent = this.getAgent(agentName)
|
||||
if (!agent) {
|
||||
throw new Error(`A2A agent "${agentName}" not found or is disabled`)
|
||||
}
|
||||
|
||||
const config = JSON.parse(agent.config) as A2aAgentConfig
|
||||
|
||||
const id = taskId ?? `task-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
||||
|
||||
const request: A2aJsonRpcRequest = {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
method: "tasks/send",
|
||||
params: {
|
||||
id,
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: message }],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(config, request)
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`A2A agent error (${response.error.code}): ${response.error.message}`)
|
||||
}
|
||||
|
||||
if (!response.result) {
|
||||
throw new Error("A2A agent returned empty result")
|
||||
}
|
||||
|
||||
// Update agent's active tasks
|
||||
const agentIndex = this.agents.findIndex((a) => a.name === agentName)
|
||||
if (agentIndex >= 0) {
|
||||
const currentAgent = this.agents[agentIndex]
|
||||
const activeTasks = currentAgent.activeTasks ?? []
|
||||
const existingIdx = activeTasks.findIndex((t) => t.id === response.result!.id)
|
||||
if (existingIdx >= 0) {
|
||||
activeTasks[existingIdx] = response.result
|
||||
} else {
|
||||
activeTasks.push(response.result)
|
||||
}
|
||||
this.agents[agentIndex] = { ...currentAgent, activeTasks }
|
||||
}
|
||||
|
||||
this.notifyWebview()
|
||||
return response.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of a task from an A2A agent.
|
||||
*/
|
||||
async getTask(agentName: string, taskId: string): Promise<A2aTask> {
|
||||
const agent = this.getAgent(agentName)
|
||||
if (!agent) {
|
||||
throw new Error(`A2A agent "${agentName}" not found or is disabled`)
|
||||
}
|
||||
|
||||
const config = JSON.parse(agent.config) as A2aAgentConfig
|
||||
|
||||
const request: A2aJsonRpcRequest = {
|
||||
jsonrpc: "2.0",
|
||||
id: `get-${taskId}`,
|
||||
method: "tasks/get",
|
||||
params: { id: taskId },
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(config, request)
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`A2A agent error (${response.error.code}): ${response.error.message}`)
|
||||
}
|
||||
|
||||
if (!response.result) {
|
||||
throw new Error("A2A agent returned empty result")
|
||||
}
|
||||
|
||||
return response.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a task on an A2A agent.
|
||||
*/
|
||||
async cancelTask(agentName: string, taskId: string): Promise<A2aTask> {
|
||||
const agent = this.getAgent(agentName)
|
||||
if (!agent) {
|
||||
throw new Error(`A2A agent "${agentName}" not found or is disabled`)
|
||||
}
|
||||
|
||||
const config = JSON.parse(agent.config) as A2aAgentConfig
|
||||
|
||||
const request: A2aJsonRpcRequest = {
|
||||
jsonrpc: "2.0",
|
||||
id: `cancel-${taskId}`,
|
||||
method: "tasks/cancel",
|
||||
params: { id: taskId },
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(config, request)
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`A2A agent error (${response.error.code}): ${response.error.message}`)
|
||||
}
|
||||
|
||||
return response.result ?? { id: taskId, status: { state: "canceled" } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the agent card from the agent's well-known endpoint.
|
||||
*/
|
||||
async fetchAgentCard(config: A2aAgentConfig): Promise<A2aAgentCard | undefined> {
|
||||
try {
|
||||
const url = new URL(config.url)
|
||||
const cardUrl = `${url.origin}/.well-known/agent.json`
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(config.headers ?? {}),
|
||||
}
|
||||
|
||||
const response = await fetch(cardUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
return (await response.json()) as A2aAgentCard
|
||||
}
|
||||
} catch {
|
||||
// Agent card is optional; ignore errors
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Settings Path Access
|
||||
// ========================================================================
|
||||
|
||||
async getA2aSettingsFilePath(): Promise<string> {
|
||||
return this.settingsFilePath
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Private Methods
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Make an HTTP request to an A2A agent.
|
||||
*/
|
||||
private async makeRequest(config: A2aAgentConfig, request: A2aJsonRpcRequest): Promise<A2aJsonRpcResponse> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(config.headers ?? {}),
|
||||
}
|
||||
|
||||
const timeoutMs = (config.timeout ?? 300) * 1000
|
||||
|
||||
const response = await fetch(config.url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(request),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`A2A HTTP error ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return (await response.json()) as A2aJsonRpcResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Load agents from settings files.
|
||||
*/
|
||||
private async loadAgents(): Promise<void> {
|
||||
const globalAgents = await this.loadSettingsFile(this.settingsFilePath, "global")
|
||||
const projectAgents = await this.loadSettingsFile(this.projectSettingsFilePath, "project")
|
||||
|
||||
// Project settings override global settings for agents with the same name
|
||||
const agentMap = new Map<string, A2aAgent>()
|
||||
|
||||
for (const agent of globalAgents) {
|
||||
agentMap.set(agent.name, agent)
|
||||
}
|
||||
|
||||
for (const agent of projectAgents) {
|
||||
agentMap.set(agent.name, agent)
|
||||
}
|
||||
|
||||
const newAgents = Array.from(agentMap.values())
|
||||
|
||||
if (!deepEqual(this.agents, newAgents)) {
|
||||
this.agents = newAgents
|
||||
|
||||
// Try to fetch agent cards for newly connected agents
|
||||
for (const agent of this.agents) {
|
||||
if (!agent.agentCard && !agent.disabled) {
|
||||
const config = JSON.parse(agent.config) as A2aAgentConfig
|
||||
this.fetchAgentCard(config).then((card) => {
|
||||
if (card) {
|
||||
agent.agentCard = card
|
||||
agent.status = "connected"
|
||||
this.notifyWebview()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.notifyWebview()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load agents from a single settings file.
|
||||
*/
|
||||
private async loadSettingsFile(filePath: string, source: "global" | "project"): Promise<A2aAgent[]> {
|
||||
try {
|
||||
if (!(await fileExistsAtPath(filePath))) {
|
||||
return []
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const settings: A2aSettingsFile = JSON.parse(content)
|
||||
|
||||
if (!settings.a2aAgents) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(settings.a2aAgents).map(([name, config]) => ({
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting" as const,
|
||||
disabled: config.disabled,
|
||||
description: config.description,
|
||||
source,
|
||||
projectPath: source === "project" ? getWorkspacePath() : undefined,
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch settings files for changes and reload.
|
||||
*/
|
||||
private watchSettingsFiles(): void {
|
||||
const watchPaths = [this.settingsFilePath]
|
||||
|
||||
if (getWorkspacePath()) {
|
||||
watchPaths.push(this.projectSettingsFilePath)
|
||||
}
|
||||
|
||||
for (const watchPath of watchPaths) {
|
||||
try {
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
||||
watcher.on("change", () => {
|
||||
if (!this.isDisposed) {
|
||||
this.loadAgents()
|
||||
}
|
||||
})
|
||||
|
||||
watcher.on("add", () => {
|
||||
if (!this.isDisposed) {
|
||||
this.loadAgents()
|
||||
}
|
||||
})
|
||||
|
||||
this.fileWatchers.push(watcher)
|
||||
} catch {
|
||||
// Ignore watcher errors for non-existent paths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the webview of agent state changes.
|
||||
*/
|
||||
private notifyWebview(): void {
|
||||
const provider = this.providerRef.deref()
|
||||
if (provider) {
|
||||
provider.postMessageToWebview({
|
||||
type: "a2aAgents",
|
||||
a2aAgents: this.getAllAgents(),
|
||||
} as any)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle an agent's disabled state.
|
||||
*/
|
||||
async toggleAgentDisabled(agentName: string, source: "global" | "project", disabled: boolean): Promise<void> {
|
||||
const filePath = source === "global" ? this.settingsFilePath : this.projectSettingsFilePath
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const settings: A2aSettingsFile = JSON.parse(content)
|
||||
|
||||
if (settings.a2aAgents?.[agentName]) {
|
||||
settings.a2aAgents[agentName].disabled = disabled
|
||||
await safeWriteJson(filePath, settings)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to toggle agent: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an agent from settings.
|
||||
*/
|
||||
async deleteAgent(agentName: string, source: "global" | "project"): Promise<void> {
|
||||
const filePath = source === "global" ? this.settingsFilePath : this.projectSettingsFilePath
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const settings: A2aSettingsFile = JSON.parse(content)
|
||||
|
||||
if (settings.a2aAgents?.[agentName]) {
|
||||
delete settings.a2aAgents[agentName]
|
||||
await safeWriteJson(filePath, settings)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete agent: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/services/a2a/__tests__/A2aHub.spec.ts
Normal file
123
src/services/a2a/__tests__/A2aHub.spec.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { A2aHub } from "../A2aHub"
|
||||
import type { ClineProvider } from "../../../core/webview/ClineProvider"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("fs/promises", () => ({
|
||||
readFile: vi.fn().mockResolvedValue(JSON.stringify({ a2aAgents: {} })),
|
||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock("chokidar", () => ({
|
||||
default: {
|
||||
watch: vi.fn().mockReturnValue({
|
||||
on: vi.fn().mockReturnThis(),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../../../utils/fs", () => ({
|
||||
fileExistsAtPath: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
vi.mock("../../../utils/path", () => ({
|
||||
getWorkspacePath: vi.fn().mockReturnValue("/test/workspace"),
|
||||
arePathsEqual: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
vi.mock("../../../utils/safeWriteJson", () => ({
|
||||
safeWriteJson: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
describe("A2aHub", () => {
|
||||
let mockProvider: Partial<ClineProvider>
|
||||
let a2aHub: A2aHub
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
context: {
|
||||
globalStorageUri: { fsPath: "/test/storage" },
|
||||
} as any,
|
||||
postMessageToWebview: vi.fn(),
|
||||
}
|
||||
a2aHub = new A2aHub(mockProvider as ClineProvider)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await a2aHub.dispose()
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create an A2aHub instance", () => {
|
||||
expect(a2aHub).toBeInstanceOf(A2aHub)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAgents", () => {
|
||||
it("should return empty array when no agents are configured", () => {
|
||||
const agents = a2aHub.getAgents()
|
||||
expect(agents).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAllAgents", () => {
|
||||
it("should return empty array when no agents are configured", () => {
|
||||
const agents = a2aHub.getAllAgents()
|
||||
expect(agents).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAgent", () => {
|
||||
it("should return undefined when agent does not exist", () => {
|
||||
const agent = a2aHub.getAgent("nonexistent")
|
||||
expect(agent).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerClient / unregisterClient", () => {
|
||||
it("should track client count", () => {
|
||||
a2aHub.registerClient()
|
||||
// Should not throw
|
||||
expect(a2aHub.getAgents()).toEqual([])
|
||||
})
|
||||
|
||||
it("should dispose when last client unregisters", async () => {
|
||||
a2aHub.registerClient()
|
||||
await a2aHub.unregisterClient()
|
||||
// After disposal, agents should be empty
|
||||
expect(a2aHub.getAllAgents()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("sendTask", () => {
|
||||
it("should throw when agent is not found", async () => {
|
||||
await expect(a2aHub.sendTask("nonexistent", "test message")).rejects.toThrow(
|
||||
'A2A agent "nonexistent" not found or is disabled',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTask", () => {
|
||||
it("should throw when agent is not found", async () => {
|
||||
await expect(a2aHub.getTask("nonexistent", "task-1")).rejects.toThrow(
|
||||
'A2A agent "nonexistent" not found or is disabled',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelTask", () => {
|
||||
it("should throw when agent is not found", async () => {
|
||||
await expect(a2aHub.cancelTask("nonexistent", "task-1")).rejects.toThrow(
|
||||
'A2A agent "nonexistent" not found or is disabled',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getA2aSettingsFilePath", () => {
|
||||
it("should return empty string before initialization", async () => {
|
||||
const path = await a2aHub.getA2aSettingsFilePath()
|
||||
expect(path).toBe("")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -2,6 +2,7 @@ export const GlobalFileNames = {
|
|||
apiConversationHistory: "api_conversation_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
mcpSettings: "mcp_settings.json",
|
||||
a2aSettings: "a2a_settings.json",
|
||||
customModes: "custom_modes.yaml",
|
||||
taskMetadata: "task_metadata.json",
|
||||
historyItem: "history_item.json",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import type { ClineAsk, ToolProgressStatus, ToolGroup, ToolName, GenerateImageParams } from "@roo-code/types"
|
||||
import type {
|
||||
ClineAsk,
|
||||
ToolProgressStatus,
|
||||
ToolGroup,
|
||||
ToolName,
|
||||
GenerateImageParams,
|
||||
ClineAskDelegateToAgent,
|
||||
} from "@roo-code/types"
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
|
||||
|
|
@ -52,6 +59,7 @@ export const toolParamNames = [
|
|||
"size",
|
||||
"query",
|
||||
"args",
|
||||
"agent_name", // delegate_to_agent parameter
|
||||
"skill", // skill tool parameter
|
||||
"start_line",
|
||||
"end_line",
|
||||
|
|
@ -115,6 +123,7 @@ export type NativeToolArgs = {
|
|||
switch_mode: { mode_slug: string; reason: string }
|
||||
update_todo_list: { todos: string }
|
||||
use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record<string, unknown> }
|
||||
delegate_to_agent: { agent_name: string; message: string }
|
||||
write_to_file: { path: string; content: string }
|
||||
// Add more tools as they are migrated to native protocol
|
||||
}
|
||||
|
|
@ -290,6 +299,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
|
|||
skill: "load skill",
|
||||
generate_image: "generate images",
|
||||
custom_tool: "use custom tools",
|
||||
delegate_to_agent: "delegate to A2A agent",
|
||||
} as const
|
||||
|
||||
// Define available tool groups.
|
||||
|
|
@ -307,6 +317,9 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
|
|||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
},
|
||||
a2a: {
|
||||
tools: ["delegate_to_agent"],
|
||||
},
|
||||
modes: {
|
||||
tools: ["switch_mode", "new_task"],
|
||||
alwaysAvailable: true,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue