Files
beaver_project/app-instance/backend/nanobot/skills/subagent-manager/scripts/subagentctl.py
steven_li 29dfd14aa6 ```
feat(agent): 添加对持久化子智能体的支持并增强委派管理

添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。
新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。

同时增强了委派管理器的功能:
- 添加了对本地委派、插件委派和本地回退的开关控制
- 实现了持久化子智能体任务的自动检测和本地执行保护
- 增加了对不同委派类型的权限验证机制

修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。

BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。
```
2026-03-27 10:15:35 +08:00

213 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Manage persistent local sub-agents."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
from typing import Any
ROOT = Path(__file__).resolve().parents[4]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from nanobot.agent.subagents import LocalSubagentStore, SubagentSpec
from nanobot.config.loader import load_config
def _store():
config = load_config()
return config, LocalSubagentStore(config.workspace_path)
def _print_json(payload: Any) -> None:
print(json.dumps(payload, indent=2, ensure_ascii=False))
def _load_spec_or_die(store: LocalSubagentStore, agent_id: str) -> SubagentSpec:
spec = store.get_subagent(agent_id)
if spec is None:
raise SystemExit(f"Sub-agent not found: {agent_id}")
return spec
def _parse_key_values(items: list[str]) -> dict[str, str]:
result: dict[str, str] = {}
for item in items:
if "=" not in item:
raise SystemExit(f"Expected KEY=VALUE, got: {item}")
key, value = item.split("=", 1)
key = key.strip()
if not key:
raise SystemExit(f"Invalid empty key in: {item}")
result[key] = value
return result
def cmd_list(_: argparse.Namespace) -> None:
_, store = _store()
_print_json([spec.to_dict() for spec in store.list_subagents()])
def cmd_show(args: argparse.Namespace) -> None:
_, store = _store()
spec = _load_spec_or_die(store, args.agent_id)
_print_json(spec.to_dict())
def cmd_create(args: argparse.Namespace) -> None:
config, store = _store()
current = store.get_subagent(args.agent_id)
payload = current.to_dict() if current is not None else {"id": args.agent_id}
payload.update({
"id": args.agent_id,
"name": args.name or payload.get("name") or args.agent_id,
"description": args.description or payload.get("description") or args.name or args.agent_id,
"enabled": not args.disabled,
"delegation_mode": payload.get("delegation_mode") or "remote_a2a_only",
})
if args.system_prompt:
payload["system_prompt"] = args.system_prompt
if args.model:
payload["model"] = args.model
spec = store.upsert_subagent(payload, config)
_print_json(spec.to_dict())
def cmd_delete(args: argparse.Namespace) -> None:
_, store = _store()
deleted = store.delete_subagent(args.agent_id)
if not deleted:
raise SystemExit(f"Sub-agent not found: {args.agent_id}")
_print_json({"ok": True, "id": args.agent_id})
def cmd_set_system_prompt(args: argparse.Namespace) -> None:
config, store = _store()
spec = _load_spec_or_die(store, args.agent_id)
payload = spec.to_dict()
payload["system_prompt"] = args.text
updated = store.upsert_subagent(payload, config)
_print_json(updated.to_dict())
def cmd_add_mcp_http(args: argparse.Namespace) -> None:
config, store = _store()
spec = _load_spec_or_die(store, args.agent_id)
payload = spec.to_dict()
payload.setdefault("mcp_servers", {})
payload["mcp_servers"][args.server_id] = {
"url": args.url,
"headers": _parse_key_values(args.header),
"auth_mode": args.auth_mode,
"auth_audience": args.auth_audience,
"auth_scopes": list(args.auth_scope),
"tool_timeout": args.tool_timeout,
"sensitive": args.sensitive,
}
updated = store.upsert_subagent(payload, config)
_print_json(updated.to_dict())
def cmd_add_mcp_stdio(args: argparse.Namespace) -> None:
config, store = _store()
spec = _load_spec_or_die(store, args.agent_id)
payload = spec.to_dict()
payload.setdefault("mcp_servers", {})
payload["mcp_servers"][args.server_id] = {
"command": args.command,
"args": list(args.arg),
"env": _parse_key_values(args.env),
"auth_mode": args.auth_mode,
"auth_audience": args.auth_audience,
"auth_scopes": list(args.auth_scope),
"tool_timeout": args.tool_timeout,
"sensitive": args.sensitive,
}
updated = store.upsert_subagent(payload, config)
_print_json(updated.to_dict())
def cmd_remove_mcp(args: argparse.Namespace) -> None:
config, store = _store()
spec = _load_spec_or_die(store, args.agent_id)
payload = spec.to_dict()
mcp_servers = payload.setdefault("mcp_servers", {})
mcp_servers.pop(args.server_id, None)
updated = store.upsert_subagent(payload, config)
_print_json(updated.to_dict())
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Manage persistent local sub-agents")
sub = parser.add_subparsers(dest="command", required=True)
list_parser = sub.add_parser("list", help="List sub-agents")
list_parser.set_defaults(func=cmd_list)
show_parser = sub.add_parser("show", help="Show one sub-agent")
show_parser.add_argument("agent_id")
show_parser.set_defaults(func=cmd_show)
create_parser = sub.add_parser("create", help="Create or update a sub-agent")
create_parser.add_argument("--id", dest="agent_id", required=True)
create_parser.add_argument("--name", default="")
create_parser.add_argument("--description", default="")
create_parser.add_argument("--system-prompt", default="")
create_parser.add_argument("--model", default="")
create_parser.add_argument("--disabled", action="store_true")
create_parser.set_defaults(func=cmd_create)
delete_parser = sub.add_parser("delete", help="Delete a sub-agent")
delete_parser.add_argument("agent_id")
delete_parser.set_defaults(func=cmd_delete)
prompt_parser = sub.add_parser("set-system-prompt", help="Update the system prompt")
prompt_parser.add_argument("agent_id")
prompt_parser.add_argument("--text", required=True)
prompt_parser.set_defaults(func=cmd_set_system_prompt)
http_parser = sub.add_parser("add-mcp-http", help="Add an HTTP MCP server")
http_parser.add_argument("agent_id")
http_parser.add_argument("--server-id", required=True)
http_parser.add_argument("--url", required=True)
http_parser.add_argument("--header", action="append", default=[])
http_parser.add_argument("--auth-mode", default="none")
http_parser.add_argument("--auth-audience", default="")
http_parser.add_argument("--auth-scope", action="append", default=[])
http_parser.add_argument("--tool-timeout", type=int, default=30)
http_parser.add_argument("--sensitive", action="store_true")
http_parser.set_defaults(func=cmd_add_mcp_http)
stdio_parser = sub.add_parser("add-mcp-stdio", help="Add a stdio MCP server")
stdio_parser.add_argument("agent_id")
stdio_parser.add_argument("--server-id", required=True)
stdio_parser.add_argument("--command", required=True)
stdio_parser.add_argument("--arg", action="append", default=[])
stdio_parser.add_argument("--env", action="append", default=[])
stdio_parser.add_argument("--auth-mode", default="none")
stdio_parser.add_argument("--auth-audience", default="")
stdio_parser.add_argument("--auth-scope", action="append", default=[])
stdio_parser.add_argument("--tool-timeout", type=int, default=30)
stdio_parser.add_argument("--sensitive", action="store_true")
stdio_parser.set_defaults(func=cmd_add_mcp_stdio)
remove_mcp = sub.add_parser("remove-mcp", help="Remove an MCP server")
remove_mcp.add_argument("agent_id")
remove_mcp.add_argument("--server-id", required=True)
remove_mcp.set_defaults(func=cmd_remove_mcp)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()