package orchestration import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit" ) // AuditMode selects how a sub-agent context is shaped for the subagent_ask // upstream WS route. It mirrors the audit.ContextAuditor surface (Full/Tail/ // Keyword) so the gateway can request exactly the window the client asks for. type AuditMode string const ( // AuditFull returns the entire conversation context. AuditFull AuditMode = "full" // AuditTail returns the last n messages of the context. AuditTail AuditMode = "tail" // AuditKeyword returns the window around the first keyword match. AuditKeyword AuditMode = "keyword" ) // AuditResult is the read-only result of auditing a sub-agent's conversation // context. Messages carries the selected window; Line and Match are populated for // a keyword query. It is the wire payload for a ws.subagent_event reply. type AuditResult struct { AgentID string `json:"agentId"` Status Status `json:"status"` Goal string `json:"goal,omitempty"` Mode AuditMode `json:"mode"` Messages []model.Message `json:"messages,omitempty"` Line int `json:"line,omitempty"` Match string `json:"match,omitempty"` } // AuditContext returns a read-only view of sub-agent id's conversation context // for the subagent_ask WS route. mode selects the window: AuditFull returns the // whole context, AuditTail returns the last n messages, AuditKeyword returns the // window around the first query hit. It wraps ErrAgentNotFound for an unknown id // and audit.ErrQueryEmpty for a blank keyword query, both with %w so callers can // classify with errors.Is. func (s *Scheduler) AuditContext(ctx context.Context, id string, mode AuditMode, n int, query string) (*AuditResult, error) { if err := ctx.Err(); err != nil { return nil, fmt.Errorf("orchestration: audit %q: %w", id, err) } s.mu.Lock() sub := s.agents[id] s.mu.Unlock() if sub == nil { return nil, fmt.Errorf("orchestration: audit %q: %w", id, ErrAgentNotFound) } a := audit.NewContextAuditor(sub.Context) res := &AuditResult{AgentID: sub.ID, Status: sub.Status(), Goal: sub.Goal, Mode: mode} switch mode { case AuditFull: res.Messages = a.Full() case AuditTail: res.Messages = a.Tail(n) case AuditKeyword: loc, err := a.Keyword(ctx, query) if err != nil { return nil, fmt.Errorf("orchestration: audit %q: %w", id, err) } if loc == nil { res.Messages = []model.Message{} break } res.Line = loc.Line res.Messages = loc.Surrounding res.Match = messageText(loc.Surrounding[len(loc.Surrounding)-1]) default: return nil, fmt.Errorf("orchestration: audit %q: unknown mode %q", id, mode) } return res, nil } // messageText concatenates the text parts of one message. It mirrors the audit // subpackage's own helper so the result payload can surface a human-readable // match without exporting a cross-package text extractor. func messageText(m model.Message) string { var text string for _, c := range m.Content { if c.Type == model.ContentTypeText && c.Text != "" { if text != "" { text += " " } text += c.Text } } return text }