Persist ComboNeedsConfirm plans at confirm tier without executing writes, then run task.run via OwnerMux and a stable idempotency key. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
4.0 KiB
Go
158 lines
4.0 KiB
Go
package intentflow
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"chat-agent-system/internal/app/uow"
|
|
"chat-agent-system/internal/intent"
|
|
)
|
|
|
|
func (p *Plans) handleTurnAutoRead(ctx context.Context, in TurnInput) (*TurnResult, error) {
|
|
tier := CurrentFeatureFlags().PlanTier
|
|
if !tier.Allows(TierAutoRead) {
|
|
return &TurnResult{}, nil
|
|
}
|
|
text := strings.TrimSpace(in.Text)
|
|
if text == "" {
|
|
return &TurnResult{}, nil
|
|
}
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(ctx, text, nil)
|
|
if err != nil || res == nil || res.PlanProposal == nil {
|
|
return &TurnResult{}, nil
|
|
}
|
|
proposal := res.PlanProposal
|
|
confirmWrites := proposal.ComboHint == intent.ComboNeedsConfirm && tier.Allows(TierConfirm)
|
|
readOnly := proposal.ComboHint == intent.ComboReadOnly && tier.Allows(TierAutoRead)
|
|
if (!confirmWrites && !readOnly) || len(proposal.Segments) < 2 || len(proposal.Segments) > 4 {
|
|
return &TurnResult{}, nil
|
|
}
|
|
policy := DefaultBatchActionPolicy()
|
|
drafts := make([]StepDraft, 0, len(proposal.Segments))
|
|
for i, seg := range proposal.Segments {
|
|
key := ""
|
|
if seg.Frame != nil {
|
|
key = seg.Frame.ActionKey
|
|
}
|
|
if key == "" {
|
|
return &TurnResult{}, nil
|
|
}
|
|
row, err := policy.Lookup(key)
|
|
if err != nil || row.RejectPlan || !tier.Allows(row.OpenFromTier) {
|
|
return &TurnResult{}, nil
|
|
}
|
|
if row.RequiresBatchConfirm && !confirmWrites {
|
|
return &TurnResult{}, nil
|
|
}
|
|
summary := strings.TrimSpace(seg.Normalized)
|
|
if summary == "" {
|
|
summary = strings.TrimSpace(seg.Text)
|
|
}
|
|
drafts = append(drafts, StepDraft{
|
|
ActionKey: key,
|
|
ActionDigest: fmt.Sprintf("%s:%d:%s", proposal.Digest, i, key),
|
|
SafeSummary: summary,
|
|
ExecutionMode: ModeLive,
|
|
})
|
|
}
|
|
turnID := in.ClientTurnID
|
|
if turnID == "" {
|
|
turnID = "turn-" + shortDigest(text)
|
|
}
|
|
digest := in.RequestDigest
|
|
if digest == "" {
|
|
digest = shortDigest(text)
|
|
}
|
|
created, err := p.CreatePlan(ctx, CreateCommand{
|
|
Principal: in.Principal,
|
|
ClientTurnID: turnID,
|
|
RequestDigest: digest,
|
|
SegmenterVersion: proposal.SegmenterVersion,
|
|
Status: PlanAwaitingConfirmation,
|
|
ExecutionMode: ModeLive,
|
|
WritePlan: confirmWrites,
|
|
SafeSummary: text,
|
|
Steps: drafts,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if confirmWrites {
|
|
return &TurnResult{Plan: created}, nil
|
|
}
|
|
confirmed, err := p.DecidePlan(ctx, DecisionCommand{
|
|
Principal: in.Principal,
|
|
PlanID: created.PlanID,
|
|
ExpectedRevision: created.Revision,
|
|
DecisionNonce: "auto-read:" + created.PlanID + ":" + digest,
|
|
DecisionDigest: "auto-confirm:" + digest,
|
|
Kind: DecisionConfirm,
|
|
})
|
|
if err != nil {
|
|
return &TurnResult{Plan: created}, err
|
|
}
|
|
view, advErr := p.AdvanceEligible(ctx, "auto-read", confirmed.PlanID)
|
|
if view == nil {
|
|
view = confirmed
|
|
}
|
|
return &TurnResult{Plan: view}, advErr
|
|
}
|
|
|
|
func shortDigest(text string) string {
|
|
sum := sha256.Sum256([]byte(text))
|
|
return hex.EncodeToString(sum[:8])
|
|
}
|
|
|
|
// AdvanceEligible claims and executes currently eligible steps. Independent
|
|
// query failures do not block later independent steps.
|
|
func (p *Plans) AdvanceEligible(ctx context.Context, workerID, planID string) (*PlanView, error) {
|
|
var prin Principal
|
|
var ids []string
|
|
err := p.withTx(ctx, func(ctx context.Context, scope uow.TxScope) error {
|
|
plan, err := p.store.GetPlan(ctx, scope, planID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prin = plan.Principal
|
|
for _, s := range plan.Steps {
|
|
if s.Status == StepEligible || s.Status == StepPending {
|
|
ids = append(ids, s.StepID)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var last *PlanView
|
|
var firstErr error
|
|
for _, id := range ids {
|
|
claimed, claimErr := p.ClaimStep(ctx, workerID, planID, id)
|
|
if claimErr != nil {
|
|
if firstErr == nil {
|
|
firstErr = claimErr
|
|
}
|
|
continue
|
|
}
|
|
if claimed == nil {
|
|
continue
|
|
}
|
|
got, execErr := p.ExecuteClaimed(ctx, claimed)
|
|
if execErr != nil {
|
|
if firstErr == nil {
|
|
firstErr = execErr
|
|
}
|
|
continue
|
|
}
|
|
last = got
|
|
}
|
|
if last == nil {
|
|
return p.GetPlan(ctx, prin, planID)
|
|
}
|
|
return last, firstErr
|
|
}
|