返回 DeepSeek-Reasonix
compaction_run.go
根目录 / internal / agent / compaction_run.go
1 package agent
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8 )
9
10 const compactionBudget = 5 * time.Minute
11
12 // SummaryError keeps the failure identity without changing the wire contract.
13 type SummaryError struct {
14 Code string
15 Cause error
16 }
17
18 func (e *SummaryError) Error() string { return fmt.Sprintf("%s: %v", e.Code, e.Cause) }
19 func (e *SummaryError) Unwrap() error { return e.Cause }
20
21 var errSummaryEmpty = errors.New("summarizer returned empty output")
22 var errSummaryBudget = &SummaryError{Code: "summary_budget_exceeded", Cause: context.DeadlineExceeded}
23
24 func summaryError(err error) error {
25 if err == nil {
26 return nil
27 }
28 var typed *SummaryError
29 var persistence *compactionPersistenceError
30 if errors.As(err, &typed) || errors.As(err, &persistence) || errors.Is(err, context.Canceled) {
31 return err
32 }
33 var code string
34 switch {
35 case errors.Is(err, errSummaryEmpty):
36 code = "summary_empty"
37 case errors.Is(err, errSummaryOutputTruncated):
38 code = "summary_output_truncated"
39 case errors.Is(err, errCheckpointRejected):
40 code = "summary_no_reduction"
41 case errors.Is(err, errCompressStaleContext):
42 code = "summary_context_changed"
43 default:
44 return err
45 }
46 return &SummaryError{Code: code, Cause: err}
47 }
48
49 // Only the summary request owner may classify an otherwise untyped provider
50 // failure. Entry-point validation, extensions, and persistence keep their errors.
51 func summaryRequestError(ctx context.Context, err error) error {
52 err = summaryError(compactionError(ctx, err))
53 var typed *SummaryError
54 var persistence *compactionPersistenceError
55 if err == nil || errors.As(err, &typed) || errors.As(err, &persistence) || errors.Is(err, context.Canceled) {
56 return err
57 }
58 return &SummaryError{Code: "summary_provider_error", Cause: err}
59 }
60
61 type compactionRunKey struct{}
62 type compactionRun struct{ work context.Context }
63
64 func currentCompactionRun(ctx context.Context) *compactionRun {
65 run, _ := ctx.Value(compactionRunKey{}).(*compactionRun)
66 return run
67 }
68
69 // Every nested summary, replan, and chunk inherits the same deadline. The
70 // caller's ordinary answer context is not cancelled by this work budget.
71 func (a *Agent) beginCompactionRun(parent context.Context) (context.Context, func(error) error) {
72 if parent == nil {
73 parent = context.Background()
74 }
75 if run := currentCompactionRun(parent); run != nil {
76 return run.work, func(err error) error { return err }
77 }
78 ctx, cancel := context.WithTimeoutCause(parent, compactionBudget, errSummaryBudget)
79 run := &compactionRun{}
80 ctx = context.WithValue(ctx, compactionRunKey{}, run)
81 run.work = ctx
82 return ctx, func(err error) error {
83 defer cancel()
84 return compactionError(ctx, err)
85 }
86 }
87
88 func compactionError(ctx context.Context, err error) error {
89 if err == nil {
90 return nil
91 }
92 var persistence *compactionPersistenceError
93 if errors.As(err, &persistence) {
94 return err
95 }
96 if errors.Is(err, context.DeadlineExceeded) && errors.Is(context.Cause(ctx), errSummaryBudget) && !errors.Is(err, errSummaryBudget) {
97 if err == context.DeadlineExceeded { //nolint:errorlint // Only the bare sentinel can be replaced without discarding an enclosing error.
98 return errSummaryBudget
99 }
100 // Keep any enclosing recovery/error identity, even when a work deadline
101 // needs its budget classification added. An expired run alone is not
102 // evidence that a later request-preparation error came from compaction.
103 return fmt.Errorf("%w: %w", err, errSummaryBudget)
104 }
105 return err
106 }
107
108 type compactionPersistenceError struct{ error }
109
110 func (e *compactionPersistenceError) Unwrap() error { return e.error }
111
111 lines GO