docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留 - 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
This commit is contained in:
1 parent
fe90f64a56
commit
eb1828ee7a
29 files changed
+751
-949
No files matched your search
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user