Files
Cyrene_v2/cmd/cyrene/subagent_tool.go
T
AskaEth 7e7f53bb41
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(orchestration,loop): 子代理现在真的收到任务(P0)+ 工具白名单 + 超时可配(裁决 02.4.2/02.4.1/02.4.4)
执行方案阶段 2 · P2-1。**02.4.2 是最伤的一条 P0**:`spawntool` 从不填 `AgentSpec.Context`、
`scheduler` 把 nil 交给 loop ⇒ 子代理模型上下文 = 角色 system + 记忆召回 + **0 条消息**,
goal/attach_info/data 一个字都到不了模型(此前测试用 mock adapter 恒答固定文本,测不出)。

- **02.4.2**:新增 `taskContext/taskUserMessage`(`spec.go`)把 **goal 正文 + attach_info + data
  渲染成同一条 user 消息**(`[任务目标]/[附加信息]/[任务数据]` 独立段落);`Spawn()` 设
  `Context: taskContext(spec)`;**system 只保留角色指令**,任务绝不进 system。
  真断言(捕获请求体的 adapter + 真 loop/agent):首请求恰好 **1 条 user 消息**、同时含 goal 与
  attach_info,且**所有 system 消息不含 goal 文本**。
- **02.4.1**:`AgentSpec.ToolWhitelist`(nil/空 = 继承全量);schema 增 `tool_whitelist`
  (顶层 + 每 task 条目,task 未带时继承批次级);`scheduler.run()` 经 ctx 传递 +
  `scopedRunner` 派生;`loop/options.go` **纯增量(+75 / −0)**:`WithToolWhitelist`/
  `ToolWhitelistFrom` + `(*Loop).WithToolWhitelist` 派生副本(浅拷贝仅替换 `hooks.CallAdmit`,
  链式保留原钩子),名单外调用否决为**软结果**回灌模型。
- **02.4.4**:`SUBAGENT_TIMEOUT_SEC`(默认 600)入 `internal/config` + `.env.example`;
  优先级 = 按调用 `timeout_seconds`(1s–24h 校验)> `WithSpawnTimeout`(生产由 cfg 填充)>
  调度器 `PerAgentTimeout`(`wire*.go` 仍是 10min 常量,未动);迟到 span 续根由测试锁定。

验证:**全树 `go build`/`go vet`/`go test ./... -count=1` = 58 包 ok / 0 FAIL**;
`GOOS=linux/amd64` + `GOOS=darwin/arm64` 交叉编译绿;前端 `tsc --noEmit` + `npm run build` 绿;
本批文件全部 ≤500 行、函数 ≤80 行。
变异验证(四条,均带还原证明):① `Context` 改回 `cloneMessages(spec.Context)` →
「1 条 user 消息」断言 FAIL(`user messages = 0`,即 P0 症状);② `WithToolWhitelist` 原样返回 →
显式白名单测试 FAIL(`alpha executed 1 times despite not being whitelisted`);
③ `subCtx` 改回全局默认 → 3 个超时测试 FAIL;④ `foldSpanFinish` 丢掉 `SpanFinished` →
迟到 span 状态断言 FAIL。

⚠️ 已知缺口(**登记,未假装已解决**):① 无真实模型端到端(全部捕获请求体 mock);
② 白名单**只挡执行、不过滤工具广告面**(模型仍能看见未列工具,调用即被拒并拿到可读原因;
过滤需改 `loop/request.go`);③ **迟到 span 的"新一轮已开始"窗口未修**(`loop/flow.go` 的
`StartRound` 会整体替换 `graphs[session]` ⇒ 新轮开始后迟到的 span 仍落进新轮图;需按轮键控图,
属后续);④ per-role 超时覆盖未实现(需 persona 旋钮管道)。
2026-10-04 11:30:39 +08:00

58 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
)
// defaultSubagentTimeout bounds one sub-agent's whole run when nothing else
// configures it. DevDocs/design/05-子代理.md §7 requires a timeout and an
// anti-runaway bound; the scheduler already implements both (PerAgentTimeout,
// Abort/CancelAll), so production must supply a value or the per-agent timeout
// stays inert (the scheduler's zero means "no timeout"). The number is
// deliberately generous — a sub-agent runs up to 8 tool rounds like any loop —
// and is the fallback behind SUBAGENT_TIMEOUT_SEC (裁决 02.4.4: 默认 10min,
// 可配置;subagentToolTimeout resolves the effective value).
const defaultSubagentTimeout = 10 * time.Minute
// subagentToolTimeout resolves the dispatch tool's default sub-agent timeout:
// SUBAGENT_TIMEOUT_SEC wins when positive, otherwise the 10-minute code default
// applies (design/05 §7 裁决 02.4.4). A per-dispatch timeout_seconds argument
// still overrides this per call at the tool layer.
func (a *app) subagentToolTimeout() time.Duration {
if a.cfg != nil && a.cfg.Subagent.TimeoutSec > 0 {
return time.Duration(a.cfg.Subagent.TimeoutSec) * time.Second
}
return defaultSubagentTimeout
}
// wireSubagentTool registers the sub-agent dispatch tool with the dispatcher, so
// the agent can finally reach the orchestration layer at runtime (before this,
// (*orchestration.Scheduler).Spawn had no production caller and design/05 was
// library-only).
//
// The capability is registered with its default reachability markers (both
// false), which keeps it owner-only: the design/11 §3 policy engine hides the
// schema from a group/stranger session and the loop's security gate rejects the
// call there. Design/05 §6/§7 give no group/stranger allowance for spawning
// background agents, and being able to spawn one is a heavy operation, so the
// safe default is chosen deliberately rather than enabled by accident.
func (a *app) wireSubagentTool() {
log := logger.WithSource(a.log, "cmd/cyrene")
if a.scheduler == nil {
log.Warn("sub-agent tool skipped: no scheduler")
return
}
spawner := orchestration.NewSpawnTool(a.scheduler,
orchestration.WithSpawnLogger(a.log),
orchestration.WithSpawnTimeout(a.subagentToolTimeout()))
if err := a.dispatcher.Register(spawner); err != nil {
log.Warn("sub-agent tool register skipped", "error", err)
return
}
log.Info("sub-agent tool registered", "tool", orchestration.SpawnToolName,
"owner_only", true)
}