784d5d7ce4
Two convergent gadfly refinements on the PostRun wiring: - PostRun now runs on detach(ctx), not the caller's ctx — a finished/cancelled caller no longer aborts artifact production (3-model: glm-5.2/minimax/deepseek). - Cleanup is panic-isolated via safeCleanup (recover+log), matching runPostRun, so a misbehaving teardown can't clobber an otherwise-successful run (deepseek). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
2.6 KiB
Go
89 lines
2.6 KiB
Go
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()
|
|
}
|