Files
Agent-System/internal/app/intentflow/turn.go
T
weichenandCursor aac42936bc fix(intentflow): keep plan step receipts durable under PG events
Normalize empty TEXT[] binds, isolate step-changed aggregates from plan
revision uniqueness, propagate AdvanceEligible errors, and emit
taskrun.completed to the registered notify consumer so finalize no longer
rolls back live receipts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 10:42:20 +00:00

162 lines
4.1 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 {
view, getErr := p.GetPlan(ctx, prin, planID)
if firstErr != nil {
return view, firstErr
}
return view, getErr
}
return last, firstErr
}