package audit import ( "context" "time" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/executus/run" ) // Sink adapts an audit Storage to the run.Audit port: StartRun opens a run row // and returns a per-run recorder (a Writer) that the executor feeds with steps, // tool calls, and the terminal roll-up. This is what plugs the audit battery // into run.Ports.Audit — mort backs it with its GORM Storage, a light host with // Memory() (or omits it entirely). type Sink struct{ storage Storage } // NewSink wraps a Storage as a run.Audit. A nil Storage yields a Sink whose // StartRun returns nil (the executor then records nothing) — so NewSink(nil) is // equivalent to leaving run.Ports.Audit unset. func NewSink(storage Storage) *Sink { return &Sink{storage: storage} } // compile-time proof the adapter satisfies the core seams. var ( _ run.Audit = (*Sink)(nil) _ run.RunRecorder = (*recorder)(nil) ) // StartRun records the run start and returns a recorder. Implements run.Audit. func (s *Sink) StartRun(ctx context.Context, info run.RunInfo) run.RunRecorder { if s == nil || s.storage == nil { return nil } started := info.StartedAt if started.IsZero() { started = time.Now() } // Best-effort: a failed StartRun must not break the user-visible run. _ = s.storage.StartRun(ctx, SkillRun{ ID: info.RunID, SkillID: info.SubjectID, CallerID: info.CallerID, ChannelID: info.ChannelID, ParentRunID: info.ParentRunID, Inputs: info.Inputs, StartedAt: started, Status: "running", }) return &recorder{w: NewWriter(s.storage, info.RunID)} } // recorder adapts a *Writer to run.RunRecorder, converting run.RunStats to the // audit RunStats on Close (the two have identical fields). type recorder struct{ w *Writer } func (r *recorder) TokenStats() (in, out, thinking int64) { return r.w.TokenStats() } func (r *recorder) ToolCallsCount() int { return r.w.ToolCallsCount() } func (r *recorder) OnStep(iter int, resp *llm.Response) { r.w.OnStep(iter, resp) } func (r *recorder) OnTool(call llm.ToolCall, result string) { r.w.OnTool(call, result) } func (r *recorder) LogEvent(eventType string, payload map[string]any) { r.w.LogEvent(eventType, payload) } func (r *recorder) LogError(msg string) { r.w.LogError(msg) } func (r *recorder) Close(ctx context.Context, s run.RunStats) { r.w.Close(ctx, RunStats{ Status: s.Status, Output: s.Output, Error: s.Error, ToolCalls: s.ToolCalls, RuntimeSeconds: s.RuntimeSeconds, InputTokens: s.InputTokens, OutputTokens: s.OutputTokens, ThinkingTokens: s.ThinkingTokens, }) }