Skip to content
Open
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
25 changes: 25 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,11 +689,14 @@ func handleInvocationSync(body io.Reader, agentName string) error {

// handleInvocationSSE handles a streaming (200 OK, text/event-stream) invocations response.
// The invocations protocol has a developer-defined SSE format, so we print data lines as they arrive.
// As a fallback, lines that are not SSE control lines (event:, id:, : comment, or blank) are
// printed as raw text output — this supports agents that stream raw text over text/event-stream.
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.

[Medium] Corrupted UTF-8 character in comment

This comment contains UTF-8 mojibake for an en-dash. Replace with an ASCII hyphen or proper en-dash.

func handleInvocationSSE(w io.Writer, body io.Reader, agentName string) error {
scanner := bufio.NewScanner(body)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)

var printed bool
var lastWasRaw bool

for scanner.Scan() {
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.

[Critical] SSE spec non-compliance: data: without trailing space

Both SSE parsers use strings.CutPrefix(line, "data: ") which requires a trailing space. The SSE specification permits data:value (no space). When an agent sends data:hello, it falls through to raw text, producing [agent] data:hello instead of hello.

Suggested fix: Parse data: first, then trim the optional leading space:

Suggested change
for scanner.Scan() {
if after, ok := strings.CutPrefix(line, "data:"); ok {
data := strings.TrimPrefix(after, " ")

Apply to both handleInvocationSSE and readSSEStream for consistency.

line := scanner.Text()
Expand Down Expand Up @@ -728,13 +731,35 @@ func handleInvocationSSE(w io.Writer, body io.Reader, agentName string) error {
printed = true
}
fmt.Fprintln(w, data)
lastWasRaw = false
continue
}

// Skip SSE control lines and blank lines
if line == "" ||
strings.HasPrefix(line, "event:") ||
strings.HasPrefix(line, "id:") ||
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The control-line filter skips event:, id:, comments, and blanks, but not other SSE control fields like retry:. If the server sends retry: <ms> (valid SSE), it will be treated as raw text and printed. Consider adding retry: (and any other expected SSE control fields) to the skip list to avoid leaking protocol lines into user output.

Suggested change
strings.HasPrefix(line, "id:") ||
strings.HasPrefix(line, "id:") ||
strings.HasPrefix(line, "retry:") ||

Copilot uses AI. Check for mistakes.
strings.HasPrefix(line, ":") {
continue
Comment on lines +739 to +743
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

Blank lines are always skipped. For the raw-text fallback, this means consecutive newlines (e.g., paragraph breaks) in a raw text stream will be lost, changing the content/formatting. Consider preserving blank lines once you’ve entered raw mode (e.g., if lastWasRaw is true, emit a newline instead of skipping).

Copilot uses AI. Check for mistakes.
}

// Raw text fallback — agents may stream plain text over text/event-stream
if !printed {
fmt.Fprintf(w, "[%s] ", agentName)
printed = true
}
fmt.Fprint(w, line)
lastWasRaw = true
Comment on lines +746 to +752
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

With the new raw-text fallback, valid SSE lines like data:foo (no space after the colon) will fall through and be printed as raw text because earlier parsing only matches "data: " with a required space. SSE allows an optional single space after data:, so consider handling data: with/without the space and trimming at most one leading space from the payload (consistent with monitor_format.go’s SSE parsing).

Copilot uses AI. Check for mistakes.
Comment on lines +746 to +752
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This concatenates consecutive raw text lines with no separator. Matches the Agents API handler at L983 which does the same for streaming tokens. The test "multiple raw text lines are concatenated" works because the first line has a trailing space in the input ("Hello "). If the intent is token-by-token streaming, a brief comment here would help clarify. If full lines are expected, fmt.Fprintln would match the data: path behavior.

}

if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading response stream: %w", err)
}

if lastWasRaw {
fmt.Fprintln(w)
}

return nil
}

Expand Down
24 changes: 24 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,30 @@ func TestHandleInvocationSSE(t *testing.T) {
agentName: "agent",
wantOutput: "[agent] line1\nline2\nline3\n",
},
{
name: "raw text without data prefix is printed as fallback",
input: "Hello from agent\n",
agentName: "raw-agent",
wantOutput: "[raw-agent] Hello from agent\n",
},
{
name: "multiple raw text lines are concatenated",
input: "Hello \nworld!\n",
agentName: "raw-agent",
wantOutput: "[raw-agent] Hello world!\n",
},
{
name: "mixed SSE data and raw text",
input: "data: sse-line\nraw-text\n\n",
agentName: "mix-agent",
wantOutput: "[mix-agent] sse-line\nraw-text\n",
},
{
name: "SSE comment lines are skipped",
input: ": this is a comment\ndata: content\n\n",
agentName: "test-agent",
wantOutput: "[test-agent] content\n",
},
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The new fallback behavior makes it important to cover additional SSE edge cases in tests, especially data: lines without a space after the colon (valid SSE) and retry: control lines. Adding table cases for those would help prevent regressions where protocol lines get printed or data lines get misclassified as raw text.

Suggested change
},
},
{
name: "SSE data lines without space after colon are handled",
input: "data:content\n\n",
agentName: "test-agent",
wantOutput: "[test-agent] content\n",
},
{
name: "SSE retry lines are ignored",
input: "retry: 1000\ndata: content\n\n",
agentName: "test-agent",
wantOutput: "[test-agent] content\n",
},

Copilot uses AI. Check for mistakes.
}
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.

[Medium] Missing test coverage for raw→data transition and data: without space

The test suite doesn't cover:

  1. Raw text followed by a data: line (the concatenation bug wbreza identified)
  2. data:value without a space after the colon (the spec compliance issue)

Suggested additions:

{
    name:       "raw text followed by SSE data is separated by newline",
    input:      "raw-text\ndata: sse-line\n",
    agentName:  "test-agent",
    wantOutput: "raw-text\n[test-agent] sse-line\n",
},
{
    name:       "SSE data without space after colon is parsed correctly",
    input:      "data:hello\n",
    agentName:  "test-agent",
    wantOutput: "[test-agent] hello\n",
},


for _, tt := range tests {
Expand Down
Loading