Skip to content

feat(agents): OpenCodeHarness session events, and Workspace tools in the example - #2568

Merged
mattzcarey merged 3 commits into
mainfrom
feat/opencode-session-events
Oct 12, 2026
Merged

mattzcarey merged 3 commits into
mainfrom
feat/opencode-session-events

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

OpenCodeHarness's session.events() now returns a snapshot of the session and then its events, in the same shape as PiHarness and ContainerHarness. The OpenCode example gives the model the Workspace tools from @cloudflare/computer (read, write, edit, delete, ls, find, grep, and exec with git) in place of its notes plugin.

This takes the parts of #2483 (thanks @aron-cf) we want on top of the harness from #2513:

const stream = await harness.session(id).events();
render(stream.snapshot); // { messages, pending, busy, permissions }
stream.start((events) => {
  for (const event of events) apply(event);
  // run_start, operation_start, message_start, text_delta, tool_start,
  // tool_end, message_end, operation_end { result }, run_end, ...
});
await stream.stop();
  • events.ts translates OpenCode's typed events. It keeps what they leave out: tool names (session.tool.called has only the call id), and which inputs the current answer is for, so operation_end carries the same result wait() returns.
  • The stream subscribes before reading the snapshot, and buffers until start(), so nothing falls in between.
  • Breaking for the experimental API: events() used to be an async iterable of OpenCode's raw events. Those are still available from log().

pnpm exec vitest run --project harness-opencode ran, with new tests for the stream, tool names, queued follow-ups, abort and failure. Ran the example against Workers AI: write and read a file, and clone a repo with exec.


Devin Review

…the example

session.events() now returns { snapshot, start, stop } with OpenCodeSessionEvents,
the same kinds of event PiHarness and ContainerHarness report, translated from
OpenCode's typed events. OpenCode's raw events stay available from session.log().

The example replaces its notes plugin with the Workspace tools from
@cloudflare/computer, offered to the model by name.

Takes the event union and the Workspace tools from #2483 (Aron).
@mattzcarey mattzcarey mentioned this pull request Oct 12, 2026
@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 908912e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +1169 to +1171
const snapshot = await this.#snapshot(opencode);
projector = new SessionEventProjector(snapshot);
for (const event of early.splice(0)) deliver(projector.project(event));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Snapshot overlap duplicates operation completions

If delivery occurs during events() snapshot reads, SessionEventProjector can seed and replay the same running operation. The stream then emits two operation_end events for one submission.

Learn more

The stream subscribes before reading session state and buffers incoming OpenCode events in early. The pending read can observe an input already delivered while early still contains its delivery event. SessionEventProjector seeds its running IDs from snapshot.pending and then appends the delivered ID during replay. The same input gets two results at run end. Other overlapping events can also reset the answer reconstructed from the snapshot.

Example: A submit delivers op-1 while events() reads the snapshot. The snapshot includes op-1 as running, and early includes its session.inbox.delivered event. Replaying it appends op-1 twice; the next run end reports op-1 twice.

Recommended fix: Reconcile buffered events with the captured snapshot using a consistent event position, or make replay idempotent against snapshot state, including operation IDs and assistant state. Test delivery and step events interleaved with each snapshot read.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 908912e. Replay is now idempotent against the snapshot: the projector skips a delivery for an input it already has as running or has already ended, so overlap can't start or end it twice. Unit tests in tests/events.test.ts cover a delivery that repeats the snapshot, and a repeat after the input ended.

Comment thread packages/agents/src/harness/opencode/harness.ts
Comment thread packages/agents/src/harness/opencode/events.ts
@agent-think

agent-think Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

🔴 agents import sizes: 1 entry point over threshold

Entry point Exports Largest gzip change Size now
🔴 agents/harness/opencode 2 resized up to +1.8 KiB (+144.33%) 3 KiB
🟡 agents/mcp 15 resized up to +3.8 KiB (+0.98%) 396.8 KiB
🟡 agents/mcp/server 1 resized +2.3 KiB (+2.8%) 82.8 KiB
🟡 agents/chat-sdk 5 resized up to +1.3 KiB (+0.49%) 267 KiB
🟡 agents 30 resized up to +1.3 KiB (+0.49%) 264.5 KiB
🟡 agents/workflows 2 resized up to +1.3 KiB (+0.49%) 265.8 KiB
🟡 agents/mcp/client 1 resized +1.3 KiB (+0.81%) 160.1 KiB
Changed exports (56)
Import Gzip change Size now
🔴 agents/harness/opencode#OpenCodeSession +1.8 KiB (+144.33%) 3 KiB
🔴 agents/harness/opencode#OpenCodeHarness +1.5 KiB (+26.11%) 7.3 KiB
🟡 agents/mcp#createMcpHandler +3.8 KiB (+0.98%) 396.8 KiB
🟡 agents/mcp#experimental_createMcpHandler +2.3 KiB (+0.61%) 383.1 KiB
🟡 agents/mcp#createLegacyMcpHandler +2.3 KiB (+0.6%) 383 KiB
🟡 agents/mcp/server#createMcpHandler +2.3 KiB (+2.8%) 82.8 KiB
🟡 agents/mcp#WorkerTransport +2.2 KiB (+0.63%) 352.9 KiB
🟡 agents/mcp#SSEEdgeClientTransport +1.9 KiB (+0.54%) 349.2 KiB
🟡 agents/mcp#StreamableHTTPEdgeClientTransport +1.9 KiB (+0.53%) 349.2 KiB
🟡 agents/mcp#getMcpAuthContext +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#MCP_SERVER_ID_MAX_LENGTH +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#RPCClientTransport +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#normalizeServerId +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#RPCServerTransport +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#DurableObjectEventStore +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#McpAgent +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#ElicitRequestSchema +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/mcp#RPC_DO_PREFIX +1.8 KiB (+0.53%) 349.1 KiB
🟡 agents/chat-sdk#ChatSdkStateAdapter +1.3 KiB (+0.49%) 267 KiB
🟡 agents/chat-sdk#createChatSdkState +1.3 KiB (+0.49%) 267.1 KiB
🟡 agents#getCurrentAgent +1.3 KiB (+0.49%) 264.5 KiB
🟡 agents#__DO_NOT_USE_WILL_BREAK__withInvocationScope +1.3 KiB (+0.49%) 264.5 KiB
🟡 agents#isDurableObjectStorageReset +1.3 KiB (+0.49%) 264.5 KiB
🟡 agents/workflows#AgentWorkflow +1.3 KiB (+0.49%) 265.8 KiB
🟡 agents/chat-sdk#ChatSdkStateAgent +1.3 KiB (+0.49%) 266.3 KiB
🟡 agents/mcp/client#MCPClientManager +1.3 KiB (+0.81%) 160.1 KiB
🟡 agents#unstable_callable +1.3 KiB (+0.49%) 264.6 KiB
🟡 agents#routeAgentRequest +1.3 KiB (+0.48%) 265.2 KiB
🟡 agents#normalizeServerId +1.3 KiB (+0.48%) 264.5 KiB
🟡 agents#isPlatformTransientError +1.3 KiB (+0.48%) 264.5 KiB
🟡 agents#buildAgentUrl +1.3 KiB (+0.48%) 265.2 KiB
🟡 agents#buildAgentPath +1.3 KiB (+0.48%) 265 KiB
🟡 agents#getAgentByName +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#camelCaseToKebabCase +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#__DO_NOT_USE_WILL_BREAK__agentContext +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#DEFAULT_AGENT_STATIC_OPTIONS +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#isDurableObjectCodeUpdateReset +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#MCP_SERVER_ID_MAX_LENGTH +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#SqlError +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#routeAgentEmail +1.3 KiB (+0.48%) 264.7 KiB
🟡 agents#parseSubAgentPath +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#isDurableObjectMemoryLimitReset +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents/chat-sdk#defaultThreadShard +1.3 KiB (+0.48%) 264.5 KiB
🟡 agents/chat-sdk#defaultKeyShard +1.3 KiB (+0.48%) 264.6 KiB
🟡 agents#DurableObjectOAuthClientProvider +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#AGENT_TOOL_MILESTONE_PART +1.3 KiB (+0.48%) 264.4 KiB
🟡 agents#Agent +1.3 KiB (+0.47%) 264.4 KiB
🟡 agents#StreamingResponse +1.2 KiB (+0.47%) 264.4 KiB
🟡 agents#SUB_PREFIX +1.2 KiB (+0.47%) 264.4 KiB
🟡 agents#createHeaderBasedEmailResolver +1.2 KiB (+0.47%) 264.6 KiB

…and 6 more exports in the workflow artifact.

How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared 48d9c36f → 908912ea · workflow run · reported by agent-think[bot]

@pkg-pr-new

pkg-pr-new Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2568

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2568

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2568

hono-agents

npm i https://pkg.pr.new/hono-agents@2568

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2568

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2568

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2568

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2568

commit: 908912e

… after stop

- An input delivered while events() reads the snapshot was both seeded from
  pending and replayed, so it ended twice. The projector now skips deliveries
  of inputs it already has or has ended.
- The answer is reset when inputs settle and when a new run starts, so a run
  with no assistant step no longer reports the previous run's text.
- start() after stop() delivers nothing, and stop() drops buffered events.
@mattzcarey
mattzcarey merged commit 46570f5 into main Oct 12, 2026
20 of 21 checks passed
@mattzcarey
mattzcarey deleted the feat/opencode-session-events branch October 12, 2026 15:51
@github-actions github-actions Bot mentioned this pull request Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant