Help us shape the future of Go! Take the 2026 Go Developer Survey

render

package
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 7 Imported by: 2

Documentation

Overview

Package render is the generic half of a model-driven agent: it turns a run's recorded steps into a message list and a model response into a decision. What is left to the consumer is the opening message, which is the only place domain facts belong, and the hooks below.

The rendering rules: an opening user message, then per recorded step an assistant turn (the decision's reason as text, plus a tool_use block with a synthesized id) and a user turn with the tool_result answering it. Results older than the recent window, and results over the content cap, are replaced by their summary. A step whose decision was not a tool call is answered with a text nudge keyed on the decision kind, so the model is told what went wrong rather than left to guess.

Both consumers are reproduced byte for byte. casework's renderer is the default plus its own ids and raw assistant turns:

render.Options{
	Opening:       func(agentrt.StepInput) string { return opening },
	RecentResults: a.RecentResults,
	ToolUseID: func(st agentrt.Step) string {
		if t, ok := turns[st.Index]; ok && t.Model == a.ModelName {
			if id := firstToolUseID(t.Raw); id != "" {
				return id
			}
		}
		return fmt.Sprintf("toolu_step_%d", st.Index)
	},
	Assistant: func(st agentrt.Step, _ string) []agentrt.ContentBlock {
		if t, ok := turns[st.Index]; ok && t.Model == a.ModelName {
			return []agentrt.ContentBlock{{Type: rawBlock, Text: string(t.Raw)}} // the adapter's RawAssistantBlock
		}
		return nil // synthesize
	},
}

repo-steward's renderer keeps the default ids and window and overrides both turns, because its assistant turn carries no reason text and its results are rendered as a labelled head plus content:

calls := func(st agentrt.Step) bool {
	return st.Decision.Kind == agentrt.DecideToolCall && st.Decision.Tool != ""
}
render.Options{
	Opening:       a.opening,
	RecentResults: a.RecentResults,
	Assistant: func(st agentrt.Step, id string) []agentrt.ContentBlock {
		if !calls(st) {
			return nil // the default text turn
		}
		return []agentrt.ContentBlock{{Type: "tool_use", ToolUseID: id, Name: st.Decision.Tool, Input: orEmpty(st.Decision.Args)}}
	},
	Observation: func(st agentrt.Step, id string, full bool) agentrt.ContentBlock {
		if !calls(st) {
			return agentrt.ContentBlock{} // the default nudge
		}
		return agentrt.ContentBlock{Type: "tool_result", ToolUseID: id, Name: st.Decision.Tool,
			Content: renderObservation(st, full), IsError: st.Observation != nil && st.Observation.Failure()}
	},
}

The guards matter: since Decide records KindNoToolCall and KindTruncated, every consumer's hooks are called for steps that called nothing, and falling back to the defaults there is what delivers the nudge. A hook that formats results its own way and wants the default for interrupted steps calls DefaultObservation, or uses InterruptedText directly.

Index

Examples

Constants

View Source
const (
	// DefaultRecentResults is how many of the latest results are rendered
	// in full.
	DefaultRecentResults = 6
	// DefaultMaxContentBytes caps one rendered result.
	DefaultMaxContentBytes = 24000
	// ReasonBytes caps the reason Decide records, the model's text and the
	// note naming any tool uses not executed together, so a long narration
	// cannot dominate the next render.
	ReasonBytes = 500
	// NameBytes caps each unexecuted tool use's name in that note: the
	// names are the model's, and nothing else bounds them.
	NameBytes = 64
)

Defaults for Options.

View Source
const (
	StopEndTurn   = "end_turn"
	StopToolUse   = "tool_use"
	StopMaxTokens = "max_tokens"
	StopRefusal   = "refusal"
	// StopContextWindowExceeded means the request no longer fits the
	// model's context window.
	StopContextWindowExceeded = "model_context_window_exceeded"
)

The stop-reason vocabulary provider adapters map onto, and Decide reads.

View Source
const InterruptedText = "" /* 170-byte string literal not displayed */

InterruptedText re-requests a tool whose outcome the interruption left unknown. The runtime never re-executes a side effect itself, so the model must ask again and the tool answers from its own record. It is exported so an Observation hook with its own result format keeps this sentence.

Variables

This section is empty.

Functions

func Args added in v0.3.0

Args is a recorded decision's arguments as a tool_use block carries them: an empty object when there were none and, for arguments that were not JSON, which the runtime records out of band in InvalidArgs, the object {"invalid_json": ...} (or "invalid_json_base64") that earlier releases stored in their place, so a rendered conversation, and every replay key taken over one, is what it was.

func Decide

Decide maps a model response onto a decision. A refusal fails the run, because a model that declined will decline again; so does a request over the context window, which only grows. A reply cut off by the output cap, even one carrying a complete tool use, and a reply with no tool use become the deliberately invalid kinds agentrt.KindTruncated and agentrt.KindNoToolCall: the driver records an invalid_decision observation and Messages nudges on the next step, which costs one step against the limits and never executes a partial tool call. Only the first tool use executes; when there are more, the reason says how many were not executed and names them, each name capped at NameBytes. The reason as composed is capped at ReasonBytes.

Example

A reply with no tool use is not a decision the runtime can act on, so Decide records the deliberately invalid kind no_tool_call. The driver observes an invalid_decision and the next Messages nudges the model from that kind.

package main

import (
	"fmt"

	agentrt "github.com/joeylking/agent-runtime"
	"github.com/joeylking/agent-runtime/render"
)

func main() {
	dec := render.Decide(agentrt.ModelResponse{
		Text:       "The counter looks right to me.",
		StopReason: "end_turn",
	})
	fmt.Println(dec.Kind)
	fmt.Println(dec.Reason)
}
Output:
no_tool_call
The counter looks right to me.

func DefaultObservation

func DefaultObservation(st agentrt.Step, toolUseID string, full bool, maxBytes int) agentrt.ContentBlock

DefaultObservation renders what a step produced, or a nudge when there was no tool call to answer: the tool_result carries the content when the step is within the recent window and under maxBytes, otherwise its summary; an interrupted observation carries InterruptedText. An Observation hook may call it for the steps it does not format itself.

func Messages

func Messages(in agentrt.StepInput, opts Options) []agentrt.Message

Messages renders the conversation for one decision from in.Steps, which the driver fills with the steps before the one being decided. A step with no recorded decision, which is one interrupted before it decided (or the step being decided, if a caller includes it), is skipped and does not count toward the recent-results window.

Example

Messages renders the opening message and then, per recorded step, the assistant turn that asked for a tool and the user turn that answers it.

package main

import (
	"encoding/json"
	"fmt"
	"strings"

	agentrt "github.com/joeylking/agent-runtime"
	"github.com/joeylking/agent-runtime/render"
)

func main() {
	step := func(index int, tool, args, reason, content, summary string) agentrt.Step {
		return agentrt.Step{
			ID: fmt.Sprintf("s%d", index), Index: index, Status: agentrt.StepDone,
			Decision:    &agentrt.Decision{Kind: agentrt.DecideToolCall, Tool: tool, Args: json.RawMessage(args), Reason: reason},
			Observation: &agentrt.Observation{Kind: agentrt.ObserveToolResult, Content: json.RawMessage(content), Summary: summary},
		}
	}
	in := agentrt.StepInput{
		Run: agentrt.Run{ID: "r1", Goal: "bring the counter to 5"},
		Steps: []agentrt.Step{
			step(0, "read", `{}`, "look first", `{"value":1}`, "value=1"),
			step(1, "add", `{"n":4}`, "add four", `{"value":5}`, "value=5"),
		},
	}
	msgs := render.Messages(in, render.Options{
		Opening: func(agentrt.StepInput) string { return "Bring the counter to 5." },
	})
	for _, m := range msgs {
		kinds := make([]string, 0, len(m.Content))
		for _, b := range m.Content {
			kinds = append(kinds, b.Type)
		}
		fmt.Printf("%s: %s\n", m.Role, strings.Join(kinds, " "))
	}
}
Output:
user: text
assistant: text tool_use
user: tool_result
assistant: text tool_use
user: tool_result

func Truncate

func Truncate(s string, n int) string

Truncate shortens s to at most n bytes, the "…" that marks the cut included, without splitting a character. When n is too small for the mark the cut is unmarked; a negative n is zero.

Types

type Options

type Options struct {
	// Opening renders the first user message from the run's facts. It is
	// the consumer's domain text and the only required option.
	Opening func(in agentrt.StepInput) string
	// RecentResults is how many of the latest results are rendered in full;
	// older ones are replaced by their summary. Zero means
	// DefaultRecentResults.
	RecentResults int
	// MaxContentBytes replaces a result larger than this by its summary.
	// Zero means DefaultMaxContentBytes. A custom Observation applies its
	// own cap.
	MaxContentBytes int
	// ToolUseID names a step's tool_use block. Zero means "step_<index>".
	// Hooks see only the Step, so its recorded Index is the identity to key
	// on; the store orders steps by it and it has no gaps.
	ToolUseID func(step agentrt.Step) string
	// Assistant renders a step's assistant turn. Returning nil falls back
	// to the synthesized turn, so a hook can override only the steps it
	// knows about. It is called for every recorded step, including one
	// whose decision was not a tool call.
	Assistant func(step agentrt.Step, toolUseID string) []agentrt.ContentBlock
	// Observation renders the user turn answering a step. Returning a block
	// with no type falls back to the default, which is DefaultObservation
	// with MaxContentBytes. It too is called for a step that called nothing;
	// falling back there delivers the nudge.
	Observation func(step agentrt.Step, toolUseID string, full bool) agentrt.ContentBlock
}

Options configure Messages. Only Opening is required; every hook may be nil and the default applies.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL