Current section

Files

Jump to
nous lib nous plugins team_tools.ex
Raw

lib/nous/plugins/team_tools.ex

defmodule Nous.Plugins.TeamTools do
@moduledoc """
Plugin that provides team communication and coordination tools for agents.
When an agent is part of a team, this plugin adds tools for peer messaging,
broadcasting, sharing discoveries, listing team members, and claiming file
regions for editing.
## Capabilities
- Direct messaging between team agents
- Team-wide broadcasts
- Shared discovery board
- File region claiming to prevent edit conflicts
- Team member listing
## Usage
agent = Agent.new("openai:gpt-4",
plugins: [Nous.Plugins.TeamTools],
deps: %{
team_id: "team_1",
agent_name: "alice",
shared_state_pid: shared_state_pid
}
)
## Configuration
The plugin reads the following keys from `ctx.deps`:
- `:team_id` — the team identifier (required)
- `:agent_name` — this agent's name in the team (required)
- `:shared_state_pid` — pid of the team's SharedState process
- `:team_coordinator_pid` — pid of the team's Coordinator process
## Tools Provided
| Tool | Description |
|------|-------------|
| `peer_message` | Send a direct message to a named agent |
| `broadcast_message` | Broadcast a message to all team agents |
| `share_discovery` | Store a finding in SharedState and broadcast |
| `list_team` | List team members and their status |
| `claim_region` | Claim file lines before editing |
## PubSub Events
- `{:peer_message, from, to, content}` — sent on agent's direct topic
- `{:team_broadcast, from, content}` — sent on team-wide topic
- `{:discovery, from, discovery}` — sent on team-wide topic
"""
@behaviour Nous.Plugin
alias Nous.Tool
alias Nous.Teams.{Comms, SharedState, Coordinator}
# ===========================================================================
# Plugin callbacks
# ===========================================================================
@impl true
def init(_agent, ctx) do
team_id = ctx.deps[:team_id]
agent_name = ctx.deps[:agent_name]
pubsub = ctx.pubsub
if team_id && agent_name do
Comms.subscribe_team(pubsub, team_id)
Comms.subscribe_agent(pubsub, team_id, agent_name)
end
ctx
end
@impl true
def tools(_agent, _ctx) do
[
peer_message_tool(),
broadcast_message_tool(),
share_discovery_tool(),
list_team_tool(),
claim_region_tool()
]
end
@impl true
def system_prompt(_agent, ctx) do
team_id = ctx.deps[:team_id]
agent_name = ctx.deps[:agent_name]
role = ctx.deps[:team_role]
role_info =
if role do
"Your role: #{role.name}. #{role.system_prompt || ""}"
else
""
end
"""
## Team Context
You are agent "#{agent_name || "unknown"}" on team "#{team_id || "unknown"}".
#{role_info}
### Team Communication Tools
- `peer_message` — send a direct message to another agent by name
- `broadcast_message` — send a message to all agents on the team
- `share_discovery` — store an important finding for the team
- `list_team` — see who is on the team and their status
- `claim_region` — claim a file region before editing to prevent conflicts
### Guidelines
- Share important findings with the team via `share_discovery`
- Claim file regions before editing with `claim_region`
- Use `peer_message` for targeted communication
- Use `broadcast_message` sparingly, only for team-wide announcements
"""
end
@impl true
def before_request(_agent, ctx, tools) do
# Inject recent team messages into context if available
{ctx, tools}
end
# ===========================================================================
# Tool definitions
# ===========================================================================
defp peer_message_tool do
%Tool{
name: "peer_message",
description: "Send a direct message to a named agent on your team.",
parameters: %{
"type" => "object",
"properties" => %{
"to" => %{
"type" => "string",
"description" => "Name of the agent to send the message to"
},
"content" => %{
"type" => "string",
"description" => "The message content"
}
},
"required" => ["to", "content"]
},
function: &__MODULE__.peer_message/2,
takes_ctx: true
}
end
defp broadcast_message_tool do
%Tool{
name: "broadcast_message",
description:
"Broadcast a message to all agents on the team. Use sparingly for important announcements.",
parameters: %{
"type" => "object",
"properties" => %{
"content" => %{
"type" => "string",
"description" => "The message to broadcast"
}
},
"required" => ["content"]
},
function: &__MODULE__.broadcast_message/2,
takes_ctx: true
}
end
defp share_discovery_tool do
%Tool{
name: "share_discovery",
description:
"Share an important finding with the team. Stores it in shared state and broadcasts to all agents.",
parameters: %{
"type" => "object",
"properties" => %{
"topic" => %{
"type" => "string",
"description" => "Short topic/title for the discovery"
},
"content" => %{
"type" => "string",
"description" => "Detailed content of the discovery"
}
},
"required" => ["topic", "content"]
},
function: &__MODULE__.share_discovery/2,
takes_ctx: true
}
end
defp list_team_tool do
%Tool{
name: "list_team",
description: "List all agents on the team and their current status.",
parameters: %{
"type" => "object",
"properties" => %{},
"required" => []
},
function: &__MODULE__.list_team/2,
takes_ctx: true
}
end
defp claim_region_tool do
%Tool{
name: "claim_region",
description:
"Claim a file region (line range) before editing to prevent conflicts with other agents.",
parameters: %{
"type" => "object",
"properties" => %{
"file" => %{
"type" => "string",
"description" => "File path to claim"
},
"start_line" => %{
"type" => "integer",
"description" => "Start line number"
},
"end_line" => %{
"type" => "integer",
"description" => "End line number"
}
},
"required" => ["file", "start_line", "end_line"]
},
function: &__MODULE__.claim_region/2,
takes_ctx: true
}
end
# ===========================================================================
# Tool implementations
# ===========================================================================
@doc false
def peer_message(ctx, %{"to" => to, "content" => content}) do
team_id = ctx.deps[:team_id]
from = ctx.deps[:agent_name]
pubsub = ctx.pubsub
Comms.send_to_agent(pubsub, team_id, to, {:peer_message, from, to, content})
%{status: "sent", from: from, to: to}
end
@doc false
def broadcast_message(ctx, %{"content" => content}) do
team_id = ctx.deps[:team_id]
from = ctx.deps[:agent_name]
pubsub = ctx.pubsub
Comms.broadcast_team(pubsub, team_id, {:team_broadcast, from, content})
%{status: "broadcast", from: from}
end
@doc false
def share_discovery(ctx, %{"topic" => topic, "content" => content}) do
team_id = ctx.deps[:team_id]
from = ctx.deps[:agent_name]
pubsub = ctx.pubsub
shared_state = ctx.deps[:shared_state_pid]
discovery = %{topic: topic, content: content}
if is_pid(shared_state) and Process.alive?(shared_state) do
SharedState.share_discovery(shared_state, from, discovery)
end
Comms.broadcast_team(pubsub, team_id, {:discovery, from, discovery})
%{status: "shared", topic: topic, from: from}
end
@doc false
def list_team(ctx, _args) do
coordinator = ctx.deps[:team_coordinator_pid]
if is_pid(coordinator) and Process.alive?(coordinator) do
agents = Coordinator.list_agents(coordinator)
%{
team_id: ctx.deps[:team_id],
agents:
Enum.map(agents, fn a ->
%{name: a.name, status: a.status}
end)
}
else
%{team_id: ctx.deps[:team_id], agents: [], note: "coordinator unavailable"}
end
end
@doc false
def claim_region(ctx, %{"file" => file, "start_line" => start_line, "end_line" => end_line}) do
agent_name = ctx.deps[:agent_name]
shared_state = ctx.deps[:shared_state_pid]
if is_pid(shared_state) and Process.alive?(shared_state) do
case SharedState.claim_region(shared_state, agent_name, file, start_line, end_line) do
:ok ->
%{status: "claimed", file: file, start_line: start_line, end_line: end_line}
{:error, :conflict} ->
%{status: "conflict", file: file, message: "Region overlaps with another agent's claim"}
end
else
%{status: "error", message: "shared state unavailable"}
end
end
end