| 1 | package tool |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | ) |
| 7 | |
| 8 | // OperationDiagnostic is content-free, host-only recovery metadata. It must |
| 9 | // not be used as an authorization or reconstructed from model-written text. |
| 10 | // AllowedRecovery and RetryBudget make a rejection machine-executable: the |
| 11 | // model picks an offered action instead of guessing another wording of the |
| 12 | // same call, and an exhausted budget means the operation belongs to the user. |
| 13 | type OperationDiagnostic struct { |
| 14 | Code string `json:"code"` |
| 15 | Path string `json:"path,omitempty"` |
| 16 | OperationID string `json:"operation_id,omitempty"` |
| 17 | ExpectedSnapshot string `json:"expected_snapshot,omitempty"` |
| 18 | ActualSnapshot string `json:"actual_snapshot,omitempty"` |
| 19 | RequiredRanges []ReadRange `json:"required_ranges,omitempty"` |
| 20 | Recovery string `json:"recovery"` |
| 21 | // AvailableReceipts are host-issued IDs the model may cite right now. |
| 22 | AvailableReceipts []string `json:"available_receipts,omitempty"` |
| 23 | // AllowedRecovery is the closed set of actions the host will accept, e.g. |
| 24 | // "use_receipt:r_123", "reread_target", "run_verifier", "mark_manual". |
| 25 | AllowedRecovery []string `json:"allowed_recovery,omitempty"` |
| 26 | Retryable bool `json:"retryable,omitempty"` |
| 27 | RetryBudget int `json:"retry_budget,omitempty"` |
| 28 | // State is the operation's host lifecycle state after this failure. |
| 29 | State string `json:"state,omitempty"` |
| 30 | } |
| 31 | |
| 32 | const ( |
| 33 | FSNotObserved = "FS_NOT_OBSERVED" |
| 34 | FSStaleVersion = "FS_STALE_VERSION" |
| 35 | FSNotFound = "FS_NOT_FOUND" |
| 36 | FSAlreadyExists = "FS_ALREADY_EXISTS" |
| 37 | ReadPartial = "READ_PARTIAL" |
| 38 | ReadCursorInvalid = "READ_CURSOR_INVALID" |
| 39 | ReadSourceChanged = "READ_SOURCE_CHANGED" |
| 40 | ReadHardStop = "READ_HARD_STOP" |
| 41 | WriteEvidenceMissing = "WRITE_EVIDENCE_MISSING" |
| 42 | WriteEvidenceStale = "WRITE_EVIDENCE_STALE" |
| 43 | WriteTargetAbsent = "WRITE_TARGET_ABSENT" |
| 44 | WriteTargetAmbiguous = "WRITE_TARGET_AMBIGUOUS" |
| 45 | // OperationNeedsUser is the terminal code for an operation the host stopped |
| 46 | // automating after the same failure twice. |
| 47 | OperationNeedsUser = "OPERATION_NEEDS_USER" |
| 48 | // VerificationReceiptMissing rejects a completion citing verification the |
| 49 | // host has no successful receipt for. |
| 50 | VerificationReceiptMissing = "VERIFICATION_RECEIPT_MISSING" |
| 51 | // VerificationReceiptMismatch rejects a real receipt that does not cover |
| 52 | // the operation being signed off. |
| 53 | VerificationReceiptMismatch = "VERIFICATION_RECEIPT_MISMATCH" |
| 54 | ) |
| 55 | |
| 56 | // Recovery actions the host offers. They are identifiers, not prose, so the |
| 57 | // model selects rather than composes. |
| 58 | const ( |
| 59 | RecoveryRereadTarget = "reread_target" |
| 60 | RecoveryRunVerifier = "run_verifier" |
| 61 | RecoveryMarkManual = "mark_manual" |
| 62 | RecoveryAbandonEdit = "abandon_edit" |
| 63 | RecoveryUseReceipt = "use_receipt:" |
| 64 | ) |
| 65 | |
| 66 | // ModelFacing renders the closed set of recovery choices as compact JSON. The |
| 67 | // model selects an action from allowed_recovery; it never has to reconstruct a |
| 68 | // command string or guess which wording the host will accept. |
| 69 | func (d OperationDiagnostic) ModelFacing() string { |
| 70 | if len(d.AllowedRecovery) == 0 && len(d.AvailableReceipts) == 0 { |
| 71 | return "" |
| 72 | } |
| 73 | payload := struct { |
| 74 | Code string `json:"code"` |
| 75 | OperationID string `json:"operation_id,omitempty"` |
| 76 | Path string `json:"path,omitempty"` |
| 77 | State string `json:"state,omitempty"` |
| 78 | AvailableReceipts []string `json:"available_receipts,omitempty"` |
| 79 | AllowedRecovery []string `json:"allowed_recovery,omitempty"` |
| 80 | Retryable bool `json:"retryable"` |
| 81 | RetryBudget int `json:"retry_budget"` |
| 82 | }{d.Code, d.OperationID, d.Path, d.State, d.AvailableReceipts, d.AllowedRecovery, d.Retryable, d.RetryBudget} |
| 83 | encoded, err := json.Marshal(payload) |
| 84 | if err != nil { |
| 85 | return "" |
| 86 | } |
| 87 | return "recovery: " + string(encoded) |
| 88 | } |
| 89 | |
| 90 | type OperationError struct { |
| 91 | Diagnostic OperationDiagnostic |
| 92 | Cause error |
| 93 | } |
| 94 | |
| 95 | func (e *OperationError) Error() string { |
| 96 | return fmt.Sprintf("%s: %v; %s", e.Diagnostic.Code, e.Cause, e.Diagnostic.Recovery) |
| 97 | } |
| 98 | func (e *OperationError) Unwrap() error { return e.Cause } |
| 99 |