1// Package modes implements zut's three run modes: print, json, interactive.
2package modes
3
4import (
5 "context"
6 "fmt"
7 "io"
8 "os"
9 "strings"
10
11 "github.com/bnema/zut/packages/core"
12 "github.com/bnema/zut/packages/provider"
13)
14
15// RunPrint runs the agent to completion and writes only the final
16// assistant text block to out. It returns usage for this invocation,
17// excluding any cumulative usage restored from a session.
18func RunPrint(ctx context.Context, ag *core.Agent, prompt string, images []provider.ImageBlock, out io.Writer) (provider.Usage, error) {
19 usage, _, err := RunPrintWithContextRecovery(ctx, ag, prompt, images, out, nil)
20 return usage, err
21}
22
23// RunPrintWithContextRecovery is RunPrint with an optional checkpoint writer
24// for a one-shot context-overflow compaction. OutputStart identifies the
25// transcript suffix that callers must persist after a successful checkpoint.
26func RunPrintWithContextRecovery(ctx context.Context, ag *core.Agent, prompt string, images []provider.ImageBlock, out io.Writer, persistCompaction func([]provider.Message) error) (provider.Usage, ContextRecoveryResult, error) {
27 var finalText strings.Builder
28 var lastAssistant string
29 var usage provider.Usage
30 var haveUsage bool
31 var runErr error
32
33 sink := func(ev core.AgentEvent) {
34 switch e := ev.(type) {
35 case core.EvAssistantMessage:
36 // Keep the most recent assistant text block; by the end it's the final answer.
37 var sb strings.Builder
38 for _, c := range e.Message.Content {
39 if tb, ok := c.(provider.TextBlock); ok {
40 if sb.Len() > 0 {
41 sb.WriteString("\n")
42 }
43 sb.WriteString(tb.Text)
44 }
45 }
46 if sb.Len() > 0 {
47 lastAssistant = sb.String()
48 }
49 case core.EvUsage:
50 if haveUsage {
51 usage = usage.Add(e.Usage)
52 } else {
53 usage = e.Usage
54 haveUsage = true
55 }
56 case core.EvTurnRecovery:
57 // The core owns the single bounded continuation; surface it
58 // on stderr so stdout keeps only the final answer. An
59 // exhausted allowance returns ErrIncompleteTurn from Prompt
60 // and never reaches here as success.
61 fmt.Fprintln(os.Stderr, "Continuing: no final answer received.")
62 case core.EvRepetitionGuard:
63 switch e.Stage {
64 case core.RepetitionGuardWarning:
65 fmt.Fprintf(os.Stderr, "Repeated content detected (%d occurrences); asking the agent to try another approach.\n", e.Count)
66 case core.RepetitionGuardStopped:
67 fmt.Fprintf(os.Stderr, "Stopped a repetitive loop after %d occurrences.\n", e.Count)
68 }
69 case core.EvTurnEnd:
70 if e.Err != nil {
71 runErr = e.Err
72 }
73 }
74 }
75
76 recovery, err := PromptWithContextRecovery(ctx, ag, prompt, images, sink, ContextRecoveryOptions{
77 PersistCompaction: persistCompaction,
78 SuppressInitialOverflowEvent: true,
79 })
80 if err != nil {
81 return usage, recovery, err
82 }
83 if runErr != nil {
84 return usage, recovery, runErr
85 }
86
87 finalText.WriteString(lastAssistant)
88 if finalText.Len() > 0 && !strings.HasSuffix(finalText.String(), "\n") {
89 finalText.WriteString("\n")
90 }
91 _, err = fmt.Fprint(out, finalText.String())
92 return usage, recovery, err
93}