mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
refactor(agent): give Claude 5 subagent tools fabro canonical names
`NativeTool` documents itself as "an identity, not a name" whose canonical form is fabro's own vocabulary, with harness names layered on as aliases: `to_string = "read_file", serialize = "Read"`. The four Claude 5 subagent tools inverted that. `ClaudeAgent` declared `to_string = "Agent"`, making the Anthropic wire name the identity and leaving `name(ToolVocabulary::Fabro)` returning `"Agent"` -- and pairing a provider-specific variant name with a generic wire name. It also meant the `Claude5` arm listed none of them: they fell through to `canonical_name()` and were correct only by accident. Rename to `BackgroundAgent` / `AgentOutput` / `StopAgent` / `MessageAgent` with fabro canonical names, keep the harness names as `serialize` aliases so `from_any_name` still resolves them, and name them explicitly in the `Claude5` vocabulary arm. Also map `Grep`/`Glob` there: that arm describes the vocabulary rather than the profile's registry, and if either were ever registered it would otherwise reach the harness lowercased. Records why these are separate identities from `spawn_agent`/`wait`/`close_agent`/`send_input` rather than aliases of them, since the capabilities genuinely differ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
27549c2358
commit
3c755a7d4e
3 changed files with 64 additions and 18 deletions
|
|
@ -73,14 +73,21 @@ pub enum NativeTool {
|
|||
Wait,
|
||||
#[strum(to_string = "close_agent")]
|
||||
CloseAgent,
|
||||
#[strum(to_string = "Agent")]
|
||||
ClaudeAgent,
|
||||
#[strum(to_string = "TaskOutput")]
|
||||
TaskOutput,
|
||||
#[strum(to_string = "TaskStop")]
|
||||
TaskStop,
|
||||
#[strum(to_string = "SendMessage")]
|
||||
SendMessage,
|
||||
// Claude 5 drives one background agent through four tools, where fabro's
|
||||
// own vocabulary uses `spawn_agent`/`wait`/`close_agent`/`send_input`.
|
||||
// They are separate identities rather than aliases of those because the
|
||||
// capabilities differ: `Agent` runs in the background or inline depending
|
||||
// on `run_in_background`, and `TaskOutput` both polls and waits. Mapping
|
||||
// them onto the fabro four would promise semantics those tools do not
|
||||
// have -- the same reason Kimi Code's `Agent` is deliberately unmapped.
|
||||
#[strum(to_string = "background_agent", serialize = "Agent")]
|
||||
BackgroundAgent,
|
||||
#[strum(to_string = "agent_output", serialize = "TaskOutput")]
|
||||
AgentOutput,
|
||||
#[strum(to_string = "stop_agent", serialize = "TaskStop")]
|
||||
StopAgent,
|
||||
#[strum(to_string = "message_agent", serialize = "SendMessage")]
|
||||
MessageAgent,
|
||||
#[strum(to_string = "use_skill", serialize = "Skill")]
|
||||
UseSkill,
|
||||
#[strum(to_string = "update_plan")]
|
||||
|
|
@ -135,9 +142,18 @@ impl NativeTool {
|
|||
Self::WriteFile => "Write",
|
||||
Self::EditFile => "Edit",
|
||||
Self::Shell => "Bash",
|
||||
// Named for completeness: this arm describes the vocabulary,
|
||||
// not the profile's registry, and the Claude 5 profile
|
||||
// deliberately registers neither.
|
||||
Self::Grep => "Grep",
|
||||
Self::Glob => "Glob",
|
||||
Self::WebSearch => "WebSearch",
|
||||
Self::WebFetch => "WebFetch",
|
||||
Self::UseSkill => "Skill",
|
||||
Self::BackgroundAgent => "Agent",
|
||||
Self::AgentOutput => "TaskOutput",
|
||||
Self::StopAgent => "TaskStop",
|
||||
Self::MessageAgent => "SendMessage",
|
||||
other => other.canonical_name(),
|
||||
},
|
||||
ToolVocabulary::KimiCode => match self {
|
||||
|
|
@ -205,10 +221,10 @@ impl NativeTool {
|
|||
| Self::SendInput
|
||||
| Self::Wait
|
||||
| Self::CloseAgent
|
||||
| Self::ClaudeAgent
|
||||
| Self::TaskOutput
|
||||
| Self::TaskStop
|
||||
| Self::SendMessage => Some(AgentToolCategory::Subagent),
|
||||
| Self::BackgroundAgent
|
||||
| Self::AgentOutput
|
||||
| Self::StopAgent
|
||||
| Self::MessageAgent => Some(AgentToolCategory::Subagent),
|
||||
// Uncategorized today. Giving these a category would change the CLI
|
||||
// permission gate, which is a behavior change rather than a
|
||||
// classification cleanup, so they keep their existing answer.
|
||||
|
|
@ -299,9 +315,39 @@ mod tests {
|
|||
"WebFetch"
|
||||
);
|
||||
assert_eq!(
|
||||
NativeTool::ClaudeAgent.name(ToolVocabulary::Claude5),
|
||||
NativeTool::BackgroundAgent.name(ToolVocabulary::Claude5),
|
||||
"Agent"
|
||||
);
|
||||
assert_eq!(
|
||||
NativeTool::AgentOutput.name(ToolVocabulary::Claude5),
|
||||
"TaskOutput"
|
||||
);
|
||||
assert_eq!(
|
||||
NativeTool::StopAgent.name(ToolVocabulary::Claude5),
|
||||
"TaskStop"
|
||||
);
|
||||
assert_eq!(
|
||||
NativeTool::MessageAgent.name(ToolVocabulary::Claude5),
|
||||
"SendMessage"
|
||||
);
|
||||
}
|
||||
|
||||
/// The harness name is how a tool is expressed, not what it is: the
|
||||
/// identity keeps a fabro name, and the harness name resolves back to it.
|
||||
#[test]
|
||||
fn claude5_subagent_tools_keep_fabro_canonical_names() {
|
||||
for (tool, canonical, claude5) in [
|
||||
(NativeTool::BackgroundAgent, "background_agent", "Agent"),
|
||||
(NativeTool::AgentOutput, "agent_output", "TaskOutput"),
|
||||
(NativeTool::StopAgent, "stop_agent", "TaskStop"),
|
||||
(NativeTool::MessageAgent, "message_agent", "SendMessage"),
|
||||
] {
|
||||
assert_eq!(tool.canonical_name(), canonical);
|
||||
assert_eq!(tool.name(ToolVocabulary::Fabro), canonical);
|
||||
assert_eq!(tool.name(ToolVocabulary::Claude5), claude5);
|
||||
assert_eq!(NativeTool::from_any_name(canonical), Some(tool));
|
||||
assert_eq!(NativeTool::from_any_name(claude5), Some(tool));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ impl AgentProfile for Claude5Profile {
|
|||
"has_agent",
|
||||
self.base
|
||||
.registry
|
||||
.get_native(NativeTool::ClaudeAgent)
|
||||
.get_native(NativeTool::BackgroundAgent)
|
||||
.is_some(),
|
||||
)
|
||||
.with_bool(
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ pub(crate) fn make_agent_tool(
|
|||
) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: definition(
|
||||
NativeTool::ClaudeAgent,
|
||||
NativeTool::BackgroundAgent,
|
||||
"Launch a child agent for an independent task. Agents run in the background by \
|
||||
default and notify the parent when they finish. Set run_in_background to false to \
|
||||
wait for the result synchronously.",
|
||||
|
|
@ -281,7 +281,7 @@ fn finished_output(
|
|||
pub(crate) fn make_task_output_tool(supervisor: SubAgentSupervisor) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: definition(
|
||||
NativeTool::TaskOutput,
|
||||
NativeTool::AgentOutput,
|
||||
"Get a background agent's current status or wait for its final output. Automatic \
|
||||
completion notifications make ordinary polling unnecessary.",
|
||||
serde_json::json!({
|
||||
|
|
@ -368,7 +368,7 @@ pub(crate) fn make_task_output_tool(supervisor: SubAgentSupervisor) -> Registere
|
|||
pub(crate) fn make_task_stop_tool(supervisor: SubAgentSupervisor) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: definition(
|
||||
NativeTool::TaskStop,
|
||||
NativeTool::StopAgent,
|
||||
"Stop a running background agent by task ID.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
|
|
@ -401,7 +401,7 @@ pub(crate) fn make_task_stop_tool(supervisor: SubAgentSupervisor) -> RegisteredT
|
|||
pub(crate) fn make_send_message_tool(supervisor: SubAgentSupervisor) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: definition(
|
||||
NativeTool::SendMessage,
|
||||
NativeTool::MessageAgent,
|
||||
"Send additional instructions to a running background agent by its task ID.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue