Repository navigation
feat(opencode-agents): add an OpenCode handler - #159
Conversation
|
bugbot run |
1 similar comment
|
bugbot run |
|
bugbot run |
|
bugbot run |
1 similar comment
|
bugbot run |
|
cursor review |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
jeffdupont
left a comment
There was a problem hiding this comment.
Reviewed at ec7b918 (own diff on top of #158). Package tests pass (uv run pytest packages/opencode-agents, 13 tests). Finding 3 (session mixing) and 10 were reproduced with a script against this branch.
Must fix
1. The AI Config and the user's input never reach OpenCode (handler.py:263-277, :201-209)
The stream function receives config, user_input, variables, history and tool_handlers, then calls events() with no arguments. In the README example, "Hello" is discarded (README.md:15); changing the model or instructions in the flag has no effect, and multi-turn history is lost. Misses TESTING.md §1.2, §1.3/§1.4, §1.11.
Fix: pass them through, e.g. events(config, user_input, variables, history), and add a test asserting events received them.
2. Judges get routed to this handler (handler.py:298-299 with client/judges.py:36-41,110,142, client.py:147,238)
It claims ('*', 'agent'), and judges use a single handler that covers their provider. A judged config sends the judge prompt here, which is ignored (see 1), and events() runs again: either another full OpenCode run or a replay of the agent answer. parse_judge_response then gets agent text, so the score is None or wrong.
Fix: only claim '*' once the handler actually runs the prompt it's given; until then, at least document and test skip_judges=True.
3. Events aren't scoped to one session, and the stream may never end (handler.py:72-79, :119-137)
The reducer never checks sessionID. Reproduced: an assistant delta from a second session on the same server appears in this run's output ('HiHi OTHER'), and a session.error from any session fails this run (:76-77). Unverified: if OpenCode's /event is a server-wide SSE stream that doesn't close per session (end of session = session.idle, which is ignored here), passing the subscription straight in means done never arrives.
Fix: give events the session id, drop other sessions' events, and return on that session's session.idle. If out of scope, README and agents.md must say the caller has to filter and terminate the iterable.
Should fix
4. Ships as a stable package with no spec section or API surface (release-please-config.json, .release-please-manifest.json, pyproject.toml)
Commit df46a04 switched it from experimental to a normal package. TESTING.md has no OpenCode section; there's no api-surface/python/launchdarkly_ai_opencode_agents.txt; usage-ids.txt has no opencode-agents.* ids (§3.27 wants them in the same PR, §5.2 wants the list merged before first release); no A.14 row.
Fix: monorepo PR first (spec section with stage marker, surface list, usage ids), or ship experimental as 60c14bd did.
5. No JS counterpart. Nothing in js-ai-sdk for OpenCode. Either widen the ticket or record in the spec why it's Python-only (as A.14 does for Vercel/TS-only).
6. No spans (whole handler.py; spec §1.5, §1.6, §1.8)
The other six handlers emit invoke_agent/chat/execute_tool spans with launchdarkly.* attributes. Per §1.5, a run without launchdarkly.operation.type won't show in the AI observability dashboard.
Fix: reuse the client span helpers for at least one invoke_agent root span; add the package to HANDLERS in tests/test_cross_handler_parity.py.
7. A missing events fails late as the wrong error (handler.py:201-205, :212-216)
create_opencode_handler() with no args succeeds; the first request raises OpenCodeSessionError, the same class as a provider failure. Fix: make events a required keyword-only argument (TypeError at build time) and delete the None branch.
8. Wrapper and usage reporting untested (handler.py:228,232-255)
No tests/test_sdk_usage.py like every other package (e.g. openai-agents/tests/test_sdk_usage.py), and no §1.7 test that variables is popped and forwarded.
9. Docs (README.md, agents.md; spec §6.1, §6.2)
The README example uses an undefined session_events and never iterates .stream(...). agents.md lacks the file map, OTel, init_client, dependencies and "common pitfalls" sections; pitfalls is where 1–3 belong.
Nice to have
- A delta without
partIDfollowed by its part snapshot duplicates text ("HiHi") (handler.py:99-103,:146-149). Drop deltas withoutpartID, as snapshots already requireid(:159). - Include
properties.error.name(e.g.ProviderAuthError) in the session error (handler.py:76-77); keeping the provider body out is right.
Verdict: fix 1, 2 and 3 first; 4 and 5 need an owner decision. Not checked: OpenCode's real event schema (message.part.delta, whether tokens.input excludes cache, whether tokens.reasoning counts as output); the judge path at runtime (from reading the code).
|
Addressed Jeff's review in
|
|
bugbot run |
|
Bugbot on
|
|
bugbot run |
Reduce OpenCode session events into text and tool events. config().stream() stays chunk then done, so a tool event does not close it. The experimental rich_stream reader is how a caller opts into those events.
Move the OpenCode handler into launchdarkly-ai-opencode-agents and export rich_stream from launchdarkly_ai_server. The handler reduces caller-supplied session events and does not depend on an OpenCode SDK. Assistant text waits until the message role is assistant, and a part snapshot emits only text that has not already been sent.
init_client() discovers launchdarkly-server-sdk at runtime, so the package README install line has to include it. Co-authored-by: alexis <alexis@launchdarkly.com>
The public stream matches the other handlers. This change does not export a rich stream reader or forward tool events.
events receives the config, input, variables, history, and session id. Other sessions are dropped and session.idle ends the run. A judged config must set skip_judges=True. The run opens an invoke_agent span.
message.updated carries sessionID on info, and message.part.updated carries it on part. A foreign event no longer latches the run.
72708e3 to
c03b511
Compare
| messages.append(text_message(role, content)) | ||
| if isinstance(user_input, str): | ||
| messages.append(text_message("user", user_input)) | ||
| return messages |
There was a problem hiding this comment.
Span input drops structured history
Low Severity
_input_messages only records history items whose content is a string, so the documented block-array history shape never reaches set_input_content_attributes. System turns are also kept, and an empty user_input is appended. The invoke_agent span therefore omits multimodal or structured history that other handlers flatten with content_to_text after compose_history.
Reviewed by Cursor Bugbot for commit c03b511. Configure here.
jeffdupont
left a comment
There was a problem hiding this comment.
Re-checked after the rebase onto main. The OpenCode package is unchanged since 72708e3 and the nested session-id fix is in. One item is still open (inline). I agree with Bugbot's Low finding on structured history at handler.py:296; it can be a follow-up.
| session_id = _session_id(raw) | ||
| if session_id is not None: | ||
| if self._session_id is None: | ||
| self._session_id = session_id |
There was a problem hiding this comment.
Without a session_id, the first session that shows up in the event stream wins. On a server that shares one OpenCode event stream across requests, that can be another user's session, so this run would report their answer. Please require session_id (raise if it's missing) rather than latching onto the first one seen.
There was a problem hiding this comment.
Fixed in c934e2d.
session_id is required. Omitting it, or passing None or "", raises TypeError before a run starts. The reducer no longer adopts the first sessionID on the stream. Events from any other session are ignored, so a shared OpenCode stream cannot report another user's answer. test_session_id_is_required and test_the_first_session_on_the_stream_is_not_adopted cover that.
A shared OpenCode event stream must not latch onto the first session it sees. session_id is required, and every other session is ignored.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c934e2d. Configure here.
jeffdupont
left a comment
There was a problem hiding this comment.
Re-checked at c934e2d: session_id is required on both entry points, and the reducer no longer adopts the first session on the stream. With the session filter removed, test_the_first_session_on_the_stream_is_not_adopted and two other session tests fail, so the fix is pinned. OpenCode suite: 25 passed; ruff and mypy are clean.
Non-blocking: events with no sessionID still pass the filter (documented in the reducer docstring). That's fine while events returns one session's stream, as the README requires.
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> OpenCode session events map onto the shared AG-UI reader. The handler emits text and tool lifecycle events; `config().stream()` still yields `chunk` then `done`. Argument text and results follow `capture_content`. Verified with the OpenCode handler tests. This branch includes the reader so the stable stream keeps dropping tool events. It does not change #159. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-8f118cf9-ed13-58e7-a6ab-e576119e7ca3?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-8f118cf9-ed13-58e7-a6ab-e576119e7ca3&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
|
@apucacao thanks for the
I also checked this against #164 now that it's on |


Summary
Adds
launchdarkly-ai-opencode-agents, a normal handler package withprovides_for('*', 'agent'). It reduces caller-supplied OpenCode session events and does not depend on an OpenCode SDK.config().stream()yieldschunkthendone. Content capture uses the tri-state read from #158.Tests
pytest(2416 passed, 11 skipped),ruff check,ruff format --check, andmypy packages/*/srcwere clean.