AnnouncementsIntroducing Arkoren

    Menu

    Streaming Replies

    Display incremental replies and tool progress while preserving cancellation, completion, and browser-delivery boundaries.

    What's on this page
    1. Consume text and agent progress without mistaking a fragment for a completed answer.
    2. Distinguish natural completion, provider failure, and a consumer-stopped stream.
    3. Deliver events with SSE or collect a finite stream without accidentally repeating model calls.
    On this page

    A long answer leaves the user waiting if you display it only after generation finishes. Streaming lets you show text as it arrives; it does not promise a shorter total generation time. Use Model.Stream for one turn and Model.RunStream for an agent’s turns and tool progress. Both return collection.TryLazySlice[llm.Event], so you can use for ... range or native collection operations. Breaking out of the loop cancels the request to the vendor.

    Construction and lazy operations such as Filter, Map, and Take do no model I/O. Traversal starts the work. Traversing the same collection again issues new provider calls, which can be billed again; an agent can also repeat tool effects. Materialize once if you need to reuse the observed events without another run.

    The examples use llm from arkoren.dev/core/service/llm and the standard fmt package. Reuse model and ctx from the driver setup. Use an operation context with a deadline; the request context is appropriate inside an HTTP handler.

    Streaming Text

    If you want to print an answer as it is written, read text fragments from Stream rather than waiting for Generate to return the whole response. Use your existing model and a request of type llm.Request, with the same shape as a single-turn request:

    for event, err := range model.Stream(ctx, request) {
    	if err != nil {
    		return err
    	}
    	if delta, ok := event.(llm.TextDelta); ok {
    		fmt.Print(delta.Text)
    	}
    }

    The events are TextDelta and ReasoningDelta fragments, ToolCallStarted and ToolCallDelta for a tool call being written, and finally Done, which carries the complete Response. A consumer that ignores the deltas still gets the result from Done. A stream that ends without Done or an error yields llm.ErrUnavailable.

    A terminal error means the stream failed; text already shown to the user does not mean a complete answer was received.

    Streaming Agents

    An agent may pause its answer while it looks up an order or calls another tool. Showing that progress helps the person waiting understand what is happening. RunStream forwards the text from each turn along with tool-progress events. Use the assistant agent from Running Agents and your stored history:

    for event, err := range model.RunStream(ctx, assistant, history) {
    	if err != nil {
    		return err
    	}
    	switch e := event.(type) {
    	case llm.TextDelta:
    		fmt.Print(e.Text)
    	case llm.ToolStarted:
    		fmt.Println("running", e.Name)
    	case llm.ToolFinished:
    		fmt.Println("finished", e.Name, "error:", e.IsError)
    	case llm.StepCompleted:
    		fmt.Println("step", e.Step, "done")
    	}
    }

    ToolStarted and ToolFinished carry the call ID and name; ToolFinished also carries an error flag. Neither carries arguments or results. ToolStarted follows admission to the concurrency slot, not merely the model’s request for a call. A queued call canceled before admission is not announced. A start does not guarantee handler execution: availability and argument validation still apply. If the tool phase completes without a fatal error, ToolFinished events arrive in call order after every admitted call has finished.

    RunStream cannot return a Result; use Run when you need the complete conversation to persist or resume. Done contains each model response, not the tool-result messages appended inside the run, so collecting Done events alone does not reconstruct that conversation.

    For a structured-output agent, streamed text can still be incomplete or fail final validation. Do not treat a text delta as a validated typed result; keep reading the stream’s error value.

    Stopping Streams

    To record a normally completed standalone turn, consume Model.Stream through its natural end. Breaking or returning from a successful event, including Done, cancels the driver context and suppresses both llm.response.generated and llm.response.failed. The turn span ends without error. A driver failure is different: its observation is finalized before its error is yielded, so stopping on that error does not retract the failure event.

    Stopping RunStream cancels the run, joins admitted tool calls, and suppresses the agent outcome event. Completed tool observations remain published. Tool handlers must honor cancellation; stopping the consumer cannot undo effects or forcibly terminate a blocked handler. A panic also propagates rather than becoming a successful stop; the observation rules explain its bounded telemetry and lack of an invented outcome event.

    Browser Delivery

    For a chat page, the fragments need to reach the browser as they arrive at your server. response.SSEJSON sends each iterator value as a JSON-encoded server-sent event. Use .Iter() to pass the collection to this standard iter.Seq2 consumer. Each event has a "type" tag the browser can use to distinguish text from tool progress.

    The iterator must cooperate with cancellation and shutdown; blocking model and tool work needs a way to finish before the streaming handler can return.

    Inside your existing Arkoren handler, with w, req, and the conversation history already available, import response from arkoren.dev/core/http/response:

    return response.SSEJSON(w, req, model.RunStream(req.Context(), assistant, history).Iter())

    In the browser, read the stream with fetch for a POST, or with EventSource for a GET, and parse each event’s data as JSON.

    SSEJSON stops consuming values on request cancellation, server shutdown, or a write or flush failure. It makes yield return false and waits for the iterator producer to exit; it does not abandon that goroutine. Producers must return when yield returns false and must not yield further values, even a final Done.

    Client disconnects cancel req.Context(). Graceful shutdown is different: an Arkoren server.Server announces it through server.ShuttingDown(req.Context()) without canceling that request context. The shutdown helper is in arkoren.dev/core/http/server. A write or flush failure does not cancel the request context either.

    The streaming helper cannot forcibly interrupt a model call or tool handler blocked outside yield. Arrange for that work to stop on request cancellation and the shutdown signal, or give it another bounded way to exit. Passing only req.Context() does not guarantee prompt graceful shutdown while work is blocked; a noncooperative producer can keep SSEJSON waiting indefinitely. Streaming Responses explains the write deadline and cooperative shutdown behavior of streaming responses.

    The headers are sent before the first event. An error that occurs later, such as a rate limit, ends the stream and is returned to the handler’s caller, but cannot change the committed status code. Check what you can before streaming. If the browser needs an error event, send an application-approved message from a wrapper iterator, not raw provider, tool, or schema diagnostics.

    Collecting Events

    To display only text, filter the stream directly; no collection conversion or additional import is required:

    text := model.Stream(ctx, request).Filter(func(_ int, item llm.Event) (bool, error) {
    	_, ok := item.(llm.TextDelta)
    	return ok, nil
    })
    for item, err := range text {
    	if err != nil {
    		return err
    	}
    	fmt.Print(item.(llm.TextDelta).Text)
    }

    Filtering out Done does not redefine completion or bypass the source’s missing-Done validation. Reading this filtered stream through its natural end still completes the source turn. Take(n) instead stops early after its limit: it cancels and joins the source under the same rules as breaking the loop.

    For finite streams, events, err := model.Stream(ctx, request).Items() collects events into a Go slice. On failure, events contains the successful prefix and err retains the terminal error. That prefix is not a completed response, even if it contains text or a Done before a later error. RunStream materialization does not produce the conversation or typed result returned by Run.

    Materialization requires a finite source and holds the collected events in memory. JSON encoding the collection also materializes it; it does not send events to the browser as they arrive. Use SSEJSON with .Iter() for that.

    Fallible Collections lists what a TryLazySlice can do.

    For every function and option, see the llm API reference and the response API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive