Skip to content

Agent Engine :streamQuery?alt=sse + AG-UI: yielding SSE strings gets JSON-encoded (double-encoding), breaking direct CopilotKit compatibility #3949

@diefergil

Description

@diefergil

🎤 Tell us your idea

Agent Engine :streamQuery?alt=sse + AG-UI: yielding SSE strings gets JSON-encoded (double-encoding), breaking direct CopilotKit compatibility

Summary

I’m deploying an ADK agent to Vertex AI Agent Engine (using vertexai.agent_engines.templates.adk.AdkApp) and trying to expose it as an AG-UI-compatible streaming endpoint so that a frontend like CopilotKit can connect directly.

However, when my async_stream_query generator yields SSE-formatted strings (e.g. "data: {...}\n\n" produced by an AG-UI EventEncoder), Agent Engine appears to serialize those strings as JSON strings inside the SSE transport. This results in “SSE inside JSON inside SSE” (double-encoding) and breaks clients that expect standard AG-UI SSE.

Goal

Enable a clean, direct path:

  • Agent Engine :streamQuery?alt=ssevalid AG-UI SSE
    without requiring an external proxy/middleware.

Current behavior

If the generator yields a string like:

  • data: {"type":"RUN_STARTED",...}\n\n

The client receives SSE where the payload is a quoted JSON string containing escaped newlines, e.g. (illustrative):

data: "data: {\"type\":\"RUN_STARTED\",\"threadId\":\"t1\",\"runId\":\"r1\"}\n\n"

So the consumer would need to:

parse SSE

JSON-decode the string

parse SSE again

Expected behavior (one of these would work)

Option A (recommended): Document/enable a pattern where the generator yields structured events/objects (dict) and Agent Engine’s SSE transport emits:

data: {"type":"RUN_STARTED","threadId":"t1","runId":"r1"}

Option B: Support a “raw SSE passthrough” mode so that if the generator yields an SSE-formatted string, it is streamed verbatim when alt=sse is used.

Minimal repro
Backend (simplified)

from vertexai.agent_engines.templates.adk import AdkApp
from ag_ui.encoder import EventEncoder
from ag_ui.core import RunStartedEvent

class MyAgentEngineApp(AdkApp):
    async def async_stream_query(self, *, user_id: str, message: str, **kwargs):
        encoder = EventEncoder()
        # encoder.encode(...) returns a string like: "data: {...}\n\n"
        yield encoder.encode(RunStartedEvent(thread_id="t1", run_id="r1"))

        # ... later
        yield encoder.encode({"type": "RUN_FINISHED"})

Call

curl \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  "https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT/locations/LOCATION/reasoningEngines/RESOURCE_ID:streamQuery?alt=sse" \
  -d '{
    "class_method":"stream_query",
    "input": {
      "user_id":"u_123",
      "session_id":"s_123",
      "message":"hello"
    }
  }'

Why this matters

AG-UI clients (and CopilotKit integrations that rely on AG-UI) expect standard SSE where each data: line is a JSON object for the event. Double-encoding forces brittle client-side hacks or extra infrastructure.

Questions

What is the recommended way to expose AG-UI from an ADK agent deployed on Agent Engine?

Is there a supported way to stream raw SSE (passthrough) from :streamQuery?alt=sse?

If the correct approach is “yield dicts/objects”:

could docs include a concrete example showing how to translate ADK/agent events → AG-UI events without using EventEncoder.encode() when running on Agent Engine?

References

References

Metadata

Metadata

Assignees

Labels

agent engine[Component] This issue is related to Vertex AI Agent Enginerequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions