| 1 | package provider |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "strings" |
| 6 | ) |
| 7 | |
| 8 | // ToolRunState is local execution evidence; it is never sent as a wire field. |
| 9 | // Unknown legacy results are classified conservatively when interrupted. |
| 10 | type ToolRunState string |
| 11 | |
| 12 | const ( |
| 13 | ToolRunPending ToolRunState = "pending" |
| 14 | ToolRunStarted ToolRunState = "started" |
| 15 | ToolRunRunning ToolRunState = "running" |
| 16 | ToolRunCompleted ToolRunState = "completed" |
| 17 | ToolRunFailed ToolRunState = "failed" |
| 18 | ToolRunCancelled ToolRunState = "cancelled" |
| 19 | ToolRunNotStarted ToolRunState = "not_started" |
| 20 | ToolRunUnknown ToolRunState = "unknown" |
| 21 | ToolRunUserConfirmed ToolRunState = "user_confirmed" |
| 22 | ) |
| 23 | |
| 24 | // ActionIdentity is the stable local identity of one logical tool action. |
| 25 | // It is provider-excluded and must not be inferred from an attempt alone. |
| 26 | type ActionIdentity struct { |
| 27 | SessionID string `json:"session_id,omitempty"` |
| 28 | TurnID string `json:"turn_id,omitempty"` |
| 29 | AttemptID string `json:"attempt_id,omitempty"` |
| 30 | CallID string `json:"call_id,omitempty"` |
| 31 | CanonicalTool string `json:"canonical_tool,omitempty"` |
| 32 | ArgumentDigest string `json:"argument_digest,omitempty"` |
| 33 | ResourceScope string `json:"resource_scope,omitempty"` |
| 34 | } |
| 35 | |
| 36 | // ToolCallRecord is a durable, provider-excluded execution receipt. |
| 37 | type ToolCallRecord struct { |
| 38 | Identity ActionIdentity `json:"identity"` |
| 39 | Arguments json.RawMessage `json:"arguments,omitempty"` |
| 40 | State ToolRunState `json:"state"` |
| 41 | ReadOnly bool `json:"read_only"` |
| 42 | IdempotencyKey string `json:"idempotency_key,omitempty"` |
| 43 | StartedAt int64 `json:"started_at,omitempty"` |
| 44 | FinishedAt int64 `json:"finished_at,omitempty"` |
| 45 | ResultDigest string `json:"result_digest,omitempty"` |
| 46 | EffectSummary string `json:"effect_summary,omitempty"` |
| 47 | Resolution string `json:"resolution,omitempty"` |
| 48 | ResolvedAt int64 `json:"resolved_at,omitempty"` |
| 49 | ResolutionSource string `json:"resolution_source,omitempty"` |
| 50 | InspectionID string `json:"inspection_id,omitempty"` |
| 51 | InspectionState string `json:"inspection_state,omitempty"` |
| 52 | SupersededBy string `json:"superseded_by,omitempty"` |
| 53 | } |
| 54 | |
| 55 | func ToolResultRunState(m Message) ToolRunState { |
| 56 | switch m.ToolRunState { |
| 57 | case ToolRunPending, ToolRunStarted, ToolRunRunning, ToolRunCompleted, ToolRunFailed, ToolRunCancelled, ToolRunNotStarted, ToolRunUnknown, ToolRunUserConfirmed: |
| 58 | return m.ToolRunState |
| 59 | case "": |
| 60 | default: |
| 61 | return ToolRunUnknown |
| 62 | } |
| 63 | text := strings.ToLower(strings.TrimSpace(m.Content)) |
| 64 | if strings.Contains(text, "write outcome unknown:") || text == interruptedToolResult { |
| 65 | return ToolRunUnknown |
| 66 | } |
| 67 | if strings.HasPrefix(text, "cancelled: context cancelled before execution") || strings.HasPrefix(text, "cancelled: tool dispatch was not durable") { |
| 68 | return ToolRunNotStarted |
| 69 | } |
| 70 | if strings.HasPrefix(text, "cancelled:") || strings.Contains(text, "context canceled") || strings.Contains(text, "context cancelled") { |
| 71 | return ToolRunUnknown |
| 72 | } |
| 73 | return ToolRunCompleted |
| 74 | } |
| 75 | |
| 76 | // IsInterruptedPlaceholder identifies the synthetic result inserted while a |
| 77 | // session is loaded. It is not execution evidence and must not override the |
| 78 | // ledger's durable start fact. |
| 79 | func IsInterruptedPlaceholder(m Message) bool { |
| 80 | return m.Role == RoleTool && m.ToolRunState == "" && strings.TrimSpace(m.Content) == interruptedToolResult |
| 81 | } |
| 82 | |
| 83 | // RecordToolRecovery retains legacy interrupted names for older readers while |
| 84 | // new readers distinguish calls proven not to have run from uncertain effects. |
| 85 | func RecordToolRecovery(r *InterruptedTurnRecovery, call InterruptedToolSummary, state ToolRunState) { |
| 86 | if call.Name == "" { |
| 87 | return |
| 88 | } |
| 89 | switch state { |
| 90 | case ToolRunCompleted: |
| 91 | r.CompletedTools = append(r.CompletedTools, call) |
| 92 | case ToolRunFailed: |
| 93 | r.FailedTools = append(r.FailedTools, call) |
| 94 | case ToolRunUserConfirmed: |
| 95 | r.UserConfirmedTools = append(r.UserConfirmedTools, call) |
| 96 | case ToolRunNotStarted, ToolRunCancelled, ToolRunPending: |
| 97 | r.NotStartedTools = append(r.NotStartedTools, call) |
| 98 | r.InterruptedTools = append(r.InterruptedTools, call.Name) |
| 99 | default: |
| 100 | r.UnknownTools = append(r.UnknownTools, call) |
| 101 | r.InterruptedTools = append(r.InterruptedTools, call.Name) |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | // InterruptedTurnRecovery is the durable, |
| 106 | // provider-excluded handoff for an unfinished turn. It contains bounded facts; |
| 107 | // raw partial reasoning remains local for display. |
| 108 | type InterruptedTurnRecovery struct { |
| 109 | TurnID string `json:"turn_id,omitempty"` |
| 110 | AttemptID string `json:"attempt_id,omitempty"` |
| 111 | Cause string `json:"cause,omitempty"` |
| 112 | TerminalStatus string `json:"terminalStatus,omitempty"` // failed | interrupted; absent preserves legacy display |
| 113 | FailureDiagnostic *FailureDiagnostic `json:"failureDiagnostic,omitempty"` |
| 114 | WriteChecks []WriteRecoveryCheck `json:"write_checks,omitempty"` |
| 115 | SatisfiedWrites []InterruptedToolSummary `json:"satisfied_writes,omitempty"` |
| 116 | Pending bool `json:"pending,omitempty"` |
| 117 | CompletedTools []InterruptedToolSummary `json:"completed_tools,omitempty"` |
| 118 | FailedTools []InterruptedToolSummary `json:"failed_tools,omitempty"` |
| 119 | UserConfirmedTools []InterruptedToolSummary `json:"user_confirmed_tools,omitempty"` |
| 120 | InterruptedTools []string `json:"interrupted_tools,omitempty"` |
| 121 | NotStartedTools []InterruptedToolSummary `json:"not_started_tools,omitempty"` |
| 122 | UnknownTools []InterruptedToolSummary `json:"unknown_tools,omitempty"` |
| 123 | ToolCalls []ToolCallRecord `json:"tool_calls,omitempty"` |
| 124 | RequiresUserDecision bool `json:"requires_user_decision,omitempty"` |
| 125 | SilentInterruption bool `json:"silent_interruption,omitempty"` |
| 126 | DroppedPartialText bool `json:"dropped_partial_text,omitempty"` |
| 127 | DroppedPartialReasoning bool `json:"dropped_partial_reasoning,omitempty"` |
| 128 | } |
| 129 | |
| 130 | // InterruptedToolSummary records a completed, fully paired tool call without duplicating arguments or results. |
| 131 | // The canonical assistant/tool messages immediately before the recovery record remain the source of truth. |
| 132 | type InterruptedToolSummary struct { |
| 133 | ID string `json:"id,omitempty"` |
| 134 | Name string `json:"name"` |
| 135 | Files []string `json:"files,omitempty"` |
| 136 | Added int `json:"added,omitempty"` |
| 137 | Removed int `json:"removed,omitempty"` |
| 138 | } |
| 139 | |
| 140 | // WriteRecoveryCheck reports a current postcondition, not an execution receipt. |
| 141 | type WriteRecoveryCheck struct { |
| 142 | CallID string `json:"call_id"` |
| 143 | Path string `json:"path"` |
| 144 | State string `json:"state"` |
| 145 | } |
| 146 |