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 // 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 // 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") ) // 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]) + "…" }