High-level shape
hrns is a small composition of six packages:
main: wires everything togetheropenai: OpenAI-compatible request and streaming clientloop: agent loop and tool executiontools: bundled tool implementationsskills: skill discovery and theload_skilltooltui: terminal UI and one-shot exec runner
Boot sequence
At startup,main.go does the following:
- creates a background context
- loads skills from the default global and local roots
- creates the
load_skilltool from the discovered skills - creates the built-in agent list and loads file-system agents
- creates
tui.TUIAppwith the built-in tools, agents, and skill metadata - starts the bundled runner
tui.Run, startup continues like this:
- load
~/.config/hrns/config.json - if the file is missing or has no providers, run onboarding
- select the saved current agent if registered, or save one registered agent as current
- compose the system message from the selected agent or base prompt plus skill metadata
- pick
currentProvider - build
openai.Clientfrom that provider’surl,key, andskipVerify - create
loop.Loopwith the current client and tools - choose interactive mode by default, or
execmode when the first CLI argument isexec
Request lifecycle
For each interactive user turn in the TUI:- the TUI appends a
usermessage to the current session - it starts
RunLoopwith the current message history and chosen model RunLoopconverts registered tools into OpenAI-style function schemasopenai.Client.StreamChatCompletionstreams SSE events from/chat/completionsloopemits chunks for assistant text, reasoning, and tool call events- if the model called tools,
loopexecutes them and appendstoolmessages - the loop repeats until a streamed response finishes without tool calls
- the TUI updates its in-memory conversation from
agent.Messages()
Stream accumulation
Theopenai.ChatCompletionAccumulator is a key part of the runtime.
It merges partial streamed deltas into complete choices by:
- concatenating text content
- preserving structured content when text concatenation does not apply
- stitching together fragmented tool-call arguments
- preserving extra provider-specific fields
RunLoop wait until the stream ends and then execute fully assembled tool calls.
Tool execution model
Tool execution is synchronous inside the loop:- tools are looked up by name in a map
- arguments are parsed from the tool call’s JSON string
- the tool returns a string result
- the result is appended as a
toolmessage
State model
Two kinds of state matter:Loop state
loop.Loop stores:
- the client
- the tool map
- the last completed message history
- a chunk channel
TUI state
The TUI stores:- the system prompt
- the tool map
- the registered slash commands
- the registered agents
- the discovered skill metadata used for system prompt context
- the current conversation history
- the loaded config
- the active model string for the session
exec mode, the same startup path is reused, but the app creates a fresh message list with the active system prompt plus one user message from -message, runs the loop once, and exits. If -provider is passed without -model, the selected provider’s saved default model is used.
Current design limits
The architecture is small on purpose, but that comes with hard edges:- tool schemas are flat and fully required
- tool results are plain strings
/connectupdates saved config, but only/provider <name>rebuilds the live clientexecis single-shot only and does not persist conversation state