design/22 §8-2(主人 2026-10-03 拍板):跨角色协作 = 把其他角色以子代理形式派出去,
该子代理以目标角色的 instructions 运行、并按目标角色的记忆作用域读写。
- AgentSpec / SubAgent / SubAgentResult 新增 PersonaID;Options.PersonaExists 作角色解析器
- scheduler:派发前校验角色(ErrUnknownPersona),批量 all-or-nothing;
run ctx 经 withPersonaEnv 只覆盖 SessionEnv.PersonaID —— ActorEnv/MemWritable 原样复制
(身份识别靠 channel 元数据,persona 决定她怎么答、不决定她能碰什么:项目红线)
- 记忆归属键改为 PersonaID 优先(回落 RoleID)→ 替班者写的是目标角色的记忆域
- spawntool:schema 顶层与 tasks[] 新增可选 persona;省略 = 与旧行为逐字节一致
- 分支根节点新增 props.persona(可观测性);SinkMemory 移入 memory.go(守 500 行红线)
- 组合根:personaExists(agent 池);cmd/cyrene/wire.go +5 行
契约:Docs/api-client.md -> v0.47(graph_update 新增可选 props.persona,纯增量)
验证:go build/vet ./... 绿;internal/runtime/orchestration + cmd/cyrene 测试绿;
go test -race ./internal/runtime/orchestration -run Persona 绿;
linux/amd64 + darwin/arm64 交叉编译绿
变异验证:① 目标角色换回父/默认角色(3 处断言 FAIL)② 关调度器侧角色校验 ③ 关工具侧预检
162 lines
6.4 KiB
Go
162 lines
6.4 KiB
Go
package orchestration
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// Status is the lifecycle state of a spawned sub-agent.
|
|
//
|
|
// The lifecycle is pending -> running -> one of the terminal states
|
|
// (done, failed, aborted). A sub-agent is terminal exactly when Terminal
|
|
// reports true.
|
|
type Status string
|
|
|
|
const (
|
|
// StatusPending means the sub-agent was created but has not started.
|
|
StatusPending Status = "pending"
|
|
// StatusRunning means the sub-agent's loop is executing.
|
|
StatusRunning Status = "running"
|
|
// StatusDone means the loop completed and produced a result.
|
|
StatusDone Status = "done"
|
|
// StatusFailed means the loop returned an error.
|
|
StatusFailed Status = "failed"
|
|
// StatusAborted means the sub-agent was canceled or timed out.
|
|
StatusAborted Status = "aborted"
|
|
)
|
|
|
|
// Terminal reports whether s is a final state. Pending and running are not
|
|
// terminal; every sub-agent ends in one of the three terminal states.
|
|
func (s Status) Terminal() bool {
|
|
return s == StatusDone || s == StatusFailed || s == StatusAborted
|
|
}
|
|
|
|
// AgentSpec describes a sub-agent to spawn. Goal is required; the rest are
|
|
// optional and bound onto the sub-agent for the upper layer to interpret.
|
|
type AgentSpec struct {
|
|
// Goal is the objective the sub-agent works toward. It is required.
|
|
Goal string
|
|
// RoleID selects the role/persona the sub-agent is bound to.
|
|
RoleID string
|
|
// PersonaID is the role the sub-agent actually runs as (design/22 §8-2): it
|
|
// answers with that role's instructions and reads/writes that role's memory
|
|
// scope — "她以该角色身份临时替班". Spawn stamps it onto the sub-agent run's
|
|
// session env, which is the single input the loop resolves the prompt, the
|
|
// memory namespace, the emotion and the thinking knobs from, so the four can
|
|
// never disagree. Blank keeps the pre-existing behavior byte for byte: the
|
|
// sub-agent inherits the spawning turn's env, i.e. the parent session's role.
|
|
// The id must be one this process can run with its own agent
|
|
// (*Scheduler).PersonaKnown; Spawn rejects an unknown one with
|
|
// ErrUnknownPersona instead of silently falling back to the default role.
|
|
PersonaID string
|
|
// Mode selects the behavior mode (e.g. "engineer") for the role.
|
|
Mode string
|
|
// Context is the initial, isolated conversation context. Spawn deep-copies
|
|
// it, so later mutation by the caller does not affect the sub-agent.
|
|
Context []model.Message
|
|
// AttachInfo is free-form attachment/injection text supplied at spawn.
|
|
AttachInfo string
|
|
// ParentSession is the session whose live flow graph owns the branch, so the
|
|
// sub-agent's span and its inner steps render as a swimlane of the spawning
|
|
// session's graph instead of as a graph of their own (design/19 §2). The
|
|
// spawning tool takes it from loop.FlowOwnerFromContext. Blank disables the
|
|
// flow attribution; it never affects which session the sub-agent runs in, so
|
|
// its checkpoint, memory and stream isolation are unchanged.
|
|
ParentSession string
|
|
// Data carries an optional structured payload for the caller's use.
|
|
Data any
|
|
}
|
|
|
|
// Progress event names published on the agent-to-agent bus via the Scheduler's
|
|
// comm probe. External subscribers can match on these to drive a monitoring
|
|
// panel (DevDocs/design/05-子代理.md §9).
|
|
const (
|
|
// EventDispatched is published when a sub-agent is created.
|
|
EventDispatched = "subagent.dispatched"
|
|
// EventStarted is published when a sub-agent begins running.
|
|
EventStarted = "subagent.started"
|
|
// EventDone is published when a sub-agent finishes successfully.
|
|
EventDone = "subagent.done"
|
|
// EventFailed is published when a sub-agent fails.
|
|
EventFailed = "subagent.failed"
|
|
// EventAborted is published when a sub-agent is canceled or times out.
|
|
EventAborted = "subagent.aborted"
|
|
)
|
|
|
|
// Progress is the status snapshot carried by a progress event's Data payload.
|
|
type Progress struct {
|
|
// AgentID is the spawned sub-agent id.
|
|
AgentID string
|
|
// Goal is the sub-agent's objective.
|
|
Goal string
|
|
// RoleID is the bound role/persona.
|
|
RoleID string
|
|
// Mode is the bound behavior mode.
|
|
Mode string
|
|
// Status is the sub-agent's status at publish time.
|
|
Status Status
|
|
}
|
|
|
|
// Sentinel errors returned by the Scheduler. They are wrapped with %w at call
|
|
// sites so callers can test with errors.Is.
|
|
var (
|
|
// ErrMissingGoal is returned by Spawn when the spec Goal is blank.
|
|
ErrMissingGoal = errors.New("orchestration: missing goal")
|
|
|
|
// ErrNoRunner is returned when a sub-agent runs with no loop runner
|
|
// configured (Options.Loop was nil).
|
|
ErrNoRunner = errors.New("orchestration: no loop runner configured")
|
|
|
|
// ErrNoApprover is returned by Approve when no *audit.Approver was injected.
|
|
ErrNoApprover = errors.New("orchestration: no approver configured")
|
|
|
|
// ErrNoMemory is returned by SinkMemory when no *memory.Store was injected.
|
|
ErrNoMemory = errors.New("orchestration: no memory store configured")
|
|
|
|
// ErrAgentNotFound is returned by Abort when the sub-agent id is unknown.
|
|
ErrAgentNotFound = errors.New("orchestration: sub-agent not found")
|
|
|
|
// ErrUnknownPersona is returned by Spawn when a spec names a role this process
|
|
// has no per-role agent for (design/22 §8-2). It is a rejection, not a
|
|
// fallback: running the sub-agent with the default role's prompt while the
|
|
// caller asked for another role would silently defeat the request.
|
|
ErrUnknownPersona = errors.New("orchestration: unknown persona")
|
|
|
|
// ErrNoScheduler is returned by the spawn tool when it was built without a
|
|
// scheduler (a wiring mistake, reported instead of panicking).
|
|
ErrNoScheduler = errors.New("orchestration: no scheduler configured")
|
|
)
|
|
|
|
// cloneMessages returns a deep copy of msgs: the message slice and each message's
|
|
// content parts are copied so the result shares no memory with the input. It is
|
|
// used at Spawn to give each sub-agent an isolated context. A nil input yields
|
|
// nil; a non-nil empty input yields a non-nil empty slice.
|
|
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 shortens s to at most n runes for inclusion in a summary, appending
|
|
// an ellipsis when it was truncated. It never splits a multi-byte rune.
|
|
func compactText(s string, n int) string {
|
|
s = strings.TrimSpace(s)
|
|
r := []rune(s)
|
|
if len(r) <= n {
|
|
return s
|
|
}
|
|
return string(r[:n]) + "…"
|
|
}
|