
Streaming in Go: io.Reader, bufio.Scanner, server-sent events, and http.Flusher
Reading and writing a stream in Go: io.Reader, bufio.Scanner, server-sent events, and flushing.
Stage 2: Call the modelA language model writes its reply one token at a time (a token is a short piece of text, often a word or part of one).
A program can wait until the whole reply exists and receive it in one piece, or it can ask the API to send each piece as it is written.
The second way is called streaming.
This page covers the Go that streaming needs, from the byte-level contract of io.Reader up to a server that sends its own stream, so that you can read one and write one.
The code is from the companion repository, where it is tested.
It assumes you know the HTTP client basics and have read the page on iterators.
Why an agent streams
Without streaming, the program sends a request and then has nothing to show until the model has written the whole reply, which can take many seconds. With streaming, the first words arrive almost at once and the rest follow as they are written, so a person watching sees text appear, and the program learns early when a connection has died. Streaming model replies gives the measured numbers: the first text shows up well before the unstreamed reply does, while the total time barely changes. The model takes about as long either way; streaming changes when you see the reply.
The idea underneath: io.Reader
A response body in Go is an io.Reader, an interface with one method:
type Reader interface {
Read(p []byte) (n int, err error)
}
You pass Read a slice, and it copies some bytes into it and tells you how many.
The documentation (go doc io.Reader) sets out the contract, and each part matters for streams:
Readreads up tolen(p)bytes. By convention, when some data is available but notlen(p)bytes, it returns what is available instead of waiting for more. On a stream, then, oneReadcall may return a single token’s worth of bytes. Never assume aReadfills the slice.Readmay wait. On anhttp.Response.Body, a call blocks until bytes arrive from the network, the stream ends, or something fails. That waiting is how a stream’s bytes “arrive over time” in a Go program: you callRead, and it returns when there is something to return.Readcan returnn > 0and an error together. The caller must process thenbytes first and only then look aterr.- The end of the data is the error
io.EOF. It means “no more input”, the normal way for data to end. A reader may return the final bytes witherr == io.EOFin the same call, or return them with a nil error and reportio.EOFwithn == 0on the next call. - A
Readthat returns0, nilmeans nothing happened, and does not mean the end.
You rarely write a Read loop yourself.
A few helpers in the standard library do it for you: io.ReadAll reads until io.EOF and returns everything, io.Copy moves everything from a reader to a writer, and bufio.Scanner hands you one line at a time.
Of the three, only the last is any use for a reply that should show up as it arrives, because the other two do not return until the stream ends.
Reading line by line: bufio.Scanner
The format a model API streams in is text with one item per line, so the natural tool is bufio.Scanner, which wraps a reader and splits what it reads into lines:
sc.Scan()reads the next line and reports whether there was one. It returnsfalseat the end of the input or on an error. By default the line ends at\n, and a trailing\rbefore it is dropped (bufio.ScanLines).sc.Bytes()returns the line as a[]byte, andsc.Text()returns astringcopy of it.sc.Err()returns the first error that was notio.EOF. A normal end of input leaves it nil, so after aScanloop ends,Err() == nilmeans the reader reachedio.EOF.
Bytes is cheaper, since it makes no copy, but the slice is only valid until the next call to Scan: the documentation warns that “the underlying array may point to data that will be overwritten by a subsequent call to Scan”.
If you keep a line past the next Scan, store sc.Text() or a copy of the bytes, never sc.Bytes() itself.
The reader in the next sections uses Bytes safely, because it finishes with each line before it scans the next one.
The 64 KiB line limit
A scanner holds a whole line in memory, so it caps the line length.
The default cap is bufio.MaxScanTokenSize, 64 KiB (65,536 bytes).
A line longer than that stops the scan: Scan returns false, and Err() returns bufio.ErrTooLong.
The same stop happens in the middle of a stream, so the lines after the long one are never read.
A model’s stream can reach that length, because a tool call’s arguments are sent as one string and a long argument can come through on a single data: line.
Scanner.Buffer raises the limit.
It takes the starting buffer and the largest size the buffer may grow to, and it must be called before the first Scan (it panics otherwise).
The companion reader does this on its second line:
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 0, 64<<10), maxBody)
The buffer starts at 64 KiB and may grow to maxBody, the client’s 16 MiB response cap, so the reader copes with an enormous line without allowing an unbounded one.
The test TestStreamLongLine feeds the reader a 100 KiB line to check it.
What the stream looks like: server-sent events
Server-sent events (SSE) is a plain-text format for a server to push messages to a client over one open HTTP response.
It is defined in the WHATWG HTML Standard, and its content type is text/event-stream.
A model API streams its reply in this format, and the example server later on this page writes it too.
The format is made of lines. An event is a group of lines ended by a blank line. Each line is one of these:
- A line starting with a colon (
:) is a comment, and a reader ignores it. Servers send comments as a keep-alive, so that a connection with nothing to say does not look dead. The standard suggests one about every 15 seconds, since some proxies drop a connection that stays silent. - Any other line is a field: a name, a colon, and a value, with one space after the colon removed if there is one.
The field
dataholds the event’s payload. If an event has severaldatalines, the reader joins them with newlines into a single payload. - The field
eventnames the event’s type, so that a client can handle different kinds differently. - The field
idsets an ID for the event. When a browser’sEventSourcereconnects after a dropped connection, it sends the last ID it saw in aLast-Event-IDheader. - The field
retrysets how many milliseconds the client waits before it reconnects. - A field with any other name is ignored.
- A blank line ends the event, and that is when the client delivers it.
A made-up stream that uses each part:
: keep-alive
event: greeting
id: 7
data: hello
data: first line
data: second line
The first event is a comment alone, which delivers nothing.
The second has type greeting, ID 7, and the data hello.
The third has the data first line and second line joined by a newline.
The standard also says that if the stream ends in the middle of an event, before its blank line, the incomplete event is thrown away.
What a model API sends
A model API’s stream uses only some of this.
Each event is a single data: line holding a JSON chunk, followed by a blank line, and the last event is the text data: [DONE]:
: OPENROUTER PROCESSING
data: {"choices":[{"delta":{"content":"Hel"}}]}
data: {"choices":[{"delta":{"content":"lo"},"finish_reason":"stop"}]}
data: [DONE]
Those chunks are shortened by hand; a real one carries more fields, as the recorded stream on Streaming model replies shows.
The [DONE] marker comes from OpenAI-compatible APIs, which OpenRouter follows; the SSE standard doesn’t define it.
A general SSE client treats it as ordinary data, so a Go reader for these APIs has to look for it.
Each JSON chunk holds a delta, the piece of the reply added since the previous chunk.
Reading a stream in Go
Here is the companion reader, from the client that talks to OpenRouter:
// sseData reads a server-sent event stream and yields the data of each
// "data:" line until "data: [DONE]". It treats each data line as a
// whole event, which is what OpenAI-compatible servers send. Comment
// lines, which start with a
// colon and keep a quiet connection open, and blank lines are skipped.
// A stream that ends without [DONE] yields io.ErrUnexpectedEOF.
func sseData(r io.Reader) iter.Seq2[[]byte, error] {
return func(yield func([]byte, error) bool) {
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 0, 64<<10), maxBody)
for sc.Scan() {
data, ok := bytes.CutPrefix(sc.Bytes(), []byte("data:"))
if !ok {
continue // a comment, a blank line, or another field
}
data = bytes.TrimPrefix(data, []byte(" "))
if string(data) == "[DONE]" {
return
}
if !yield(data, nil) {
return
}
}
err := sc.Err()
if err == nil {
err = io.ErrUnexpectedEOF
}
yield(nil, err)
}
}
Reading it from the top:
sseDatatakes anio.Reader, which in practice is the response body, and returns aniter.Seq2[[]byte, error]. That is an iterator of pairs: you range over it withfor data, err := range sseData(body), and each step gives you one payload and an error that is nil unless something went wrong. Because the function takes a plainio.Reader, a test can hand it astrings.Readerand no network is involved.- It builds the scanner and raises its line limit, as above.
- The loop calls
sc.Scan(), which waits for the next line, and takes the prefixdata:off the line withbytes.CutPrefix. A line without that prefix (a comment, a blank line, or anevent:orid:line) is skipped withcontinue. - It removes one space after the colon, as the standard says to, and checks for the
[DONE]marker. On[DONE]itreturns, which ends the iteration normally. - Otherwise it calls
yield(data, nil), which hands the payload to the loop body. Ifyieldreturns false, the caller has left its loop withbreakorreturn, so the function stops too, without reading any more. - When
Scanreturns false, the stream is over, and the function has to decide why. Ifsc.Err()is not nil, something failed: the connection broke, the context was cancelled, or a line was too long, and that error is yielded. If it is nil, the reader reachedio.EOFwithout ever seeing[DONE], so the function yieldsio.ErrUnexpectedEOF.
The last case is deliberate.
For the client, io.EOF alone does not mean the reply is complete, because a connection that drops in the middle of a reply can also end with io.EOF.
Only [DONE] says the server finished, so a stream that ends without it is an error.
The code that reads the chunks decides what to do next; Streaming model replies shows it accepting a reply that already carried its finish reason and treating anything else as incomplete.
What this reader leaves out
The reader follows the parts of the standard that these APIs use and simplifies the rest:
| Standard | This reader |
|---|---|
A data field’s lines are joined, and the event is delivered at the blank line |
Each data: line is yielded at once as its own event, which is how OpenAI-compatible servers send them |
A line data with no colon is a data field with an empty value |
Not recognized, so the line is skipped |
event, id, and retry fields have effects |
Skipped like any other line |
A line can end in \n, \r\n, or a lone \r |
bufio.ScanLines, which accepts \n and \r\n |
| A leading byte order mark is removed | Not handled |
| An event cut off before its blank line is discarded | A data: line is yielded as soon as it is read |
None of these differences matters for the APIs the book uses.
They would matter for a reader of arbitrary SSE servers, which is what a browser’s EventSource is, so use a library for that case.
Stopping a stream
A stream can run for minutes, so a program needs ways to end one early, and a way to notice when one has stopped by itself.
Cancel the context.
The request was built with a context, and cancelling that context ends the request.
The connection closes, and a Read that was waiting on the body returns an error.
The scanning loop ends, and sc.Err() reports the cancellation (context canceled).
The same chain makes a person’s Ctrl-C stop a run, since the context that Ctrl-C cancels is the parent of every request.
Close the body.
Every response from http.Client.Do has a body that the caller must close, even when it reads nothing from it.
For a stream the rule matters more, since a stream is long-lived and the body holds the connection open.
The usual form is one line, right after the error check:
wire := c.wireRequest(req)
wire.Stream = true
wire.StreamOptions = &streamOptions{IncludeUsage: true}
resp, err := c.post(ctx, wire) // the wait for headers counts
idle.Stop() // from here, idleReader arms it
if err != nil {
yield(agent.Delta{}, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
yield(agent.Delta{}, statusError(resp, body))
return
}
body := &idleReader{resp.Body, idle, c.idleTimeout()}
readStream(ctx, body, yield)
}
}
The defer runs however the function ends, whether the loop finished, the consumer broke out, or an error stopped it.
The net/http documentation says the default transport may not reuse a connection when the body is not read to completion and closed, so a skipped Close also costs the reuse that the HTTP client page describes.
Notice a stream that goes quiet.
A connection can stay open while the server sends nothing at all, and a Read on it can then wait with no end in sight.
A deadline on the whole request would also cut off healthy streams that are simply long, so the client uses an idle timeout: a limit on the gap between arrivals instead of the total time.
Streaming model replies covers the design and the numbers behind the 30-second default.
The mechanism is a small reader that wraps the body.
The reader that restarts the timer:
// idleReader runs an idle timer only while it waits for the network,
// so the timer fires when the stream itself has gone quiet for the
// whole timeout, not when the program is busy with what it already
// has. If the timer has already fired, Reset runs it again, which is
// harmless: cancelling twice keeps the first cause.
type idleReader struct {
r io.Reader
timer *time.Timer
timeout time.Duration
}
func (ir *idleReader) Read(p []byte) (int, error) {
ir.timer.Reset(ir.timeout)
n, err := ir.r.Read(p)
ir.timer.Stop()
return n, err //nolint:wrapcheck // Reader errors pass as is
}
ir.r is the real body.
Before every Read, the timer is reset to the timeout, and after the Read returns it is stopped.
The timer therefore runs only while the program is waiting on the network.
Time the program spends handling a chunk it already has does not count against the stream.
If the timer fires, a function started by time.AfterFunc cancels the stream’s context with the cause ErrStreamIdle, which makes the blocked Read fail, and the caller can tell an idle stream from a Ctrl-C by that cause.
A comment line is bytes like any other, so the : OPENROUTER PROCESSING keep-alives restart the timer, which TestStreamCommentsKeepItAlive checks.
This works because idleReader is itself an io.Reader: it satisfies the same interface as the body it wraps, so the scanner needs no change.
Wrapping a reader in another reader is the usual way to add behavior to a stream in Go, and io.LimitReader and io.TeeReader work the same way.
Writing a stream from a Go server
The other end of a stream is a server that sends one, as the book’s agent does when it shows a run in a browser.
A handler writes its response in pieces, with one write per event.
The handler’s http.ResponseWriter does not send each write at once, though.
It collects small writes in a buffer and sends them when the buffer fills or the handler returns, which is efficient for an ordinary page and wrong for a stream.
So a handler that writes one event and then waits a second, without flushing, sends nothing during that second: the client gets the event only when the handler finishes.
To send what has been written so far, the handler flushes.
The older way is the http.Flusher interface, which has one method, Flush(), and which a handler gets by checking the writer with a type assertion (w.(http.Flusher)).
The net/http documentation says that a handler should always test for it at runtime, because a wrapper around a ResponseWriter may not provide it.
The current way is http.ResponseController, added in Go 1.20:
rc := http.NewResponseController(w)
err := rc.Flush()
Its Flush method does the same job and returns an error.
It finds the flush method through wrappers that expose an Unwrap method, and when the writer cannot flush at all it returns an error you can check, where a Flusher assertion leaves you to handle the missing method yourself.
(A ResponseWriter with no flush support gives the error feature not supported.)
The controller also has SetWriteDeadline, which limits how long one write may take, so that a client that has stopped reading cannot hold the handler forever.
The documentation notes that a write after the deadline “will not block, but may succeed if the data has been buffered”, so the deadline is paired with a flush: the flush is what forces the write onto the network, where the deadline can apply.
The companion’s example server sends each event with one small function:
// stream writes an event stream to one client.
type stream struct {
w io.Writer
rc *http.ResponseController
wait time.Duration
}
// send writes msg and flushes it to the client at once. A client that
// doesn't take it within s.wait is dropped: the write fails, and the
// handler stops.
func (s *stream) send(msg string) error {
err := s.rc.SetWriteDeadline(time.Now().Add(s.wait))
if err != nil {
return fmt.Errorf("set write deadline: %w", err)
}
if _, err := io.WriteString(s.w, msg); err != nil {
return fmt.Errorf("write event: %w", err)
}
if err := s.rc.Flush(); err != nil {
return fmt.Errorf("flush event: %w", err)
}
return nil
}
It sets a deadline, writes the event text, and flushes. Calling it once per event is what makes the stream appear to a client as the agent runs.
A handler also sets the response headers, before the first write, since the headers go out with the first flush. The example’s handler sets three:
h := w.Header()
h.Set("Content-Type", "text/event-stream")
h.Set("Cache-Control", "no-cache")
h.Set("X-Accel-Buffering", "no")
Content-Type: text/event-stream tells the client what it is receiving, and a browser’s EventSource fails the connection when the response has any other type.
Cache-Control: no-cache asks caches not to reuse the response without checking with the server first.
X-Accel-Buffering: no is for nginx, which by default buffers a proxied response and would hold the events back; the header asks it to pass them straight through.
The server code is a larger topic than this page: it also starts the agent in its own goroutine, sends heartbeat comments, and stops the run when the client leaves. Streaming an agent’s progress to a browser walks through all of it.
Mistakes to avoid
- Forgetting to flush. Without
Flush, the events wait in the server’s buffer and a client sees them in a burst at the end, or when the buffer fills, which makes the stream pointless. - Holding
sc.Bytes()past the nextScan. The slice can be overwritten. Copy the bytes, or usesc.Text(). - A line longer than the scanner’s buffer. The default 64 KiB limit ends the scan with
bufio.ErrTooLong, and tests with short lines never find it. CallBufferbefore the firstScan. - Not closing the body. The connection is held open and cannot be reused.
Put
defer resp.Body.Close()right after the error check. - Treating
io.EOFas a failure. It is how everyReadloop normally ends.bufio.Scanneralready hides it, so after the loop,Err() == nilmeans the input ended normally. - Reading a stream with
io.ReadAll. It returns only when the stream ends, so the program gets nothing until the end, which gives up the whole reason to stream. On a stream that never ends it also never returns. - Treating a missing
[DONE]as success. A stream that ends without the marker may be a reply cut off by a dropped connection. Report it as an error, as the companion reader does. - Forgetting that the browser reconnects. When an
EventSourcestream ends, the browser reconnects on its own, and for a stream that starts work, a reconnect starts the work again. The example page closes theEventSourceitself when it sees the end. - A proxy that buffers the response.
http.Flusher’s documentation warns that behind a proxy, buffered data may not reach the client until the response completes. Check the proxy’s settings, and send a header likeX-Accel-Buffering: nowhere one exists.
Try it
Everything here runs offline, with no API key.
From the code directory of a clone of the companion repository:
-
Run two tests of the reader. The first sends a recorded stream in pieces of 1, 7, and 4,096 bytes, so that the reader proves it does not depend on how
Readsplits the bytes. The second sends a 100 KiB line.terminal $ go test ./ch06/openrouter/ -run 'TestStreamSplitAnywhere|TestStreamLongLine' -v === RUN TestStreamSplitAnywhere --- PASS: TestStreamSplitAnywhere (0.08s) === RUN TestStreamLongLine --- PASS: TestStreamLongLine (0.01s) PASS ok github.com/blackwell-systems/go-agents-book/code/ch06/openrouter 0.929s -
Start the example server, which serves a scripted run of the agent.
terminal $ go run ./ssebrowser open http://127.0.0.1:8080 -
In a second terminal, read its stream with curl. The
-Nflag turns off curl’s own output buffering, which would otherwise hold lines back and hide the streaming. Add-ito see the response headers too.terminal $ curl -s -N -i 'http://localhost:8080/events?task=fix' HTTP/1.1 200 OK Cache-Control: no-cache Content-Type: text/event-stream X-Accel-Buffering: no Date: Fri, 23 Oct 2026 08:54:40 GMT Transfer-Encoding: chunked : run started event: text data: {"text":"I'll "} event: text data: {"text":"run "}The lines keep arriving for about ten seconds, a few words at a time. In one run, the stream held 77
textevents, 6callevents, 6resultevents, 3turnevents, and a finalstoppedevent. YourDateline will differ. Compare the output with the format section above: the first line is a comment, and each event is anevent:line, adata:line, and a blank line. -
Stop the server with Ctrl-C in its terminal. It shuts down and ends any stream still open.
If port 8080 is taken, add -addr localhost:8099 to the go run command and use that port in the curl URL.
Where next
- Streaming model replies builds on this page: tool calls that arrive in fragments, errors in the middle of a stream, and the numbers behind the idle timeout.
- Streaming an agent’s progress to a browser is the full server: heartbeats, a deadline on every write, and a shutdown that ends open streams.
- Iterators in an agent explains the
iter.Seq2shape thatsseDatareturns. - Context explains the cancellation that stops a stream.
var sc *bufio.Scanner
var sc *bufio.Scanner. In ch06/openrouter/sse.go, line 21.
package bufio ("bufio")
Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O.
Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O. package bufio ("bufio"). Documented at pkg.go.dev, bufio.
func NewScanner(r io.Reader) *Scanner
NewScanner returns a new [Scanner] to read from r.
NewScanner returns a new [Scanner] to read from r. func NewScanner(r io.Reader) *Scanner. Documented at pkg.go.dev, bufio.NewScanner.
func (s *Scanner) Buffer(buf []byte, max int)
Buffer controls memory allocation by the Scanner.
Buffer controls memory allocation by the Scanner. func (s *Scanner) Buffer(buf []byte, max int). Documented at pkg.go.dev, bufio.Scanner.Buffer.
func make(t Type, size ...IntegerType) Type
The make built-in function allocates and initializes an object of type slice, map, or chan (only).
The make built-in function allocates and initializes an object of type slice, map, or chan (only). func make(t Type, size ...IntegerType) Type. Documented at pkg.go.dev, builtin.make.
type byte = uint8
byte is an alias for uint8 and is equivalent to uint8 in all ways.
byte is an alias for uint8 and is equivalent to uint8 in all ways. type byte = uint8. Documented at pkg.go.dev, builtin.byte.
const maxBody = 16 << 20
maxBody is the most of a response the client reads: far more than any reply, but a limit on a broken server.
maxBody is the most of a response the client reads: far more than any reply, but a limit on a broken server. const maxBody = 16 << 20. In ch06/openrouter/client.go, line 35.
func sseData(r io.Reader) iter.Seq2[[]byte, error]
sseData reads a server-sent event stream and yields the data of each "data:" line until "data: [DONE]".
sseData reads a server-sent event stream and yields the data of each "data:" line until "data: [DONE]". func sseData(r io.Reader) iter.Seq2[[]byte, error]. In ch06/openrouter/sse.go, line 19.
package io ("io")
Package io provides basic interfaces to I/O primitives.
Package io provides basic interfaces to I/O primitives. package io ("io"). Documented at pkg.go.dev, io.
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader is the interface that wraps the basic Read method.
Reader is the interface that wraps the basic Read method. type Reader interface { Read(p []byte) (n int, err error) }. Documented at pkg.go.dev, io.Reader.
package iter ("iter")
Package iter provides basic definitions and operations related to iterators over sequences.
Package iter provides basic definitions and operations related to iterators over sequences. package iter ("iter"). Documented at pkg.go.dev, iter.
type Seq2[K, V any] func(yield func(K, V) bool)
Seq2 is an iterator over sequences of pairs of values, most commonly key-value pairs.
Seq2 is an iterator over sequences of pairs of values, most commonly key-value pairs. type Seq2[K, V any] func(yield func(K, V) bool). Documented at pkg.go.dev, iter.Seq2.
type error interface {
Error() string
}
The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error.
The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error. type error interface { Error() string }. Documented at pkg.go.dev, builtin.error.
var yield func([]byte, error) bool
var yield func([]byte, error) bool. In ch06/openrouter/sse.go, line 20.
type bool bool
bool is the set of boolean values, true and false.
bool is the set of boolean values, true and false. type bool bool. Documented at pkg.go.dev, builtin.bool.
func (s *Scanner) Scan() bool
Scan advances the [Scanner] to the next token, which will then be available through the [Scanner.Bytes] or [Scanner.Text] method.
Scan advances the [Scanner] to the next token, which will then be available through the [Scanner.Bytes] or [Scanner.Text] method. func (s *Scanner) Scan() bool. Documented at pkg.go.dev, bufio.Scanner.Scan.
package bytes ("bytes")
Package bytes implements functions for the manipulation of byte slices.
Package bytes implements functions for the manipulation of byte slices. package bytes ("bytes"). Documented at pkg.go.dev, bytes.
func CutPrefix(s, prefix []byte) (after []byte, found bool)
CutPrefix returns s without the provided leading prefix byte slice and reports whether it found the prefix.
CutPrefix returns s without the provided leading prefix byte slice and reports whether it found the prefix. func CutPrefix(s, prefix []byte) (after []byte, found bool). Documented at pkg.go.dev, bytes.CutPrefix.
func (s *Scanner) Bytes() []byte
Bytes returns the most recent token generated by a call to [Scanner.Scan].
Bytes returns the most recent token generated by a call to [Scanner.Scan]. func (s *Scanner) Bytes() []byte. Documented at pkg.go.dev, bufio.Scanner.Bytes.
func TrimPrefix(s, prefix []byte) []byte
TrimPrefix returns s without the provided leading prefix string.
TrimPrefix returns s without the provided leading prefix string. func TrimPrefix(s, prefix []byte) []byte. Documented at pkg.go.dev, bytes.TrimPrefix.
type string string
string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text.
string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. type string string. Documented at pkg.go.dev, builtin.string.
var nil Type
nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type.
nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type. var nil Type. Documented at pkg.go.dev, builtin.nil.
func (s *Scanner) Err() error
Err returns the first non-EOF error that was encountered by the [Scanner].
Err returns the first non-EOF error that was encountered by the [Scanner]. func (s *Scanner) Err() error. Documented at pkg.go.dev, bufio.Scanner.Err.
var ErrUnexpectedEOF = errors.New("unexpected EOF")
ErrUnexpectedEOF means that EOF was encountered in the middle of reading a fixed-size block or data structure.
ErrUnexpectedEOF means that EOF was encountered in the middle of reading a fixed-size block or data structure. var ErrUnexpectedEOF = errors.New("unexpected EOF"). Documented at pkg.go.dev, io.ErrUnexpectedEOF.
var wire chatRequest
var wire chatRequest. In ch06/openrouter/stream.go, line 71.
func (c *Client) wireRequest(req agent.Request) chatRequest
wireRequest turns the agent's request into the API's JSON shape.
wireRequest turns the agent's request into the API's JSON shape. func (c *Client) wireRequest(req agent.Request) chatRequest. In ch06/openrouter/client.go, line 114.
var req agent.Request
var req agent.Request. In ch06/openrouter/stream.go, line 60.
field Stream bool `json:"stream,omitzero"`
Streaming (chapter 6).
Streaming (chapter 6). field Stream bool `json:"stream,omitzero"`. In ch06/openrouter/wire.go, line 28.
const true = 0 == 0
Untyped bool.
Untyped bool. const true = 0 == 0. Documented at pkg.go.dev, builtin.true.
field StreamOptions *streamOptions `json:"stream_options,omitzero"`
field StreamOptions *streamOptions `json:"stream_options,omitzero"`. In ch06/openrouter/wire.go, line 29.
type streamOptions struct {
IncludeUsage bool `json:"include_usage"`
}
streamOptions asks for usage in the last chunk of a stream.
streamOptions asks for usage in the last chunk of a stream. type streamOptions struct { IncludeUsage bool `json:"include_usage"` }. In ch06/openrouter/wire.go, line 33.
field IncludeUsage bool `json:"include_usage"`
field IncludeUsage bool `json:"include_usage"`. In ch06/openrouter/wire.go, line 34.
var resp *http.Response
var resp *http.Response. In ch06/openrouter/stream.go, line 74.
func (c *Client) post(ctx context.Context,
wire chatRequest) (*http.Response, error)
post sends a request to the chat completions endpoint and returns the response, whatever its status.
post sends a request to the chat completions endpoint and returns the response, whatever its status. func (c *Client) post(ctx context.Context, wire chatRequest) (*http.Response, error). In ch06/openrouter/client.go, line 88.
var ctx context.Context
var ctx context.Context. In ch06/openrouter/stream.go, line 62.
var idle *time.Timer
var idle *time.Timer. In ch06/openrouter/stream.go, line 64.
func (t *Timer) Stop() bool
Stop prevents the [Timer] from firing.
Stop prevents the [Timer] from firing. func (t *Timer) Stop() bool. Documented at pkg.go.dev, time.Timer.Stop.
var yield func(agent.Delta, error) bool
var yield func(agent.Delta, error) bool. In ch06/openrouter/stream.go, line 61.
package agent ("github.com/blackwell-systems/go-agents-book/code/ch06/agent")
Package agent is the agent loop and the types it works with.
Package agent is the agent loop and the types it works with. package agent ("github.com/blackwell-systems/go-agents-book/code/ch06/agent"). In ch06/agent.
type Delta struct {
Text string // a piece of the reply's text
Call *CallDelta // a piece of one tool call
FinishReason string // why the reply ended, when it has
Usage *Usage // what the call consumed, at the end
// ReasoningDetails holds reasoning records, a JSON array, which the
// assembler joins into the reply's ReasoningDetails.
ReasoningDetails jsontext.Value
}
A Delta is one piece of a streamed reply.
A Delta is one piece of a streamed reply. type Delta struct { Text string // a piece of the reply's text Call *CallDelta // a piece of one tool call FinishReason string // why the reply ended, when it has Usage *Usage // what the call consumed, at the end // ReasoningDetails holds reasoning records, a JSON array, which the // assembler joins into the reply's ReasoningDetails. ReasoningDetails jsontext.Value }. In ch06/agent/stream.go, line 17.
field Body io.ReadCloser
Body represents the response body.
Body represents the response body. field Body io.ReadCloser. Documented at pkg.go.dev, net/http.Response.Body.
func (Closer) Close() error
func (Closer) Close() error. Documented at pkg.go.dev, io.Closer.Close.
field StatusCode int
e.g.
e.g. field StatusCode int. Documented at pkg.go.dev, net/http.Response.StatusCode.
package http ("net/http")
Package http provides HTTP client and server implementations.
Package http provides HTTP client and server implementations. package http ("net/http"). Documented at pkg.go.dev, net/http.
const StatusOK = 200
RFC 9110, 15.3.1
RFC 9110, 15.3.1 const StatusOK = 200. Documented at pkg.go.dev, net/http.StatusOK.
package io ("io")
Package io provides basic interfaces to I/O primitives.
Package io provides basic interfaces to I/O primitives. package io ("io"). Documented at pkg.go.dev, io.
func ReadAll(r Reader) ([]byte, error)
ReadAll reads from r until an error or EOF and returns the data it read.
ReadAll reads from r until an error or EOF and returns the data it read. func ReadAll(r Reader) ([]byte, error). Documented at pkg.go.dev, io.ReadAll.
func LimitReader(r Reader, n int64) Reader
LimitReader returns a Reader that reads from r but stops with EOF after n bytes.
LimitReader returns a Reader that reads from r but stops with EOF after n bytes. func LimitReader(r Reader, n int64) Reader. Documented at pkg.go.dev, io.LimitReader.
func statusError(resp *http.Response, body []byte) *APIError
statusError builds the APIError for a response that wasn't 200, using the API's message when the body has one.
statusError builds the APIError for a response that wasn't 200, using the API's message when the body has one. func statusError(resp *http.Response, body []byte) *APIError. In ch06/openrouter/errors.go, line 43.
var body *idleReader
var body *idleReader. In ch06/openrouter/stream.go, line 86.
type idleReader struct {
r io.Reader
timer *time.Timer
timeout time.Duration
}
idleReader runs an idle timer only while it waits for the network, so the timer fires when the stream itself has gone quiet for the whole timeout, not when the program is busy with what it already has.
idleReader runs an idle timer only while it waits for the network, so the timer fires when the stream itself has gone quiet for the whole timeout, not when the program is busy with what it already has. type idleReader struct { r io.Reader timer *time.Timer timeout time.Duration }. In ch06/openrouter/sse.go, line 53.
func (c *Client) idleTimeout() time.Duration
func (c *Client) idleTimeout() time.Duration. In ch06/openrouter/client.go, line 281.
func readStream(ctx context.Context, body io.Reader,
yield func(agent.Delta, error) bool)
readStream turns the chunks of a stream into Deltas.
readStream turns the chunks of a stream into Deltas. func readStream(ctx context.Context, body io.Reader, yield func(agent.Delta, error) bool). In ch06/openrouter/stream.go, line 96.
field timer *time.Timer
field timer *time.Timer. In ch06/openrouter/sse.go, line 55.
package time ("time")
Package time provides functionality for measuring and displaying time.
Package time provides functionality for measuring and displaying time. package time ("time"). Documented at pkg.go.dev, time.
type Timer struct {
C <-chan Time
initTimer bool
}
The Timer type represents a single event.
The Timer type represents a single event. type Timer struct { C <-chan Time initTimer bool }. Documented at pkg.go.dev, time.Timer.
field timeout time.Duration
field timeout time.Duration. In ch06/openrouter/sse.go, line 56.
type Duration int64
A Duration represents the elapsed time between two instants as an int64 nanosecond count.
A Duration represents the elapsed time between two instants as an int64 nanosecond count. type Duration int64. Documented at pkg.go.dev, time.Duration.
func (ir *idleReader) Read(p []byte) (int, error)
func (ir *idleReader) Read(p []byte) (int, error). In ch06/openrouter/sse.go, line 59.
type int int
int is a signed integer type that is at least 32 bits in size.
int is a signed integer type that is at least 32 bits in size. type int int. Documented at pkg.go.dev, builtin.int.
func (t *Timer) Reset(d Duration) bool
Reset changes the timer to expire after duration d.
Reset changes the timer to expire after duration d. func (t *Timer) Reset(d Duration) bool. Documented at pkg.go.dev, time.Timer.Reset.
func (Reader) Read(p []byte) (n int, err error)
func (Reader) Read(p []byte) (n int, err error). Documented at pkg.go.dev, io.Reader.Read.
type stream struct {
w io.Writer
rc *http.ResponseController
wait time.Duration
}
stream writes an event stream to one client.
stream writes an event stream to one client. type stream struct { w io.Writer rc *http.ResponseController wait time.Duration }. In ssebrowser/server.go, line 142.
package io ("io")
Package io provides basic interfaces to I/O primitives.
Package io provides basic interfaces to I/O primitives. package io ("io"). Documented at pkg.go.dev, io.
type Writer interface {
Write(p []byte) (n int, err error)
}
Writer is the interface that wraps the basic Write method.
Writer is the interface that wraps the basic Write method. type Writer interface { Write(p []byte) (n int, err error) }. Documented at pkg.go.dev, io.Writer.
field rc *http.ResponseController
field rc *http.ResponseController. In ssebrowser/server.go, line 144.
package http ("net/http")
Package http provides HTTP client and server implementations.
Package http provides HTTP client and server implementations. package http ("net/http"). Documented at pkg.go.dev, net/http.
type ResponseController struct {
rw ResponseWriter
}
A ResponseController is used by an HTTP handler to control the response.
A ResponseController is used by an HTTP handler to control the response. type ResponseController struct { rw ResponseWriter }. Documented at pkg.go.dev, net/http.ResponseController.
field wait time.Duration
field wait time.Duration. In ssebrowser/server.go, line 145.
package time ("time")
Package time provides functionality for measuring and displaying time.
Package time provides functionality for measuring and displaying time. package time ("time"). Documented at pkg.go.dev, time.
func (s *stream) send(msg string) error
send writes msg and flushes it to the client at once.
send writes msg and flushes it to the client at once. func (s *stream) send(msg string) error. In ssebrowser/server.go, line 151.
func (c *ResponseController) SetWriteDeadline(deadline time.Time) error
SetWriteDeadline sets the deadline for writing the response.
net/http.ResponseController.SetWriteDeadline
SetWriteDeadline sets the deadline for writing the response. func (c *ResponseController) SetWriteDeadline(deadline time.Time) error. Documented at pkg.go.dev, net/http.ResponseController.SetWriteDeadline.
func Now() Time
Now returns the current local time.
Now returns the current local time. func Now() Time. Documented at pkg.go.dev, time.Now.
func (t Time) Add(d Duration) Time
Add returns the time t+d.
Add returns the time t+d. func (t Time) Add(d Duration) Time. Documented at pkg.go.dev, time.Time.Add.
package fmt ("fmt")
Package fmt implements formatted I/O with functions analogous to C's printf and scanf.
Package fmt implements formatted I/O with functions analogous to C's printf and scanf. package fmt ("fmt"). Documented at pkg.go.dev, fmt.
func Errorf(format string, a ...any) (err error)
Errorf formats according to a format specifier and returns the string as a value that satisfies error.
Errorf formats according to a format specifier and returns the string as a value that satisfies error. func Errorf(format string, a ...any) (err error). Documented at pkg.go.dev, fmt.Errorf.
func WriteString(w Writer, s string) (n int, err error)
WriteString writes the contents of the string s to w, which accepts a slice of bytes.
WriteString writes the contents of the string s to w, which accepts a slice of bytes. func WriteString(w Writer, s string) (n int, err error). Documented at pkg.go.dev, io.WriteString.
func (c *ResponseController) Flush() error
Flush flushes buffered data to the client.
net/http.ResponseController.Flush
Flush flushes buffered data to the client. func (c *ResponseController) Flush() error. Documented at pkg.go.dev, net/http.ResponseController.Flush.
var w http.ResponseWriter
var w http.ResponseWriter. In ssebrowser/server.go, line 53.
func (ResponseWriter) Header() Header
Header returns the header map that will be sent by [ResponseWriter.WriteHeader].
net/http.ResponseWriter.Header
Header returns the header map that will be sent by [ResponseWriter.WriteHeader]. func (ResponseWriter) Header() Header. Documented at pkg.go.dev, net/http.ResponseWriter.Header.
func (h Header) Set(key, value string)
Set sets the header entries associated with key to the single element value.
Set sets the header entries associated with key to the single element value. func (h Header) Set(key, value string). Documented at pkg.go.dev, net/http.Header.Set.
var EOF = errors.New("EOF")
EOF is the error returned by Read when no more input is available.
EOF is the error returned by Read when no more input is available. var EOF = errors.New("EOF"). Documented at pkg.go.dev, io.EOF.
func Copy(dst Writer, src Reader) (written int64, err error)
Copy copies from src to dst until either EOF is reached on src or an error occurs.
Copy copies from src to dst until either EOF is reached on src or an error occurs. func Copy(dst Writer, src Reader) (written int64, err error). Documented at pkg.go.dev, io.Copy.
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
// ...
}
Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text.
Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. type Scanner struct { r io.Reader // The reader provided by the client. split SplitFunc // The function to split the tokens. maxTokenSize int // Maximum size of a token; modified by tests. token []byte // Last token returned by split. buf []byte // Buffer used as argument to split. start int // First non-processed byte in buf. end int // End of data in buf. err error // Sticky error. empties int // Count of successive empty tokens. // ... }. Documented at pkg.go.dev, bufio.Scanner.
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanLines is a split function for a [Scanner] that returns each line of text, stripped of any trailing end-of-line marker.
ScanLines is a split function for a [Scanner] that returns each line of text, stripped of any trailing end-of-line marker. func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error). Documented at pkg.go.dev, bufio.ScanLines.
const MaxScanTokenSize = 64 * 1024
MaxScanTokenSize is the maximum size used to buffer a token unless the user provides an explicit buffer with [Scanner.Buffer].
MaxScanTokenSize is the maximum size used to buffer a token unless the user provides an explicit buffer with [Scanner.Buffer]. const MaxScanTokenSize = 64 * 1024. Documented at pkg.go.dev, bufio.MaxScanTokenSize.
var ErrTooLong = errors.New("bufio.Scanner: token too long")
var ErrTooLong = errors.New("bufio.Scanner: token too long"). Documented at pkg.go.dev, bufio.ErrTooLong.
func (c *Client) Do(req *Request) (*Response, error)
Do sends an HTTP request and returns an HTTP response, following policy (such as redirects, cookies, auth) as configured on the client.
Do sends an HTTP request and returns an HTTP response, following policy (such as redirects, cookies, auth) as configured on the client. func (c *Client) Do(req *Request) (*Response, error). Documented at pkg.go.dev, net/http.Client.Do.
func AfterFunc(d Duration, f func()) *Timer
AfterFunc waits for the duration to elapse and then calls f in its own goroutine.
AfterFunc waits for the duration to elapse and then calls f in its own goroutine. func AfterFunc(d Duration, f func()) *Timer. Documented at pkg.go.dev, time.AfterFunc.
func TeeReader(r Reader, w Writer) Reader
TeeReader returns a [Reader] that writes to w what it reads from r.
TeeReader returns a [Reader] that writes to w what it reads from r. func TeeReader(r Reader, w Writer) Reader. Documented at pkg.go.dev, io.TeeReader.
type ResponseWriter interface {
// Header returns the header map that will be sent by
// [ResponseWriter.WriteHeader]. The [Header] map also is the mechanism with which
// [Handler] implementations can set HTTP trailers.
//
// Changing the header map after a call to [ResponseWriter.WriteHeader] (or
// [ResponseWriter.Write]) has no effect unless the HTTP status code was of the
// 1xx class or the modified headers are trailers.
//
// There are two ways to set Trailers. The preferred way is to
// ...
}
A ResponseWriter interface is used by an HTTP handler to construct an HTTP response.
A ResponseWriter interface is used by an HTTP handler to construct an HTTP response. type ResponseWriter interface { // Header returns the header map that will be sent by // [ResponseWriter.WriteHeader]. The [Header] map also is the mechanism with which // [Handler] implementations can set HTTP trailers. // // Changing the header map after a call to [ResponseWriter.WriteHeader] (or // [ResponseWriter.Write]) has no effect unless the HTTP status code was of the // 1xx class or the modified headers are trailers. // // There are two ways to set Trailers. The preferred way is to // ... }. Documented at pkg.go.dev, net/http.ResponseWriter.
type Flusher interface {
// Flush sends any buffered data to the client.
Flush()
}
The Flusher interface is implemented by ResponseWriters that allow an HTTP handler to flush buffered data to the client.
The Flusher interface is implemented by ResponseWriters that allow an HTTP handler to flush buffered data to the client. type Flusher interface { // Flush sends any buffered data to the client. Flush() }. Documented at pkg.go.dev, net/http.Flusher.