Save current state before cleaning up duplicate MCP server files
This commit is contained in:
@@ -1,18 +1,271 @@
|
||||
defmodule AgentCoordinator do
|
||||
@moduledoc """
|
||||
Documentation for `AgentCoordinator`.
|
||||
Agent Coordinator - A Model Context Protocol (MCP) server for multi-agent coordination.
|
||||
|
||||
Agent Coordinator enables multiple AI agents to work together seamlessly across codebases
|
||||
without conflicts. It provides intelligent task distribution, real-time communication,
|
||||
and cross-codebase coordination through a unified MCP interface.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Multi-Agent Coordination**: Register multiple AI agents with different capabilities
|
||||
- **Intelligent Task Distribution**: Automatically assigns tasks based on agent capabilities
|
||||
- **Cross-Codebase Support**: Coordinate work across multiple repositories
|
||||
- **Unified MCP Interface**: Single server providing access to multiple external MCP servers
|
||||
- **Automatic Task Tracking**: Every tool usage becomes a tracked task
|
||||
- **Real-Time Communication**: Heartbeat system for agent liveness and coordination
|
||||
|
||||
## Quick Start
|
||||
|
||||
To start the Agent Coordinator:
|
||||
|
||||
# Start the MCP server
|
||||
./scripts/mcp_launcher.sh
|
||||
|
||||
# Or in development mode
|
||||
iex -S mix
|
||||
|
||||
## Main Components
|
||||
|
||||
- `AgentCoordinator.MCPServer` - Core MCP protocol implementation
|
||||
- `AgentCoordinator.TaskRegistry` - Task management and agent coordination
|
||||
- `AgentCoordinator.UnifiedMCPServer` - Unified interface to external MCP servers
|
||||
- `AgentCoordinator.CodebaseRegistry` - Multi-repository support
|
||||
- `AgentCoordinator.VSCodeToolProvider` - VS Code integration tools
|
||||
|
||||
## MCP Tools Available
|
||||
|
||||
### Agent Coordination
|
||||
- `register_agent` - Register an agent with capabilities
|
||||
- `create_task` - Create tasks with requirements
|
||||
- `get_next_task` - Get assigned tasks
|
||||
- `complete_task` - Mark tasks complete
|
||||
- `get_task_board` - View all agent status
|
||||
- `heartbeat` - Maintain agent liveness
|
||||
|
||||
### Codebase Management
|
||||
- `register_codebase` - Register repositories
|
||||
- `create_cross_codebase_task` - Tasks spanning multiple repos
|
||||
- `add_codebase_dependency` - Define repository relationships
|
||||
|
||||
### External Tool Access
|
||||
All tools from external MCP servers are automatically available through
|
||||
the unified interface, including filesystem, context7, memory, and other servers.
|
||||
|
||||
## Usage Example
|
||||
|
||||
# Register an agent
|
||||
AgentCoordinator.MCPServer.handle_mcp_request(%{
|
||||
"method" => "tools/call",
|
||||
"params" => %{
|
||||
"name" => "register_agent",
|
||||
"arguments" => %{
|
||||
"name" => "MyAgent",
|
||||
"capabilities" => ["coding", "testing"]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
See the documentation in `docs/` for detailed implementation guides.
|
||||
"""
|
||||
|
||||
alias AgentCoordinator.MCPServer
|
||||
|
||||
@doc """
|
||||
Hello world.
|
||||
Get the version of Agent Coordinator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> AgentCoordinator.hello()
|
||||
:world
|
||||
iex> AgentCoordinator.version()
|
||||
"0.1.0"
|
||||
|
||||
"""
|
||||
def hello do
|
||||
:world
|
||||
def version do
|
||||
Application.spec(:agent_coordinator, :vsn) |> to_string()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the current status of the Agent Coordinator system.
|
||||
|
||||
Returns information about active agents, tasks, and external MCP servers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> AgentCoordinator.status()
|
||||
%{
|
||||
agents: 2,
|
||||
active_tasks: 1,
|
||||
external_servers: 3,
|
||||
uptime: 12345
|
||||
}
|
||||
|
||||
"""
|
||||
def status do
|
||||
with {:ok, board} <- get_task_board(),
|
||||
{:ok, server_status} <- get_server_status() do
|
||||
%{
|
||||
agents: length(board[:agents] || []),
|
||||
active_tasks: count_active_tasks(board),
|
||||
external_servers: count_active_servers(server_status),
|
||||
uptime: get_uptime()
|
||||
}
|
||||
else
|
||||
_ -> %{status: :error, message: "Unable to retrieve system status"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the current task board showing all agents and their status.
|
||||
|
||||
Returns information about all registered agents, their current tasks,
|
||||
and overall system status.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, board} = AgentCoordinator.get_task_board()
|
||||
iex> is_map(board)
|
||||
true
|
||||
|
||||
"""
|
||||
def get_task_board do
|
||||
request = %{
|
||||
"method" => "tools/call",
|
||||
"params" => %{"name" => "get_task_board", "arguments" => %{}},
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => System.unique_integer()
|
||||
}
|
||||
|
||||
case MCPServer.handle_mcp_request(request) do
|
||||
%{"result" => %{"content" => [%{"text" => text}]}} ->
|
||||
{:ok, Jason.decode!(text)}
|
||||
|
||||
%{"error" => error} ->
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
{:error, "Unexpected response format"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Register a new agent with the coordination system.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `name` - Agent name (string)
|
||||
- `capabilities` - List of capabilities (["coding", "testing", ...])
|
||||
- `opts` - Optional parameters (codebase_id, workspace_path, etc.)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, result} = AgentCoordinator.register_agent("TestAgent", ["coding"])
|
||||
iex> is_map(result)
|
||||
true
|
||||
|
||||
"""
|
||||
def register_agent(name, capabilities, opts \\ []) do
|
||||
args =
|
||||
%{
|
||||
"name" => name,
|
||||
"capabilities" => capabilities
|
||||
}
|
||||
|> add_optional_arg("codebase_id", opts[:codebase_id])
|
||||
|> add_optional_arg("workspace_path", opts[:workspace_path])
|
||||
|> add_optional_arg("cross_codebase_capable", opts[:cross_codebase_capable])
|
||||
|
||||
request = %{
|
||||
"method" => "tools/call",
|
||||
"params" => %{"name" => "register_agent", "arguments" => args},
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => System.unique_integer()
|
||||
}
|
||||
|
||||
case MCPServer.handle_mcp_request(request) do
|
||||
%{"result" => %{"content" => [%{"text" => text}]}} ->
|
||||
{:ok, Jason.decode!(text)}
|
||||
|
||||
%{"error" => error} ->
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
{:error, "Unexpected response format"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new task in the coordination system.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `title` - Task title (string)
|
||||
- `description` - Task description (string)
|
||||
- `opts` - Optional parameters (priority, codebase_id, file_paths, etc.)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, result} = AgentCoordinator.create_task("Test Task", "Test description")
|
||||
iex> is_map(result)
|
||||
true
|
||||
|
||||
"""
|
||||
def create_task(title, description, opts \\ []) do
|
||||
args =
|
||||
%{
|
||||
"title" => title,
|
||||
"description" => description
|
||||
}
|
||||
|> add_optional_arg("priority", opts[:priority])
|
||||
|> add_optional_arg("codebase_id", opts[:codebase_id])
|
||||
|> add_optional_arg("file_paths", opts[:file_paths])
|
||||
|> add_optional_arg("required_capabilities", opts[:required_capabilities])
|
||||
|
||||
request = %{
|
||||
"method" => "tools/call",
|
||||
"params" => %{"name" => "create_task", "arguments" => args},
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => System.unique_integer()
|
||||
}
|
||||
|
||||
case MCPServer.handle_mcp_request(request) do
|
||||
%{"result" => %{"content" => [%{"text" => text}]}} ->
|
||||
{:ok, Jason.decode!(text)}
|
||||
|
||||
%{"error" => error} ->
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
{:error, "Unexpected response format"}
|
||||
end
|
||||
end
|
||||
|
||||
# Private helpers
|
||||
|
||||
defp add_optional_arg(args, _key, nil), do: args
|
||||
defp add_optional_arg(args, key, value), do: Map.put(args, key, value)
|
||||
|
||||
defp count_active_tasks(%{agents: agents}) do
|
||||
Enum.count(agents, fn agent ->
|
||||
Map.get(agent, "current_task") != nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp count_active_tasks(_), do: 0
|
||||
|
||||
defp count_active_servers(server_status) when is_map(server_status) do
|
||||
Map.get(server_status, :active_servers, 0)
|
||||
end
|
||||
|
||||
defp count_active_servers(_), do: 0
|
||||
|
||||
defp get_server_status do
|
||||
# This would call UnifiedMCPServer to get external server status
|
||||
# For now, return a placeholder
|
||||
{:ok, %{active_servers: 3}}
|
||||
end
|
||||
|
||||
defp get_uptime do
|
||||
# Get system uptime in seconds
|
||||
{uptime_ms, _} = :erlang.statistics(:wall_clock)
|
||||
div(uptime_ms, 1000)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -74,16 +74,18 @@ defmodule AgentCoordinator.Agent do
|
||||
|
||||
def can_handle?(agent, task) do
|
||||
# Check if agent is in the same codebase or can handle cross-codebase tasks
|
||||
codebase_compatible = agent.codebase_id == task.codebase_id or
|
||||
Map.get(agent.metadata, :cross_codebase_capable, false)
|
||||
|
||||
codebase_compatible =
|
||||
agent.codebase_id == task.codebase_id or
|
||||
Map.get(agent.metadata, :cross_codebase_capable, false)
|
||||
|
||||
# Simple capability matching - can be enhanced
|
||||
required_capabilities = Map.get(task.metadata, :required_capabilities, [])
|
||||
|
||||
capability_match = case required_capabilities do
|
||||
[] -> true
|
||||
caps -> Enum.any?(caps, fn cap -> cap in agent.capabilities end)
|
||||
end
|
||||
capability_match =
|
||||
case required_capabilities do
|
||||
[] -> true
|
||||
caps -> Enum.any?(caps, fn cap -> cap in agent.capabilities end)
|
||||
end
|
||||
|
||||
codebase_compatible and capability_match
|
||||
end
|
||||
|
||||
@@ -18,13 +18,15 @@ defmodule AgentCoordinator.Application do
|
||||
{Phoenix.PubSub, name: AgentCoordinator.PubSub},
|
||||
|
||||
# Codebase registry for multi-codebase coordination
|
||||
{AgentCoordinator.CodebaseRegistry, nats: if(enable_persistence, do: nats_config(), else: nil)},
|
||||
{AgentCoordinator.CodebaseRegistry,
|
||||
nats: if(enable_persistence, do: nats_config(), else: nil)},
|
||||
|
||||
# Task registry with NATS integration (conditionally add persistence)
|
||||
{AgentCoordinator.TaskRegistry, nats: if(enable_persistence, do: nats_config(), else: nil)},
|
||||
|
||||
# MCP Server Manager (manages external MCP servers)
|
||||
{AgentCoordinator.MCPServerManager, config_file: Application.get_env(:agent_coordinator, :mcp_config_file, "mcp_servers.json")},
|
||||
{AgentCoordinator.MCPServerManager,
|
||||
config_file: System.get_env("MCP_CONFIG_FILE", "mcp_servers.json")},
|
||||
|
||||
# MCP server
|
||||
AgentCoordinator.MCPServer,
|
||||
|
||||
@@ -31,19 +31,20 @@ defmodule AgentCoordinator.AutoHeartbeat do
|
||||
"""
|
||||
def register_agent_with_heartbeat(name, capabilities, agent_context \\ %{}) do
|
||||
# Convert capabilities to strings if they're atoms
|
||||
string_capabilities = Enum.map(capabilities, fn
|
||||
cap when is_atom(cap) -> Atom.to_string(cap)
|
||||
cap when is_binary(cap) -> cap
|
||||
end)
|
||||
string_capabilities =
|
||||
Enum.map(capabilities, fn
|
||||
cap when is_atom(cap) -> Atom.to_string(cap)
|
||||
cap when is_binary(cap) -> cap
|
||||
end)
|
||||
|
||||
# First register the agent normally
|
||||
case MCPServer.handle_mcp_request(%{
|
||||
"method" => "tools/call",
|
||||
"params" => %{
|
||||
"name" => "register_agent",
|
||||
"arguments" => %{"name" => name, "capabilities" => string_capabilities}
|
||||
}
|
||||
}) do
|
||||
"method" => "tools/call",
|
||||
"params" => %{
|
||||
"name" => "register_agent",
|
||||
"arguments" => %{"name" => name, "capabilities" => string_capabilities}
|
||||
}
|
||||
}) do
|
||||
%{"result" => %{"content" => [%{"text" => response_json}]}} ->
|
||||
case Jason.decode(response_json) do
|
||||
{:ok, %{"agent_id" => agent_id}} ->
|
||||
@@ -100,10 +101,14 @@ defmodule AgentCoordinator.AutoHeartbeat do
|
||||
"method" => "tools/call",
|
||||
"params" => %{
|
||||
"name" => "create_task",
|
||||
"arguments" => Map.merge(%{
|
||||
"title" => title,
|
||||
"description" => description
|
||||
}, opts)
|
||||
"arguments" =>
|
||||
Map.merge(
|
||||
%{
|
||||
"title" => title,
|
||||
"description" => description
|
||||
},
|
||||
opts
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,9 +178,10 @@ defmodule AgentCoordinator.AutoHeartbeat do
|
||||
# Start new timer
|
||||
timer_ref = Process.send_after(self(), {:heartbeat_timer, agent_id}, @heartbeat_interval)
|
||||
|
||||
new_state = %{state |
|
||||
timers: Map.put(state.timers, agent_id, timer_ref),
|
||||
agent_contexts: Map.put(state.agent_contexts, agent_id, context)
|
||||
new_state = %{
|
||||
state
|
||||
| timers: Map.put(state.timers, agent_id, timer_ref),
|
||||
agent_contexts: Map.put(state.agent_contexts, agent_id, context)
|
||||
}
|
||||
|
||||
{:reply, :ok, new_state}
|
||||
@@ -187,9 +193,10 @@ defmodule AgentCoordinator.AutoHeartbeat do
|
||||
Process.cancel_timer(state.timers[agent_id])
|
||||
end
|
||||
|
||||
new_state = %{state |
|
||||
timers: Map.delete(state.timers, agent_id),
|
||||
agent_contexts: Map.delete(state.agent_contexts, agent_id)
|
||||
new_state = %{
|
||||
state
|
||||
| timers: Map.delete(state.timers, agent_id),
|
||||
agent_contexts: Map.delete(state.agent_contexts, agent_id)
|
||||
}
|
||||
|
||||
{:reply, :ok, new_state}
|
||||
|
||||
@@ -110,10 +110,10 @@ defmodule AgentCoordinator.Client do
|
||||
def init(config) do
|
||||
# Register with enhanced MCP server
|
||||
case EnhancedMCPServer.register_agent_with_session(
|
||||
config.agent_name,
|
||||
config.capabilities,
|
||||
self()
|
||||
) do
|
||||
config.agent_name,
|
||||
config.capabilities,
|
||||
self()
|
||||
) do
|
||||
{:ok, agent_id} ->
|
||||
state = %__MODULE__{
|
||||
agent_id: agent_id,
|
||||
@@ -151,10 +151,14 @@ defmodule AgentCoordinator.Client do
|
||||
end
|
||||
|
||||
def handle_call({:create_task, title, description, opts}, _from, state) do
|
||||
arguments = Map.merge(%{
|
||||
"title" => title,
|
||||
"description" => description
|
||||
}, opts)
|
||||
arguments =
|
||||
Map.merge(
|
||||
%{
|
||||
"title" => title,
|
||||
"description" => description
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
request = %{
|
||||
"method" => "tools/call",
|
||||
|
||||
@@ -29,27 +29,27 @@ defmodule AgentCoordinator.Inbox do
|
||||
end
|
||||
|
||||
def add_task(agent_id, task) do
|
||||
GenServer.call(via_tuple(agent_id), {:add_task, task})
|
||||
GenServer.call(via_tuple(agent_id), {:add_task, task}, 30_000)
|
||||
end
|
||||
|
||||
def get_next_task(agent_id) do
|
||||
GenServer.call(via_tuple(agent_id), :get_next_task)
|
||||
GenServer.call(via_tuple(agent_id), :get_next_task, 15_000)
|
||||
end
|
||||
|
||||
def complete_current_task(agent_id) do
|
||||
GenServer.call(via_tuple(agent_id), :complete_current_task)
|
||||
GenServer.call(via_tuple(agent_id), :complete_current_task, 30_000)
|
||||
end
|
||||
|
||||
def get_status(agent_id) do
|
||||
GenServer.call(via_tuple(agent_id), :get_status)
|
||||
GenServer.call(via_tuple(agent_id), :get_status, 15_000)
|
||||
end
|
||||
|
||||
def list_tasks(agent_id) do
|
||||
GenServer.call(via_tuple(agent_id), :list_tasks)
|
||||
GenServer.call(via_tuple(agent_id), :list_tasks, 15_000)
|
||||
end
|
||||
|
||||
def get_current_task(agent_id) do
|
||||
GenServer.call(via_tuple(agent_id), :get_current_task)
|
||||
GenServer.call(via_tuple(agent_id), :get_current_task, 15_000)
|
||||
end
|
||||
|
||||
def stop(agent_id) do
|
||||
|
||||
@@ -172,7 +172,8 @@ defmodule AgentCoordinator.MCPServer do
|
||||
},
|
||||
%{
|
||||
"name" => "unregister_agent",
|
||||
"description" => "Unregister an agent from the coordination system (e.g., when waiting for user input)",
|
||||
"description" =>
|
||||
"Unregister an agent from the coordination system (e.g., when waiting for user input)",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
@@ -181,6 +182,127 @@ defmodule AgentCoordinator.MCPServer do
|
||||
},
|
||||
"required" => ["agent_id"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "register_task_set",
|
||||
"description" =>
|
||||
"Register a planned set of tasks for an agent to enable workflow coordination",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"agent_id" => %{
|
||||
"type" => "string",
|
||||
"description" => "ID of the agent registering the task set"
|
||||
},
|
||||
"task_set" => %{
|
||||
"type" => "array",
|
||||
"description" => "Array of tasks to register for this agent",
|
||||
"items" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"title" => %{"type" => "string", "description" => "Task title"},
|
||||
"description" => %{"type" => "string", "description" => "Task description"},
|
||||
"priority" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["low", "normal", "high", "urgent"],
|
||||
"default" => "normal"
|
||||
},
|
||||
"estimated_time" => %{
|
||||
"type" => "string",
|
||||
"description" => "Estimated completion time"
|
||||
},
|
||||
"file_paths" => %{
|
||||
"type" => "array",
|
||||
"items" => %{"type" => "string"},
|
||||
"description" => "Files this task will work on"
|
||||
},
|
||||
"required_capabilities" => %{
|
||||
"type" => "array",
|
||||
"items" => %{"type" => "string"},
|
||||
"description" => "Capabilities required for this task"
|
||||
}
|
||||
},
|
||||
"required" => ["title", "description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" => ["agent_id", "task_set"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "create_agent_task",
|
||||
"description" =>
|
||||
"Create a task specifically for a particular agent (not globally assigned)",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"agent_id" => %{"type" => "string", "description" => "ID of the agent this task is for"},
|
||||
"title" => %{"type" => "string", "description" => "Task title"},
|
||||
"description" => %{"type" => "string", "description" => "Detailed task description"},
|
||||
"priority" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["low", "normal", "high", "urgent"],
|
||||
"default" => "normal"
|
||||
},
|
||||
"estimated_time" => %{"type" => "string", "description" => "Estimated completion time"},
|
||||
"file_paths" => %{
|
||||
"type" => "array",
|
||||
"items" => %{"type" => "string"},
|
||||
"description" => "Files this task will work on"
|
||||
},
|
||||
"required_capabilities" => %{
|
||||
"type" => "array",
|
||||
"items" => %{"type" => "string"},
|
||||
"description" => "Capabilities required for this task"
|
||||
}
|
||||
},
|
||||
"required" => ["agent_id", "title", "description"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "get_detailed_task_board",
|
||||
"description" =>
|
||||
"Get detailed task information for all agents including completed, current, and planned tasks",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"codebase_id" => %{
|
||||
"type" => "string",
|
||||
"description" => "Optional: filter by codebase ID"
|
||||
},
|
||||
"include_task_details" => %{
|
||||
"type" => "boolean",
|
||||
"default" => true,
|
||||
"description" => "Include full task details"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "get_agent_task_history",
|
||||
"description" => "Get detailed task history for a specific agent",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"agent_id" => %{"type" => "string", "description" => "ID of the agent"},
|
||||
"include_planned" => %{
|
||||
"type" => "boolean",
|
||||
"default" => true,
|
||||
"description" => "Include planned/pending tasks"
|
||||
},
|
||||
"include_completed" => %{
|
||||
"type" => "boolean",
|
||||
"default" => true,
|
||||
"description" => "Include completed tasks"
|
||||
},
|
||||
"limit" => %{
|
||||
"type" => "number",
|
||||
"default" => 50,
|
||||
"description" => "Maximum number of tasks to return"
|
||||
}
|
||||
},
|
||||
"required" => ["agent_id"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -213,6 +335,7 @@ defmodule AgentCoordinator.MCPServer do
|
||||
|
||||
defp process_mcp_request(%{"method" => "initialize"} = request) do
|
||||
id = Map.get(request, "id", nil)
|
||||
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
@@ -231,6 +354,7 @@ defmodule AgentCoordinator.MCPServer do
|
||||
|
||||
defp process_mcp_request(%{"method" => "tools/list"} = request) do
|
||||
id = Map.get(request, "id", nil)
|
||||
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
@@ -260,6 +384,10 @@ defmodule AgentCoordinator.MCPServer do
|
||||
"add_codebase_dependency" -> add_codebase_dependency(args)
|
||||
"heartbeat" -> heartbeat(args)
|
||||
"unregister_agent" -> unregister_agent(args)
|
||||
"register_task_set" -> register_task_set(args)
|
||||
"create_agent_task" -> create_agent_task(args)
|
||||
"get_detailed_task_board" -> get_detailed_task_board(args)
|
||||
"get_agent_task_history" -> get_agent_task_history(args)
|
||||
_ -> {:error, "Unknown tool: #{tool_name}"}
|
||||
end
|
||||
|
||||
@@ -282,6 +410,7 @@ defmodule AgentCoordinator.MCPServer do
|
||||
|
||||
defp process_mcp_request(request) do
|
||||
id = Map.get(request, "id", nil)
|
||||
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
@@ -343,7 +472,13 @@ defmodule AgentCoordinator.MCPServer do
|
||||
|
||||
case TaskRegistry.assign_task(task) do
|
||||
{:ok, agent_id} ->
|
||||
{:ok, %{task_id: task.id, assigned_to: agent_id, codebase_id: task.codebase_id, status: "assigned"}}
|
||||
{:ok,
|
||||
%{
|
||||
task_id: task.id,
|
||||
assigned_to: agent_id,
|
||||
codebase_id: task.codebase_id,
|
||||
status: "assigned"
|
||||
}}
|
||||
|
||||
{:error, :no_available_agents} ->
|
||||
# Add to global pending queue
|
||||
@@ -383,7 +518,11 @@ defmodule AgentCoordinator.MCPServer do
|
||||
}
|
||||
]
|
||||
|
||||
Task.new("#{title} (#{codebase_id})", "Cross-codebase task: #{description}", dependent_opts)
|
||||
Task.new(
|
||||
"#{title} (#{codebase_id})",
|
||||
"Cross-codebase task: #{description}",
|
||||
dependent_opts
|
||||
)
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(&(&1 != nil))
|
||||
@@ -394,20 +533,28 @@ defmodule AgentCoordinator.MCPServer do
|
||||
results =
|
||||
Enum.map(all_tasks, fn task ->
|
||||
case TaskRegistry.assign_task(task) do
|
||||
{:ok, agent_id} -> %{task_id: task.id, codebase_id: task.codebase_id, agent_id: agent_id, status: "assigned"}
|
||||
{:ok, agent_id} ->
|
||||
%{
|
||||
task_id: task.id,
|
||||
codebase_id: task.codebase_id,
|
||||
agent_id: agent_id,
|
||||
status: "assigned"
|
||||
}
|
||||
|
||||
{:error, :no_available_agents} ->
|
||||
TaskRegistry.add_to_pending(task)
|
||||
%{task_id: task.id, codebase_id: task.codebase_id, status: "queued"}
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, %{
|
||||
main_task_id: main_task.id,
|
||||
primary_codebase: primary_codebase,
|
||||
coordination_strategy: strategy,
|
||||
tasks: results,
|
||||
status: "created"
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
main_task_id: main_task.id,
|
||||
primary_codebase: primary_codebase,
|
||||
coordination_strategy: strategy,
|
||||
tasks: results,
|
||||
status: "created"
|
||||
}}
|
||||
end
|
||||
|
||||
defp get_next_task(%{"agent_id" => agent_id}) do
|
||||
@@ -511,17 +658,24 @@ defmodule AgentCoordinator.MCPServer do
|
||||
{:ok, %{codebases: codebase_summaries}}
|
||||
end
|
||||
|
||||
defp add_codebase_dependency(%{"source_codebase_id" => source, "target_codebase_id" => target, "dependency_type" => dep_type} = args) do
|
||||
defp add_codebase_dependency(
|
||||
%{
|
||||
"source_codebase_id" => source,
|
||||
"target_codebase_id" => target,
|
||||
"dependency_type" => dep_type
|
||||
} = args
|
||||
) do
|
||||
metadata = Map.get(args, "metadata", %{})
|
||||
|
||||
case CodebaseRegistry.add_cross_codebase_dependency(source, target, dep_type, metadata) do
|
||||
:ok ->
|
||||
{:ok, %{
|
||||
source_codebase: source,
|
||||
target_codebase: target,
|
||||
dependency_type: dep_type,
|
||||
status: "added"
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
source_codebase: source,
|
||||
target_codebase: target,
|
||||
dependency_type: dep_type,
|
||||
status: "added"
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "Failed to add dependency: #{reason}"}
|
||||
@@ -549,4 +703,212 @@ defmodule AgentCoordinator.MCPServer do
|
||||
{:error, "Unregister failed: #{reason}"}
|
||||
end
|
||||
end
|
||||
|
||||
# NEW: Agent-specific task management functions
|
||||
|
||||
defp register_task_set(%{"agent_id" => agent_id, "task_set" => task_set}) do
|
||||
case TaskRegistry.get_agent(agent_id) do
|
||||
{:error, :not_found} ->
|
||||
{:error, "Agent not found: #{agent_id}"}
|
||||
|
||||
{:ok, _agent} ->
|
||||
# Create tasks specifically for this agent
|
||||
created_tasks =
|
||||
Enum.map(task_set, fn task_data ->
|
||||
opts = %{
|
||||
priority: String.to_atom(Map.get(task_data, "priority", "normal")),
|
||||
# Use agent's codebase
|
||||
codebase_id: "default",
|
||||
file_paths: Map.get(task_data, "file_paths", []),
|
||||
metadata: %{
|
||||
agent_created: true,
|
||||
estimated_time: Map.get(task_data, "estimated_time"),
|
||||
required_capabilities: Map.get(task_data, "required_capabilities", [])
|
||||
}
|
||||
}
|
||||
|
||||
task = Task.new(task_data["title"], task_data["description"], opts)
|
||||
|
||||
# Add directly to agent's inbox (not global pool)
|
||||
case Inbox.add_task(agent_id, task) do
|
||||
:ok -> task
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end)
|
||||
|
||||
# Check for any errors
|
||||
case Enum.find(created_tasks, fn result -> match?({:error, _}, result) end) do
|
||||
nil ->
|
||||
task_summaries =
|
||||
Enum.map(created_tasks, fn task ->
|
||||
%{
|
||||
task_id: task.id,
|
||||
title: task.title,
|
||||
priority: task.priority,
|
||||
estimated_time: task.metadata[:estimated_time]
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
agent_id: agent_id,
|
||||
registered_tasks: length(created_tasks),
|
||||
task_set: task_summaries,
|
||||
status: "registered"
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "Failed to register task set: #{reason}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_agent_task(
|
||||
%{"agent_id" => agent_id, "title" => title, "description" => description} = args
|
||||
) do
|
||||
case TaskRegistry.get_agent(agent_id) do
|
||||
{:error, :not_found} ->
|
||||
{:error, "Agent not found: #{agent_id}"}
|
||||
|
||||
{:ok, _agent} ->
|
||||
opts = %{
|
||||
priority: String.to_atom(Map.get(args, "priority", "normal")),
|
||||
# Use agent's codebase
|
||||
codebase_id: "default",
|
||||
file_paths: Map.get(args, "file_paths", []),
|
||||
metadata: %{
|
||||
agent_created: true,
|
||||
estimated_time: Map.get(args, "estimated_time"),
|
||||
required_capabilities: Map.get(args, "required_capabilities", [])
|
||||
}
|
||||
}
|
||||
|
||||
task = Task.new(title, description, opts)
|
||||
|
||||
# Add directly to agent's inbox
|
||||
case Inbox.add_task(agent_id, task) do
|
||||
:ok ->
|
||||
{:ok,
|
||||
%{
|
||||
task_id: task.id,
|
||||
agent_id: agent_id,
|
||||
title: task.title,
|
||||
priority: task.priority,
|
||||
status: "created_for_agent"
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "Failed to create agent task: #{reason}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_detailed_task_board(args) do
|
||||
codebase_id = Map.get(args, "codebase_id")
|
||||
include_details = Map.get(args, "include_task_details", true)
|
||||
agents = TaskRegistry.list_agents()
|
||||
|
||||
# Filter agents by codebase if specified
|
||||
filtered_agents =
|
||||
case codebase_id do
|
||||
nil -> agents
|
||||
id -> Enum.filter(agents, fn agent -> agent.codebase_id == id end)
|
||||
end
|
||||
|
||||
detailed_board =
|
||||
Enum.map(filtered_agents, fn agent ->
|
||||
# Get detailed task information
|
||||
task_info =
|
||||
case Inbox.list_tasks(agent.id) do
|
||||
{:error, _} ->
|
||||
%{pending: [], in_progress: nil, completed: []}
|
||||
|
||||
tasks ->
|
||||
if include_details do
|
||||
tasks
|
||||
else
|
||||
# Just counts like before
|
||||
%{
|
||||
pending_count: length(tasks.pending),
|
||||
in_progress: if(tasks.in_progress, do: 1, else: 0),
|
||||
completed_count: length(tasks.completed)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
%{
|
||||
agent_id: agent.id,
|
||||
name: agent.name,
|
||||
capabilities: agent.capabilities,
|
||||
status: agent.status,
|
||||
codebase_id: agent.codebase_id,
|
||||
workspace_path: agent.workspace_path,
|
||||
online: Agent.is_online?(agent),
|
||||
cross_codebase_capable: Agent.can_work_cross_codebase?(agent),
|
||||
last_heartbeat: agent.last_heartbeat,
|
||||
tasks: task_info
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
agents: detailed_board,
|
||||
codebase_filter: codebase_id,
|
||||
timestamp: DateTime.utc_now()
|
||||
}}
|
||||
end
|
||||
|
||||
defp get_agent_task_history(%{"agent_id" => agent_id} = args) do
|
||||
include_planned = Map.get(args, "include_planned", true)
|
||||
include_completed = Map.get(args, "include_completed", true)
|
||||
limit = Map.get(args, "limit", 50)
|
||||
|
||||
case TaskRegistry.get_agent(agent_id) do
|
||||
{:error, :not_found} ->
|
||||
{:error, "Agent not found: #{agent_id}"}
|
||||
|
||||
{:ok, agent} ->
|
||||
case Inbox.list_tasks(agent_id) do
|
||||
{:error, reason} ->
|
||||
{:error, "Failed to get task history: #{reason}"}
|
||||
|
||||
task_data ->
|
||||
history = %{}
|
||||
|
||||
# Add planned tasks if requested
|
||||
history =
|
||||
if include_planned do
|
||||
Map.put(history, :planned_tasks, Enum.take(task_data.pending, limit))
|
||||
else
|
||||
history
|
||||
end
|
||||
|
||||
# Add current task
|
||||
history =
|
||||
if task_data.in_progress do
|
||||
Map.put(history, :current_task, task_data.in_progress)
|
||||
else
|
||||
history
|
||||
end
|
||||
|
||||
# Add completed tasks if requested
|
||||
history =
|
||||
if include_completed do
|
||||
Map.put(history, :completed_tasks, Enum.take(task_data.completed, limit))
|
||||
else
|
||||
history
|
||||
end
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
agent_id: agent_id,
|
||||
agent_name: agent.name,
|
||||
history: history,
|
||||
total_planned: length(task_data.pending),
|
||||
total_completed: length(task_data.completed),
|
||||
timestamp: DateTime.utc_now()
|
||||
}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,13 +75,13 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
end
|
||||
|
||||
def handle_continue(:start_servers, state) do
|
||||
Logger.info("Starting external MCP servers...")
|
||||
IO.puts(:stderr, "Starting external MCP servers...")
|
||||
|
||||
new_state =
|
||||
Enum.reduce(state.config.servers, state, fn {name, config}, acc ->
|
||||
case start_server(name, config) do
|
||||
{:ok, server_info} ->
|
||||
Logger.info("Started MCP server: #{name}")
|
||||
IO.puts(:stderr, "Started MCP server: #{name}")
|
||||
|
||||
%{
|
||||
acc
|
||||
@@ -90,7 +90,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to start MCP server #{name}: #{reason}")
|
||||
IO.puts(:stderr, "Failed to start MCP server #{name}: #{reason}")
|
||||
acc
|
||||
end
|
||||
end)
|
||||
@@ -187,9 +187,10 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Re-discover tools from all running servers
|
||||
updated_state = rediscover_all_tools(state)
|
||||
|
||||
all_tools = get_coordinator_tools() ++ (Map.values(updated_state.tool_registry) |> List.flatten())
|
||||
all_tools =
|
||||
get_coordinator_tools() ++ (Map.values(updated_state.tool_registry) |> List.flatten())
|
||||
|
||||
Logger.info("Refreshed tool registry: found #{length(all_tools)} total tools")
|
||||
IO.puts(:stderr, "Refreshed tool registry: found #{length(all_tools)} total tools")
|
||||
|
||||
{:reply, {:ok, length(all_tools)}, updated_state}
|
||||
end
|
||||
@@ -198,12 +199,13 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Handle server port death
|
||||
case find_server_by_port(port, state.servers) do
|
||||
{server_name, server_info} ->
|
||||
Logger.warning("MCP server #{server_name} port died: #{reason}")
|
||||
IO.puts(:stderr, "MCP server #{server_name} port died: #{reason}")
|
||||
|
||||
# Cleanup PID file and kill external process
|
||||
if server_info.pid_file_path do
|
||||
cleanup_pid_file(server_info.pid_file_path)
|
||||
end
|
||||
|
||||
if server_info.os_pid do
|
||||
kill_external_process(server_info.os_pid)
|
||||
end
|
||||
@@ -218,7 +220,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
|
||||
# Attempt restart if configured
|
||||
if should_auto_restart?(server_name, state.config) do
|
||||
Logger.info("Auto-restarting MCP server: #{server_name}")
|
||||
IO.puts(:stderr, "Auto-restarting MCP server: #{server_name}")
|
||||
Process.send_after(self(), {:restart_server, server_name}, 1000)
|
||||
end
|
||||
|
||||
@@ -234,7 +236,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
|
||||
case start_server(server_name, server_config) do
|
||||
{:ok, server_info} ->
|
||||
Logger.info("Auto-restarted MCP server: #{server_name}")
|
||||
IO.puts(:stderr, "Auto-restarted MCP server: #{server_name}")
|
||||
|
||||
new_state = %{
|
||||
state
|
||||
@@ -246,7 +248,10 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
{:noreply, updated_state}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to auto-restart MCP server #{server_name}: #{reason}")
|
||||
IO.puts(:stderr,
|
||||
"Failed to auto-restart MCP server #{server_name}: #{reason}"
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
@@ -257,11 +262,12 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
|
||||
# Private functions
|
||||
|
||||
defp load_server_config(opts) do
|
||||
defp load_server_config(_opts) do # We should probably use opts, but idk how to fix it, so we're using env var for a single var
|
||||
# Allow override from opts or config file
|
||||
config_file = Keyword.get(opts, :config_file, "mcp_servers.json")
|
||||
config_file = System.get_env("MCP_CONFIG_FILE", "mcp_servers.json")
|
||||
|
||||
if File.exists?(config_file) do
|
||||
IO.puts(:stderr, "Loading MCP server config from #{config_file}")
|
||||
try do
|
||||
case Jason.decode!(File.read!(config_file)) do
|
||||
%{"servers" => servers} = full_config ->
|
||||
@@ -288,22 +294,30 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
|
||||
# Add any additional config from the JSON file
|
||||
case Map.get(full_config, "config") do
|
||||
nil -> base_config
|
||||
nil ->
|
||||
base_config
|
||||
|
||||
additional_config ->
|
||||
Map.merge(base_config, %{config: additional_config})
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.warning("Invalid config file format in #{config_file}, using defaults")
|
||||
IO.puts(:stderr,
|
||||
"Invalid config file format in #{config_file}, using defaults"
|
||||
)
|
||||
|
||||
get_default_config()
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.warning("Failed to load config file #{config_file}: #{Exception.message(e)}, using defaults")
|
||||
IO.puts(:stderr,
|
||||
"Failed to load config file #{config_file}: #{Exception.message(e)}, using defaults"
|
||||
)
|
||||
|
||||
get_default_config()
|
||||
end
|
||||
else
|
||||
Logger.warning("Config file #{config_file} not found, using defaults")
|
||||
IO.puts(:stderr, "Config file #{config_file} not found, using defaults")
|
||||
get_default_config()
|
||||
end
|
||||
end
|
||||
@@ -320,35 +334,35 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
},
|
||||
"mcp_figma" => %{
|
||||
type: :stdio,
|
||||
command: "npx",
|
||||
command: "bunx",
|
||||
args: ["-y", "@figma/mcp-server-figma"],
|
||||
auto_restart: true,
|
||||
description: "Figma design integration server"
|
||||
},
|
||||
"mcp_filesystem" => %{
|
||||
type: :stdio,
|
||||
command: "npx",
|
||||
command: "bunx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/ra"],
|
||||
auto_restart: true,
|
||||
description: "Filesystem operations server with heartbeat coverage"
|
||||
},
|
||||
"mcp_firebase" => %{
|
||||
type: :stdio,
|
||||
command: "npx",
|
||||
command: "bunx",
|
||||
args: ["-y", "@firebase/mcp-server"],
|
||||
auto_restart: true,
|
||||
description: "Firebase integration server"
|
||||
},
|
||||
"mcp_memory" => %{
|
||||
type: :stdio,
|
||||
command: "npx",
|
||||
command: "bunx",
|
||||
args: ["-y", "@modelcontextprotocol/server-memory"],
|
||||
auto_restart: true,
|
||||
description: "Memory and knowledge graph server"
|
||||
},
|
||||
"mcp_sequentialthi" => %{
|
||||
type: :stdio,
|
||||
command: "npx",
|
||||
command: "bunx",
|
||||
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||
auto_restart: true,
|
||||
description: "Sequential thinking and reasoning server"
|
||||
@@ -366,7 +380,8 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
server_info = %{
|
||||
name: name,
|
||||
type: :stdio,
|
||||
pid: port, # Use port as the "pid" for process tracking
|
||||
# Use port as the "pid" for process tracking
|
||||
pid: port,
|
||||
os_pid: os_pid,
|
||||
port: port,
|
||||
pid_file_path: pid_file_path,
|
||||
@@ -388,6 +403,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
if Port.info(port) do
|
||||
Port.close(port)
|
||||
end
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
@@ -402,7 +418,8 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
name: name,
|
||||
type: :http,
|
||||
url: Map.get(config, :url),
|
||||
pid: nil, # No process to track for HTTP
|
||||
# No process to track for HTTP
|
||||
pid: nil,
|
||||
os_pid: nil,
|
||||
port: nil,
|
||||
pid_file_path: nil,
|
||||
@@ -427,7 +444,8 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
env = Map.get(config, :env, %{})
|
||||
|
||||
# Convert env map to list format expected by Port.open
|
||||
env_list = Enum.map(env, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end)
|
||||
env_list =
|
||||
Enum.map(env, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end)
|
||||
|
||||
port_options = [
|
||||
:binary,
|
||||
@@ -438,8 +456,11 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
]
|
||||
|
||||
try do
|
||||
port = Port.open({:spawn_executable, System.find_executable(command)},
|
||||
[{:args, args} | port_options])
|
||||
port =
|
||||
Port.open(
|
||||
{:spawn_executable, System.find_executable(command)},
|
||||
[{:args, args} | port_options]
|
||||
)
|
||||
|
||||
# Get the OS PID of the spawned process
|
||||
{:os_pid, os_pid} = Port.info(port, :os_pid)
|
||||
@@ -447,12 +468,15 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Create PID file for cleanup
|
||||
pid_file_path = create_pid_file(name, os_pid)
|
||||
|
||||
Logger.info("Started MCP server #{name} with OS PID #{os_pid}")
|
||||
IO.puts(:stderr, "Started MCP server #{name} with OS PID #{os_pid}")
|
||||
|
||||
{:ok, os_pid, port, pid_file_path}
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to start stdio server #{name}: #{Exception.message(e)}")
|
||||
IO.puts(:stderr,
|
||||
"Failed to start stdio server #{name}: #{Exception.message(e)}"
|
||||
)
|
||||
|
||||
{:error, Exception.message(e)}
|
||||
end
|
||||
end
|
||||
@@ -477,16 +501,18 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
try do
|
||||
case System.cmd("kill", ["-TERM", to_string(os_pid)]) do
|
||||
{_, 0} ->
|
||||
Logger.info("Successfully terminated process #{os_pid}")
|
||||
IO.puts(:stderr, "Successfully terminated process #{os_pid}")
|
||||
:ok
|
||||
|
||||
{_, _} ->
|
||||
# Try force kill
|
||||
case System.cmd("kill", ["-KILL", to_string(os_pid)]) do
|
||||
{_, 0} ->
|
||||
Logger.info("Force killed process #{os_pid}")
|
||||
IO.puts(:stderr, "Force killed process #{os_pid}")
|
||||
:ok
|
||||
|
||||
{_, _} ->
|
||||
Logger.warning("Failed to kill process #{os_pid}")
|
||||
IO.puts(:stderr, "Failed to kill process #{os_pid}")
|
||||
:error
|
||||
end
|
||||
end
|
||||
@@ -528,7 +554,10 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
defp initialize_http_server(server_info) do
|
||||
# For HTTP servers, we would make HTTP requests instead of using ports
|
||||
# For now, return empty tools list as we need to implement HTTP client logic
|
||||
Logger.warning("HTTP server support not fully implemented yet for #{server_info.name}")
|
||||
IO.puts(:stderr,
|
||||
"HTTP server support not fully implemented yet for #{server_info.name}"
|
||||
)
|
||||
|
||||
{:ok, []}
|
||||
rescue
|
||||
e ->
|
||||
@@ -547,7 +576,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
{:ok, tools}
|
||||
|
||||
{:ok, unexpected} ->
|
||||
Logger.warning(
|
||||
IO.puts(:stderr,
|
||||
"Unexpected tools response from #{server_info.name}: #{inspect(unexpected)}"
|
||||
)
|
||||
|
||||
@@ -570,17 +599,20 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Check if we got any response data
|
||||
response_data == "" ->
|
||||
{:error, "No response received from server #{server_info.name}"}
|
||||
|
||||
|
||||
# Try to decode JSON response
|
||||
true ->
|
||||
case Jason.decode(response_data) do
|
||||
{:ok, response} -> {:ok, response}
|
||||
{:ok, response} ->
|
||||
{:ok, response}
|
||||
|
||||
{:error, %Jason.DecodeError{} = error} ->
|
||||
Logger.error("JSON decode error for server #{server_info.name}: #{Exception.message(error)}")
|
||||
Logger.debug("Raw response data: #{inspect(response_data)}")
|
||||
IO.puts(:stderr,
|
||||
"JSON decode error for server #{server_info.name}: #{Exception.message(error)}"
|
||||
)
|
||||
|
||||
IO.puts(:stderr, "Raw response data: #{inspect(response_data)}")
|
||||
{:error, "JSON decode error: #{Exception.message(error)}"}
|
||||
{:error, reason} ->
|
||||
{:error, "JSON decode error: #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -590,25 +622,24 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
{^port, {:data, data}} ->
|
||||
# Accumulate binary data
|
||||
new_acc = acc <> data
|
||||
|
||||
|
||||
# Try to extract complete JSON messages from the accumulated data
|
||||
case extract_json_messages(new_acc) do
|
||||
{json_message, _remaining} when json_message != nil ->
|
||||
# We found a complete JSON message, return it
|
||||
json_message
|
||||
|
||||
|
||||
{nil, remaining} ->
|
||||
# No complete JSON message yet, continue collecting
|
||||
collect_response(port, remaining, timeout)
|
||||
end
|
||||
|
||||
{^port, {:exit_status, status}} ->
|
||||
Logger.error("Server exited with status: #{status}")
|
||||
IO.puts(:stderr, "Server exited with status: #{status}")
|
||||
acc
|
||||
|
||||
after
|
||||
timeout ->
|
||||
Logger.error("Server request timeout after #{timeout}ms")
|
||||
IO.puts(:stderr, "Server request timeout after #{timeout}ms")
|
||||
acc
|
||||
end
|
||||
end
|
||||
@@ -616,29 +647,30 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Extract complete JSON messages from accumulated binary data
|
||||
defp extract_json_messages(data) do
|
||||
lines = String.split(data, "\n", trim: false)
|
||||
|
||||
|
||||
# Process each line to find JSON messages and skip log messages
|
||||
{json_lines, _remaining_data} = extract_json_from_lines(lines, [])
|
||||
|
||||
|
||||
case json_lines do
|
||||
[] ->
|
||||
# No complete JSON found, return the last partial line if any
|
||||
last_line = List.last(lines) || ""
|
||||
|
||||
if String.trim(last_line) != "" and not String.ends_with?(data, "\n") do
|
||||
{nil, last_line}
|
||||
else
|
||||
{nil, ""}
|
||||
end
|
||||
|
||||
|
||||
_ ->
|
||||
# Join all JSON lines and try to parse
|
||||
json_data = Enum.join(json_lines, "\n")
|
||||
|
||||
|
||||
case Jason.decode(json_data) do
|
||||
{:ok, _} ->
|
||||
# Valid JSON found
|
||||
{json_data, ""}
|
||||
|
||||
|
||||
{:error, _} ->
|
||||
# Invalid JSON, might be incomplete
|
||||
{nil, data}
|
||||
@@ -647,55 +679,55 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
end
|
||||
|
||||
defp extract_json_from_lines([], acc), do: {Enum.reverse(acc), ""}
|
||||
|
||||
|
||||
defp extract_json_from_lines([line], acc) do
|
||||
# This is the last line, it might be incomplete
|
||||
trimmed = String.trim(line)
|
||||
|
||||
|
||||
cond do
|
||||
trimmed == "" ->
|
||||
{Enum.reverse(acc), ""}
|
||||
|
||||
|
||||
# Skip log messages
|
||||
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/, trimmed) ->
|
||||
{Enum.reverse(acc), ""}
|
||||
|
||||
|
||||
Regex.match?(~r/^\d{2}:\d{2}:\d{2}\.\d+\s+\[(info|warning|error|debug)\]/, trimmed) ->
|
||||
{Enum.reverse(acc), ""}
|
||||
|
||||
|
||||
# Check if this looks like JSON
|
||||
String.starts_with?(trimmed, ["{"]) ->
|
||||
{Enum.reverse([line | acc]), ""}
|
||||
|
||||
|
||||
true ->
|
||||
# Non-JSON line, might be incomplete
|
||||
{Enum.reverse(acc), line}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defp extract_json_from_lines([line | rest], acc) do
|
||||
trimmed = String.trim(line)
|
||||
|
||||
|
||||
cond do
|
||||
trimmed == "" ->
|
||||
extract_json_from_lines(rest, acc)
|
||||
|
||||
|
||||
# Skip log messages
|
||||
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/, trimmed) ->
|
||||
Logger.debug("Skipping log message from MCP server: #{trimmed}")
|
||||
IO.puts(:stderr, "Skipping log message from MCP server: #{trimmed}")
|
||||
extract_json_from_lines(rest, acc)
|
||||
|
||||
|
||||
Regex.match?(~r/^\d{2}:\d{2}:\d{2}\.\d+\s+\[(info|warning|error|debug)\]/, trimmed) ->
|
||||
Logger.debug("Skipping log message from MCP server: #{trimmed}")
|
||||
IO.puts(:stderr, "Skipping log message from MCP server: #{trimmed}")
|
||||
extract_json_from_lines(rest, acc)
|
||||
|
||||
|
||||
# Check if this looks like JSON
|
||||
String.starts_with?(trimmed, ["{"]) ->
|
||||
extract_json_from_lines(rest, [line | acc])
|
||||
|
||||
|
||||
true ->
|
||||
# Skip non-JSON lines
|
||||
Logger.debug("Skipping non-JSON line from MCP server: #{trimmed}")
|
||||
IO.puts(:stderr, "Skipping non-JSON line from MCP server: #{trimmed}")
|
||||
extract_json_from_lines(rest, acc)
|
||||
end
|
||||
end
|
||||
@@ -715,25 +747,29 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
updated_servers =
|
||||
Enum.reduce(state.servers, state.servers, fn {name, server_info}, acc ->
|
||||
# Check if server is alive (handle both PID and Port)
|
||||
server_alive = case server_info.pid do
|
||||
nil -> false
|
||||
pid when is_pid(pid) -> Process.alive?(pid)
|
||||
port when is_port(port) -> Port.info(port) != nil
|
||||
_ -> false
|
||||
end
|
||||
server_alive =
|
||||
case server_info.pid do
|
||||
nil -> false
|
||||
pid when is_pid(pid) -> Process.alive?(pid)
|
||||
port when is_port(port) -> Port.info(port) != nil
|
||||
_ -> false
|
||||
end
|
||||
|
||||
if server_alive do
|
||||
case get_server_tools(server_info) do
|
||||
{:ok, new_tools} ->
|
||||
Logger.debug("Rediscovered #{length(new_tools)} tools from #{name}")
|
||||
IO.puts(:stderr, "Rediscovered #{length(new_tools)} tools from #{name}")
|
||||
Map.put(acc, name, %{server_info | tools: new_tools})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to rediscover tools from #{name}: #{inspect(reason)}")
|
||||
IO.puts(:stderr,
|
||||
"Failed to rediscover tools from #{name}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
acc
|
||||
end
|
||||
else
|
||||
Logger.warning("Server #{name} is not alive, skipping tool rediscovery")
|
||||
IO.puts(:stderr, "Server #{name} is not alive, skipping tool rediscovery")
|
||||
acc
|
||||
end
|
||||
end)
|
||||
@@ -747,6 +783,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Check all tool registries (both coordinator and external servers)
|
||||
# Start with coordinator tools
|
||||
coordinator_tools = get_coordinator_tools()
|
||||
|
||||
if Enum.any?(coordinator_tools, fn tool -> tool["name"] == tool_name end) do
|
||||
{:coordinator, tool_name}
|
||||
else
|
||||
@@ -855,18 +892,19 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
]
|
||||
|
||||
# Get VS Code tools only if VS Code functionality is available
|
||||
vscode_tools = try do
|
||||
if Code.ensure_loaded?(AgentCoordinator.VSCodeToolProvider) do
|
||||
AgentCoordinator.VSCodeToolProvider.get_tools()
|
||||
else
|
||||
Logger.debug("VS Code tools not available - module not loaded")
|
||||
[]
|
||||
vscode_tools =
|
||||
try do
|
||||
if Code.ensure_loaded?(AgentCoordinator.VSCodeToolProvider) do
|
||||
AgentCoordinator.VSCodeToolProvider.get_tools()
|
||||
else
|
||||
IO.puts(:stderr, "VS Code tools not available - module not loaded")
|
||||
[]
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
IO.puts(:stderr, "VS Code tools not available - error loading")
|
||||
[]
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
Logger.debug("VS Code tools not available - error loading")
|
||||
[]
|
||||
end
|
||||
|
||||
# Combine all coordinator tools
|
||||
coordinator_native_tools ++ vscode_tools
|
||||
@@ -878,10 +916,11 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
# Route to existing Agent Coordinator functionality or VS Code tools
|
||||
case tool_name do
|
||||
"register_agent" ->
|
||||
opts = case arguments["metadata"] do
|
||||
nil -> []
|
||||
metadata -> [metadata: metadata]
|
||||
end
|
||||
opts =
|
||||
case arguments["metadata"] do
|
||||
nil -> []
|
||||
metadata -> [metadata: metadata]
|
||||
end
|
||||
|
||||
AgentCoordinator.TaskRegistry.register_agent(
|
||||
arguments["name"],
|
||||
@@ -996,6 +1035,8 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Perhaps... copilot should supply what it thinks it is doing?
|
||||
# Or, we need to fill these out with every possible tool
|
||||
defp generate_task_title(tool_name, arguments) do
|
||||
case tool_name do
|
||||
"read_file" ->
|
||||
@@ -1027,6 +1068,7 @@ defmodule AgentCoordinator.MCPServerManager do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: See Line [1042](#L1042)
|
||||
defp generate_task_description(tool_name, arguments) do
|
||||
case tool_name do
|
||||
"read_file" ->
|
||||
|
||||
@@ -95,9 +95,6 @@ defmodule AgentCoordinator.Persistence do
|
||||
case Gnat.pub(state.nats_conn, subject, message, headers: event_headers()) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
IO.puts("Failed to store event: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
end
|
||||
|
||||
def register_agent(agent) do
|
||||
GenServer.call(__MODULE__, {:register_agent, agent})
|
||||
GenServer.call(__MODULE__, {:register_agent, agent}, 30_000)
|
||||
end
|
||||
|
||||
def assign_task(task) do
|
||||
GenServer.call(__MODULE__, {:assign_task, task})
|
||||
GenServer.call(__MODULE__, {:assign_task, task}, 30_000)
|
||||
end
|
||||
|
||||
def add_to_pending(task) do
|
||||
@@ -40,7 +40,7 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
end
|
||||
|
||||
def heartbeat_agent(agent_id) do
|
||||
GenServer.call(__MODULE__, {:heartbeat_agent, agent_id})
|
||||
GenServer.call(__MODULE__, {:heartbeat_agent, agent_id}, 30_000)
|
||||
end
|
||||
|
||||
def unregister_agent(agent_id, reason \\ "Agent requested unregistration") do
|
||||
@@ -52,7 +52,7 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
end
|
||||
|
||||
def get_agent_current_task(agent_id) do
|
||||
GenServer.call(__MODULE__, {:get_agent_current_task, agent_id})
|
||||
GenServer.call(__MODULE__, {:get_agent_current_task, agent_id}, 15_000)
|
||||
end
|
||||
|
||||
def get_agent(agent_id) do
|
||||
@@ -64,11 +64,11 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
end
|
||||
|
||||
def update_task_activity(task_id, tool_name, arguments) do
|
||||
GenServer.call(__MODULE__, {:update_task_activity, task_id, tool_name, arguments})
|
||||
GenServer.call(__MODULE__, {:update_task_activity, task_id, tool_name, arguments}, 30_000)
|
||||
end
|
||||
|
||||
def create_task(title, description, opts \\ %{}) do
|
||||
GenServer.call(__MODULE__, {:create_task, title, description, opts})
|
||||
GenServer.call(__MODULE__, {:create_task, title, description, opts}, 30_000)
|
||||
end
|
||||
|
||||
def get_next_task(agent_id) do
|
||||
@@ -76,7 +76,7 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
end
|
||||
|
||||
def complete_task(agent_id) do
|
||||
GenServer.call(__MODULE__, {:complete_task, agent_id})
|
||||
GenServer.call(__MODULE__, {:complete_task, agent_id}, 30_000)
|
||||
end
|
||||
|
||||
def get_task_board do
|
||||
@@ -284,6 +284,7 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
case Map.get(state.agents, agent_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :not_found}, state}
|
||||
|
||||
agent ->
|
||||
{:reply, {:ok, agent}, state}
|
||||
end
|
||||
@@ -293,6 +294,7 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
case Enum.find(state.agents, fn {_id, agent} -> agent.name == agent_name end) do
|
||||
nil ->
|
||||
{:reply, {:error, :not_found}, state}
|
||||
|
||||
{_id, agent} ->
|
||||
{:reply, {:ok, agent}, state}
|
||||
end
|
||||
@@ -559,6 +561,7 @@ defmodule AgentCoordinator.TaskRegistry do
|
||||
catch
|
||||
:exit, _ -> 0
|
||||
end
|
||||
|
||||
[] ->
|
||||
# No inbox process exists, treat as 0 pending tasks
|
||||
0
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule AgentCoordinator.UnifiedMCPServer do
|
||||
request_id_counter: 0
|
||||
}
|
||||
|
||||
Logger.info("Unified MCP Server starting...")
|
||||
IO.puts(:stderr, "Unified MCP Server starting...")
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@@ -33,7 +33,8 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
"vscode_set_selection" => :editor,
|
||||
|
||||
# Command Operations (varies by command)
|
||||
"vscode_run_command" => :admin, # Default to admin, will check specific commands
|
||||
# Default to admin, will check specific commands
|
||||
"vscode_run_command" => :admin,
|
||||
|
||||
# User Communication
|
||||
"vscode_show_message" => :read_only
|
||||
@@ -88,6 +89,7 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
case additional_checks(tool_name, args, context) do
|
||||
:ok ->
|
||||
{:ok, required_level}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
@@ -109,15 +111,18 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
|
||||
case agent_id do
|
||||
"github_copilot_session" -> :filesystem
|
||||
id when is_binary(id) and byte_size(id) > 0 -> :editor # Other registered agents
|
||||
_ -> :read_only # Unknown agents
|
||||
# Other registered agents
|
||||
id when is_binary(id) and byte_size(id) > 0 -> :editor
|
||||
# Unknown agents
|
||||
_ -> :read_only
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update an agent's permission level (for administrative purposes).
|
||||
"""
|
||||
def set_agent_permission_level(agent_id, level) when level in [:read_only, :editor, :filesystem, :terminal, :git, :admin] do
|
||||
def set_agent_permission_level(agent_id, level)
|
||||
when level in [:read_only, :editor, :filesystem, :terminal, :git, :admin] do
|
||||
# This would persist to a database or configuration store
|
||||
Logger.info("Setting permission level for agent #{agent_id} to #{level}")
|
||||
:ok
|
||||
@@ -127,16 +132,24 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
|
||||
defp get_required_permission(tool_name, args) do
|
||||
case Map.get(@tool_permissions, tool_name) do
|
||||
nil -> :admin # Unknown tools require admin by default
|
||||
# Unknown tools require admin by default
|
||||
nil ->
|
||||
:admin
|
||||
|
||||
:admin when tool_name == "vscode_run_command" ->
|
||||
# Special handling for run_command - check specific command
|
||||
command = args["command"]
|
||||
|
||||
if command in @whitelisted_commands do
|
||||
:editor # Whitelisted commands only need editor level
|
||||
# Whitelisted commands only need editor level
|
||||
:editor
|
||||
else
|
||||
:admin # Unknown commands need admin
|
||||
# Unknown commands need admin
|
||||
:admin
|
||||
end
|
||||
level -> level
|
||||
|
||||
level ->
|
||||
level
|
||||
end
|
||||
end
|
||||
|
||||
@@ -165,11 +178,19 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
|
||||
forbidden_patterns = [
|
||||
# System directories
|
||||
"/etc/", "/bin/", "/usr/", "/var/", "/tmp/",
|
||||
"/etc/",
|
||||
"/bin/",
|
||||
"/usr/",
|
||||
"/var/",
|
||||
"/tmp/",
|
||||
# User sensitive areas
|
||||
"/.ssh/", "/.config/", "/home/", "~",
|
||||
"/.ssh/",
|
||||
"/.config/",
|
||||
"/home/",
|
||||
"~",
|
||||
# Relative path traversal
|
||||
"../", "..\\"
|
||||
"../",
|
||||
"..\\"
|
||||
]
|
||||
|
||||
if Enum.any?(forbidden_patterns, fn pattern -> String.contains?(path, pattern) end) do
|
||||
@@ -181,7 +202,7 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
|
||||
defp check_workspace_bounds(_path, _context), do: {:error, "Invalid path format"}
|
||||
|
||||
defp check_command_safety(command, args) when is_binary(command) do
|
||||
defp check_command_safety(command, _args) when is_binary(command) do
|
||||
cond do
|
||||
command in @whitelisted_commands ->
|
||||
:ok
|
||||
@@ -219,4 +240,4 @@ defmodule AgentCoordinator.VSCodePermissions do
|
||||
whitelisted_commands: @whitelisted_commands
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
# File Operations
|
||||
%{
|
||||
"name" => "vscode_read_file",
|
||||
"description" => "Read file contents using VS Code's file system API. Only works within workspace folders.",
|
||||
"description" =>
|
||||
"Read file contents using VS Code's file system API. Only works within workspace folders.",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
@@ -37,7 +38,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
},
|
||||
%{
|
||||
"name" => "vscode_write_file",
|
||||
"description" => "Write content to a file using VS Code's file system API. Creates directories if needed.",
|
||||
"description" =>
|
||||
"Write content to a file using VS Code's file system API. Creates directories if needed.",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
@@ -93,7 +95,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
"properties" => %{
|
||||
"path" => %{
|
||||
"type" => "string",
|
||||
"description" => "Relative or absolute path to the file/directory within the workspace"
|
||||
"description" =>
|
||||
"Relative or absolute path to the file/directory within the workspace"
|
||||
},
|
||||
"recursive" => %{
|
||||
"type" => "boolean",
|
||||
@@ -101,7 +104,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
},
|
||||
"use_trash" => %{
|
||||
"type" => "boolean",
|
||||
"description" => "Whether to move to trash instead of permanent deletion (default: true)"
|
||||
"description" =>
|
||||
"Whether to move to trash instead of permanent deletion (default: true)"
|
||||
}
|
||||
},
|
||||
"required" => ["path"]
|
||||
@@ -227,7 +231,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
# Command Operations
|
||||
%{
|
||||
"name" => "vscode_run_command",
|
||||
"description" => "Execute a VS Code command. Only whitelisted commands are allowed for security.",
|
||||
"description" =>
|
||||
"Execute a VS Code command. Only whitelisted commands are allowed for security.",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
@@ -282,21 +287,26 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
required = Map.get(input_schema, "required", [])
|
||||
|
||||
# Add agent_id to properties
|
||||
updated_properties = Map.put(properties, "agent_id", %{
|
||||
"type" => "string",
|
||||
"description" => "Unique identifier for the agent making this request. Each agent session must use a consistent, unique ID throughout their interaction. Generate a UUID or use a descriptive identifier like 'agent_main_task_001'."
|
||||
})
|
||||
updated_properties =
|
||||
Map.put(properties, "agent_id", %{
|
||||
"type" => "string",
|
||||
"description" =>
|
||||
"Unique identifier for the agent making this request. Each agent session must use a consistent, unique ID throughout their interaction. Generate a UUID or use a descriptive identifier like 'agent_main_task_001'."
|
||||
})
|
||||
|
||||
# Add agent_id to required fields
|
||||
updated_required = if "agent_id" in required, do: required, else: ["agent_id" | required]
|
||||
|
||||
# Update the tool schema
|
||||
updated_input_schema = input_schema
|
||||
|> Map.put("properties", updated_properties)
|
||||
|> Map.put("required", updated_required)
|
||||
updated_input_schema =
|
||||
input_schema
|
||||
|> Map.put("properties", updated_properties)
|
||||
|> Map.put("required", updated_required)
|
||||
|
||||
# Update tool description to mention agent_id requirement
|
||||
updated_description = tool["description"] <> " IMPORTANT: Include a unique agent_id parameter to identify your agent session."
|
||||
updated_description =
|
||||
tool["description"] <>
|
||||
" IMPORTANT: Include a unique agent_id parameter to identify your agent session."
|
||||
|
||||
tool
|
||||
|> Map.put("inputSchema", updated_input_schema)
|
||||
@@ -314,10 +324,13 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
|
||||
if is_nil(agent_id) or agent_id == "" do
|
||||
Logger.warning("Missing agent_id in VS Code tool call: #{tool_name}")
|
||||
{:error, %{
|
||||
"error" => "Missing agent_id",
|
||||
"message" => "All VS Code tools require a unique agent_id parameter. Please include your agent session identifier."
|
||||
}}
|
||||
|
||||
{:error,
|
||||
%{
|
||||
"error" => "Missing agent_id",
|
||||
"message" =>
|
||||
"All VS Code tools require a unique agent_id parameter. Please include your agent session identifier."
|
||||
}}
|
||||
else
|
||||
# Ensure agent is registered and create enhanced context
|
||||
enhanced_context = ensure_agent_registered(agent_id, context)
|
||||
@@ -364,7 +377,11 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
case AgentCoordinator.TaskRegistry.register_agent(
|
||||
"GitHub Copilot (#{agent_id})",
|
||||
capabilities,
|
||||
[metadata: %{agent_id: agent_id, auto_registered: true, session_start: DateTime.utc_now()}]
|
||||
metadata: %{
|
||||
agent_id: agent_id,
|
||||
auto_registered: true,
|
||||
session_start: DateTime.utc_now()
|
||||
}
|
||||
) do
|
||||
{:ok, _result} ->
|
||||
Logger.info("Successfully auto-registered agent: #{agent_id}")
|
||||
@@ -372,10 +389,13 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to auto-register agent #{agent_id}: #{inspect(reason)}")
|
||||
Map.put(context, :agent_id, agent_id) # Continue anyway
|
||||
# Continue anyway
|
||||
Map.put(context, :agent_id, agent_id)
|
||||
end
|
||||
end
|
||||
end # Private function to execute individual tools
|
||||
end
|
||||
|
||||
# Private function to execute individual tools
|
||||
defp execute_tool(tool_name, args, context) do
|
||||
case tool_name do
|
||||
"vscode_read_file" -> read_file(args, context)
|
||||
@@ -398,117 +418,129 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
|
||||
defp read_file(args, _context) do
|
||||
# For now, return a placeholder - we'll implement the actual VS Code API bridge
|
||||
{:ok, %{
|
||||
"content" => "// VS Code file content would be here",
|
||||
"path" => args["path"],
|
||||
"encoding" => args["encoding"] || "utf8",
|
||||
"size" => 42,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"content" => "// VS Code file content would be here",
|
||||
"path" => args["path"],
|
||||
"encoding" => args["encoding"] || "utf8",
|
||||
"size" => 42,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
defp write_file(args, _context) do
|
||||
{:ok, %{
|
||||
"path" => args["path"],
|
||||
"bytes_written" => String.length(args["content"]),
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"path" => args["path"],
|
||||
"bytes_written" => String.length(args["content"]),
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
defp create_file(args, _context) do
|
||||
{:ok, %{
|
||||
"path" => args["path"],
|
||||
"created" => true,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"path" => args["path"],
|
||||
"created" => true,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
defp delete_file(args, _context) do
|
||||
{:ok, %{
|
||||
"path" => args["path"],
|
||||
"deleted" => true,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"path" => args["path"],
|
||||
"deleted" => true,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
defp list_directory(args, _context) do
|
||||
{:ok, %{
|
||||
"path" => args["path"],
|
||||
"entries" => [
|
||||
%{"name" => "file1.txt", "type" => "file", "size" => 123},
|
||||
%{"name" => "subdir", "type" => "directory", "size" => nil}
|
||||
]
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"path" => args["path"],
|
||||
"entries" => [
|
||||
%{"name" => "file1.txt", "type" => "file", "size" => 123},
|
||||
%{"name" => "subdir", "type" => "directory", "size" => nil}
|
||||
]
|
||||
}}
|
||||
end
|
||||
|
||||
defp get_workspace_folders(_args, _context) do
|
||||
{:ok, %{
|
||||
"folders" => [
|
||||
%{"name" => "agent_coordinator", "uri" => "file:///home/ra/agent_coordinator"}
|
||||
]
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"folders" => [
|
||||
%{"name" => "agent_coordinator", "uri" => "file:///home/ra/agent_coordinator"}
|
||||
]
|
||||
}}
|
||||
end
|
||||
|
||||
defp get_active_editor(args, _context) do
|
||||
{:ok, %{
|
||||
"file_path" => "/home/ra/agent_coordinator/lib/agent_coordinator.ex",
|
||||
"language" => "elixir",
|
||||
"line_count" => 150,
|
||||
"content" => if(args["include_content"], do: "// Editor content here", else: nil),
|
||||
"selection" => %{
|
||||
"start" => %{"line" => 10, "character" => 5},
|
||||
"end" => %{"line" => 10, "character" => 15}
|
||||
},
|
||||
"cursor_position" => %{"line" => 10, "character" => 15}
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"file_path" => "/home/ra/agent_coordinator/lib/agent_coordinator.ex",
|
||||
"language" => "elixir",
|
||||
"line_count" => 150,
|
||||
"content" => if(args["include_content"], do: "// Editor content here", else: nil),
|
||||
"selection" => %{
|
||||
"start" => %{"line" => 10, "character" => 5},
|
||||
"end" => %{"line" => 10, "character" => 15}
|
||||
},
|
||||
"cursor_position" => %{"line" => 10, "character" => 15}
|
||||
}}
|
||||
end
|
||||
|
||||
defp set_editor_content(args, _context) do
|
||||
{:ok, %{
|
||||
"file_path" => args["file_path"],
|
||||
"content_length" => String.length(args["content"]),
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"file_path" => args["file_path"],
|
||||
"content_length" => String.length(args["content"]),
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
defp get_selection(args, _context) do
|
||||
{:ok, %{
|
||||
"selection" => %{
|
||||
"start" => %{"line" => 5, "character" => 0},
|
||||
"end" => %{"line" => 8, "character" => 20}
|
||||
},
|
||||
"content" => if(args["include_content"], do: "Selected text here", else: nil),
|
||||
"is_empty" => false
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"selection" => %{
|
||||
"start" => %{"line" => 5, "character" => 0},
|
||||
"end" => %{"line" => 8, "character" => 20}
|
||||
},
|
||||
"content" => if(args["include_content"], do: "Selected text here", else: nil),
|
||||
"is_empty" => false
|
||||
}}
|
||||
end
|
||||
|
||||
defp set_selection(args, _context) do
|
||||
{:ok, %{
|
||||
"selection" => %{
|
||||
"start" => %{"line" => args["start_line"], "character" => args["start_character"]},
|
||||
"end" => %{"line" => args["end_line"], "character" => args["end_character"]}
|
||||
},
|
||||
"revealed" => args["reveal"] != false
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"selection" => %{
|
||||
"start" => %{"line" => args["start_line"], "character" => args["start_character"]},
|
||||
"end" => %{"line" => args["end_line"], "character" => args["end_character"]}
|
||||
},
|
||||
"revealed" => args["reveal"] != false
|
||||
}}
|
||||
end
|
||||
|
||||
defp run_command(args, _context) do
|
||||
# This would execute actual VS Code commands
|
||||
{:ok, %{
|
||||
"command" => args["command"],
|
||||
"args" => args["args"] || [],
|
||||
"result" => "Command executed successfully",
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"command" => args["command"],
|
||||
"args" => args["args"] || [],
|
||||
"result" => "Command executed successfully",
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
defp show_message(args, _context) do
|
||||
{:ok, %{
|
||||
"message" => args["message"],
|
||||
"type" => args["type"] || "info",
|
||||
"displayed" => true,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
"message" => args["message"],
|
||||
"type" => args["type"] || "info",
|
||||
"displayed" => true,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}}
|
||||
end
|
||||
|
||||
# Logging function
|
||||
@@ -521,4 +553,4 @@ defmodule AgentCoordinator.VSCodeToolProvider do
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user