diff --git a/pkg/runtime/client.go b/pkg/runtime/client.go index 195e83c9b..947b4e664 100644 --- a/pkg/runtime/client.go +++ b/pkg/runtime/client.go @@ -400,6 +400,10 @@ func (c *Client) runAgentWithAgentName(ctx context.Context, sessionID, agent, ag defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) + // A single SSE line can carry a large tool response; raise the cap + // above bufio's 64 KiB default so an oversized line does not silently + // truncate the stream (bufio.ErrTooLong). + scanner.Buffer(make([]byte, 0, bufio.MaxScanTokenSize), maxSSELineBytes) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 || line[0] == ':' { @@ -438,7 +442,12 @@ func (c *Client) runAgentWithAgentName(ctx context.Context, sessionID, agent, ag eventChan <- e } + // Surface a read failure (e.g. an over-long line) instead of ending + // the stream silently — otherwise the run appears to stop with no + // error after the last event that fit. if err := scanner.Err(); err != nil { + slog.DebugContext(ctx, "event", "scanner_error", err) + eventChan <- Error(fmt.Sprintf("reading event stream: %v", err)) return } }() @@ -541,6 +550,10 @@ func (c *Client) StreamSessionEvents(ctx context.Context, sessionID string) (<-c defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) + // A single SSE line can carry a large tool response; raise the cap + // above bufio's 64 KiB default so an oversized line does not silently + // truncate the stream (bufio.ErrTooLong). + scanner.Buffer(make([]byte, 0, bufio.MaxScanTokenSize), maxSSELineBytes) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 || line[0] == ':' { @@ -579,8 +592,12 @@ func (c *Client) StreamSessionEvents(ctx context.Context, sessionID string) (<-c eventChan <- e } + // Surface a read failure (e.g. an over-long line) instead of ending + // the stream silently — otherwise the run appears to stop with no + // error after the last event that fit. if err := scanner.Err(); err != nil { slog.DebugContext(ctx, "scanner error", "error", err) + eventChan <- Error(fmt.Sprintf("reading event stream: %v", err)) } }() diff --git a/pkg/runtime/defaults.go b/pkg/runtime/defaults.go index 36983ecf7..13ecfc371 100644 --- a/pkg/runtime/defaults.go +++ b/pkg/runtime/defaults.go @@ -12,6 +12,17 @@ import "time" // so the buffer size is set in exactly one place. const defaultEventChannelCapacity = 128 +// maxSSELineBytes bounds a single Server-Sent-Events line the client SSE +// reader will accept. Each event is delivered as one `data: {json}` line, and +// a tool result can be large (e.g. a big tool-response payload), so the default +// bufio.Scanner cap of 64 KiB (bufio.MaxScanTokenSize) truncates the stream: +// the oversized line trips bufio.ErrTooLong, the scan ends, and the run appears +// to stop silently after the preceding event. The reader raises its buffer to +// this ceiling and surfaces any remaining scanner error as an Error event +// rather than closing the stream without a trace. Sized to comfortably hold a +// large tool response while still bounding per-line memory. +const maxSSELineBytes = 16 * 1024 * 1024 + // defaultMaxOverflowCompactions caps the number of consecutive // context-overflow auto-compactions that the run loop will attempt before // giving up and surfacing the error to the caller. The runtime's