docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
This commit is contained in:
AskaEth committed 2026-10-04 13:30:04 +08:00
1 parent fe90f64a56
commit eb1828ee7a
29 files changed
+751 -949

No files matched your search

+75 -104
View File
@@ -19,22 +19,19 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// runner executes one sub-agent's loop. A *loop.Loop satisfies it; tests inject a
// mock so the scheduler can be exercised without a model backend.
// runner 执行一个子代理的循环。*loop.Loop 满足它;测试注入 mock,使调度器无需模型
// 后端即可被演练。
type runner interface {
Run(ctx context.Context, sessionID string, messages []model.Message) (*loop.Result, error)
}
// Scheduler creates and runs sub-agents in the background, bounding concurrency,
// applying a per-agent timeout and publishing progress over the agent-to-agent
// bus. It is safe for concurrent use.
// Scheduler 在后台创建并运行子代理:限制并发、施加按代理时限、经 agent 间总线发布
// 进度。它可安全并发使用。
//
// The lifecycle is: Spawn registers a sub-agent and launches it asynchronously;
// the sub-agent's goroutine acquires a parallel slot (gated by MaxParallel),
// transitions to running, runs the injected loop template in its isolated
// context under the per-agent timeout, writes a summary and publishes a terminal
// progress event. Abort/CancelAll interrupt running or queued sub-agents. Join
// blocks until every sub-agent is terminal and returns the merged outcome.
// 生命周期为:Spawn 登记一个子代理并异步启动它;子代理的 goroutine 取得一个并行槽位
// (受 MaxParallel 把守)、转入 running、在按代理时限下于隔离上下文中运行注入的循环
// 模板、写入摘要并发布终结态进度事件。Abort/CancelAll 中断运行中或排队中的子代理。
// Join 阻塞到每个子代理终结,返回合并结果。
type Scheduler struct {
maxParallel int
perAgentTimeout time.Duration
@@ -47,8 +44,8 @@ type Scheduler struct {
approver *audit.Approver
log *slog.Logger
agentID string
// personaExists answers "can this process run that role with its own
// instructions" (Options.PersonaExists); nil means no role can be named.
// personaExists 回答「本进程能否以自身指令运行那个角色」
// (Options.PersonaExists);nil 表示不能指名任何角色。
personaExists func(personaID string) bool
mu sync.Mutex
@@ -61,20 +58,19 @@ type Scheduler struct {
prefix string
}
// noopRunner is the fallback runner used when no *loop.Loop template was
// configured. It makes every sub-agent fail fast with ErrNoRunner rather than
// silently doing nothing.
// noopRunner 是未配置 *loop.Loop 模板时所用的兜底 runner。它使每个子代理以
// ErrNoRunner 快速失败,而不是悄悄什么都不做。
type noopRunner struct{}
// Run returns a wrapped ErrNoRunner.
// Run 返回包装后的 ErrNoRunner。
func (noopRunner) Run(context.Context, string, []model.Message) (*loop.Result, error) {
return nil, fmt.Errorf("orchestration: run sub-agent: %w", ErrNoRunner)
}
// New builds a Scheduler from opts, applying defaults. MaxParallel non-positive
// selects DefaultMaxParallel; a nil Loop is replaced by a no-op runner; a nil
// Comm is built from Bus (when present) using AgentID (default "orchestrator"); a
// nil Logger is replaced by a discard logger tagged with source "orchestration".
// New 依据 opts 构建 Scheduler 并套用默认值。MaxParallel 非正时选用
// DefaultMaxParallel;Loop 为 nil 时替换为空操作 runner;Comm 为 nil 时用 Bus(若存在)
// 配合 AgentID(默认 "orchestrator")构建;Logger 为 nil 时替换为标注了来源
// "orchestration" 的丢弃式 logger。
func New(opts Options) *Scheduler {
maxParallel := opts.MaxParallel
if maxParallel <= 0 {
@@ -110,22 +106,20 @@ func New(opts Options) *Scheduler {
return s
}
// Bus returns the injected event bus (may be nil).
// Bus 返回注入的事件总线(可能为 nil)。
func (s *Scheduler) Bus() bus.Bus { return s.bus }
// Comm returns the injected (or auto-built) comm probe (may be nil).
// Comm 返回注入的(或自动构建的)comm 探针(可能为 nil)。
func (s *Scheduler) Comm() *comm.Probe { return s.comm }
// Spawn creates a sub-agent from spec, registers it, publishes a "dispatched"
// progress event and launches its execution in the background. It returns
// immediately so the front session is not blocked; the sub-agent runs
// asynchronously and reports progress over the bus. It returns ErrMissingGoal
// when spec.Goal is blank, and a wrapped ErrUnknownPersona when spec.PersonaID
// names a role this process has no agent for (design/22 §8-2).
// Spawn 依据 spec 创建子代理、登记它、发布一条「已派发」进度事件,并在后台启动其
// 执行。它立即返回,因此前台会话不被阻塞;子代理异步运行并经总线报告进度。spec.Goal
// 为空时返回 ErrMissingGoal;spec.PersonaID 指名了本进程没有对应 agent 的角色时返回
// 包装后的 ErrUnknownPersona(design/22 §8-2)。
//
// The sub-agent's Context is a deep copy of spec.Context plus the task itself
// as one trailing user message (裁决 2026-10-04), and its WorkingMemory is
// a fresh, isolated window, so it never shares state with the front session.
// 子代理的 Context 是 spec.Context 的深拷贝加上作为末尾一条 user 消息的任务本身
// (裁决 2026-10-04),其 WorkingMemory 是全新隔离的窗口,因此它绝不与前台会话共享
// 状态。
func (s *Scheduler) Spawn(ctx context.Context, spec AgentSpec) (*SubAgent, error) {
if strings.TrimSpace(spec.Goal) == "" {
return nil, fmt.Errorf("orchestration: spawn: %w", ErrMissingGoal)
@@ -169,9 +163,8 @@ func (s *Scheduler) Spawn(ctx context.Context, spec AgentSpec) (*SubAgent, error
return sub, nil
}
// Run explicitly starts every registered sub-agent that has not started yet. It
// is idempotent and is provided for callers that prefer to defer execution; Spawn
// already auto-launches, so this is normally a no-op on freshly spawned batches.
// Run 显式启动每个尚未启动的已登记子代理。它幂等,供偏好延迟执行的调用方使用;Spawn
// 已自动启动,因此对刚派发的批次它通常是空操作。
func (s *Scheduler) Run(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
@@ -187,9 +180,8 @@ func (s *Scheduler) Run(ctx context.Context) {
}
}
// Join blocks until every spawned sub-agent is terminal (or ctx is canceled) and
// returns the merged outcome. On ctx cancellation it returns the partial merged
// outcome together with the wrapped ctx error.
// Join 阻塞到每个已派发的子代理终结(或 ctx 被取消),返回合并结果。ctx 取消时它
// 返回部分合并结果以及包装后的 ctx 错误。
func (s *Scheduler) Join(ctx context.Context) (*MergedResult, error) {
if ctx == nil {
ctx = context.Background()
@@ -206,9 +198,8 @@ func (s *Scheduler) Join(ctx context.Context) (*MergedResult, error) {
}
}
// Abort cancels the sub-agent with the given id. If it is running its loop is
// interrupted; if it is queued it is released without running; if it already
// finished the call is a no-op. It returns ErrAgentNotFound for an unknown id.
// Abort 取消给定 id 的子代理。它正在运行时中断其循环;排队中时不经运行即释放;
// 已经结束时该调用是空操作。未知 id 返回 ErrAgentNotFound。
func (s *Scheduler) Abort(ctx context.Context, id string) error {
if ctx == nil {
ctx = context.Background()
@@ -223,9 +214,8 @@ func (s *Scheduler) Abort(ctx context.Context, id string) error {
return nil
}
// CancelAll cancels every spawned sub-agent, queued or running. It is the
// anti-runaway escape hatch: after it returns each sub-agent converges to the
// aborted state.
// CancelAll 取消每个已派发的子代理,无论排队中还是运行中。它是防失控的逃生口:返回后
// 每个子代理都收敛到中止态。
func (s *Scheduler) CancelAll(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
@@ -242,8 +232,7 @@ func (s *Scheduler) CancelAll(ctx context.Context) error {
return nil
}
// Agents returns a snapshot of every registered sub-agent in spawn order. It is
// safe to read the returned snapshots at any time.
// Agents 按派发顺序返回每个已登记子代理的快照。随时读取返回的快照都是安全的。
func (s *Scheduler) Agents() []SubAgentResult {
s.mu.Lock()
subs := make([]*SubAgent, 0, len(s.agents))
@@ -259,11 +248,9 @@ func (s *Scheduler) Agents() []SubAgentResult {
return out
}
// Approve triggers a main-agent review-and-approve (代审) of a sub-agent's
// sensitive action through the injected *audit.Approver. Whether an action is
// sensitive and whether it is granted is decided by the upper policy layer; this
// method only records the audit entry. It returns ErrNoApprover when no approver
// was injected.
// Approve 经注入的 *audit.Approver 触发对某个子代理敏感操作的「主代理代审」。某项
// 操作是否敏感、是否获准由上层策略决定;本方法只记录审计条目。未注入 approver 时
// 返回 ErrNoApprover。
func (s *Scheduler) Approve(ctx context.Context, actor, action, describe string) (*audit.Approval, error) {
if s.approver == nil {
return nil, fmt.Errorf("orchestration: approve: %w", ErrNoApprover)
@@ -271,20 +258,17 @@ func (s *Scheduler) Approve(ctx context.Context, actor, action, describe string)
return s.approver.ReviewAndApprove(ctx, actor, action, describe)
}
// launch starts the sub-agent's background execution exactly once. It is called
// by Spawn (auto-launch) and by Run (explicit start); launchOnce guarantees a
// single run even if both happen.
// launch 恰好一次地启动子代理的后台执行。它由 Spawn(自动启动)与 Run(显式启动)
// 调用;launchOnce 保证即使两者都发生也只运行一次。
func (s *Scheduler) launch(ctx context.Context, sub *SubAgent) {
sub.launchOnce.Do(func() {
go s.run(ctx, sub)
})
}
// run drives one sub-agent: acquire a parallel slot, transition to running, run
// the loop in the sub-agent's isolated context under the per-agent timeout, then
// finalize with a summary and a terminal progress event. It always reaches
// exactly one finish, so active is decremented once and Join unblocks exactly
// when every sub-agent is terminal.
// run 驱动一个子代理:取得并行槽位、转入 running、在按代理时限下于子代理的隔离上下文
// 中运行循环,然后以摘要与终结态进度事件收尾。它总是恰好到达一次 finish,因此 active
// 只递减一次,Join 恰在每个子代理终结时解除阻塞。
func (s *Scheduler) run(ctx context.Context, sub *SubAgent) {
if ctx == nil {
ctx = context.Background()
@@ -308,16 +292,14 @@ func (s *Scheduler) run(ctx context.Context, sub *SubAgent) {
s.publish(EventStarted, sub.progress())
runCtx, cancel := s.subCtx(ctx, sub)
// A sub-agent must not leak its inner monologue into the caller's chat stream
// (design/05 §8: 少说话、多做事; design/04 §2: 不污染前台): muting suppresses the
// UI stream/capacity broadcast while progress still reaches the frontend over
// the bus and the flow lane. The flow branch attributes every span this Run
// emits to the spawning session's graph instead of opening a second graph.
// 子代理不得把内心独白泄漏进调用方的聊天流
// (design/05 §8: 少说话、多做事; design/04 §2: 不污染前台):静音抑制
// UI 流/容量广播,而进度仍经总线与泳道到达前端。flow 分支把本次 Run 发射的
// 每个 span 都归属到派发会话的图,而不是另开第二张图。
runCtx = loop.WithMutedBroadcast(runCtx)
runCtx = loop.WithFlowBranch(runCtx, sub.ParentSession)
// A dispatched target role decides the prompt and the memory scope for every step
// of this run (design/22 §8-2). Blank keeps the inherited env: the pre-existing
// behavior, where a sub-agent runs as the spawning turn's role.
// 被派发的目标角色决定本次运行每一步的提示词与记忆作用域(design/22 §8-2)。
// 留空则保持继承来的 env:既有行为,即子代理按派发回合的角色运行。
runCtx = withPersonaEnv(runCtx, sub.PersonaID)
// 工具白名单(design/05 §7 裁决 02.4.1)随运行上下文传递:名单非空时
// 只放行所列工具;空名单=全开,与挂载前的行为一致。
@@ -342,10 +324,9 @@ func (s *Scheduler) run(ctx context.Context, sub *SubAgent) {
s.finish(sub, StatusDone, doneSummary(res), nil)
}
// abortOrFail classifies an error after a sub-agent's run: a deadline exceeded or
// a cancellation yields the aborted status (timeout/stop), anything else is a
// real failure. The timeout notice names the deadline that actually applied to
// this sub-agent (per-call override or scheduler default), not the global one.
// abortOrFail 对子代理运行后的错误归类:期限超出或取消产出中止态(超时/停止),其余
// 都是真正的失败。超时提示点名的是实际施加于该子代理的期限(按调用覆盖或调度器默认),
// 而不是全局那个。
func (s *Scheduler) abortOrFail(sub *SubAgent, err error) {
switch {
case errors.Is(err, context.DeadlineExceeded):
@@ -357,9 +338,8 @@ func (s *Scheduler) abortOrFail(sub *SubAgent, err error) {
}
}
// finish finalizes a sub-agent exactly once: it records the terminal status and
// summary, publishes the terminal progress event and decrements the active count,
// closing the allDone channel when the last sub-agent finalizes.
// finish 恰好一次地给子代理收尾:记录终结状态与摘要、发布终结态进度事件、递减 active
// 计数,并在最后一个子代理收尾时关闭 allDone channel。
func (s *Scheduler) finish(sub *SubAgent, status Status, summary string, err error) {
sub.finishOnce.Do(func() {
sub.setOutcome(status, summary, err)
@@ -375,10 +355,9 @@ func (s *Scheduler) finish(sub *SubAgent, status Status, summary string, err err
})
}
// subCtx derives the run context, applying the sub-agent's effective timeout:
// a per-call AgentSpec.Timeout (design/05 §7 裁决 02.4.4 按调用覆盖) wins over
// the scheduler's PerAgentTimeout; neither configured means no deadline beyond
// cancellation.
// subCtx 派生运行上下文并施加该子代理的生效时限:按调用的 AgentSpec.Timeout
// (design/05 §7 裁决 02.4.4 按调用覆盖)优先于调度器的 PerAgentTimeout;两者都未配置
// 即除取消外无期限。
func (s *Scheduler) subCtx(parent context.Context, sub *SubAgent) (context.Context, context.CancelFunc) {
if sub.timeout > 0 {
return context.WithTimeout(parent, sub.timeout)
@@ -386,13 +365,11 @@ func (s *Scheduler) subCtx(parent context.Context, sub *SubAgent) (context.Conte
return context.WithCancel(parent)
}
// subSpan starts a sub-agent span when a tracer is configured, chaining it as a
// child of any span already in ctx. When the sub-agent is attributed to a flow
// session (AgentSpec.ParentSession) the span is also folded into that session's
// live graph as a subagent lane root, so the branch renders inside the graph of
// the session that spawned it rather than in a graph of its own (design/19 §2). It
// returns the ctx carrying the span and the span itself (nil when the tracer is
// nil).
// subSpan 在配置了 tracer 时开启一个子代理 span,把它串成 ctx 中已有 span 的子节点。
// 子代理被归属到某个 flow 会话(AgentSpec.ParentSession)时,该 span 还会作为子代理
// 泳道根折进那个会话的实时图,使分支渲染在派发它的会话图内,而不是自成一张图
// (design/19 §2)。它返回携带该 span 的 ctx 与 span 自身(tracer 为 nil 时 span 为
// nil)。
func (s *Scheduler) subSpan(ctx context.Context, sub *SubAgent) (context.Context, *trace.Span) {
if s.tracer == nil {
return ctx, nil
@@ -402,21 +379,20 @@ func (s *Scheduler) subSpan(ctx context.Context, sub *SubAgent) (context.Context
_ = span.SetProp("session", sess)
s.flow.SpanStarted(sess, span)
}
// The target role rides the branch node so the flow panel shows which role the
// sub-agent answered as (design/22 §8-2 可观测性), not just the sub-agent id.
// 目标角色随分支节点传播,使流程图面板展示子代理以哪个角色作答
// (design/22 §8-2 可观测性),而不只是子代理 id。
if sub.PersonaID != "" {
_ = span.SetProp("persona", sub.PersonaID)
}
return sctx, span
}
// subagentEventDone is the log event type recorded on a sub-agent span when its
// run settles, so the dashboard node carries the outcome summary (design/19 §1
// 节点实时日志).
// subagentEventDone 是子代理运行落定时记在其 span 上的日志事件类型,使面板节点带上
// 结果摘要(design/19 §1 节点实时日志)。
const subagentEventDone = "subagent.done"
// spanOK finishes a sub-agent span as ok, recording the result text and a
// desensitized outcome event, then folds the closed span into the parent flow.
// spanOK 把子代理 span 以 ok 收尾,记录结果文本与一条脱敏的结果事件,随后把已关闭的
// span 折回父 flow。
func (s *Scheduler) spanOK(sub *SubAgent, span *trace.Span, res *loop.Result) {
if span == nil {
return
@@ -433,8 +409,7 @@ func (s *Scheduler) spanOK(sub *SubAgent, span *trace.Span, res *loop.Result) {
s.foldSpanFinish(sub, span)
}
// spanErr finishes a sub-agent span as an error, recording the cause, then folds
// the closed span into the parent flow.
// spanErr 把子代理 span 以 error 收尾,记录原因,随后把已关闭的 span 折回父 flow。
func (s *Scheduler) spanErr(sub *SubAgent, span *trace.Span, err error) {
if span == nil {
return
@@ -448,10 +423,8 @@ func (s *Scheduler) spanErr(sub *SubAgent, span *trace.Span, err error) {
s.foldSpanFinish(sub, span)
}
// foldSpanFinish folds a just-closed sub-agent span into its parent session's
// flow graph, flipping the lane root to its final status. The span already
// carries the parent session as its "session" prop, so the publisher knows which
// graph owns it.
// foldSpanFinish 把一个刚关闭的子代理 span 折进其父会话的流程图,把泳道根翻转为终态。
// 该 span 已把父会话带在 "session" 属性上,因此发布器知道哪张图拥有它。
func (s *Scheduler) foldSpanFinish(sub *SubAgent, span *trace.Span) {
if span == nil || s.flow == nil || sub.ParentSession == "" {
return
@@ -459,9 +432,8 @@ func (s *Scheduler) foldSpanFinish(sub *SubAgent, span *trace.Span) {
s.flow.SpanFinished(sub.ParentSession, span)
}
// publish broadcasts a progress event over the bus. It is a no-op when no comm
// probe is configured. Publishing uses a background context so a canceled run
// still delivers its terminal event before the sub-agent is done.
// publish 经总线广播一条进度事件。未配置 comm 探针时是空操作。发布使用后台上下文,
// 因此被取消的运行仍能在子代理结束前送出它的终结态事件。
func (s *Scheduler) publish(name string, data Progress) {
if s.comm == nil {
return
@@ -471,13 +443,12 @@ func (s *Scheduler) publish(name string, data Progress) {
}
}
// nextID mints a unique sub-agent id from the scheduler's random prefix and an
// atomic sequence. It is safe for concurrent use.
// nextID 用调度器的随机前缀与一个原子序号生成唯一的子代理 id。可安全并发使用。
func (s *Scheduler) nextID() string {
return fmt.Sprintf("%s-%d", s.prefix, s.seq.Add(1))
}
// eventFor maps a terminal status to its progress event name.
// eventFor 把终结状态映射为对应的进度事件名。
func eventFor(status Status) string {
switch status {
case StatusDone: