package run import ( "context" "log/slog" "strings" "sync" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) // runPostRun invokes a SessionToolFactory's PostRun hook with panic isolation: // a PostRun panic (or a slow artifact build that the hook mishandles) must not // fail an otherwise-successful run — artifacts are best-effort, the agent's text // output is the source of truth. func runPostRun(ctx context.Context, hook func(context.Context, []llm.Message, string, error) *tool.PostRunResult, transcript []llm.Message, output string, runErr error) (prr *tool.PostRunResult) { defer func() { if r := recover(); r != nil { slog.Error("run: PostRun hook panicked; no artifacts produced", "panic", r) prr = nil } }() return hook(ctx, transcript, output, runErr) } // steerMailbox is a thread-safe queue of messages a session tool (via // tool.Invocation.AttachImages) wants injected into the agent loop before its // next step — the same WithSteer mechanism the critic uses for nudges, exposed // to ordinary tools so they can show the model content (e.g. a rendered // preview) it must SEE, not just be told about. type steerMailbox struct { mu sync.Mutex msgs []llm.Message } func (m *steerMailbox) push(msg llm.Message) { m.mu.Lock() m.msgs = append(m.msgs, msg) m.mu.Unlock() } // drain returns and clears the queued messages (nil when empty). func (m *steerMailbox) drain() []llm.Message { m.mu.Lock() defer m.mu.Unlock() if len(m.msgs) == 0 { return nil } out := m.msgs m.msgs = nil return out } // runSession implements tool.AgentSession over a steer mailbox: AttachImages // queues a user-role multimodal message the agent loop injects before its next // step. Replaces legacy agentkit's Agent.AttachImages — majordomo's *agent.Agent // is immutable mid-run, so mutation flows through the run-scoped steer mailbox. type runSession struct{ mailbox *steerMailbox } func (s *runSession) AttachImages(text string, images ...llm.ImagePart) { parts := make([]llm.Part, 0, len(images)+1) if strings.TrimSpace(text) != "" { parts = append(parts, llm.Text(text)) } for _, img := range images { parts = append(parts, img) } if len(parts) == 0 { return } s.mailbox.push(llm.UserParts(parts...)) } // safeCleanup runs a SessionTools.Cleanup with panic isolation, so a misbehaving // teardown (temp-dir removal, handle close) can't clobber an otherwise-successful // run via the executor's top-level recover. func safeCleanup(fn func()) { defer func() { if r := recover(); r != nil { slog.Error("run: session Cleanup panicked", "panic", r) } }() fn() }