package orchestration import ( "log/slog" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/bus" "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/orchestration/audit" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/comm" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // Defaults for the orchestration layer. const ( // DefaultMaxParallel is the maximum number of sub-agents that run at once // when Options.MaxParallel is non-positive. DefaultMaxParallel = 4 // defaultAgentID is the comm probe identity used when Options.AgentID is // blank and a probe must be built from Bus. defaultAgentID = "orchestrator" ) // Options configures a Scheduler built by New. All fields are optional; each nil // dependency degrades gracefully (a missing Loop makes sub-agents fail fast, a // missing Comm skips progress events, a missing Approver makes Approve error, // and so on). type Options struct { // MaxParallel caps how many sub-agents execute concurrently. Excess sub-agents // queue until a slot frees. Non-positive selects DefaultMaxParallel (4). MaxParallel int // PerAgentTimeout bounds every sub-agent's run. When it elapses the sub-agent // is aborted (status aborted). Non-positive means no timeout. PerAgentTimeout time.Duration // Loop is the execution template: a *loop.Loop used to run every sub-agent. // It must be safe for concurrent Run calls (as loop.Loop documents); the // template's agent/tools/guardrails/persona apply to every sub-agent. When // nil, sub-agents fail fast with ErrNoRunner. Loop *loop.Loop // Bus is the in-process event bus used for progress events and, when Comm is // nil, to build a default comm probe. When nil no events are published. Bus bus.Bus // Comm is the comm probe used to publish progress events. When nil, a probe // bound to AgentID is built from Bus (when Bus is non-nil). Comm *comm.Probe // Memory is the long-term memory store used by SinkMemory. Optional. Memory *memory.Store // Tracer emits a per-sub-agent span (kind subagent) around each run. Optional. Tracer *trace.Tracer // Flow, when non-nil, receives the sub-agent span transitions so a spawned // branch appears in the flow graph of the session named by // AgentSpec.ParentSession (design/19 §2). *loop.FlowPublisher is the // production implementation. Optional: a nil sink leaves sub-agent branches // unvisualized (the sub-agent's own inner spans still fold into the parent // graph through the loop's flow branch). Flow FlowSpanSink // Approver records sensitive-operation review-and-approve (代审) triggered by // Approve. Policy judgement belongs to the upper layer. Optional. Approver *audit.Approver // PersonaExists reports whether a role id can run with its own instructions, // i.e. whether the per-role agent pool behind Loop (loop.Options.Agents) has an // entry for it (design/22 §8-2). It gates AgentSpec.PersonaID: a dispatch naming // an id this returns false for is rejected with ErrUnknownPersona rather than // running as the default role's prompt. Nil means "this deployment knows no // role", so a dispatch that names a role is rejected too — deciding nothing // silently is the point. Omitting the persona is unaffected by this. PersonaExists func(personaID string) bool // Logger is the base logger. Nil is replaced with a discard logger tagged with // source "orchestration". Logger *slog.Logger // AgentID is the comm probe identity. When blank and a probe must be built // from Bus, "orchestrator" is used. AgentID string } // FlowSpanSink folds a span transition into a session's live flow graph so the // realtime visualization shows the work in flight (design/19 §1). *loop.FlowPublisher // is the production implementation; tests substitute a capturing fake. The // interface keeps orchestration from depending on the concrete publisher beyond // the loop template it already takes. type FlowSpanSink interface { // SpanStarted registers a running placeholder node for a span that just began. SpanStarted(sessionID string, sp *trace.Span) // SpanFinished folds a just-finished span into the session graph. SpanFinished(sessionID string, sp *trace.Span) } // wrapLoop adapts a *loop.Loop into the runner interface, substituting a no-op // runner (fail-fast) when the template is nil. func wrapLoop(l *loop.Loop) runner { if l == nil { return noopRunner{} } return l } // scopedRunner picks the runner that executes sub: without a tool whitelist the // configured template runs as-is(缺省全开,继承主 agent 全量工具集);with one, // a *loop.Loop template is derived through WithToolWhitelist so only the listed // tools pass(design/05 §7 裁决 02.4.1). A non-loop runner (tests inject mocks) // is returned unchanged — the whitelist still rides the run context for it to // observe via loop.ToolWhitelistFrom. func (s *Scheduler) scopedRunner(sub *SubAgent) runner { if len(sub.ToolWhitelist) == 0 { return s.runner } if lp, ok := s.runner.(*loop.Loop); ok { return lp.WithToolWhitelist(sub.ToolWhitelist) } return s.runner } // effectiveTimeout resolves the deadline one sub-agent runs under: a positive // per-call override wins; otherwise the scheduler's PerAgentTimeout applies // (non-positive = no deadline). func effectiveTimeout(override, schedulerDefault time.Duration) time.Duration { if override > 0 { return override } return schedulerDefault }