feat(orchestration): 子代理编排调度器(创建/生命周期/并行上限4/超时/防失控/结果合并/进度事件/隔离上下文+独立工作记忆)
This commit is contained in:
1 parent
20f4a91d02
commit
d90ec7bd4f
8 files changed
+1618
No files matched your search
@@ -0,0 +1,214 @@
|
||||
package orchestration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// SubAgent is a single spawned sub-agent instance. The identity fields (ID, Goal,
|
||||
// RoleID, Mode, AttachInfo, Data, Context, WorkingMemory) are set once at Spawn
|
||||
// and never mutated. The lifecycle fields (Status, Summary, Result, Err) are
|
||||
// written by the sub-agent's background goroutine and read through the
|
||||
// synchronized accessors below, so reading them before the sub-agent finishes is
|
||||
// safe. A sub-agent is terminal exactly once its Done reports true; Join returns
|
||||
// once every sub-agent is terminal.
|
||||
//
|
||||
// SubAgent is created by (*Scheduler).Spawn; it should not be built directly.
|
||||
type SubAgent struct {
|
||||
// ID uniquely identifies the sub-agent within its scheduler.
|
||||
ID string
|
||||
// Goal is the objective the sub-agent pursues.
|
||||
Goal string
|
||||
// RoleID is the bound role/persona.
|
||||
RoleID string
|
||||
// Mode is the bound behavior mode.
|
||||
Mode string
|
||||
// AttachInfo is the free-form attachment text supplied at spawn.
|
||||
AttachInfo string
|
||||
// Data is the optional structured payload supplied at spawn.
|
||||
Data any
|
||||
// Context is the isolated conversation context. It is a deep copy of the
|
||||
// AgentSpec.Context and is never shared with the front session.
|
||||
Context []model.Message
|
||||
// WorkingMemory is the sub-agent's own working-memory window, created fresh
|
||||
// at Spawn so it never overlaps another sub-agent or the front session.
|
||||
WorkingMemory *memory.WorkingMemory
|
||||
|
||||
mu sync.RWMutex
|
||||
status Status
|
||||
summary string
|
||||
result *loop.Result
|
||||
runErr error
|
||||
started time.Time
|
||||
finished time.Time
|
||||
|
||||
done chan struct{}
|
||||
cancel context.CancelFunc
|
||||
abortReq bool
|
||||
doneClosed bool
|
||||
|
||||
launchOnce sync.Once
|
||||
finishOnce sync.Once
|
||||
}
|
||||
|
||||
// Status returns the sub-agent's current lifecycle status.
|
||||
func (s *SubAgent) Status() Status {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.status
|
||||
}
|
||||
|
||||
// Summary returns the execution summary written when the sub-agent finalized:
|
||||
// a completion summary on success, or a failure/abort reason otherwise. It is
|
||||
// empty until the sub-agent is terminal.
|
||||
func (s *SubAgent) Summary() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.summary
|
||||
}
|
||||
|
||||
// Result returns the loop result on a successful run, or nil when the run failed
|
||||
// or was aborted.
|
||||
func (s *SubAgent) Result() *loop.Result {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.result
|
||||
}
|
||||
|
||||
// Err returns the error that ended a failed or aborted run (nil on success).
|
||||
func (s *SubAgent) Err() error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.runErr
|
||||
}
|
||||
|
||||
// StartedAt returns when the sub-agent transitioned to running (zero if it never
|
||||
// started).
|
||||
func (s *SubAgent) StartedAt() time.Time {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.started
|
||||
}
|
||||
|
||||
// FinishedAt returns when the sub-agent reached a terminal state (zero if not
|
||||
// terminal yet).
|
||||
func (s *SubAgent) FinishedAt() time.Time {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.finished
|
||||
}
|
||||
|
||||
// Done reports whether the sub-agent reached a terminal state.
|
||||
func (s *SubAgent) Done() bool { return s.Status().Terminal() }
|
||||
|
||||
// Snapshot returns a point-in-time copy of the sub-agent's state, safe to read
|
||||
// while the sub-agent is still running.
|
||||
func (s *SubAgent) Snapshot() SubAgentResult {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
snap := SubAgentResult{
|
||||
ID: s.ID,
|
||||
Goal: s.Goal,
|
||||
RoleID: s.RoleID,
|
||||
Mode: s.Mode,
|
||||
AttachInfo: s.AttachInfo,
|
||||
Status: s.status,
|
||||
Summary: s.summary,
|
||||
Err: s.runErr,
|
||||
StartedAt: s.started,
|
||||
FinishedAt: s.finished,
|
||||
}
|
||||
if s.result != nil {
|
||||
snap.Text = s.result.Text
|
||||
snap.Steps = s.result.Steps
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
// transitionRunning moves a pending sub-agent to running. It returns false when
|
||||
// the sub-agent was already aborted (so it must not run) or is not pending.
|
||||
func (s *SubAgent) transitionRunning() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.abortReq || s.status != StatusPending {
|
||||
return false
|
||||
}
|
||||
s.status = StatusRunning
|
||||
s.started = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
// progress returns the current status snapshot for a progress event.
|
||||
func (s *SubAgent) progress() Progress {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return Progress{
|
||||
AgentID: s.ID,
|
||||
Goal: s.Goal,
|
||||
RoleID: s.RoleID,
|
||||
Mode: s.Mode,
|
||||
Status: s.status,
|
||||
}
|
||||
}
|
||||
|
||||
// setOutcome records the terminal status, summary and error after the run ends.
|
||||
// It is called at most once per sub-agent, guarded by finishOnce.
|
||||
func (s *SubAgent) setOutcome(status Status, summary string, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.status = status
|
||||
s.summary = summary
|
||||
s.runErr = err
|
||||
s.finished = time.Now()
|
||||
}
|
||||
|
||||
// setResult records the successful loop result.
|
||||
func (s *SubAgent) setResult(res *loop.Result) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.result = res
|
||||
}
|
||||
|
||||
// setCancel stores the run context's cancel function so Abort can interrupt the
|
||||
// loop. If the sub-agent was already aborted (cancelRun raced ahead), the stored
|
||||
// cancel is invoked immediately.
|
||||
func (s *SubAgent) setCancel(c context.CancelFunc) {
|
||||
s.mu.Lock()
|
||||
s.cancel = c
|
||||
aborted := s.abortReq
|
||||
s.mu.Unlock()
|
||||
if aborted {
|
||||
c()
|
||||
}
|
||||
}
|
||||
|
||||
// cancelRun requests cancellation of the sub-agent. It is idempotent and safe for
|
||||
// concurrent use: the done channel is closed once and the stored cancel (when set)
|
||||
// is invoked once. A pending sub-agent unblocks from its parallel slot via done; a
|
||||
// running sub-agent is interrupted via the cancel func.
|
||||
func (s *SubAgent) cancelRun() {
|
||||
s.mu.Lock()
|
||||
s.abortReq = true
|
||||
c := s.cancel
|
||||
first := !s.doneClosed
|
||||
s.doneClosed = true
|
||||
s.mu.Unlock()
|
||||
if first {
|
||||
close(s.done)
|
||||
}
|
||||
if c != nil {
|
||||
c()
|
||||
}
|
||||
}
|
||||
|
||||
// aborted reports whether a cancellation was requested for the sub-agent.
|
||||
func (s *SubAgent) aborted() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.abortReq
|
||||
}
|
||||
Reference in new issue
Block a user