| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "sync" |
| 6 | |
| 7 | "reasonix/internal/plancontract" |
| 8 | ) |
| 9 | |
| 10 | // PlanSubmission is where submit_plan leaves an accepted plan for the host that |
| 11 | // armed the planning turn. It rides the context rather than the tool so one |
| 12 | // shared registry can serve concurrent turns with no mutable state between them, |
| 13 | // and so a call made outside a planning turn has nowhere to land. |
| 14 | type PlanSubmission struct { |
| 15 | mu sync.Mutex |
| 16 | plan plancontract.Plan |
| 17 | previous plancontract.Plan |
| 18 | attempts int |
| 19 | } |
| 20 | |
| 21 | type planSubmissionKey struct{} |
| 22 | |
| 23 | // WithPlanSubmission arms submit_plan for one planning turn and returns the |
| 24 | // submission the host reads once the planner finishes. |
| 25 | func WithPlanSubmission(ctx context.Context) (context.Context, *PlanSubmission) { |
| 26 | s := &PlanSubmission{} |
| 27 | return context.WithValue(ctx, planSubmissionKey{}, s), s |
| 28 | } |
| 29 | |
| 30 | func planSubmissionFromContext(ctx context.Context) (*PlanSubmission, bool) { |
| 31 | s, ok := ctx.Value(planSubmissionKey{}).(*PlanSubmission) |
| 32 | return s, ok && s != nil |
| 33 | } |
| 34 | |
| 35 | // Plan returns the submitted plan, or ok=false when the planner never submitted |
| 36 | // one and the host must fall back to reading its prose. |
| 37 | func (s *PlanSubmission) Plan() (plancontract.Plan, bool) { |
| 38 | if s == nil { |
| 39 | return plancontract.Plan{}, false |
| 40 | } |
| 41 | s.mu.Lock() |
| 42 | defer s.mu.Unlock() |
| 43 | return s.plan, s.attempts > 0 |
| 44 | } |
| 45 | |
| 46 | // record keeps the latest submission and stamps its revision. A planner that |
| 47 | // resubmits after fixing a validation error gets revision 2, so the count is the |
| 48 | // host's own record of how many tries the plan took. |
| 49 | func (s *PlanSubmission) record(p plancontract.Plan) plancontract.Plan { |
| 50 | s.mu.Lock() |
| 51 | defer s.mu.Unlock() |
| 52 | s.previous = s.plan |
| 53 | s.attempts++ |
| 54 | p.Revision = s.attempts |
| 55 | s.plan = p |
| 56 | return p |
| 57 | } |
| 58 | |
| 59 | // Revised compares a resubmission against the revision it replaces, so the |
| 60 | // planner is told what its own edit changed rather than assuming. ok is false |
| 61 | // for a first submission, which replaces nothing. |
| 62 | func (s *PlanSubmission) Revised() (plancontract.Diff, bool) { |
| 63 | if s == nil { |
| 64 | return plancontract.Diff{}, false |
| 65 | } |
| 66 | s.mu.Lock() |
| 67 | defer s.mu.Unlock() |
| 68 | if s.attempts < 2 { |
| 69 | return plancontract.Diff{}, false |
| 70 | } |
| 71 | return plancontract.Compare(s.previous, s.plan), true |
| 72 | } |
| 73 |