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 旋钮管道)。
This commit is contained in:
1 parent
6e7ec56f9a
commit
7e7f53bb41
15 files changed
+974
-50
No files matched your search
@@ -176,3 +176,78 @@ type Options struct {
|
||||
// "runtime/loop" 的丢弃 logger。
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
// toolWhitelistKey 是每 Run 工具白名单的 context key(design/05 §7
|
||||
// 裁决 02.4.1)。编排层在驱动子代理 Run 之前挂载它。
|
||||
type toolWhitelistKey struct{}
|
||||
|
||||
// WithToolWhitelist 把该次运行的工具白名单挂到 ctx 上(design/05 §7):
|
||||
// nil/空名单原样返回 ctx,即「缺省全开」——继承主 agent 的全量工具集,
|
||||
// 与挂载前的行为逐字节一致。名单会被复制,调用方后续修改不影响已挂载值。
|
||||
func WithToolWhitelist(ctx context.Context, names []string) context.Context {
|
||||
if ctx == nil || len(names) == 0 {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, toolWhitelistKey{}, append([]string(nil), names...))
|
||||
}
|
||||
|
||||
// ToolWhitelistFrom 返回 WithToolWhitelist 挂到 ctx 上的白名单;
|
||||
// 未挂载或为空返回 nil,语义为「全开」。它是 WithToolWhitelist 的读侧,
|
||||
// 供拿到 context 的调用方(子代理 runner、测试)观察本次运行的能力面。
|
||||
func ToolWhitelistFrom(ctx context.Context) []string {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
names, _ := ctx.Value(toolWhitelistKey{}).([]string)
|
||||
return names
|
||||
}
|
||||
|
||||
// toolWhitelistAllows 报告 tool 是否在名单内。
|
||||
func toolWhitelistAllows(names []string, tool string) bool {
|
||||
for _, n := range names {
|
||||
if n == tool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// whitelistVetoReason 是白名单外调用被否决后回灌给模型的软提示,
|
||||
// 与 design/11 §3 安全门的 not_allowed 文案区分开:这是子代理的
|
||||
// 能力面收缩(design/05 §7),不是会话身份限制。
|
||||
func whitelistVetoReason(name string) string {
|
||||
return "not_allowed: tool " + name + " is not in this sub-agent's tool whitelist"
|
||||
}
|
||||
|
||||
// WithToolWhitelist 返回工具面被限定为 names 的 Loop 派生副本(design/05
|
||||
// §7 裁决 02.4.1:显式列出 = 只放行所列)。它是浅拷贝:除 hooks 外的所有
|
||||
// 依赖(agent、dispatcher、记忆、追踪)与原模板共享,因此并发 Run 契约
|
||||
// 与原 Loop 一致;唯一的差异是 CallAdmit 钩子——白名单外的调用被否决为
|
||||
// 携带原因的跳过结果回灌给模型(软结果,绝不 panic),白名单内的调用
|
||||
// 继续交给模板上已配置的钩子链(先跑原钩子,被原钩子拦截的保持拦截)。
|
||||
// 空/nil 名单返回原模板(全开)。编排层按子代理逐个派生,主会话的
|
||||
// Loop 不受影响。
|
||||
func (l *Loop) WithToolWhitelist(names []string) *Loop {
|
||||
if l == nil || len(names) == 0 {
|
||||
return l
|
||||
}
|
||||
cp := *l
|
||||
prev := l.hooks
|
||||
cp.hooks = ToolLoopHooks{
|
||||
BatchAdmit: prev.BatchAdmit,
|
||||
AfterCall: prev.AfterCall,
|
||||
AfterOutcome: prev.AfterOutcome,
|
||||
CallAdmit: func(ctx context.Context, call agent.ToolCall) *ToolCallVeto {
|
||||
if prev.CallAdmit != nil {
|
||||
if v := prev.CallAdmit(ctx, call); v != nil && v.Block {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if !toolWhitelistAllows(names, call.Name) {
|
||||
return &ToolCallVeto{Block: true, Reason: whitelistVetoReason(call.Name)}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
Reference in new issue
Block a user