Pin Agent-Native as a submodule so the module builds without the multi-repo sibling path. Co-authored-by: Cursor <cursoragent@cursor.com>
611 lines
18 KiB
Go
611 lines
18 KiB
Go
// Package cli 提供 agent-squad 命令行:serve / role / blueprint / run / runs / demo。
|
||
package cli
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"agent-native/runtime/llm"
|
||
"agent-squad/server"
|
||
"agent-squad/squad"
|
||
"agent-squad/store"
|
||
)
|
||
|
||
// Main CLI 入口;返回进程退出码。
|
||
func Main(args []string) int {
|
||
if len(args) == 0 {
|
||
usage()
|
||
return 2
|
||
}
|
||
switch args[0] {
|
||
case "serve":
|
||
return cmdServe(args[1:])
|
||
case "role":
|
||
return cmdRole(args[1:])
|
||
case "blueprint":
|
||
return cmdBlueprint(args[1:])
|
||
case "run":
|
||
return cmdRun(args[1:])
|
||
case "runs":
|
||
return cmdRuns(args[1:])
|
||
case "demo":
|
||
return cmdDemo(args[1:])
|
||
case "help", "-h", "--help":
|
||
usage()
|
||
return 0
|
||
default:
|
||
fmt.Fprintf(os.Stderr, "未知命令 %q\n\n", args[0])
|
||
usage()
|
||
return 2
|
||
}
|
||
}
|
||
|
||
func usage() {
|
||
fmt.Print(`agent-squad — 通用智能体小队编排底座
|
||
|
||
用法:
|
||
agent-squad serve [--addr :8090] [--data DIR] [--planner rule|llm] [--model M]
|
||
agent-squad role list|add|rm|recommend|derive ...
|
||
agent-squad blueprint list|add|rm ...
|
||
agent-squad run "目标文本" [--task-key K] [--backend B] [--materials DIR] [--hitl-mode interactive|auto|disabled]
|
||
agent-squad runs list|inspect|pending|respond|blackboard ...
|
||
agent-squad demo [--keep]
|
||
|
||
环境变量:
|
||
AGENT_SQUAD_DATA 数据目录(默认 ./agent-squad-data;role/blueprint 子命令也读它)
|
||
AGENT_SQUAD_LLM_BASE_URL OpenAI 兼容端点(agent-native 后端与 llm 规划器)
|
||
AGENT_SQUAD_LLM_API_KEY API key
|
||
AGENT_SQUAD_LLM_MODEL 默认模型
|
||
`)
|
||
}
|
||
|
||
// app 一次 CLI 执行的组装根。
|
||
type app struct {
|
||
engine *squad.SquadEngine
|
||
sink *squad.FanoutSink
|
||
files *store.FileEventSink
|
||
st *store.FileStore
|
||
}
|
||
|
||
func dataDir() string {
|
||
if v := os.Getenv("AGENT_SQUAD_DATA"); v != "" {
|
||
return v
|
||
}
|
||
return "./agent-squad-data"
|
||
}
|
||
|
||
// assemble 组装引擎;planner=llm 且未配置模型时回退 rule 并提示。
|
||
func assemble(dir, plannerName, modelOverride string) (*app, error) {
|
||
st, err := store.New(dir)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
reg := squad.NewRoleRegistry()
|
||
if err := st.LoadRoles(reg); err != nil {
|
||
return nil, err
|
||
}
|
||
bps := squad.NewBlueprintRegistry()
|
||
if err := st.LoadBlueprints(bps); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
backends := squad.NewBackendResolver()
|
||
baseURL := os.Getenv("AGENT_SQUAD_LLM_BASE_URL")
|
||
apiKey := os.Getenv("AGENT_SQUAD_LLM_API_KEY")
|
||
model := first(modelOverride, os.Getenv("AGENT_SQUAD_LLM_MODEL"))
|
||
planner := squad.Planner(squad.NewRulePlanner())
|
||
if plannerName == "llm" {
|
||
if baseURL == "" || apiKey == "" || model == "" {
|
||
fmt.Fprintln(os.Stderr, "提示:llm 规划器需要 AGENT_SQUAD_LLM_BASE_URL/API_KEY/MODEL,回退 rule 规划器")
|
||
} else {
|
||
client := &llm.Client{BaseURL: baseURL, APIKey: apiKey}
|
||
planner = squad.NewLLMPlanner(client, model)
|
||
}
|
||
}
|
||
// 真实智能体执行一律经 agent-native 自研内核(角色 backend=agent-native)。
|
||
if baseURL != "" && apiKey != "" && model != "" {
|
||
backends.RegisterAgentNative(&llm.Client{BaseURL: baseURL, APIKey: apiKey}, model, 12, 10*time.Minute)
|
||
}
|
||
|
||
// 事件双层:FileEventSink 落盘(seq+脱敏,重启可审计)→ FanoutSink(SSE 实时/重放)。
|
||
fanout := squad.NewFanoutSink(8192)
|
||
fileSink := store.NewFileEventSink(dir, fanout)
|
||
engine, err := squad.NewEngine(squad.Options{
|
||
DataDir: dir, Registry: reg, Blueprints: bps, Planner: planner,
|
||
Backends: backends, Sink: fileSink,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &app{engine: engine, sink: fanout, files: fileSink, st: st}, nil
|
||
}
|
||
|
||
func first(vals ...string) string {
|
||
for _, v := range vals {
|
||
if v != "" {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// ---- serve ----
|
||
|
||
func cmdServe(args []string) int {
|
||
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
|
||
addr := fs.String("addr", ":8090", "监听地址")
|
||
data := fs.String("data", dataDir(), "数据目录")
|
||
planner := fs.String("planner", "rule", "规划器:rule|llm")
|
||
model := fs.String("model", "", "默认模型(llm 规划器/后端)")
|
||
if err := fs.Parse(args); err != nil {
|
||
return 2
|
||
}
|
||
a, err := assemble(*data, *planner, *model)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "组装失败:", err)
|
||
return 1
|
||
}
|
||
srv := server.New(a.engine, a.sink, a.st)
|
||
stopSweep := srv.StartWakeupSweeper(15 * time.Second)
|
||
defer stopSweep()
|
||
fmt.Printf("agent-squad 服务启动 http://localhost%s (data=%s, roles=%d, blueprints=%d, wakeup-sweep=15s)\n",
|
||
*addr, *data, len(a.engine.Registry().List()), len(a.engine.Blueprints().List()))
|
||
return httpListen(*addr, srv.Handler())
|
||
}
|
||
|
||
func httpListen(addr string, h http.Handler) int {
|
||
if err := http.ListenAndServe(addr, h); err != nil {
|
||
fmt.Fprintln(os.Stderr, "服务退出:", err)
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// ---- role ----
|
||
|
||
func cmdRole(args []string) int {
|
||
if len(args) == 0 {
|
||
fmt.Println("用法: agent-squad role list|add|rm|recommend|derive ...")
|
||
return 2
|
||
}
|
||
data := dataDir()
|
||
a, err := assemble(data, "rule", "")
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "组装失败:", err)
|
||
return 1
|
||
}
|
||
switch args[0] {
|
||
case "list":
|
||
roles := a.engine.Registry().List()
|
||
fmt.Printf("%-20s %-24s %-10s %-16s %s\n", "ROLE", "NAME", "SOURCE", "BACKEND", "MODEL")
|
||
for _, r := range roles {
|
||
fmt.Printf("%-20s %-24s %-10s %-16s %s\n", r.Role, r.Name, r.Source, r.Backend, r.Model)
|
||
}
|
||
if len(roles) == 0 {
|
||
fmt.Println("(空注册表:通过 `agent-squad role add` 注册角色)")
|
||
}
|
||
return 0
|
||
case "add":
|
||
fs := flag.NewFlagSet("role add", flag.ContinueOnError)
|
||
f := fs.String("f", "", "角色 JSON 文件")
|
||
role := fs.String("role", "", "角色 key")
|
||
name := fs.String("name", "", "角色名")
|
||
backend := fs.String("backend", "stub", "绑定智能体:stub|agent-native|pi-agent|…(真实执行用 agent-native)")
|
||
model := fs.String("model", "", "角色模型")
|
||
keywords := fs.String("keywords", "", "推荐关键词(逗号分隔)")
|
||
instrFile := fs.String("instructions-file", "", "指令文件路径")
|
||
if err := fs.Parse(args[1:]); err != nil {
|
||
return 2
|
||
}
|
||
var spec squad.RoleSpec
|
||
if *f != "" {
|
||
data, err := os.ReadFile(*f)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
if err := json.Unmarshal(data, &spec); err != nil {
|
||
fmt.Fprintln(os.Stderr, "解析角色 JSON 失败:", err)
|
||
return 1
|
||
}
|
||
}
|
||
if *role != "" {
|
||
spec.Role = *role
|
||
}
|
||
if *name != "" {
|
||
spec.Name = *name
|
||
}
|
||
if *backend != "" {
|
||
spec.Backend = *backend
|
||
}
|
||
if *model != "" {
|
||
spec.Model = *model
|
||
}
|
||
if *keywords != "" {
|
||
for _, k := range strings.Split(*keywords, ",") {
|
||
if k = strings.TrimSpace(k); k != "" {
|
||
spec.Keywords = append(spec.Keywords, k)
|
||
}
|
||
}
|
||
}
|
||
if *instrFile != "" {
|
||
spec.InstructionsFile = *instrFile
|
||
}
|
||
if err := a.engine.Registry().Register(spec); err != nil {
|
||
fmt.Fprintln(os.Stderr, "注册失败:", err)
|
||
return 1
|
||
}
|
||
fmt.Printf("已注册角色 %s(backend=%s model=%s)→ %s\n", spec.Role, spec.Backend, spec.Model,
|
||
filepath.Join(data, "roles.d", spec.Role+".json"))
|
||
return 0
|
||
case "rm":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad role rm <key>")
|
||
return 2
|
||
}
|
||
if !a.engine.Registry().Remove(args[1]) {
|
||
fmt.Fprintf(os.Stderr, "角色 %s 不存在\n", args[1])
|
||
return 1
|
||
}
|
||
a.st.DeleteRole(args[1])
|
||
fmt.Println("已删除", args[1])
|
||
return 0
|
||
case "recommend":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad role recommend <查询文本>")
|
||
return 2
|
||
}
|
||
spec, ok := a.engine.Registry().Recommend(strings.Join(args[1:], " "))
|
||
if !ok {
|
||
fmt.Println("无匹配角色")
|
||
return 1
|
||
}
|
||
fmt.Printf("%-20s %s\n", spec.Role, spec.Name)
|
||
return 0
|
||
case "derive":
|
||
if len(args) < 2 {
|
||
fmt.Println(`用法: agent-squad role derive '{"role_key":"gmp_expert","base_role":"engineer","domain_topic":"GMP"}'`)
|
||
return 2
|
||
}
|
||
var req squad.DeriveRequest
|
||
if err := json.Unmarshal([]byte(args[1]), &req); err != nil {
|
||
fmt.Fprintln(os.Stderr, "解析派生请求 JSON 失败:", err)
|
||
return 1
|
||
}
|
||
spec, err := a.engine.Registry().Derive(req)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "派生失败:", err)
|
||
return 1
|
||
}
|
||
fmt.Printf("已派生角色 %s(基线 %s)\n", spec.Role, spec.BaseRole)
|
||
return 0
|
||
default:
|
||
fmt.Println("未知子命令", args[0])
|
||
return 2
|
||
}
|
||
}
|
||
|
||
// ---- blueprint ----
|
||
|
||
func cmdBlueprint(args []string) int {
|
||
if len(args) == 0 {
|
||
fmt.Println("用法: agent-squad blueprint list|add|rm ...")
|
||
return 2
|
||
}
|
||
a, err := assemble(dataDir(), "rule", "")
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "组装失败:", err)
|
||
return 1
|
||
}
|
||
switch args[0] {
|
||
case "list":
|
||
for _, bp := range a.engine.Blueprints().List() {
|
||
fmt.Printf("%-24s %2d 步 %s\n", bp.Key, len(bp.Steps), bp.Title)
|
||
}
|
||
return 0
|
||
case "add":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad blueprint add <file.json>")
|
||
return 2
|
||
}
|
||
data, err := os.ReadFile(args[1])
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
var bp squad.Blueprint
|
||
if err := json.Unmarshal(data, &bp); err != nil {
|
||
fmt.Fprintln(os.Stderr, "解析蓝图 JSON 失败:", err)
|
||
return 1
|
||
}
|
||
if err := a.engine.Blueprints().Register(bp); err != nil {
|
||
fmt.Fprintln(os.Stderr, "注册失败:", err)
|
||
return 1
|
||
}
|
||
fmt.Printf("已注册蓝图 %s(%d 步)\n", bp.Key, len(bp.Steps))
|
||
return 0
|
||
case "rm":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad blueprint rm <key>")
|
||
return 2
|
||
}
|
||
if !a.engine.Blueprints().Remove(args[1]) {
|
||
fmt.Fprintf(os.Stderr, "蓝图 %s 不存在\n", args[1])
|
||
return 1
|
||
}
|
||
a.st.DeleteBlueprint(args[1])
|
||
fmt.Println("已删除", args[1])
|
||
return 0
|
||
default:
|
||
fmt.Println("未知子命令", args[0])
|
||
return 2
|
||
}
|
||
}
|
||
|
||
// ---- run(终端交互式 HITL)----
|
||
|
||
func cmdRun(args []string) int {
|
||
// 目标文本在前、flag 在后(标准 flag 包遇位置参数即停止解析,需手动切分)。
|
||
goalArgs, flagArgs := splitPositionalThenFlags(args)
|
||
fs := flag.NewFlagSet("run", flag.ContinueOnError)
|
||
taskKey := fs.String("task-key", "", "显式蓝图模板键")
|
||
backend := fs.String("backend", "", "运行级默认后端")
|
||
materialsDir := fs.String("materials", "", "输入材料目录")
|
||
hitlMode := fs.String("hitl-mode", "interactive", "interactive|auto|disabled")
|
||
planner := fs.String("planner", "rule", "rule|llm")
|
||
model := fs.String("model", "", "默认模型")
|
||
timeout := fs.Duration("timeout", 30*time.Minute, "总超时")
|
||
mode := fs.String("mode", "standard", "编排模式:standard|leader(leader 委派循环)")
|
||
leaderRole := fs.String("leader-role", "", "leader 模式的 leader 角色 key")
|
||
maxTurns := fs.Int("max-leader-turns", 8, "leader 模式轮次上限")
|
||
if err := fs.Parse(flagArgs); err != nil {
|
||
return 2
|
||
}
|
||
goalText := strings.Join(goalArgs, " ")
|
||
if goalText == "" {
|
||
fmt.Println("用法: agent-squad run \"目标文本\" [flags]")
|
||
return 2
|
||
}
|
||
a, err := assemble(dataDir(), *planner, *model)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "组装失败:", err)
|
||
return 1
|
||
}
|
||
goal := squad.Goal{Text: goalText, HitlMode: squad.HitlMode(*hitlMode)}
|
||
if *materialsDir != "" {
|
||
ents, err := os.ReadDir(*materialsDir)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "读取材料目录失败:", err)
|
||
return 1
|
||
}
|
||
for _, ent := range ents {
|
||
if ent.IsDir() {
|
||
continue
|
||
}
|
||
goal.Materials = append(goal.Materials, squad.MaterialFile{
|
||
Filename: ent.Name(), Path: filepath.Join(*materialsDir, ent.Name()),
|
||
})
|
||
}
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||
defer cancel()
|
||
res, err := a.engine.Run(ctx, goal, squad.RunOptions{
|
||
TaskKey: *taskKey, Backend: *backend, Mode: *mode,
|
||
LeaderRole: *leaderRole, MaxLeaderTurns: *maxTurns,
|
||
})
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "运行失败:", err)
|
||
return 1
|
||
}
|
||
reader := bufio.NewReader(os.Stdin)
|
||
for err == nil && res.Status.IsHeld() && res.Pending != nil {
|
||
printPending(res.Pending)
|
||
decision := readDecision(reader, res.Pending)
|
||
res, err = a.engine.Respond(ctx, res.RunID, decision)
|
||
}
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "运行失败:", err)
|
||
return 1
|
||
}
|
||
printResult(res)
|
||
if res.Status == squad.RunFailed {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func printPending(req *squad.HumanRequest) {
|
||
fmt.Println("\n⏸ 需要人工输入 —", req.Question)
|
||
if req.Context != "" {
|
||
for _, line := range strings.Split(req.Context, "\n") {
|
||
fmt.Println(" ", line)
|
||
}
|
||
}
|
||
for i, o := range req.Options {
|
||
fmt.Printf(" [%d] %s(%s)\n", i+1, o.Label, o.Value)
|
||
}
|
||
fmt.Println("输入选项序号/值,或直接输入自由文本回答:")
|
||
}
|
||
|
||
func readDecision(reader *bufio.Reader, req *squad.HumanRequest) squad.RespondDecision {
|
||
for attempt := 0; attempt < 3; attempt++ {
|
||
line, _ := reader.ReadString('\n')
|
||
line = strings.TrimSpace(line)
|
||
if line == "" && len(req.Options) > 0 {
|
||
return squad.RespondDecision{RequestID: req.ID, Choice: req.Options[0].Value}
|
||
}
|
||
// 选项序号匹配。
|
||
if idx, err := strconv.Atoi(line); err == nil && idx >= 1 && idx <= len(req.Options) {
|
||
return squad.RespondDecision{RequestID: req.ID, Choice: req.Options[idx-1].Value}
|
||
}
|
||
// 选项值匹配。
|
||
for _, o := range req.Options {
|
||
if strings.EqualFold(o.Value, line) {
|
||
return squad.RespondDecision{RequestID: req.ID, Choice: o.Value}
|
||
}
|
||
}
|
||
// 带选项的请求不接受自由文本(防误触 abort);两类例外:纯文本请求
|
||
// 直接作为回答;澄清类请求自由文本即答案(choice 留空 = 继续并采纳)。
|
||
if len(req.Options) == 0 || req.Kind == squad.HitlClarification {
|
||
return squad.RespondDecision{RequestID: req.ID, Answer: line}
|
||
}
|
||
fmt.Println(" 无法识别,请输入选项序号或选项值:")
|
||
}
|
||
choice := "abort"
|
||
if len(req.Options) > 0 {
|
||
choice = req.Options[len(req.Options)-1].Value
|
||
}
|
||
return squad.RespondDecision{RequestID: req.ID, Choice: choice}
|
||
}
|
||
|
||
func printResult(res *squad.RunResult) {
|
||
fmt.Printf("\n▶ 运行 %s 终态:%s\n", res.RunID, res.Status)
|
||
if res.Summary != "" {
|
||
fmt.Println(" ", res.Summary)
|
||
}
|
||
if res.Error != "" {
|
||
fmt.Println(" 错误:", res.Error)
|
||
}
|
||
if res.Deliverable != "" && res.WorkDir != "" {
|
||
fmt.Printf(" 交付物: %s\n", filepath.Join(res.WorkDir, res.Deliverable))
|
||
}
|
||
if res.Quality != nil {
|
||
fmt.Printf(" 质检: passed=%v score=%.0f manual=%v\n", res.Quality.Passed, res.Quality.Score, res.Quality.ManualPass)
|
||
}
|
||
if res.WorkDir != "" {
|
||
fmt.Println(" 工作区:", res.WorkDir)
|
||
}
|
||
}
|
||
|
||
// ---- runs ----
|
||
|
||
func cmdRuns(args []string) int {
|
||
if len(args) == 0 {
|
||
fmt.Println("用法: agent-squad runs list|inspect|pending|respond|blackboard ...")
|
||
return 2
|
||
}
|
||
a, err := assemble(dataDir(), "rule", "")
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "组装失败:", err)
|
||
return 1
|
||
}
|
||
ctx := context.Background()
|
||
switch args[0] {
|
||
case "list":
|
||
runs, err := a.engine.ListRuns()
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
for _, m := range runs {
|
||
fmt.Printf("%-32s %-22s %s\n", m.ID, m.Status, truncateOne(m.Goal.Text, 48))
|
||
}
|
||
return 0
|
||
case "inspect":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad runs inspect <run_id>")
|
||
return 2
|
||
}
|
||
res, err := a.engine.Inspect(args[1])
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
enc, _ := json.MarshalIndent(res, "", " ")
|
||
fmt.Println(string(enc))
|
||
return 0
|
||
case "pending":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad runs pending <run_id>")
|
||
return 2
|
||
}
|
||
req, _ := a.engine.Pending(args[1])
|
||
if req == nil {
|
||
fmt.Println("(无挂起请求)")
|
||
return 0
|
||
}
|
||
enc, _ := json.MarshalIndent(req, "", " ")
|
||
fmt.Println(string(enc))
|
||
return 0
|
||
case "respond":
|
||
pos, flagArgs := splitPositionalThenFlags(args[1:])
|
||
fs := flag.NewFlagSet("runs respond", flag.ContinueOnError)
|
||
choice := fs.String("choice", "", "选项值:approve/reject/skip/abort/…")
|
||
answer := fs.String("answer", "", "自由文本回答")
|
||
reqID := fs.String("request-id", "", "请求 ID(多挂起时校验)")
|
||
if err := fs.Parse(flagArgs); err != nil {
|
||
return 2
|
||
}
|
||
if len(pos) < 1 {
|
||
fmt.Println("用法: agent-squad runs respond <run_id> --choice approve [--answer …]")
|
||
return 2
|
||
}
|
||
res, err := a.engine.Respond(ctx, pos[0], squad.RespondDecision{
|
||
RequestID: *reqID, Choice: *choice, Answer: *answer,
|
||
})
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
printResult(res)
|
||
return 0
|
||
case "steer":
|
||
if len(args) < 3 {
|
||
fmt.Println("用法: agent-squad runs steer <run_id> \"补充指令文本\"")
|
||
return 2
|
||
}
|
||
if err := a.engine.Steer(args[1], strings.Join(args[2:], " ")); err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
fmt.Println("已加入补充指令(下一次步骤边界注入当前运行)")
|
||
return 0
|
||
case "blackboard":
|
||
if len(args) < 2 {
|
||
fmt.Println("用法: agent-squad runs blackboard <run_id>")
|
||
return 2
|
||
}
|
||
bb, err := a.engine.Blackboard(args[1])
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
enc, _ := json.MarshalIndent(bb, "", " ")
|
||
fmt.Println(string(enc))
|
||
return 0
|
||
default:
|
||
fmt.Println("未知子命令", args[0])
|
||
return 2
|
||
}
|
||
}
|
||
|
||
func truncateOne(s string, n int) string {
|
||
r := []rune(s)
|
||
if len(r) <= n {
|
||
return s
|
||
}
|
||
return string(r[:n]) + "…"
|
||
}
|
||
|
||
// splitPositionalThenFlags 把参数切为前缀位置参数与其后的 flag 段
|
||
// (标准 flag 包遇到首个位置参数即停止解析,目标文本须先取出)。
|
||
// 负数不是本 CLI 的合法位置参数,直接按 "-" 前缀判定。
|
||
func splitPositionalThenFlags(args []string) (positional, flags []string) {
|
||
for i, a := range args {
|
||
if strings.HasPrefix(a, "-") {
|
||
return append([]string{}, args[:i]...), append([]string{}, args[i:]...)
|
||
}
|
||
}
|
||
return append([]string{}, args...), nil
|
||
}
|