opencode-ruby 0.0.1.alpha2 → 0.0.1.alpha6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +47 -3
- data/lib/opencode/client.rb +162 -20
- data/lib/opencode/response_parser.rb +2 -2
- data/lib/opencode/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebac8d73ce010347ee830b23a1280b45a6ff71effd1109aad787743ec60fae45
|
|
4
|
+
data.tar.gz: c815940b8e87bcd5724535e987833e360ee9a725081f753cd451f5ae6938f70d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 902785c4c78de0f30affcb638bec979844f2e1481cabbb4ce3a2a0fc23ca8d384ed6e2cdbd1e49da7050599728dfb619b1eb7b7c3f011e7b18a793a00e00ca40
|
|
7
|
+
data.tar.gz: 7e7cc7a596386dd3846dbe1baa91f3cbeba049e46bc8748e05823375ddd07b145849a217ef249d97ff732d3e9f90c3e3e2de1ee7b6ba2021ef30e8cd04bf362d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.1.alpha6 - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Make `Opencode::Client#stream` wait for OpenCode's initial
|
|
8
|
+
`server.connected` SSE readiness frame before submitting `prompt_async`,
|
|
9
|
+
closing the fast-response window where a turn could emit events before the
|
|
10
|
+
client was listening.
|
|
11
|
+
- Keep prompt submission at-most-once across automatic SSE reconnects. A
|
|
12
|
+
reconnect now reopens only the event stream; it never posts the user prompt
|
|
13
|
+
again, and prompt transport failures remain visible to the caller.
|
|
14
|
+
|
|
15
|
+
## 0.0.1.alpha5 - 2026-07-15
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Extend `Opencode::Client#create_session` with OpenCode's native parent,
|
|
20
|
+
agent, model, metadata, and workspace fields while preserving the existing
|
|
21
|
+
title and permission call shape. Session model strings are encoded with the
|
|
22
|
+
session endpoint's `{ providerID, id }` shape rather than the message
|
|
23
|
+
endpoint's `{ providerID, modelID }` shape.
|
|
24
|
+
|
|
25
|
+
## 0.0.1.alpha4 - 2026-07-12
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- End SSE streams on current OpenCode `session.status` idle events while
|
|
30
|
+
retaining compatibility with legacy `session.idle` events.
|
|
31
|
+
- Reconcile every assistant message in the current user turn after multi-step
|
|
32
|
+
tool loops, preserving stream-only parts without duplicating final text.
|
|
33
|
+
- Parse terminal tool parts in standalone Ruby clients without relying on the
|
|
34
|
+
Rails-loaded `Object#in?` extension.
|
|
35
|
+
|
|
36
|
+
## 0.0.1.alpha3 - 2026-07-10
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `Opencode::Client#update_session` for applying permission rules through
|
|
41
|
+
OpenCode's session PATCH endpoint. OpenCode appends these rules, so hosts
|
|
42
|
+
should fingerprint their ordered policy and call this only when it changes.
|
|
43
|
+
|
|
3
44
|
## 0.0.1.alpha2 — 2026-05-20
|
|
4
45
|
|
|
5
46
|
### Added
|
data/README.md
CHANGED
|
@@ -48,6 +48,27 @@ Multi-tenant apps construct multiple clients with different `base_url`s — each
|
|
|
48
48
|
|
|
49
49
|
## Core API
|
|
50
50
|
|
|
51
|
+
### Configured and parent-linked sessions
|
|
52
|
+
|
|
53
|
+
OpenCode can create a session under an existing parent and select its agent,
|
|
54
|
+
model, metadata, workspace, and permission policy in the same request:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
child = client.create_session(
|
|
58
|
+
title: "Destination curator",
|
|
59
|
+
parent_id: parent_session_id,
|
|
60
|
+
agent: "destination-list-curator",
|
|
61
|
+
model: "openai/gpt-5.5",
|
|
62
|
+
metadata: { run_id: "9" },
|
|
63
|
+
workspace_id: workspace_id,
|
|
64
|
+
permissions: permission_rules
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Model strings use OpenCode's `provider/model` form; a preformatted model hash
|
|
69
|
+
with `providerID` and `id` keys is also accepted. These configured-session
|
|
70
|
+
fields require OpenCode 1.16.1 or newer.
|
|
71
|
+
|
|
51
72
|
### Streaming (the headline)
|
|
52
73
|
|
|
53
74
|
```ruby
|
|
@@ -65,6 +86,11 @@ reply.reasoning_text # => the model's hidden reasoning, if any
|
|
|
65
86
|
reply.parts_json # => the full ordered parts array, ready for persistence
|
|
66
87
|
```
|
|
67
88
|
|
|
89
|
+
`stream` waits for OpenCode's initial `server.connected` SSE readiness frame
|
|
90
|
+
before it submits the asynchronous prompt. If the event connection drops
|
|
91
|
+
afterward, the client reconnects only the subscription; it never reposts the
|
|
92
|
+
prompt. This prevents both missed fast responses and duplicate turns.
|
|
93
|
+
|
|
68
94
|
### Synchronous send (no streaming)
|
|
69
95
|
|
|
70
96
|
```ruby
|
|
@@ -72,13 +98,24 @@ result = client.send_message(session_id, "Quick yes/no: is Ruby fun?")
|
|
|
72
98
|
# result is the OpenCode response hash; see API docs for fields.
|
|
73
99
|
```
|
|
74
100
|
|
|
101
|
+
### Updating session permissions
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
client.update_session(session_id, permissions: permission_rules)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
OpenCode appends PATCHed permission rules and evaluates the last matching
|
|
108
|
+
rule. Hosts should send a complete ordered policy and fingerprint it so the
|
|
109
|
+
same policy is not appended on every turn. This endpoint requires OpenCode
|
|
110
|
+
1.16.1 or newer; the rest of the client remains compatible with 1.15.
|
|
111
|
+
|
|
75
112
|
### Lower-level event firehose
|
|
76
113
|
|
|
77
114
|
If you need raw SSE events (every server tick, todo update, prompt asked/replied), use `stream_events` directly:
|
|
78
115
|
|
|
79
116
|
```ruby
|
|
80
117
|
client.stream_events(session_id: session_id) do |event|
|
|
81
|
-
puts event[:type] # "message.part.delta", "todo.updated", "session.
|
|
118
|
+
puts event[:type] # "message.part.delta", "todo.updated", "session.status", ...
|
|
82
119
|
end
|
|
83
120
|
```
|
|
84
121
|
|
|
@@ -101,7 +138,7 @@ begin
|
|
|
101
138
|
rescue Opencode::ConnectionError # server unreachable
|
|
102
139
|
rescue Opencode::TimeoutError # client-side timeout
|
|
103
140
|
rescue Opencode::SessionNotFoundError # 404 on a session
|
|
104
|
-
rescue Opencode::StaleSessionError # session
|
|
141
|
+
rescue Opencode::StaleSessionError # no session event arrived after the prompt
|
|
105
142
|
rescue Opencode::IdleStreamError # mid-turn SSE wedge
|
|
106
143
|
rescue Opencode::ServerError # 5xx
|
|
107
144
|
rescue Opencode::BadRequestError # 4xx other than 404
|
|
@@ -155,7 +192,14 @@ bundle install
|
|
|
155
192
|
bundle exec rake test
|
|
156
193
|
```
|
|
157
194
|
|
|
158
|
-
|
|
195
|
+
The smoke suite covers Client end-to-end against WebMock-stubbed OpenCode
|
|
196
|
+
endpoints, including subscription-before-prompt ordering and
|
|
197
|
+
reconnect-without-repost.
|
|
198
|
+
|
|
199
|
+
Releases use RubyGems trusted publishing. After the repository's
|
|
200
|
+
`release.yml` workflow is registered as a trusted publisher with the `release`
|
|
201
|
+
environment, pushing a `v*` tag builds, attests, and publishes the gem without
|
|
202
|
+
a long-lived RubyGems API key.
|
|
159
203
|
|
|
160
204
|
## License
|
|
161
205
|
|
data/lib/opencode/client.rb
CHANGED
|
@@ -25,8 +25,24 @@ module Opencode
|
|
|
25
25
|
@workspace = workspace
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def create_session(
|
|
29
|
-
|
|
28
|
+
def create_session(
|
|
29
|
+
title: nil,
|
|
30
|
+
permissions: nil,
|
|
31
|
+
parent_id: nil,
|
|
32
|
+
agent: nil,
|
|
33
|
+
model: nil,
|
|
34
|
+
metadata: nil,
|
|
35
|
+
workspace_id: nil
|
|
36
|
+
)
|
|
37
|
+
body = {
|
|
38
|
+
title: title,
|
|
39
|
+
permission: permissions,
|
|
40
|
+
parentID: parent_id,
|
|
41
|
+
agent: agent,
|
|
42
|
+
model: format_session_model(model),
|
|
43
|
+
metadata: metadata,
|
|
44
|
+
workspaceID: workspace_id
|
|
45
|
+
}.compact
|
|
30
46
|
post("/session", body)
|
|
31
47
|
end
|
|
32
48
|
|
|
@@ -115,21 +131,35 @@ module Opencode
|
|
|
115
131
|
on_activity_tick: nil,
|
|
116
132
|
&block
|
|
117
133
|
)
|
|
118
|
-
send_message_async(
|
|
119
|
-
session_id, text,
|
|
120
|
-
model: model, agent: agent, system: system, message_id: message_id
|
|
121
|
-
)
|
|
122
|
-
|
|
123
134
|
reply = Opencode::Reply.new
|
|
124
135
|
reply.add_observer(StreamBlockObserver.new(&block)) if block_given?
|
|
125
136
|
|
|
126
|
-
|
|
137
|
+
# Opening the event stream after prompt_async leaves a race where a fast
|
|
138
|
+
# turn can emit (and finish) before the client is subscribed. Wait for
|
|
139
|
+
# OpenCode's initial server.connected SSE frame, then submit the prompt
|
|
140
|
+
# exactly once. Reconnects invoke on_subscribed again, so mark the attempt
|
|
141
|
+
# before the POST: an ambiguous prompt response must never cause the same
|
|
142
|
+
# turn to be submitted twice.
|
|
143
|
+
prompt_attempted = false
|
|
144
|
+
on_subscribed = lambda do
|
|
145
|
+
next false if prompt_attempted
|
|
146
|
+
|
|
147
|
+
prompt_attempted = true
|
|
148
|
+
send_message_async(
|
|
149
|
+
session_id, text,
|
|
150
|
+
model: model, agent: agent, system: system, message_id: message_id
|
|
151
|
+
)
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
consume_event_stream(
|
|
127
156
|
session_id: session_id,
|
|
128
157
|
timeout: stream_timeout,
|
|
129
158
|
first_event_timeout: first_event_timeout,
|
|
130
159
|
idle_stream_timeout: idle_stream_timeout,
|
|
131
160
|
reply: reply,
|
|
132
|
-
on_activity_tick: on_activity_tick
|
|
161
|
+
on_activity_tick: on_activity_tick,
|
|
162
|
+
on_subscribed: on_subscribed
|
|
133
163
|
) do |event|
|
|
134
164
|
reply.apply(event)
|
|
135
165
|
end
|
|
@@ -144,6 +174,10 @@ module Opencode
|
|
|
144
174
|
execute(request)
|
|
145
175
|
end
|
|
146
176
|
|
|
177
|
+
def update_session(session_id, permissions:)
|
|
178
|
+
patch("/session/#{session_id}", { permission: permissions })
|
|
179
|
+
end
|
|
180
|
+
|
|
147
181
|
def children(session_id)
|
|
148
182
|
uri = build_uri("/session/#{session_id}/children")
|
|
149
183
|
request = Net::HTTP::Get.new(uri)
|
|
@@ -233,8 +267,10 @@ module Opencode
|
|
|
233
267
|
].freeze
|
|
234
268
|
|
|
235
269
|
# Opens SSE connection to GET /event, yields parsed events filtered by session_id.
|
|
236
|
-
# Blocks until session
|
|
237
|
-
# event-stream connections.
|
|
270
|
+
# Blocks until the session reports idle or timeout, reconnecting across
|
|
271
|
+
# dropped event-stream connections. Current OpenCode emits
|
|
272
|
+
# `session.status` with `status.type == "idle"`; older versions emitted the
|
|
273
|
+
# standalone `session.idle` event, so both remain terminal.
|
|
238
274
|
#
|
|
239
275
|
# first_event_timeout: seconds to wait for a session-specific event before
|
|
240
276
|
# declaring the session stale. Server heartbeats don't count — they're global
|
|
@@ -263,6 +299,20 @@ module Opencode
|
|
|
263
299
|
def stream_events(session_id:, timeout: 600, first_event_timeout: 120,
|
|
264
300
|
idle_stream_timeout: nil,
|
|
265
301
|
reply: nil, on_activity_tick: nil, &block)
|
|
302
|
+
consume_event_stream(
|
|
303
|
+
session_id: session_id,
|
|
304
|
+
timeout: timeout,
|
|
305
|
+
first_event_timeout: first_event_timeout,
|
|
306
|
+
idle_stream_timeout: idle_stream_timeout,
|
|
307
|
+
reply: reply,
|
|
308
|
+
on_activity_tick: on_activity_tick,
|
|
309
|
+
&block
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
private def consume_event_stream(session_id:, timeout:, first_event_timeout:,
|
|
314
|
+
idle_stream_timeout:, reply:, on_activity_tick:,
|
|
315
|
+
on_subscribed: nil, &block)
|
|
266
316
|
uri = build_uri("/event")
|
|
267
317
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
268
318
|
first_event_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + first_event_timeout
|
|
@@ -297,6 +347,8 @@ module Opencode
|
|
|
297
347
|
http.open_timeout = 10
|
|
298
348
|
http.read_timeout = 30
|
|
299
349
|
|
|
350
|
+
subscription_callback_error = nil
|
|
351
|
+
subscription_ready = on_subscribed.nil?
|
|
300
352
|
begin
|
|
301
353
|
buffer = String.new
|
|
302
354
|
|
|
@@ -330,6 +382,36 @@ module Opencode
|
|
|
330
382
|
event = parse_sse_event(raw_event, session_id)
|
|
331
383
|
next unless event
|
|
332
384
|
|
|
385
|
+
unless subscription_ready
|
|
386
|
+
# Every supported OpenCode server starts /event with this
|
|
387
|
+
# frame. Receiving it proves the stream body is flowing; on
|
|
388
|
+
# current servers the bus listener is registered eagerly,
|
|
389
|
+
# and on older lazy-stream servers it is the strongest
|
|
390
|
+
# available readiness handshake before prompting.
|
|
391
|
+
next unless event[:type] == "server.connected"
|
|
392
|
+
|
|
393
|
+
begin
|
|
394
|
+
turn_started = on_subscribed.call
|
|
395
|
+
rescue StandardError => error
|
|
396
|
+
# Prompt submission happens inside the open SSE response.
|
|
397
|
+
# Do not mistake its transport failure for an SSE disconnect
|
|
398
|
+
# and hide it behind a reconnect/first-event timeout.
|
|
399
|
+
subscription_callback_error = error
|
|
400
|
+
raise
|
|
401
|
+
end
|
|
402
|
+
if turn_started
|
|
403
|
+
# Before this fix stream_events began only after the prompt
|
|
404
|
+
# POST returned. Preserve those timeout semantics: the turn
|
|
405
|
+
# and first-session-event windows begin after prompt_async
|
|
406
|
+
# succeeds, not while establishing the readiness handshake.
|
|
407
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
408
|
+
deadline = started_at + timeout
|
|
409
|
+
first_event_deadline = started_at + first_event_timeout
|
|
410
|
+
last_meaningful_event_at = started_at
|
|
411
|
+
end
|
|
412
|
+
subscription_ready = true
|
|
413
|
+
end
|
|
414
|
+
|
|
333
415
|
unless event[:type]&.start_with?("server.")
|
|
334
416
|
received_session_event = true
|
|
335
417
|
last_meaningful_event_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -341,13 +423,16 @@ module Opencode
|
|
|
341
423
|
# the reaper doesn't kill it mid-wait.
|
|
342
424
|
on_activity_tick&.call(event)
|
|
343
425
|
block.call(event)
|
|
344
|
-
return if event
|
|
426
|
+
return if terminal_session_event?(event)
|
|
345
427
|
end
|
|
346
428
|
end
|
|
347
429
|
end
|
|
348
430
|
rescue *TRANSIENT_SSE_ERRORS
|
|
431
|
+
raise if subscription_callback_error
|
|
432
|
+
|
|
349
433
|
# Treat transport-level SSE disconnects like clean EOF: reconnect
|
|
350
|
-
# until session
|
|
434
|
+
# until an idle session event, the overall timeout, or first-event
|
|
435
|
+
# timeout.
|
|
351
436
|
ensure
|
|
352
437
|
begin
|
|
353
438
|
http&.finish if http&.started?
|
|
@@ -381,13 +466,16 @@ module Opencode
|
|
|
381
466
|
# the caller's reply is still a usable Result either way.
|
|
382
467
|
def merge_final_exchange(session_id, reply)
|
|
383
468
|
exchange = get_messages(session_id)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
469
|
+
polled = current_turn_parts(exchange)
|
|
470
|
+
return if polled.empty?
|
|
471
|
+
|
|
472
|
+
merged = merge_stream_only_parts(reply.result.parts_json, polled)
|
|
473
|
+
reply.sync_recovered_parts(merged)
|
|
474
|
+
# sync_recovered_parts intentionally never deletes live parts because it
|
|
475
|
+
# is also used during mid-stream recovery. This is the terminal poll, so
|
|
476
|
+
# the wire snapshot is authoritative: remove any replayed trailing wire
|
|
477
|
+
# part after observers have seen recovered additions/updates.
|
|
478
|
+
reply.replace_parts(merged) unless reply.result.parts_json == merged
|
|
391
479
|
rescue Opencode::Error
|
|
392
480
|
# Stream's result is still complete; the merge was a polish, not a
|
|
393
481
|
# requirement.
|
|
@@ -404,6 +492,45 @@ module Opencode
|
|
|
404
492
|
deadline
|
|
405
493
|
end
|
|
406
494
|
|
|
495
|
+
def terminal_session_event?(event)
|
|
496
|
+
return true if event[:type] == "session.idle"
|
|
497
|
+
return false unless event[:type] == "session.status"
|
|
498
|
+
|
|
499
|
+
status = event.dig(:properties, :status)
|
|
500
|
+
status = status[:type] || status["type"] if status.is_a?(Hash)
|
|
501
|
+
status == "idle"
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# OpenCode persists one assistant message per model step. A tool loop can
|
|
505
|
+
# therefore produce several assistant messages for one user turn (for
|
|
506
|
+
# example skill -> task -> final text). Reconcile the complete current turn
|
|
507
|
+
# instead of aligning the live parts array with only the last assistant
|
|
508
|
+
# message, which corrupts tool parts and duplicates final text.
|
|
509
|
+
def current_turn_parts(exchange)
|
|
510
|
+
messages = Array(exchange)
|
|
511
|
+
last_user_index = messages.rindex { |message| message.dig(:info, :role) == "user" }
|
|
512
|
+
current_turn = last_user_index ? messages.drop(last_user_index + 1) : messages
|
|
513
|
+
|
|
514
|
+
current_turn
|
|
515
|
+
.select { |message| message.dig(:info, :role) == "assistant" }
|
|
516
|
+
.flat_map { |message| Opencode::ResponseParser.extract_interleaved_parts(message) }
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def merge_stream_only_parts(stream_parts, wire_parts)
|
|
520
|
+
remaining_wire = Array(wire_parts).dup
|
|
521
|
+
merged = []
|
|
522
|
+
|
|
523
|
+
Array(stream_parts).each do |part|
|
|
524
|
+
if Opencode::PartSource.stream_only?(part)
|
|
525
|
+
merged << part
|
|
526
|
+
elsif remaining_wire.any?
|
|
527
|
+
merged << remaining_wire.shift
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
merged.concat(remaining_wire)
|
|
532
|
+
end
|
|
533
|
+
|
|
407
534
|
def prompt_payload(text, parts:, model:, agent:, system:, message_id:, no_reply:, tools:, format:, variant:)
|
|
408
535
|
message_parts = parts || [ { type: "text", text: text } ]
|
|
409
536
|
{
|
|
@@ -427,6 +554,14 @@ module Opencode
|
|
|
427
554
|
{ providerID: provider, modelID: model_id }
|
|
428
555
|
end
|
|
429
556
|
|
|
557
|
+
def format_session_model(model)
|
|
558
|
+
return nil unless model
|
|
559
|
+
return model if model.is_a?(Hash)
|
|
560
|
+
|
|
561
|
+
provider, model_id = model.split("/", 2)
|
|
562
|
+
{ providerID: provider, id: model_id }
|
|
563
|
+
end
|
|
564
|
+
|
|
430
565
|
def post(path, body)
|
|
431
566
|
uri = build_uri(path)
|
|
432
567
|
request = Net::HTTP::Post.new(uri)
|
|
@@ -434,6 +569,13 @@ module Opencode
|
|
|
434
569
|
execute(request)
|
|
435
570
|
end
|
|
436
571
|
|
|
572
|
+
def patch(path, body)
|
|
573
|
+
uri = build_uri(path)
|
|
574
|
+
request = Net::HTTP::Patch.new(uri)
|
|
575
|
+
request.body = body.to_json
|
|
576
|
+
execute(request)
|
|
577
|
+
end
|
|
578
|
+
|
|
437
579
|
def build_uri(path, scoped: true)
|
|
438
580
|
uri = @uri.dup
|
|
439
581
|
uri.path = path
|
|
@@ -27,7 +27,7 @@ module Opencode
|
|
|
27
27
|
def self.extract_tool_summary(response_body)
|
|
28
28
|
parts = response_body[:parts] || []
|
|
29
29
|
parts
|
|
30
|
-
.select { |p| p[:type] == "tool" && p.dig(:state, :status)
|
|
30
|
+
.select { |p| p[:type] == "tool" && TERMINAL_STATUSES.include?(p.dig(:state, :status)) }
|
|
31
31
|
.map { |p| build_tool_summary(p) }
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -42,7 +42,7 @@ module Opencode
|
|
|
42
42
|
{ "type" => "reasoning", "content" => part[:text] }
|
|
43
43
|
when "tool"
|
|
44
44
|
status = part.dig(:state, :status)
|
|
45
|
-
next unless
|
|
45
|
+
next unless TERMINAL_STATUSES.include?(status)
|
|
46
46
|
|
|
47
47
|
build_tool_summary(part)
|
|
48
48
|
else
|
data/lib/opencode/version.rb
CHANGED