返回 DeepSeek-Reasonix
receipt.go
根目录 / internal / evidence / receipt.go
1 package evidence
2
3 import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "strings"
8 )
9
10 // Receipt kinds classify observed execution facts for display and reporting.
11 // They do not authorize, settle, or block later operations.
12 const (
13 ReceiptKindRead = "read"
14 ReceiptKindMutation = "mutation"
15 ReceiptKindVerification = "verification"
16 ReceiptKindReview = "review"
17 ReceiptKindCommand = "command"
18 )
19
20 // Receipt is the host-runtime record of one tool call. It stays in memory for
21 // the current agent turn and is not serialized into prompts or session state.
22 type Receipt struct {
23 // ID is a stable host-issued reference for model-facing evidence citations.
24 ID string `json:"id,omitempty"`
25 OperationID string `json:"operation_id,omitempty"`
26 // ToolCallID links UI inspection to the source call without entering prompts.
27 ToolCallID string `json:"-"`
28 Interrupted bool `json:"-"`
29 Sequence uint64 `json:"-"`
30 ToolName string `json:"tool_name"`
31 Args json.RawMessage `json:"args,omitempty"`
32 Profile string `json:"profile,omitempty"`
33 Success bool `json:"success"`
34 Command string `json:"command,omitempty"`
35 Step string `json:"step,omitempty"`
36 StepProof bool `json:"step_proof,omitempty"`
37 TodoStep *TodoStepMatch `json:"todo_step,omitempty"`
38 Paths []string `json:"paths,omitempty"`
39 Read bool `json:"read,omitempty"`
40 Write bool `json:"write,omitempty"`
41 Mutation bool `json:"mutation,omitempty"`
42 // DeliveryScope separates scratch-only execution from project delivery debt.
43 // It is turn-local evidence and is never persisted or provider-visible.
44 DeliveryScope WriteScope `json:"-"`
45 Todos []TodoItem `json:"todos,omitempty"`
46 // OutputBytes is the host-observed length of the tool's (redacted, trimmed)
47 // output. Content-evidence checks require it to be non-zero so a command
48 // that printed nothing (head -n 0, >/dev/null) can never count as reading.
49 OutputBytes int `json:"output_bytes,omitempty"`
50 // OutputDigest is a bounded host-derived identity for the model-visible
51 // output. Goal progress uses it to distinguish a genuinely changed read or
52 // command result from an exact successful repeat without retaining content.
53 OutputDigest string `json:"output_digest,omitempty"`
54 // ExitCode is the status the child process actually returned. Success only
55 // says the tool call itself completed, so a failing test run the tool
56 // reported cleanly stays distinguishable here. Zero differs from unset.
57 ExitCode *int `json:"exit_code,omitempty"`
58 // Verification is the host's classification of a shell call: one of the
59 // Verification* values. Empty means the host never classified this receipt.
60 Verification string `json:"verification,omitempty"`
61 // PolicyFloor is the session quality floor in force when this write was
62 // committed ("delivery" or empty). Host-only replay fact: the contract
63 // rebuild reads it back so a floor change never rewrites history.
64 PolicyFloor string `json:"policy_floor,omitempty"`
65 }
66
67 // ReceiptRef is the bounded, model-safe projection of a host receipt: enough
68 // to cite a fact by ID, never enough to leak local paths beyond the ones the
69 // call already named, internal state, or raw output.
70 type ReceiptRef struct {
71 ID string `json:"id"`
72 Kind string `json:"kind"`
73 Success bool `json:"success"`
74 OperationID string `json:"operation_id,omitempty"`
75 Paths []string `json:"paths,omitempty"`
76 Digest string `json:"digest,omitempty"`
77 // Summary is display-only. It must never be used as an identity key: that
78 // is what made shell prefixes and quoting differences reject real work.
79 Summary string `json:"summary,omitempty"`
80 }
81
82 // Ref projects the receipt into its citable form.
83 func (r Receipt) Ref() ReceiptRef {
84 return ReceiptRef{
85 ID: r.ID,
86 Kind: r.Kind(),
87 Success: r.Success,
88 OperationID: r.OperationID,
89 Paths: append([]string(nil), r.Paths...),
90 Digest: r.OutputDigest,
91 Summary: receiptSummary(r),
92 }
93 }
94
95 // Kind classifies what a receipt proves. A command is only a verification when
96 // the host recognized the verifier: an unclassified command stays a command,
97 // which is a recorded success rather than a rejection.
98 func (r Receipt) Kind() string {
99 switch {
100 case r.Mutation || r.Write:
101 return ReceiptKindMutation
102 case r.ToolName == "review_report" || r.ToolName == "review":
103 return ReceiptKindReview
104 case r.Command != "":
105 if r.Verification == VerificationPassed || r.Verification == VerificationFailed {
106 return ReceiptKindVerification
107 }
108 if IsVerificationCommand(r.Command) {
109 return ReceiptKindVerification
110 }
111 return ReceiptKindCommand
112 case r.Read:
113 return ReceiptKindRead
114 default:
115 return ReceiptKindCommand
116 }
117 }
118
119 // receiptSummary is a short, bounded label for display. Long commands are
120 // truncated because nothing matches on this text.
121 func receiptSummary(r Receipt) string {
122 summary := strings.TrimSpace(r.Command)
123 if summary == "" {
124 summary = r.ToolName
125 if len(r.Paths) > 0 {
126 summary += " " + r.Paths[0]
127 }
128 }
129 if runes := []rune(summary); len(runes) > 80 {
130 summary = string(runes[:80]) + "…"
131 }
132 return summary
133 }
134
135 // ObserveOutput records the trimmed output size and a compact digest without
136 // retaining model-visible content in the evidence ledger.
137 func (r *Receipt) ObserveOutput(output string) {
138 if r == nil {
139 return
140 }
141 trimmed := strings.TrimSpace(output)
142 r.OutputBytes = len(trimmed)
143 if trimmed == "" {
144 r.OutputDigest = ""
145 return
146 }
147 sum := sha256.Sum256([]byte(trimmed))
148 r.OutputDigest = fmt.Sprintf("%x", sum[:16])
149 }
150
151 // Verification classifications mirror tool.ShellVerification*, duplicated so
152 // this package keeps importing nothing from the tool layer.
153 const (
154 VerificationNotVerification = "not_verification"
155 VerificationNotRun = "not_run"
156 VerificationPassed = "passed"
157 VerificationFailed = "failed"
158 )
159
159 lines GO