Files
Cyrene_v2/internal/runtime/orchestration/spec.go
T
AskaEth eb1828ee7a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
2026-10-04 13:30:04 +08:00

231 lines
9.1 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 orchestration
import (
"encoding/json"
"errors"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// Status 是已派发子代理的生命周期状态。
//
// 生命周期为 pending -> running -> 终结态之一(done、failed、aborted)。
// 当且仅当 Terminal 报告 true 时,子代理处于终结态。
type Status string
const (
// StatusPending 表示子代理已创建但尚未开始。
StatusPending Status = "pending"
// StatusRunning 表示子代理的循环正在执行。
StatusRunning Status = "running"
// StatusDone 表示循环已完成并产出结果。
StatusDone Status = "done"
// StatusFailed 表示循环返回了错误。
StatusFailed Status = "failed"
// StatusAborted 表示子代理被取消或超时。
StatusAborted Status = "aborted"
)
// Terminal 报告 s 是否为终态。pending 与 running 不是终态;每个子代理都以三种终结态
// 之一收尾。
func (s Status) Terminal() bool {
return s == StatusDone || s == StatusFailed || s == StatusAborted
}
// AgentSpec 描述一个待派发的子代理。Goal 必填;其余字段可选,绑定到子代理上供上层
// 解读。
type AgentSpec struct {
// Goal 是子代理要达成的目标。必填。
Goal string
// RoleID 选择子代理绑定的角色/persona。
RoleID string
// PersonaID 是子代理实际充当的角色(design/22 §8-2):它以该角色的指令作答,
// 读写该角色的记忆作用域 ——「她以该角色身份临时替班」。Spawn 把它盖到子代理
// 运行的会话 env 上,而那是循环解析提示词、记忆命名空间、情绪与思考旋钮的唯一
// 输入,因此四者不可能互相矛盾。留空则逐字节保持既有行为:子代理继承派发回合的
// env,即父会话的角色。该 id 必须是本进程能以自身 agent 运行的角色
// ((*Scheduler).PersonaKnown);Spawn 会拒绝未知 id 并返回 ErrUnknownPersona,
// 而不是静默回落到默认角色。
PersonaID string
// Mode 选择该角色的行为模式(例如 "engineer")。
Mode string
// Context 是初始的、隔离的会话上下文。Spawn 会深拷贝它,因此调用方此后的修改
// 不会影响子代理。
Context []model.Message
// AttachInfo 是派发时提供的自由形式附加/注入文本。
AttachInfo string
// ParentSession 是拥有该分支的会话,其实时流程图将承载子代理的 span 及其内部
// 步骤,使它们渲染成派发会话图的一条泳道,而不是自成一张图(design/19 §2)。
// 派发工具从 loop.FlowOwnerFromContext 取得它。留空则关闭流图归属;它绝不影响
// 子代理在哪个会话里运行,因此其 checkpoint、记忆与流隔离都不变。
ParentSession string
// Data 承载供调用方使用的可选结构化载荷。
Data any
// ToolWhitelist 限定该子代理可执行的工具集合(design/05 §7 裁决 02.4.1):
// 缺省(nil/空)=继承主 agent 的全量工具集;显式列出=只放行所列工具,
// 名单外的调用在工具循环层被否决并以软结果回灌。它只收缩能力面,绝不扩展。
ToolWhitelist []string
// Timeout 覆盖该子代理的运行时限(design/05 §7 裁决 02.4.4 的按调用覆盖)。
// 非正时回退到调度器的 Options.PerAgentTimeout。
Timeout time.Duration
}
// 经 comm 探针发布到 agent 间总线上的进度事件名。外部订阅方可据此匹配,驱动一个
// 监控面板(DevDocs/design/05-子代理.md §9)。
const (
// EventDispatched 在子代理被创建时发布。
EventDispatched = "subagent.dispatched"
// EventStarted 在子代理开始运行时发布。
EventStarted = "subagent.started"
// EventDone 在子代理成功结束时发布。
EventDone = "subagent.done"
// EventFailed 在子代理失败时发布。
EventFailed = "subagent.failed"
// EventAborted 在子代理被取消或超时时发布。
EventAborted = "subagent.aborted"
)
// Progress 是进度事件 Data 载荷所承载的状态快照。
type Progress struct {
// AgentID 是已派发的子代理 id。
AgentID string
// Goal 是子代理的目标。
Goal string
// RoleID 是绑定的角色/persona。
RoleID string
// Mode 是绑定的行为模式。
Mode string
// Status 是子代理在发布时刻的状态。
Status Status
}
// Scheduler 返回的哨兵错误。调用点以 %w 包装它们,因此调用方可用 errors.Is 判断。
var (
// ErrMissingGoal 在 spec 的 Goal 为空时由 Spawn 返回。
ErrMissingGoal = errors.New("orchestration: missing goal")
// ErrNoRunner 在子代理运行时未配置循环 runner(Options.Loop 为 nil)时返回。
ErrNoRunner = errors.New("orchestration: no loop runner configured")
// ErrNoApprover 在未注入 *audit.Approver 时由 Approve 返回。
ErrNoApprover = errors.New("orchestration: no approver configured")
// ErrNoMemory 在未注入 *memory.Store 时由 SinkMemory 返回。
ErrNoMemory = errors.New("orchestration: no memory store configured")
// ErrAgentNotFound 在子代理 id 未知时由 Abort 返回。
ErrAgentNotFound = errors.New("orchestration: sub-agent not found")
// ErrUnknownPersona 在 spec 指名了一个本进程没有对应按角色 agent 的角色时由
// Spawn 返回(design/22 §8-2)。它是拒绝,不是回退:调用方要的是另一个角色,
// 却用默认角色的提示词运行子代理,那会静默地背离请求本意。
ErrUnknownPersona = errors.New("orchestration: unknown persona")
// ErrNoScheduler 在派发工具被构建时未带调度器(接线错误,如实报告而不 panic)
// 时由该工具返回。
ErrNoScheduler = errors.New("orchestration: no scheduler configured")
)
// cloneMessages 返回 msgs 的深拷贝:消息切片与每条消息的内容分段都被复制,因此结果
// 与入参不共享内存。它在 Spawn 处用于给每个子代理一个隔离上下文。nil 入参产出 nil;
// 非 nil 的空入参产出非 nil 的空切片。
func cloneMessages(msgs []model.Message) []model.Message {
if msgs == nil {
return nil
}
out := make([]model.Message, len(msgs))
for i, m := range msgs {
if m.Content != nil {
parts := make([]model.Content, len(m.Content))
copy(parts, m.Content)
m.Content = parts
}
out[i] = m
}
return out
}
// compactText 把 s 截短到至多 n 个字符以用于摘要,被截断时追加省略号。它绝不切开
// 多字节字符。
func compactText(s string, n int) string {
s = strings.TrimSpace(s)
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}
// taskContext 返回子代理的隔离上下文:调用方显式给出的消息在前(深拷贝),
// 任务本身作为最后一条 user 消息注入。裁决 2026-10-04:goal 正文 +
// attach_info/data 作为同一条 user 消息内的独立段落;system 只承载角色与
// 安全约束(循环前置的 agent 指令),绝不把任务塞进 system——否则子代理
// 的模型上下文里没有任何一轮用户消息,任务一个字都到不了模型。
func taskContext(spec AgentSpec) []model.Message {
return append(cloneMessages(spec.Context), taskUserMessage(spec))
}
// taskUserMessage 把任务渲染为一条 user 消息(裁决 2026-10-04):
// [任务目标] 段固定存在(Spawn 已保证 goal 非空),[附加信息] 与
// [任务数据] 段仅在对应字段非空时出现;Data 以 JSON 编码,编码失败时
// 整段跳过——宁可少一段噪声,不注入一段坏文本。
func taskUserMessage(spec AgentSpec) model.Message {
var b strings.Builder
b.WriteString("[任务目标]\n")
b.WriteString(spec.Goal)
if spec.AttachInfo != "" {
b.WriteString("\n\n[附加信息]\n")
b.WriteString(spec.AttachInfo)
}
if spec.Data != nil {
if data, err := json.Marshal(spec.Data); err == nil {
b.WriteString("\n\n[任务数据]\n")
b.Write(data)
}
}
return model.TextMessage(model.RoleUser, b.String())
}
// cloneStrings 返回 names 的拷贝,使子代理持有的白名单与调用方的切片
// 不共享内存。nil 进 nil 出。
func cloneStrings(names []string) []string {
if names == nil {
return nil
}
return append([]string(nil), names...)
}
// stringListArg 读取字符串数组参数:容忍缺失/非数组/非字符串元素,
// 逐项去空白并丢弃空项,使名单恒为「显式列出的工具名」。缺失返回 nil(全开)。
func stringListArg(m map[string]any, key string) []string {
raw, ok := m[key].([]any)
if !ok {
return nil
}
out := make([]string, 0, len(raw))
for _, v := range raw {
s, _ := v.(string)
if s = strings.TrimSpace(s); s != "" {
out = append(out, s)
}
}
return out
}
// timeoutArg 读取 timeout_seconds 按调用覆盖(JSON 解码为 float64):
// 缺失/非数/越界(<1s 或 >24h)视为未提供并沿用 dft;越界值不静默夹取——
// 回退到默认而不是替模型猜一个邻近值。
func timeoutArg(m map[string]any, key string, dft time.Duration) time.Duration {
f, ok := m[key].(float64)
if !ok {
return dft
}
sec := int64(f)
if sec < minSpawnTimeoutSeconds || sec > maxSpawnTimeoutSeconds {
return dft
}
return time.Duration(sec) * time.Second
}