Module: AgentCliRuntime::OpenCode::ResultParser
- Defined in:
- lib/agent_cli_runtime/opencode/result_parser.rb
Constant Summary collapse
- MAX_RUN_BYTES =
4 * 1024 * 1024
- MAX_EXPORT_BYTES =
4 * 1024 * 1024
- MAX_FINAL_MESSAGE_BYTES =
1024 * 1024
- MAX_EVENTS =
10_000- MAX_UNKNOWN_EVENTS =
16- TERMINAL_REASONS =
%w[stop length content-filter].freeze
Class Method Summary collapse
- .normalize(captured, requested_route:, profile:) ⇒ Object
- .parse_inspection(output, session_id:, message_id:) ⇒ Object
- .parse_run(stdout) ⇒ Object
Class Method Details
.normalize(captured, requested_route:, profile:) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/agent_cli_runtime/opencode/result_parser.rb', line 109 def normalize(captured, requested_route:, profile:) unless captured.is_a?(CapturedResult) raise ArgumentError, "captured must be an AgentCliRuntime::CapturedResult" end route = requested_route.is_a?(Route) ? requested_route : Route.parse(requested_route) termination = captured.termination return failure_outcome( profile, route, termination, :timed_out, "OpenCode run timed out" ) if termination.timed_out return failure_outcome( profile, route, termination, :cancelled, "OpenCode run was cancelled" ) if termination.cancelled unless termination.success? kind, diagnostic = classify_failure(captured) return failure_outcome( profile, route, termination, kind, diagnostic ) end parsed = parse_run(captured.stdout) inspection = parse_inspection( captured.inspection_output, session_id: parsed.session_id, message_id: parsed. ) actual = inspection.fetch(:route) NormalizedOutcome.new( provider: profile.name, launcher_identity: profile.launcher_identity, kind: :completed, termination: termination, final_message: parsed., final_message_truncated: parsed., identity: RouteIdentity.new(requested: route, actual: actual), usage: inspection.fetch(:usage), diagnostic: nil, unknown_events: parsed.unknown_events, session_id: parsed.session_id, message_id: parsed. ) rescue MalformedOutput => e malformed_outcome(profile, requested_route, captured, e) end |
.parse_inspection(output, session_id:, message_id:) ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/agent_cli_runtime/opencode/result_parser.rb', line 154 def parse_inspection(output, session_id:, message_id:) if output.nil? malformed!("OpenCode sanitized export evidence is required") end bounded_input!(output, MAX_EXPORT_BYTES, "OpenCode sanitized export") export = JSON.parse(output) malformed!("OpenCode sanitized export must be an object") unless export.is_a?(Hash) info = required_hash(export, "info", "export info") unless required_string(info, "id", "export session id") == session_id malformed!("OpenCode sanitized export session does not match the run") end = export["messages"] malformed!("OpenCode sanitized export messages must be an array") unless .is_a?(Array) matches = .filter_map do || next unless .is_a?(Hash) && ["info"].is_a?(Hash) record = .fetch("info") next unless record["id"] == record end unless matches.one? malformed!("OpenCode sanitized export must contain one terminal assistant record") end assistant = matches.fetch(0) unless assistant["role"] == "assistant" && assistant["sessionID"] == session_id malformed!("OpenCode sanitized export terminal record is not correlated") end unless TERMINAL_REASONS.include?(assistant["finish"]) malformed!("OpenCode sanitized export terminal record is incomplete") end provider = required_string( assistant, "providerID", "assistant providerID" ) model = required_string(assistant, "modelID", "assistant modelID") tokens = assistant["tokens"] unless tokens.nil? || tokens.is_a?(Hash) malformed!("OpenCode assistant tokens must be an object") end tokens ||= {} cache = tokens["cache"] unless cache.nil? || cache.is_a?(Hash) malformed!("OpenCode assistant cache tokens must be an object") end cache ||= {} { route: Route.new(provider:, model:), usage: NormalizedUsage.new( input: numeric(tokens, "input", integer: true), output: numeric(tokens, "output", integer: true), cache_read: numeric(cache, "read", integer: true), cache_write: numeric(cache, "write", integer: true), reasoning: numeric(tokens, "reasoning", integer: true), cost: numeric(assistant, "cost", integer: false) ) }.freeze rescue JSON::ParserError => e raise MalformedOutput, Redactor.diagnostic("OpenCode sanitized export is malformed: #{e.}") rescue ArgumentError => e raise MalformedOutput, Redactor.diagnostic(e) end |
.parse_run(stdout) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/agent_cli_runtime/opencode/result_parser.rb', line 30 def parse_run(stdout) bounded_input!(stdout, MAX_RUN_BYTES, "OpenCode run output") session_id = nil terminal = nil texts = [] unknown = [] error_seen = false event_count = 0 stdout.each_line.with_index(1) do |line, line_number| next if line.strip.empty? event_count += 1 malformed!("OpenCode run output contains too many events") if event_count > MAX_EVENTS event = parse_json_line(line, line_number) type = required_string(event, "type", "event type") unless KNOWN_EVENT_TYPES.include?(type) additive_session = validate_additive_session!(event, session_id) session_id ||= additive_session if unknown.length < MAX_UNKNOWN_EVENTS unknown << Redactor.diagnostic( "unknown OpenCode event #{type}", bytes: 128 ) end next end event_session = required_string(event, "sessionID", "event sessionID") session_id ||= event_session malformed!("OpenCode run sessionID changed within one capture") unless session_id == event_session if type == "error" validate_error!(event) error_seen = true next end part = required_hash(event, "part", "event part") validate_part!(part, type, session_id) = required_string(part, "messageID", "part messageID") case type when "text" text = part["text"] malformed!("OpenCode text part must contain text") unless text.is_a?(String) texts << [ , text ] when "step_finish" terminal = terminal_part(part, ) end end malformed!("OpenCode run emitted an error on a zero exit") if error_seen malformed!("OpenCode run has no recognized terminal step") unless terminal unless TERMINAL_REASONS.include?(terminal.fetch(:reason)) malformed!("OpenCode terminal step has an unrecognized finish reason") end = texts.filter_map do |, text| text if == terminal.fetch(:message_id) end.join malformed!("OpenCode terminal assistant message is empty") if .empty? = .bytesize > MAX_FINAL_MESSAGE_BYTES ParsedRun.new( session_id: session_id, terminal_message_id: terminal.fetch(:message_id), terminal_reason: terminal.fetch(:reason), final_message: bounded_string(, MAX_FINAL_MESSAGE_BYTES), final_message_truncated: , preliminary_usage: terminal.fetch(:usage), unknown_events: unknown.compact ) rescue MalformedOutput raise rescue StandardError => e raise MalformedOutput, Redactor.diagnostic(e) end |