Current section
Files
Jump to
Current section
Files
lib/cmdc/sub_agent/supervisor.ex
defmodule CMDC.SubAgent.Supervisor do
@moduledoc """
子代理进程树动态监督者。
使用 `DynamicSupervisor` 管理子代理进程的生命周期。
每个子代理对应一个独立的 `CMDC.Agent` 进程,崩溃不传染父 Agent。
## 进程树位置
CMDC.SessionServer (:one_for_one)
├── CMDC.SubAgent.Supervisor ← 此模块
│ ├── CMDC.Agent (sub-coder)
│ ├── CMDC.Agent (sub-researcher)
│ └── ...
└── CMDC.Agent (main)
## 示例
{:ok, sup} = CMDC.SubAgent.Supervisor.start_link([])
{:ok, pid} = CMDC.SubAgent.Supervisor.start_child(sup, agent_opts)
:ok = CMDC.SubAgent.Supervisor.stop_child(sup, pid)
[pid1, pid2] = CMDC.SubAgent.Supervisor.list_children(sup)
"""
use DynamicSupervisor
# ==========================================================================
# Public API
# ==========================================================================
@doc "启动 SubAgent 动态监督者。"
@spec start_link(keyword()) :: Supervisor.on_start()
def start_link(opts) do
name = Keyword.get(opts, :name)
start_opts = if name, do: [name: name], else: []
DynamicSupervisor.start_link(__MODULE__, [], start_opts)
end
@doc """
在此监督者下启动一个子代理进程。
`agent_opts` 为传递给 `CMDC.Agent.start_link/1` 的选项列表,
必须包含 `:session_id` 字段。
返回 `{:ok, pid}` 或 `{:error, reason}`。
"""
@spec start_child(pid() | atom(), keyword()) :: DynamicSupervisor.on_start_child()
def start_child(supervisor, agent_opts) when is_list(agent_opts) do
sub_session_id = Keyword.fetch!(agent_opts, :session_id)
parent_session_id = Keyword.get(agent_opts, :parent_session_id, "_unknown")
name = Keyword.get(agent_opts, :name, "sub-agent")
spec = %{
id: {CMDC.Agent, sub_session_id},
start: {CMDC.Agent, :start_link, [agent_opts]},
restart: :temporary
}
result = DynamicSupervisor.start_child(supervisor, spec)
CMDC.Telemetry.execute(
[:cmdc, :subagent, :spawn, :start],
%{system_time: System.system_time(:millisecond)},
%{
parent_session_id: parent_session_id,
sub_session_id: sub_session_id,
name: name,
outcome: match_outcome(result)
}
)
result
end
defp match_outcome({:ok, _}), do: :ok
defp match_outcome({:ok, _, _}), do: :ok
defp match_outcome({:error, _}), do: :error
defp match_outcome(_), do: :error
@doc """
停止指定 pid 的子代理进程。
进程不存在时返回 `:ok`(幂等)。
显式调用此函数时会 emit `[:cmdc, :subagent, :spawn, :stop]` telemetry 事件。
子 Agent **自然 terminate**(如 `:normal` exit / Tool.Task 收集结果完毕)的路径
不走这里,由 Tool.Task 在收集结果时单独 emit `:stop`。
"""
@spec stop_child(pid() | atom(), pid()) :: :ok | {:error, :not_found}
def stop_child(supervisor, child_pid) when is_pid(child_pid) do
started_at = System.monotonic_time(:millisecond)
result = DynamicSupervisor.terminate_child(supervisor, child_pid)
duration_ms = System.monotonic_time(:millisecond) - started_at
CMDC.Telemetry.execute(
[:cmdc, :subagent, :spawn, :stop],
%{duration_ms: duration_ms},
%{
sub_session_id: "_unknown",
outcome: if(result == :ok, do: :terminated, else: :error)
}
)
result
end
@doc "列出此监督者下所有活跃子代理 pid。"
@spec list_children(pid() | atom()) :: [pid()]
def list_children(supervisor) do
DynamicSupervisor.which_children(supervisor)
|> Enum.flat_map(fn
{_, pid, :worker, _} when is_pid(pid) -> [pid]
_ -> []
end)
end
@doc "返回活跃子代理数量。"
@spec count(pid() | atom()) :: non_neg_integer()
def count(supervisor) do
DynamicSupervisor.count_children(supervisor).active
end
# ==========================================================================
# DynamicSupervisor Callbacks
# ==========================================================================
@impl true
def init(_args) do
DynamicSupervisor.init(strategy: :one_for_one)
end
end