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 ¶
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 from a model's text, so a // long narration cannot dominate the next render. ReasonBytes = 500 )
Defaults for Options.
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 Decide ¶
func Decide(resp agentrt.ModelResponse) agentrt.Decision
Decide maps a model response onto a decision. A refusal fails the run, because a model that declined will decline again. A reply cut off by the output cap 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.
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 ¶
Messages renders the conversation for one decision. Steps with no recorded decision, which is the step being decided and any step interrupted before it decided, are skipped.
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
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.