执行方案阶段 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 旋钮管道)。
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
package orchestration
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
|
)
|
|
|
|
// SubAgentResult is a point-in-time snapshot of a SubAgent. It is used both as
|
|
// the per-sub-agent entry in MergedResult and as the return value of
|
|
// (*SubAgent).Snapshot for monitoring.
|
|
type SubAgentResult struct {
|
|
// ID is the sub-agent id.
|
|
ID string
|
|
// Goal is the sub-agent's objective.
|
|
Goal string
|
|
// RoleID is the bound role/persona.
|
|
RoleID string
|
|
// PersonaID is the target role the sub-agent runs as (blank = the spawning
|
|
// turn's role), whose instructions and memory scope its run used (design/22 §8-2).
|
|
PersonaID string
|
|
// Mode is the bound behavior mode.
|
|
Mode string
|
|
// AttachInfo is the supplied attachment text.
|
|
AttachInfo string
|
|
// Status is the terminal (or current) status.
|
|
Status Status
|
|
// Summary is the execution summary written on finalization.
|
|
Summary string
|
|
// Text is the loop result text on a successful run.
|
|
Text string
|
|
// Steps is the number of loop steps executed on a successful run.
|
|
Steps int
|
|
// Err is the error that ended a failed/aborted run (nil on success).
|
|
Err error
|
|
// StartedAt is when the sub-agent began running.
|
|
StartedAt time.Time
|
|
// FinishedAt is when the sub-agent finalized.
|
|
FinishedAt time.Time
|
|
}
|
|
|
|
// MergedResult aggregates the outcomes of every spawned sub-agent, produced by
|
|
// (*Scheduler).Join.
|
|
type MergedResult struct {
|
|
// Agents holds one entry per spawned sub-agent, in spawn order.
|
|
Agents []SubAgentResult
|
|
// Total is the number of spawned sub-agents.
|
|
Total int
|
|
// Succeeded is how many sub-agents finished with StatusDone.
|
|
Succeeded int
|
|
// Failed is how many sub-agents finished with StatusFailed.
|
|
Failed int
|
|
// Aborted is how many sub-agents finished with StatusAborted.
|
|
Aborted int
|
|
// Summary is a one-line aggregate summary of all sub-agents.
|
|
Summary string
|
|
}
|
|
|
|
// summaryTextMax caps how many runes of the result text appear in a summary.
|
|
const summaryTextMax = 80
|
|
|
|
// doneSummary builds the execution summary written when a sub-agent succeeds.
|
|
func doneSummary(res *loop.Result) string {
|
|
if res == nil {
|
|
return "完成"
|
|
}
|
|
text := compactText(res.Text, summaryTextMax)
|
|
if res.StopReason != "" {
|
|
return fmt.Sprintf("完成 (%s, 步骤=%d): %s", res.StopReason, res.Steps, text)
|
|
}
|
|
return fmt.Sprintf("完成 (步骤=%d): %s", res.Steps, text)
|
|
}
|
|
|
|
// failedSummary builds the summary written when a sub-agent fails.
|
|
func failedSummary(err error) string {
|
|
if err == nil {
|
|
return "失败"
|
|
}
|
|
return "失败: " + err.Error()
|
|
}
|
|
|
|
// canceledSummary builds the summary written when a sub-agent is aborted.
|
|
func canceledSummary() string {
|
|
return "中止: 已取消"
|
|
}
|
|
|
|
// timeoutSummary builds the summary written when a sub-agent times out.
|
|
func timeoutSummary(d time.Duration) string {
|
|
return fmt.Sprintf("中止: 超时 (%s)", d)
|
|
}
|
|
|
|
// timeoutSummaryFor 是按子代理生效时限渲染的超时摘要:无期限(0)时不再
|
|
// 显示误导性的 "(0s)",只说明超时本身。
|
|
func timeoutSummaryFor(d time.Duration) string {
|
|
if d <= 0 {
|
|
return "中止: 超时"
|
|
}
|
|
return timeoutSummary(d)
|
|
}
|
|
|
|
// summarizeMerged produces the one-line aggregate summary of a merged result.
|
|
func summarizeMerged(r *MergedResult) string {
|
|
return fmt.Sprintf("子代理汇总: 共 %d 个, %d 成功, %d 失败, %d 中止",
|
|
r.Total, r.Succeeded, r.Failed, r.Aborted)
|
|
}
|