Skip to content

feat(go): real-time SSE message streaming + instant "Thinking…" indicator#480

Merged
zomux merged 1 commit into
openagents-org:developfrom
baryhuang:feat/go-sse-thinking-upstream
Jun 11, 2026
Merged

feat(go): real-time SSE message streaming + instant "Thinking…" indicator#480
zomux merged 1 commit into
openagents-org:developfrom
baryhuang:feat/go-sse-thinking-upstream

Conversation

@baryhuang

Copy link
Copy Markdown
Collaborator

Summary

Makes the OpenAgents Go clients feel real-time: messages arrive push-driven over SSE instead of on a polling timer, and there's instant feedback the moment you send (no dead gap before the agent's first frame). Brings packages/go to parity with the workspace/frontend SSE reference.

Changes

SSE-first message streaming (Swift app + packages/go/web)

Consume /v1/events/stream for push updates, fall back to adaptive polling when the stream is unavailable. While the stream is healthy there is no steady message polling.

  • Swift (WorkspaceAPI.swift): new streamEvents(channel:) — a URLSession.bytes SSE client that yields a signal per data: frame and throws on connect failure.
  • Swift (WorkspaceStore.swift): the message loop is now SSE-first — each frame triggers an incremental forward poll, reusing pollNewMessages' dedup / optimistic-reconcile / notification path rather than a second decode path. Adaptive-polling fallback (1.5s active / 3s idle) on SSE failure; reopens on session switch.
  • Web (packages/go/web): adds getSSEUrl() + the EventSource-first effect in use-polling.ts — now byte-identical to the workspace/frontend reference.

Instant "Thinking…" indicator

The ~1–3s gap after sending is genuine agent latency (nothing has been emitted yet). Show a client-side affordance immediately:

  • Flag-based (awaitingFirstResponseSessionIds) — not a placeholder message, which would reorder against the user's real message during reconciliation.
  • Set on send (gated on the channel having an agent), cleared when the agent emits any output, with a 45s safety timeout.
  • The send button swaps to stop immediately on send.

Backend dependency

SSE delivery uses the existing stream_events Redis pub/sub path. Where Redis isn't configured (REDIS_URL unset), subscribe_events returns immediately and clients transparently fall back to adaptive polling — so this change is safe regardless of backend Redis availability.

Verification

  • xcodebuild OpenAgentsGo_macOSBUILD SUCCEEDED.
  • Verified end-to-end against a Redis-backed deployment: [sse] stream open held open (no reconnect churn), idle message polling drops to zero, ~150ms push latency; "Thinking…" appears instantly on send and is replaced by the agent's first frame.

Test plan

  • Open a chat with an agent, send a message → "Thinking…" appears instantly, replaced by the agent's response on its first frame.
  • With Redis configured: confirm [sse] stream open stays open and no message polling while idle.
  • Without Redis: confirm graceful fallback to adaptive polling.

Bring the OpenAgents Go clients (Swift app + packages/go/web) up to parity
with the workspace/frontend reference: consume the /v1/events/stream SSE
endpoint for push-based message updates, falling back to adaptive polling
when the stream is unavailable. While the stream is healthy there is no
steady message polling — updates arrive push-driven.

Swift (packages/go/OpenAgents):
- WorkspaceAPI.streamEvents(channel:): a URLSession.bytes SSE client that
  yields a signal per `data:` frame and throws on connect failure.
- WorkspaceStore message loop is now SSE-first: each frame triggers an
  incremental forward poll (reusing the dedup / optimistic-reconcile /
  notification path in pollNewMessages rather than a second decode path),
  with an adaptive-polling fallback (1.5s active / 3s idle) on SSE failure.

Web (packages/go/web): adds getSSEUrl() and the EventSource-first effect
in use-polling.ts — now identical to the workspace/frontend reference.

Thinking indicator: show an immediate "Thinking…" row the moment a message
is sent to an agent channel, bridging the genuine agent-latency gap before
its first status frame. Flag-based (awaitingFirstResponseSessionIds) rather
than a placeholder message so it never reorders against the user's real
message during reconciliation; cleared when the agent emits any output or by
a 45s safety timeout. The send button also swaps to stop immediately on send.

SSE delivery requires Redis pub/sub on the backend (the existing
stream_events path). Without it, clients transparently fall back to polling.
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

@baryhuang is attempting to deploy a commit to the Raphael's projects Team on Vercel.

A member of the Team first needs to authorize it.

@zomux zomux 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.

Clean, well-designed SSE streaming + thinking indicator. Signal-based approach on Swift (reusing pollNewMessages) is solid, graceful polling fallback on both clients, proper lifecycle/cancellation handling. LGTM.

@zomux
zomux merged commit 432a5f5 into openagents-org:develop Jun 11, 2026
1 of 2 checks passed
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.

2 participants