Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 5 additions & 27 deletions packages/opencode-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,22 @@ pip install launchdarkly-ai-server launchdarkly-server-sdk launchdarkly-ai-openc

```python
import asyncio
from collections.abc import AsyncIterator

from launchdarkly_ai_opencode_agents import create_opencode_handler
from launchdarkly_ai_server import config, init_client, shutdown


async def session_events(config, user_input, variables, history, session_id):
"""Subscribe to session_id, send user_input, and stop at session.idle."""
del config, variables, history
yield {"type": "message.updated", "properties": {"info": {"id": "m1", "role": "assistant", "time": {"created": 1}}}}
yield {
"type": "message.part.delta",
"properties": {
"sessionID": session_id,
"messageID": "m1",
"partID": "p1",
"field": "text",
"delta": "Hello",
},
}
yield {"type": "session.idle", "properties": {"sessionID": session_id}}


async def main() -> None:
await init_client()
handler = create_opencode_handler(events=session_events, session_id="ses")
async for event in config(
key="my-agent", handler=handler, skip_judges=True
).stream("Hello", {"kind": "user", "key": "user-1"}):
handler = create_opencode_handler(base_url="http://127.0.0.1:4096")
async for event in config(key="my-agent", handler=handler).stream(
"Hello", {"kind": "user", "key": "user-1"}
):
print(event)
await shutdown()


asyncio.run(main())
```

`events` is required. Each invocation calls `events(config, user_input, variables, history, session_id)` and the return value must be a fresh async iterable of that session only. The iterable must end: return it after `session.idle`, or close it yourself. The handler yields AG-UI text and tool events. `config().stream()` yields `chunk` then `done`.

Pass `skip_judges=True`. This handler matches every agent-mode provider, so judge selection can land here. It does not run the judge prompt: that call raises without calling `events`, so the judge is tracked as a failed generation and logged as an error.

Do not pass this handler to `run_judge`. It would be picked as the agent fallback and `run_judge` would raise.
`base_url` is a running `opencode serve`. Each call opens its own session, including a judge routed to this handler. A judge runs as a full OpenCode agent: the server's tools, in the server's working directory, apply to the judge prompt. There is no tool-less judge mode. The handler yields AG-UI text and tool events. `config().stream()` yields `chunk` then `done`.
8 changes: 4 additions & 4 deletions packages/opencode-agents/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ A run opens one `invoke_agent` span and sets `launchdarkly.operation.type`. The

## Call shape

`create_opencode_handler(*, events, session_id, capture_content=None)` returns a `ProviderHandler`. `events` and `session_id` are required. Each invocation calls `events(config, user_input, variables, history, session_id)` and expects a fresh async iterable.
`create_opencode_handler(*, base_url, capture_content=None, model=None, agent=None, client=None)` returns a `ProviderHandler`. `base_url` is a running `opencode serve`. Each call opens its own session.

Assistant text is emitted only after `message.updated` marks that message `role == "assistant"`. A part snapshot is the full text of that part so far; a later delta appends. A text delta without `partID` is dropped.

## Common pitfalls

The AI Config, user input, variables, and history are arguments to `events`. A callable that ignores them never sees the prompt.
The handler posts the prompt for the session it just opened. A caller that only watches a pre-existing session never sees that prompt.

`provides_for` is `('*', 'agent')`, so judge selection still lands here. Pass `skip_judges=True`. The handler does not run that prompt. A judge evaluation raises without calling `events`, so the judge is tracked as a failed generation. Do not pass this handler to `run_judge`, which would raise.
`provides_for` is `('*', 'agent')`, so a judge is routed here too. Each call opens its own session, so the judge prompt does not enter the agent's session and does not need `skip_judges=True`. The judge still runs as a full OpenCode agent, with the server's tools, in the server's working directory. This package does not take a tool-less judge agent.

`session_id` is required. The reducer does not adopt the first session on a shared stream. Other sessions are dropped, including their `session.error`. `session.idle` for this session ends the run. If the iterable is a server-wide stream and you neither send `session.idle` nor close it, `done` never arrives.
The reducer does not adopt the first session on the bus. Other sessions are dropped, including their `session.error`. `session.idle` for this session ends the run.
1 change: 1 addition & 0 deletions packages/opencode-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"launchdarkly-ai-server",
"httpx>=0.27",
"opentelemetry-api>=1.25",
]
description = "OpenCode agent handler for LaunchDarkly AI SDK"
Expand Down
Loading
Loading