Current section
Files
Jump to
Current section
Files
config/tools/call_subagent.exs
defmodule Eai.Tool.CallSubagent do
@moduledoc """
子代理派发工具。支持会话复用和前缀缓存。
- 首次调用时创建独立 chat session,可通过 pre_context 加载历史前缀。
- 后续通过 chat_session 参数追加消息,复用同一会话历史。
- 子代理完成后不自动关闭,需显式调用 close_chat_session 或等待系统回收。
"""
@behaviour Eai.Tool
require Logger
@impl true
def schema do
%{
type: "function",
function: %{
name: "call_subagent",
description: """
Dispatch a sub-task to a fresh, independent AI agent with minimal context.
The subagent starts with ONLY its system prompt + your message — it does NOT
inherit the main conversation history. This makes subagent tool calls ~50× cheaper
per round-trip than running the same task in the main context.
**When to use:** Context-independent work (compilation, file ops, research,
benchmarks). Any task you can describe in one sentence without referencing
"what we discussed earlier" is a good candidate.
**When NOT to use:** Trivial one-liners (echo, pwd) — spawn overhead > savings.
Tasks that need conversation context ("continue what I was doing").
Supports session reuse via `chat_session`, prefix caching via `pre_context`,
and prompt/model selection. Use `close_chat_session` when done.
Poll results with `get_subagent_result` (same poll_cooldown_ms cost model).
""",
parameters: %{
type: "object",
properties: %{
message: %{
type: "string",
description: "The task instruction or question for the sub-agent."
},
chat_session: %{
type: "string",
description:
"Optional. Reuse an existing sub-agent session. If not given, a new session is created."
},
pre_context: %{
type: "string",
description: """
Optional. Path to an exported history .gzip file to load ONCE when creating a new session.
Ignored if the session already exists.
Enables LLM prefix caching when the same history is reused across calls.
"""
},
format: %{
type: "string",
description:
"Format of the pre_context file ('converse', 'openai', 'anthropic'). Default 'converse'."
},
pty_session_id: %{
type: "string",
description:
"Optional. PTY session for shell isolation. Defaults to the chat_session ID."
},
model: %{
type: "string",
description: "Optional model name (e.g., 'gpt4o', 'claude_sonnet', 'deepseek')."
},
prompt: %{
type: "string",
description: "Optional system prompt name (e.g., 'coder', 'analyst')."
}
},
required: ["message"]
}
}
}
end
@impl true
def execute(args, _pty_session_id, _chat_session_id) do
message = Map.get(args, "message", "")
model_opt = args |> Map.get("model") |> maybe_atom()
prompt_opt = args |> Map.get("prompt") |> maybe_atom()
pre_context_path = Map.get(args, "pre_context")
format_opt = Map.get(args, "format", "converse")
existing_session = Map.get(args, "chat_session")
chat_session_id =
existing_session || "subagent_#{System.unique_integer([:positive, :monotonic])}"
pty_session_id = Map.get(args, "pty_session_id", chat_session_id)
subagent_task_id = "satask_#{System.unique_integer([:positive, :monotonic])}"
Eai.Naming.cache().put("subagent_result:#{subagent_task_id}", %{
status: "running",
started_at: System.monotonic_time(:millisecond)
})
Task.start(fn ->
result_entry =
try do
# pre_context 仅在首次创建(无 existing_session)时加载
if is_nil(existing_session) && pre_context_path && File.exists?(pre_context_path) do
case Eai.Chat.replace_history(pre_context_path, chat_session_id, format_opt) do
{:ok, count} ->
Logger.info(
"Subagent pre_context loaded: #{count} messages from #{pre_context_path}"
)
{:error, reason} ->
Logger.error("Subagent pre_context load failed: #{reason}")
end
end
talk_result =
Eai.Chat.talk(
content: message,
mod: :f,
chat_session: chat_session_id,
pty_session_id: pty_session_id,
model: model_opt,
prompt: prompt_opt,
timeout: 120_000
)
case talk_result do
{:ok, response} ->
%{status: "complete", answer: response, pty_session_id: pty_session_id}
{:error, reason} ->
%{status: "error", reason: inspect(reason), pty_session_id: pty_session_id}
end
rescue
e ->
Logger.error("Subagent task #{subagent_task_id} crashed: #{Exception.message(e)}")
%{status: "error", reason: Exception.message(e), pty_session_id: pty_session_id}
end
Eai.Naming.cache().put("subagent_result:#{subagent_task_id}", result_entry)
end)
%{
subagent_task_id: subagent_task_id,
chat_session: chat_session_id,
status: "queued",
pty_session_id: pty_session_id
}
|> Eai.Utils.sanitize_value()
|> Jason.encode!()
end
defp maybe_atom(nil), do: nil
defp maybe_atom(s), do: String.to_atom(s)
end