Module: Opencode::ToolPart
- Defined in:
- lib/opencode/tool_part.rb
Overview
Canonical shape of a tool part in an assistant reply.
A tool part starts ‘pending` and transitions through `running` to a terminal `completed` or `error`. The complete representation carries seven fields, all string-keyed so views read consistent keys whether the part came from a live streaming event or a post-stream message poll:
"type" => "tool"
"tool" => "edit"
"status" => "completed"
"title" => "Edited /INDEX.md"
"input" => { ... } # full args the agent passed, deep-stringified
"metadata" => { ... } # tool-specific output: diff, preview, stdout, etc.
"output" => "Edited successfully."
"error" => "..." # only when status == "error", truncated to 200 chars
The shape is produced two ways:
1. Opencode::Reply#apply_tool_state — live, mid-stream, merging
incoming event state into an in-memory record (previous values
survive when the new event omits a field).
2. Opencode::ResponseParser.build_tool_summary — post-stream, built
fresh from a complete OpenCode message returned by
/session/:id/message during recovery / final-exchange polling.
Existence reason: the two paths used to drift. ResponseParser stripped ‘metadata` and whitelisted `input` to a fixed key list, so `parts_json` saved on finalize had strictly less data than the streaming DOM had shown. The visible symptom was “I saw the diff while streaming and it disappeared when the turn finished”. This class is the single source of truth that prevents that drift.
Constant Summary collapse
- MAX_ERROR_LEN =
200- INVALID_TOOL =
"invalid"
Class Method Summary collapse
-
.from_message_part(part) ⇒ Object
Build a fresh canonical tool-part hash from one OpenCode message part (the shape that arrives through /session/:id/message).
-
.merge_streaming_state(record, part) ⇒ Object
Merge an incoming ‘message.part.updated` event state into an existing record.
Class Method Details
.from_message_part(part) ⇒ Object
Build a fresh canonical tool-part hash from one OpenCode message part (the shape that arrives through /session/:id/message). Used by ResponseParser for recovery and final-exchange polling.
46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/opencode/tool_part.rb', line 46 def (part) state = state_of(part) build_canonical( tool: part[:tool] || part["tool"], status: state_value(state, :status), title: state_value(state, :title), input: state_value(state, :input), metadata: state_value(state, :metadata), output: state_value(state, :output), error: state_value(state, :error) ) end |
.merge_streaming_state(record, part) ⇒ Object
Merge an incoming ‘message.part.updated` event state into an existing record. Used by Reply#apply_tool_state during streaming.
Fields the event omits (or that arrive empty) leave the record’s previous value intact. Mid-tool events are partial by design.
In addition to the canonical render fields (status, title, input, metadata, output, error), this also persists ‘callID` and `messageID` from the incoming state. Those identifiers are needed by downstream lookups (e.g. matching an ask-user reply event back to the originating tool part by callID) and would otherwise be silently dropped on the way into Reply.parts JSON.
Returns the (mutated) record for chaining.
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 108 109 110 111 112 |
# File 'lib/opencode/tool_part.rb', line 73 def merge_streaming_state(record, part) state = state_of(part) tool = part[:tool] || part["tool"] # Preserve original tool name if OpenCode later renames to "invalid" # mid-session — we want to keep rendering the original name. record["tool"] = tool if tool.present? && tool != INVALID_TOOL status = state_value(state, :status) record["status"] = status if status title = state_value(state, :title) record["title"] = title if title.present? input = state_value(state, :input) record["input"] = stringify_deep(input) if input.present? = state_value(state, :metadata) record["metadata"] = stringify_deep() if .present? output = state_value(state, :output) record["output"] = output if output.present? error = state_value(state, :error) record["error"] = error.to_s.truncate(MAX_ERROR_LEN) if error.present? # callID and messageID moved from state.* to the part's top level # somewhere in opencode v1.15.x. Read top-level first, fall back # to state.* for any older versions that may still be in flight. # Without this, merge_pending_question_into_existing_tool_part # (which searches @parts by callID) silently no-ops, and the # question form renders with no questions or routing IDs. call_id = part[:callID] || part["callID"] || state_value(state, :callID) record["callID"] = call_id if call_id.present? = part[:messageID] || part["messageID"] || state_value(state, :messageID) record["messageID"] = if .present? record end |