This commit is contained in:
@@ -16,7 +16,10 @@ defmodule AgentCoordinator.Agent do
|
||||
:metadata,
|
||||
:current_activity,
|
||||
:current_files,
|
||||
:activity_history
|
||||
:activity_history,
|
||||
:role,
|
||||
:managed_agents,
|
||||
:oversight_scope
|
||||
]}
|
||||
defstruct [
|
||||
:id,
|
||||
@@ -30,11 +33,23 @@ defmodule AgentCoordinator.Agent do
|
||||
:metadata,
|
||||
:current_activity,
|
||||
:current_files,
|
||||
:activity_history
|
||||
:activity_history,
|
||||
:role,
|
||||
:managed_agents,
|
||||
:oversight_scope
|
||||
]
|
||||
|
||||
@type status :: :idle | :busy | :offline | :error
|
||||
@type capability :: :coding | :testing | :documentation | :analysis | :review
|
||||
@type capability ::
|
||||
:coding
|
||||
| :testing
|
||||
| :documentation
|
||||
| :analysis
|
||||
| :review
|
||||
| :management
|
||||
| :coordination
|
||||
| :oversight
|
||||
@type role :: :standard | :director | :project_manager
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
@@ -48,7 +63,10 @@ defmodule AgentCoordinator.Agent do
|
||||
metadata: map(),
|
||||
current_activity: String.t() | nil,
|
||||
current_files: [String.t()],
|
||||
activity_history: [map()]
|
||||
activity_history: [map()],
|
||||
role: role(),
|
||||
managed_agents: [String.t()],
|
||||
oversight_scope: :codebase | :global
|
||||
}
|
||||
|
||||
def new(name, capabilities, opts \\ []) do
|
||||
@@ -75,6 +93,9 @@ defmodule AgentCoordinator.Agent do
|
||||
)
|
||||
end
|
||||
|
||||
# Determine role based on capabilities
|
||||
role = determine_role(capabilities)
|
||||
|
||||
%__MODULE__{
|
||||
id: UUID.uuid4(),
|
||||
name: name,
|
||||
@@ -87,7 +108,11 @@ defmodule AgentCoordinator.Agent do
|
||||
metadata: Keyword.get(opts, :metadata, %{}),
|
||||
current_activity: nil,
|
||||
current_files: [],
|
||||
activity_history: []
|
||||
activity_history: [],
|
||||
role: role,
|
||||
managed_agents: [],
|
||||
oversight_scope:
|
||||
if(role == :director, do: Keyword.get(opts, :oversight_scope, :codebase), else: :codebase)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -153,4 +178,55 @@ defmodule AgentCoordinator.Agent do
|
||||
def can_work_cross_codebase?(agent) do
|
||||
Map.get(agent.metadata, :cross_codebase_capable, false)
|
||||
end
|
||||
|
||||
# Director-specific functions
|
||||
|
||||
def is_director?(agent) do
|
||||
agent.role == :director
|
||||
end
|
||||
|
||||
def is_manager?(agent) do
|
||||
agent.role in [:director, :project_manager]
|
||||
end
|
||||
|
||||
def can_manage_agent?(director, target_agent) do
|
||||
case director.oversight_scope do
|
||||
:global -> true
|
||||
:codebase -> director.codebase_id == target_agent.codebase_id
|
||||
end
|
||||
end
|
||||
|
||||
def add_managed_agent(director, agent_id) do
|
||||
if is_manager?(director) do
|
||||
managed_agents = [agent_id | director.managed_agents] |> Enum.uniq()
|
||||
%{director | managed_agents: managed_agents}
|
||||
else
|
||||
director
|
||||
end
|
||||
end
|
||||
|
||||
def remove_managed_agent(director, agent_id) do
|
||||
if is_manager?(director) do
|
||||
managed_agents = director.managed_agents |> Enum.reject(&(&1 == agent_id))
|
||||
%{director | managed_agents: managed_agents}
|
||||
else
|
||||
director
|
||||
end
|
||||
end
|
||||
|
||||
# Private helper to determine role from capabilities
|
||||
defp determine_role(capabilities) do
|
||||
management_caps = [:management, :coordination, :oversight]
|
||||
|
||||
cond do
|
||||
Enum.any?(management_caps, &(&1 in capabilities)) and :oversight in capabilities ->
|
||||
:director
|
||||
|
||||
:management in capabilities ->
|
||||
:project_manager
|
||||
|
||||
true ->
|
||||
:standard
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,10 @@ defmodule AgentCoordinator.Inbox do
|
||||
GenServer.call(via_tuple(agent_id), {:add_task, task}, 30_000)
|
||||
end
|
||||
|
||||
def remove_task(agent_id, task_id) do
|
||||
GenServer.call(via_tuple(agent_id), {:remove_task, task_id}, 30_000)
|
||||
end
|
||||
|
||||
def get_next_task(agent_id) do
|
||||
GenServer.call(via_tuple(agent_id), :get_next_task, 15_000)
|
||||
end
|
||||
@@ -92,6 +96,47 @@ defmodule AgentCoordinator.Inbox do
|
||||
{:reply, :ok, new_state}
|
||||
end
|
||||
|
||||
def handle_call({:remove_task, task_id}, _from, state) do
|
||||
# Remove task from pending tasks
|
||||
{removed_task, remaining_pending} =
|
||||
Enum.reduce(state.pending_tasks, {nil, []}, fn task, {found_task, acc} ->
|
||||
if task.id == task_id do
|
||||
{task, acc}
|
||||
else
|
||||
{found_task, [task | acc]}
|
||||
end
|
||||
end)
|
||||
|
||||
# Check if task is currently in progress
|
||||
{new_in_progress, removed_from_progress} =
|
||||
if state.in_progress_task && state.in_progress_task.id == task_id do
|
||||
{nil, state.in_progress_task}
|
||||
else
|
||||
{state.in_progress_task, nil}
|
||||
end
|
||||
|
||||
final_removed_task = removed_task || removed_from_progress
|
||||
|
||||
if final_removed_task do
|
||||
new_state = %{
|
||||
state
|
||||
| pending_tasks: Enum.reverse(remaining_pending),
|
||||
in_progress_task: new_in_progress
|
||||
}
|
||||
|
||||
# Broadcast task removed
|
||||
Phoenix.PubSub.broadcast(
|
||||
AgentCoordinator.PubSub,
|
||||
"agent:#{state.agent_id}",
|
||||
{:task_removed, final_removed_task}
|
||||
)
|
||||
|
||||
{:reply, :ok, new_state}
|
||||
else
|
||||
{:reply, {:error, :task_not_found}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:get_next_task, _from, state) do
|
||||
case state.pending_tasks do
|
||||
[] ->
|
||||
|
||||
@@ -47,7 +47,16 @@ defmodule AgentCoordinator.MCPServer do
|
||||
"type" => "array",
|
||||
"items" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["coding", "testing", "documentation", "analysis", "review"]
|
||||
"enum" => [
|
||||
"coding",
|
||||
"testing",
|
||||
"documentation",
|
||||
"analysis",
|
||||
"review",
|
||||
"management",
|
||||
"coordination",
|
||||
"oversight"
|
||||
]
|
||||
}
|
||||
},
|
||||
"codebase_id" => %{
|
||||
@@ -377,6 +386,128 @@ defmodule AgentCoordinator.MCPServer do
|
||||
},
|
||||
"required" => ["agent_id", "workspace_path"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "observe_all_agents",
|
||||
"description" =>
|
||||
"[Director Only] Get comprehensive view of all agents and their activities for management oversight",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"agent_id" => %{"type" => "string"},
|
||||
"include_activity_history" => %{"type" => "boolean", "default" => true},
|
||||
"scope" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["codebase", "global"],
|
||||
"default" => "codebase"
|
||||
}
|
||||
},
|
||||
"required" => ["agent_id"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "modify_agent_tasks",
|
||||
"description" =>
|
||||
"[Director Only] Add, remove, or update tasks for other agents under management",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"director_id" => %{"type" => "string"},
|
||||
"target_agent_id" => %{"type" => "string"},
|
||||
"action" => %{"type" => "string", "enum" => ["add", "remove", "update"]},
|
||||
"task_id" => %{
|
||||
"type" => "string",
|
||||
"description" => "Required for remove/update actions"
|
||||
},
|
||||
"task" => %{
|
||||
"type" => "object",
|
||||
"description" => "Required for add/update actions",
|
||||
"properties" => %{
|
||||
"title" => %{"type" => "string"},
|
||||
"description" => %{"type" => "string"},
|
||||
"priority" => %{"type" => "string", "enum" => ["low", "normal", "high", "urgent"]},
|
||||
"file_paths" => %{"type" => "array", "items" => %{"type" => "string"}}
|
||||
}
|
||||
},
|
||||
"reason" => %{"type" => "string", "description" => "Reason for the modification"}
|
||||
},
|
||||
"required" => ["director_id", "target_agent_id", "action"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "add_task_feedback",
|
||||
"description" => "[Director Only] Add feedback or guidance notes to a specific task",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"director_id" => %{"type" => "string"},
|
||||
"task_id" => %{"type" => "string"},
|
||||
"feedback" => %{
|
||||
"type" => "string",
|
||||
"description" => "Feedback for the agent working on this task"
|
||||
},
|
||||
"notes" => %{"type" => "string", "description" => "Private director notes"},
|
||||
"blocking_issues" => %{"type" => "array", "items" => %{"type" => "string"}}
|
||||
},
|
||||
"required" => ["director_id", "task_id"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "detect_redundant_tasks",
|
||||
"description" =>
|
||||
"[Director Only] Analyze all agent tasks to detect redundant or overlapping work",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"director_id" => %{"type" => "string"},
|
||||
"scope" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["codebase", "global"],
|
||||
"default" => "codebase"
|
||||
},
|
||||
"similarity_threshold" => %{
|
||||
"type" => "number",
|
||||
"default" => 0.7,
|
||||
"description" => "Task similarity threshold for redundancy detection"
|
||||
}
|
||||
},
|
||||
"required" => ["director_id"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "reassign_tasks",
|
||||
"description" =>
|
||||
"[Director Only] Move tasks between agents based on workload or capability analysis",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"director_id" => %{"type" => "string"},
|
||||
"task_ids" => %{"type" => "array", "items" => %{"type" => "string"}},
|
||||
"from_agent_id" => %{"type" => "string"},
|
||||
"to_agent_id" => %{"type" => "string"},
|
||||
"reason" => %{"type" => "string"}
|
||||
},
|
||||
"required" => ["director_id", "task_ids", "from_agent_id", "to_agent_id", "reason"]
|
||||
}
|
||||
},
|
||||
%{
|
||||
"name" => "send_agent_input",
|
||||
"description" =>
|
||||
"[Director Only] Send VSCode input/commands to specific agents for human-out-of-loop workflows",
|
||||
"inputSchema" => %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"director_id" => %{"type" => "string"},
|
||||
"target_agent_id" => %{"type" => "string"},
|
||||
"input_type" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["chat_message", "command", "file_action"]
|
||||
},
|
||||
"content" => %{"type" => "string"},
|
||||
"context" => %{"type" => "object", "description" => "Additional context for the input"}
|
||||
},
|
||||
"required" => ["director_id", "target_agent_id", "input_type", "content"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1264,6 +1395,353 @@ defmodule AgentCoordinator.MCPServer do
|
||||
Enum.reverse(recommendations)
|
||||
end
|
||||
|
||||
# Director-specific tool implementations
|
||||
|
||||
defp observe_all_agents(%{"agent_id" => agent_id} = args) do
|
||||
# Verify this agent is a director
|
||||
case TaskRegistry.get_agent(agent_id) do
|
||||
{:error, :not_found} ->
|
||||
{:error, "Agent not found: #{agent_id}"}
|
||||
|
||||
{:ok, agent} ->
|
||||
unless Agent.is_director?(agent) do
|
||||
{:error, "Access denied: Only directors can use this tool"}
|
||||
else
|
||||
scope = Map.get(args, "scope", "codebase")
|
||||
include_history = Map.get(args, "include_activity_history", true)
|
||||
|
||||
all_agents = TaskRegistry.list_agents()
|
||||
|
||||
# Filter based on director's oversight scope
|
||||
filtered_agents =
|
||||
case scope do
|
||||
"global" ->
|
||||
if agent.oversight_scope == :global do
|
||||
all_agents
|
||||
else
|
||||
{:error, "Director does not have global oversight scope"}
|
||||
end
|
||||
|
||||
"codebase" ->
|
||||
Enum.filter(all_agents, fn a -> a.codebase_id == agent.codebase_id end)
|
||||
end
|
||||
|
||||
case filtered_agents do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
agents ->
|
||||
detailed_agents =
|
||||
Enum.map(agents, fn target_agent ->
|
||||
task_info =
|
||||
case Inbox.list_tasks(target_agent.id) do
|
||||
{:error, _} -> %{pending: [], in_progress: nil, completed: []}
|
||||
tasks -> tasks
|
||||
end
|
||||
|
||||
base_info = %{
|
||||
agent_id: target_agent.id,
|
||||
name: target_agent.name,
|
||||
role: target_agent.role,
|
||||
capabilities: target_agent.capabilities,
|
||||
status: target_agent.status,
|
||||
codebase_id: target_agent.codebase_id,
|
||||
workspace_path: target_agent.workspace_path,
|
||||
online: Agent.is_online?(target_agent),
|
||||
last_heartbeat: target_agent.last_heartbeat,
|
||||
current_activity: target_agent.current_activity,
|
||||
current_files: target_agent.current_files || [],
|
||||
managed_by_director: target_agent.id in agent.managed_agents,
|
||||
tasks: task_info
|
||||
}
|
||||
|
||||
if include_history do
|
||||
Map.put(base_info, :activity_history, target_agent.activity_history || [])
|
||||
else
|
||||
base_info
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
director_id: agent_id,
|
||||
scope: scope,
|
||||
oversight_capability: agent.oversight_scope,
|
||||
agents: detailed_agents,
|
||||
total_agents: length(detailed_agents),
|
||||
timestamp: DateTime.utc_now()
|
||||
}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp modify_agent_tasks(args) do
|
||||
director_id = Map.get(args, "director_id")
|
||||
target_agent_id = Map.get(args, "target_agent_id")
|
||||
action = Map.get(args, "action")
|
||||
|
||||
with {:ok, director} <- TaskRegistry.get_agent(director_id),
|
||||
true <- Agent.is_director?(director),
|
||||
{:ok, target_agent} <- TaskRegistry.get_agent(target_agent_id),
|
||||
true <- Agent.can_manage_agent?(director, target_agent) do
|
||||
case action do
|
||||
"add" ->
|
||||
task_data = Map.get(args, "task")
|
||||
reason = Map.get(args, "reason", "Director assigned task")
|
||||
|
||||
opts = %{
|
||||
priority: String.to_atom(Map.get(task_data, "priority", "normal")),
|
||||
codebase_id: target_agent.codebase_id,
|
||||
file_paths: Map.get(task_data, "file_paths", []),
|
||||
assignment_reason: reason,
|
||||
metadata: %{
|
||||
director_assigned: true,
|
||||
director_id: director_id
|
||||
}
|
||||
}
|
||||
|
||||
task = Task.new(task_data["title"], task_data["description"], opts)
|
||||
|
||||
case Inbox.add_task(target_agent_id, task) do
|
||||
:ok ->
|
||||
# Add target agent to director's managed list
|
||||
updated_director = Agent.add_managed_agent(director, target_agent_id)
|
||||
TaskRegistry.update_agent(director_id, updated_director)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
action: "task_added",
|
||||
task_id: task.id,
|
||||
target_agent_id: target_agent_id,
|
||||
reason: reason
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "Failed to add task: #{reason}"}
|
||||
end
|
||||
|
||||
"remove" ->
|
||||
task_id = Map.get(args, "task_id")
|
||||
reason = Map.get(args, "reason", "Director removed task")
|
||||
|
||||
case Inbox.remove_task(target_agent_id, task_id) do
|
||||
:ok ->
|
||||
{:ok,
|
||||
%{
|
||||
action: "task_removed",
|
||||
task_id: task_id,
|
||||
target_agent_id: target_agent_id,
|
||||
reason: reason
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "Failed to remove task: #{reason}"}
|
||||
end
|
||||
|
||||
"update" ->
|
||||
task_id = Map.get(args, "task_id")
|
||||
task_data = Map.get(args, "task")
|
||||
reason = Map.get(args, "reason", "Director updated task")
|
||||
|
||||
# This would require implementing task update functionality in Inbox
|
||||
{:error, "Task update functionality not yet implemented"}
|
||||
end
|
||||
else
|
||||
{:error, :not_found} -> {:error, "Director or target agent not found"}
|
||||
false -> {:error, "Access denied: Only directors can modify agent tasks"}
|
||||
end
|
||||
end
|
||||
|
||||
defp add_task_feedback(args) do
|
||||
director_id = Map.get(args, "director_id")
|
||||
task_id = Map.get(args, "task_id")
|
||||
feedback = Map.get(args, "feedback")
|
||||
notes = Map.get(args, "notes")
|
||||
blocking_issues = Map.get(args, "blocking_issues", [])
|
||||
|
||||
with {:ok, director} <- TaskRegistry.get_agent(director_id),
|
||||
true <- Agent.is_director?(director) do
|
||||
# This would require implementing task lookup and update functionality
|
||||
# For now, return a placeholder implementation
|
||||
{:ok,
|
||||
%{
|
||||
task_id: task_id,
|
||||
feedback_added: feedback != nil,
|
||||
notes_added: notes != nil,
|
||||
blocking_issues_added: length(blocking_issues),
|
||||
director_id: director_id,
|
||||
timestamp: DateTime.utc_now()
|
||||
}}
|
||||
else
|
||||
{:error, :not_found} -> {:error, "Director not found"}
|
||||
false -> {:error, "Access denied: Only directors can add task feedback"}
|
||||
end
|
||||
end
|
||||
|
||||
defp detect_redundant_tasks(args) do
|
||||
director_id = Map.get(args, "director_id")
|
||||
scope = Map.get(args, "scope", "codebase")
|
||||
threshold = Map.get(args, "similarity_threshold", 0.7)
|
||||
|
||||
with {:ok, director} <- TaskRegistry.get_agent(director_id),
|
||||
true <- Agent.is_director?(director) do
|
||||
all_agents = TaskRegistry.list_agents()
|
||||
|
||||
# Filter agents based on scope
|
||||
target_agents =
|
||||
case scope do
|
||||
"global" when director.oversight_scope == :global -> all_agents
|
||||
"codebase" -> Enum.filter(all_agents, fn a -> a.codebase_id == director.codebase_id end)
|
||||
"global" -> {:error, "Director does not have global scope"}
|
||||
end
|
||||
|
||||
case target_agents do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
agents ->
|
||||
# Collect all tasks from all agents
|
||||
all_tasks =
|
||||
Enum.flat_map(agents, fn agent ->
|
||||
case Inbox.list_tasks(agent.id) do
|
||||
{:error, _} -> []
|
||||
tasks -> tasks.pending ++ if tasks.in_progress, do: [tasks.in_progress], else: []
|
||||
end
|
||||
end)
|
||||
|
||||
# Simple redundancy detection based on title/description similarity
|
||||
redundant_groups = detect_similar_tasks(all_tasks, threshold)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
director_id: director_id,
|
||||
scope: scope,
|
||||
total_tasks_analyzed: length(all_tasks),
|
||||
redundant_groups: redundant_groups,
|
||||
total_redundant_tasks: Enum.sum(Enum.map(redundant_groups, &length(&1.tasks))),
|
||||
similarity_threshold: threshold,
|
||||
timestamp: DateTime.utc_now()
|
||||
}}
|
||||
end
|
||||
else
|
||||
{:error, :not_found} -> {:error, "Director not found"}
|
||||
false -> {:error, "Access denied: Only directors can detect redundant tasks"}
|
||||
end
|
||||
end
|
||||
|
||||
defp detect_similar_tasks(tasks, threshold) do
|
||||
# Simple implementation - group tasks with similar titles
|
||||
tasks
|
||||
|> Enum.group_by(fn task ->
|
||||
# Normalize title for comparison
|
||||
String.downcase(task.title)
|
||||
|> String.replace(~r/[^\w\s]/, "")
|
||||
|> String.split()
|
||||
|> Enum.take(3)
|
||||
|> Enum.join(" ")
|
||||
end)
|
||||
|> Enum.filter(fn {_key, group_tasks} -> length(group_tasks) > 1 end)
|
||||
|> Enum.map(fn {key, group_tasks} ->
|
||||
%{
|
||||
similarity_key: key,
|
||||
tasks:
|
||||
Enum.map(group_tasks, fn task ->
|
||||
%{
|
||||
task_id: task.id,
|
||||
title: task.title,
|
||||
description: String.slice(task.description, 0, 100) <> "...",
|
||||
agent_id: task.agent_id,
|
||||
codebase_id: task.codebase_id
|
||||
}
|
||||
end),
|
||||
task_count: length(group_tasks)
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp reassign_tasks(args) do
|
||||
director_id = Map.get(args, "director_id")
|
||||
task_ids = Map.get(args, "task_ids")
|
||||
from_agent_id = Map.get(args, "from_agent_id")
|
||||
to_agent_id = Map.get(args, "to_agent_id")
|
||||
reason = Map.get(args, "reason")
|
||||
|
||||
with {:ok, director} <- TaskRegistry.get_agent(director_id),
|
||||
true <- Agent.is_director?(director),
|
||||
{:ok, from_agent} <- TaskRegistry.get_agent(from_agent_id),
|
||||
{:ok, to_agent} <- TaskRegistry.get_agent(to_agent_id),
|
||||
true <- Agent.can_manage_agent?(director, from_agent),
|
||||
true <- Agent.can_manage_agent?(director, to_agent) do
|
||||
results =
|
||||
Enum.map(task_ids, fn task_id ->
|
||||
# This would require implementing task transfer between inboxes
|
||||
# For now, return placeholder
|
||||
%{
|
||||
task_id: task_id,
|
||||
status: "reassigned",
|
||||
from_agent_id: from_agent_id,
|
||||
to_agent_id: to_agent_id,
|
||||
reason: reason
|
||||
}
|
||||
end)
|
||||
|
||||
# Add both agents to director's managed list
|
||||
updated_director =
|
||||
director
|
||||
|> Agent.add_managed_agent(from_agent_id)
|
||||
|> Agent.add_managed_agent(to_agent_id)
|
||||
|
||||
TaskRegistry.update_agent(director_id, updated_director)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
director_id: director_id,
|
||||
reassigned_tasks: results,
|
||||
total_reassigned: length(results),
|
||||
timestamp: DateTime.utc_now()
|
||||
}}
|
||||
else
|
||||
{:error, :not_found} -> {:error, "Agent not found"}
|
||||
false -> {:error, "Access denied: Director cannot manage one or more agents"}
|
||||
end
|
||||
end
|
||||
|
||||
defp send_agent_input(args) do
|
||||
director_id = Map.get(args, "director_id")
|
||||
target_agent_id = Map.get(args, "target_agent_id")
|
||||
input_type = Map.get(args, "input_type")
|
||||
content = Map.get(args, "content")
|
||||
context = Map.get(args, "context", %{})
|
||||
|
||||
with {:ok, director} <- TaskRegistry.get_agent(director_id),
|
||||
true <- Agent.is_director?(director),
|
||||
{:ok, target_agent} <- TaskRegistry.get_agent(target_agent_id),
|
||||
true <- Agent.can_manage_agent?(director, target_agent) do
|
||||
# This would integrate with VSCode APIs to send input to agents
|
||||
# For now, return a placeholder that logs the action
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"Director #{director_id} sending #{input_type} to agent #{target_agent_id}: #{content}"
|
||||
)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
director_id: director_id,
|
||||
target_agent_id: target_agent_id,
|
||||
input_type: input_type,
|
||||
content_length: String.length(content),
|
||||
context: context,
|
||||
status: "input_queued",
|
||||
timestamp: DateTime.utc_now(),
|
||||
note: "VSCode integration pending - input logged for future implementation"
|
||||
}}
|
||||
else
|
||||
{:error, :not_found} -> {:error, "Director or target agent not found"}
|
||||
false -> {:error, "Access denied: Director cannot manage target agent"}
|
||||
end
|
||||
end
|
||||
|
||||
# External MCP server management functions
|
||||
|
||||
defp start_external_server(name, %{type: :stdio} = config) do
|
||||
@@ -1338,6 +1816,7 @@ defmodule AgentCoordinator.MCPServer do
|
||||
port_options = [
|
||||
:binary,
|
||||
:stream,
|
||||
{:cd, workspace_root()},
|
||||
{:env, env_list},
|
||||
:exit_status,
|
||||
:hide
|
||||
@@ -1583,6 +2062,25 @@ defmodule AgentCoordinator.MCPServer do
|
||||
pid_file_path
|
||||
end
|
||||
|
||||
defp workspace_root do
|
||||
["MCP_WORKSPACE_DIR", "WORKSPACE_DIR"]
|
||||
|> Enum.find_value(fn var ->
|
||||
case System.get_env(var) do
|
||||
path when is_binary(path) and path != "" ->
|
||||
expanded = Path.expand(path)
|
||||
|
||||
if File.dir?(expanded), do: expanded, else: nil
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
nil -> File.cwd!()
|
||||
path -> path
|
||||
end
|
||||
end
|
||||
|
||||
defp get_all_unified_tools_from_state(state) do
|
||||
# Combine coordinator tools with external server tools from state
|
||||
coordinator_tools = @mcp_tools
|
||||
@@ -1663,6 +2161,12 @@ defmodule AgentCoordinator.MCPServer do
|
||||
"get_detailed_task_board" -> get_detailed_task_board(args)
|
||||
"get_agent_task_history" -> get_agent_task_history(args)
|
||||
"discover_codebase_info" -> discover_codebase_info(args)
|
||||
"observe_all_agents" -> observe_all_agents(args)
|
||||
"modify_agent_tasks" -> modify_agent_tasks(args)
|
||||
"add_task_feedback" -> add_task_feedback(args)
|
||||
"detect_redundant_tasks" -> detect_redundant_tasks(args)
|
||||
"reassign_tasks" -> reassign_tasks(args)
|
||||
"send_agent_input" -> send_agent_input(args)
|
||||
_ -> {:error, "Unknown coordinator tool: #{tool_name}"}
|
||||
end
|
||||
end
|
||||
@@ -1789,7 +2293,19 @@ defmodule AgentCoordinator.MCPServer do
|
||||
end
|
||||
|
||||
defp load_server_config do
|
||||
config_file = System.get_env("MCP_CONFIG_FILE", "mcp_servers.json")
|
||||
workspace_dir = workspace_root()
|
||||
|
||||
config_file =
|
||||
case System.get_env("MCP_CONFIG_FILE") do
|
||||
nil ->
|
||||
Path.join(workspace_dir, "mcp_servers.json")
|
||||
path ->
|
||||
if Path.type(path) == :absolute do
|
||||
path
|
||||
else
|
||||
Path.expand(path, workspace_dir)
|
||||
end
|
||||
end
|
||||
|
||||
if File.exists?(config_file) do
|
||||
try do
|
||||
@@ -1797,20 +2313,23 @@ defmodule AgentCoordinator.MCPServer do
|
||||
%{"servers" => servers} ->
|
||||
normalized_servers =
|
||||
Enum.into(servers, %{}, fn {name, config} ->
|
||||
normalized_config = normalize_server_config(config)
|
||||
normalized_config =
|
||||
config
|
||||
|> normalize_server_config()
|
||||
|> update_workspace_paths(workspace_dir)
|
||||
{name, normalized_config}
|
||||
end)
|
||||
|
||||
%{servers: normalized_servers}
|
||||
|
||||
_ ->
|
||||
get_default_server_config()
|
||||
get_default_server_config(workspace_dir)
|
||||
end
|
||||
rescue
|
||||
_ -> get_default_server_config()
|
||||
_ -> get_default_server_config(workspace_dir)
|
||||
end
|
||||
else
|
||||
get_default_server_config()
|
||||
get_default_server_config(workspace_dir)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1828,13 +2347,28 @@ defmodule AgentCoordinator.MCPServer do
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_default_server_config do
|
||||
defp update_workspace_paths(config, workspace_dir) do
|
||||
# Update filesystem server args to use the current workspace directory
|
||||
case config do
|
||||
%{command: "bunx", args: args} = config ->
|
||||
updated_args =
|
||||
case args do
|
||||
["-y", "@modelcontextprotocol/server-filesystem", _old_path | rest] ->
|
||||
["-y", "@modelcontextprotocol/server-filesystem", workspace_dir | rest]
|
||||
other -> other
|
||||
end
|
||||
Map.put(config, :args, updated_args)
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp get_default_server_config(workspace_dir) do
|
||||
%{
|
||||
servers: %{
|
||||
"mcp_filesystem" => %{
|
||||
type: :stdio,
|
||||
command: "bunx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/ra"],
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", workspace_dir],
|
||||
auto_restart: true,
|
||||
description: "Filesystem operations server"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,12 @@ defmodule AgentCoordinator.Task do
|
||||
:cross_codebase_dependencies,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:metadata
|
||||
:metadata,
|
||||
:feedback,
|
||||
:director_notes,
|
||||
:assignment_reason,
|
||||
:refinement_history,
|
||||
:blocking_issues
|
||||
]}
|
||||
defstruct [
|
||||
:id,
|
||||
@@ -32,7 +37,12 @@ defmodule AgentCoordinator.Task do
|
||||
:cross_codebase_dependencies,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:metadata
|
||||
:metadata,
|
||||
:feedback,
|
||||
:director_notes,
|
||||
:assignment_reason,
|
||||
:refinement_history,
|
||||
:blocking_issues
|
||||
]
|
||||
|
||||
@type status :: :pending | :in_progress | :completed | :failed | :blocked
|
||||
@@ -51,7 +61,12 @@ defmodule AgentCoordinator.Task do
|
||||
cross_codebase_dependencies: [%{codebase_id: String.t(), task_id: String.t()}],
|
||||
created_at: DateTime.t(),
|
||||
updated_at: DateTime.t(),
|
||||
metadata: map()
|
||||
metadata: map(),
|
||||
feedback: String.t() | nil,
|
||||
director_notes: String.t() | nil,
|
||||
assignment_reason: String.t() | nil,
|
||||
refinement_history: [map()],
|
||||
blocking_issues: [String.t()]
|
||||
}
|
||||
|
||||
def new(title, description, opts \\ []) do
|
||||
@@ -78,7 +93,12 @@ defmodule AgentCoordinator.Task do
|
||||
cross_codebase_dependencies: get_opt.(:cross_codebase_dependencies, []),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metadata: get_opt.(:metadata, %{})
|
||||
metadata: get_opt.(:metadata, %{}),
|
||||
feedback: nil,
|
||||
director_notes: nil,
|
||||
assignment_reason: nil,
|
||||
refinement_history: [],
|
||||
blocking_issues: []
|
||||
}
|
||||
end
|
||||
|
||||
@@ -115,4 +135,109 @@ defmodule AgentCoordinator.Task do
|
||||
dependencies = [dependency | task.cross_codebase_dependencies]
|
||||
%{task | cross_codebase_dependencies: dependencies, updated_at: DateTime.utc_now()}
|
||||
end
|
||||
|
||||
# Director management functions
|
||||
|
||||
def add_feedback(task, feedback, director_id) do
|
||||
refinement_entry = %{
|
||||
type: "feedback_added",
|
||||
director_id: director_id,
|
||||
content: feedback,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
|
||||
%{
|
||||
task
|
||||
| feedback: feedback,
|
||||
refinement_history: [refinement_entry | task.refinement_history],
|
||||
updated_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
def add_director_notes(task, notes, director_id) do
|
||||
refinement_entry = %{
|
||||
type: "director_notes_added",
|
||||
director_id: director_id,
|
||||
content: notes,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
|
||||
%{
|
||||
task
|
||||
| director_notes: notes,
|
||||
refinement_history: [refinement_entry | task.refinement_history],
|
||||
updated_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
def set_assignment_reason(task, reason, director_id) do
|
||||
refinement_entry = %{
|
||||
type: "assignment_reason_set",
|
||||
director_id: director_id,
|
||||
reason: reason,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
|
||||
%{
|
||||
task
|
||||
| assignment_reason: reason,
|
||||
refinement_history: [refinement_entry | task.refinement_history],
|
||||
updated_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
def add_blocking_issue(task, issue, director_id) do
|
||||
new_issues = [issue | task.blocking_issues] |> Enum.uniq()
|
||||
|
||||
refinement_entry = %{
|
||||
type: "blocking_issue_added",
|
||||
director_id: director_id,
|
||||
issue: issue,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
|
||||
%{
|
||||
task
|
||||
| blocking_issues: new_issues,
|
||||
refinement_history: [refinement_entry | task.refinement_history],
|
||||
updated_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
def remove_blocking_issue(task, issue, director_id) do
|
||||
new_issues = task.blocking_issues |> Enum.reject(&(&1 == issue))
|
||||
|
||||
refinement_entry = %{
|
||||
type: "blocking_issue_removed",
|
||||
director_id: director_id,
|
||||
issue: issue,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
|
||||
%{
|
||||
task
|
||||
| blocking_issues: new_issues,
|
||||
refinement_history: [refinement_entry | task.refinement_history],
|
||||
updated_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
def reassign(task, new_agent_id, director_id, reason) do
|
||||
refinement_entry = %{
|
||||
type: "task_reassigned",
|
||||
director_id: director_id,
|
||||
from_agent_id: task.agent_id,
|
||||
to_agent_id: new_agent_id,
|
||||
reason: reason,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
|
||||
%{
|
||||
task
|
||||
| agent_id: new_agent_id,
|
||||
assignment_reason: reason,
|
||||
refinement_history: [refinement_entry | task.refinement_history],
|
||||
updated_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user