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

uagent

module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: Apache-2.0

README

uagent

CI Docs checked by Memoria Release Go reference

uagent is a wrapper around uah-core-runner that adds safety guards and everyday ergonomics while staying as close to the original runner as possible.

uah, a Codex-style TUI, is built on it.

[!NOTE] The docs are kept in sync with the code by Memoria: CI fails when code changes and its README hasn't been reviewed.

  1. Why a wrapper
  2. Install
  3. Run a task
  4. CLI reference
  5. What uagent adds
  6. Use it from Go
  7. Development
  8. License

Why a wrapper

uah-core-runner is the headless runner of uah-core, the Go agent runtime under uah, whose tool calls run in the background while the model keeps working. uah-core derives from unreal-agent, and uagent was first written for its runner. It runs one task from a JSON request, writes session items as JSONL, and exits, much like codex exec or claude -p.

The runner, as uagent first found it (unreal-agent-runner v0.1.x), is capable but raw. It stored sessions inside the workspace, loads the workspace .env, has no overall timeout, and prints session items rather than an answer. uagent keeps the runner itself unchanged: the same request fields reach it on stdin, its output is saved byte for byte, and nothing is added to the prompt. What uagent adds sits around the runner: guards before and during the run, readable progress, a plain answer on stdout, and a record of every run.

Install

GOBIN="$HOME/.local/bin" go install github.com/viktordanov/uah-core/cmd/uah-core-runner@latest
GOBIN="$HOME/.local/bin" go install github.com/viktordanov/uagent/cmd/uagent@latest
codex login   # for the default openai-codex provider

Run a task

uagent "Summarize this project."
uagent -e xhigh -t 20m -C ~/code/proj "Fix the failing test in pkg/foo"
echo "Explain the build" | uagent -q
uagent --json "..." | jq .stats.tokens     # answer and statistics as JSON
uagent --stream "..."                       # JSONL events for another program
uagent stats ~/.local/state/uagent/runs/<run-id>

The prompt comes from the arguments, or from stdin when there are none or the only argument is -. Flags can come before or after the prompt.

CLI reference

uagent [flags] <prompt> runs one task. uagent stats [--json] <run-dir|events.jsonl> recomputes the summary of a saved run.

Choose a backend and model
Flag Default Environment Meaning
--provider openai-codex UAH_LLM_PROVIDER How the runner authenticates and which endpoint it calls by default
-m, --model gpt-6-sol for openai-codex, gpt-6-astra for openai UAH_LLM_MODEL The provider's model ID, passed through unchanged
-e, --effort high Thinking level: low, medium, high, xhigh, or max
--base-url the provider's endpoint UAH_LLM_BASE_URL Send requests to another server
--max-attempts the runner's default (5) Retries for failed model requests; 1 disables retries
--system-prompt the runner's prompt Replace the runner's system prompt
--disallow A tool the model may not use (Bash, ViewImage, or SkillUse); repeat for more

Every provider speaks the OpenAI Responses API and posts to <base URL>/responses. The provider decides the credentials and the default endpoint:

Provider Credentials Default endpoint Default model
openai-codex codex login (~/.codex/auth.json or $CODEX_HOME/auth.json), or OPENAI_CODEX_ACCESS_TOKEN https://chatgpt.com/backend-api/codex none; uagent uses gpt-6-sol
openai OPENAI_API_KEY https://api.openai.com/v1 gpt-6-astra
openrouter OPENROUTER_API_KEY https://openrouter.ai/api/v1 none; pass --model
fireworks FIREWORKS_API_KEY https://api.fireworks.ai/inference/v1 none; pass --model
ollama none http://localhost:11434/v1 none; pass --model

UAH_LLM_API_KEY works for every keyed provider. uagent checks credentials and the model before starting, so a missing key or model fails immediately with exit code 2.

# Codex subscription (the default backend)
uagent --provider openai-codex --model gpt-6-sol --effort high "..."

# OpenAI API key
uagent --provider openai --model gpt-6-astra --effort medium "..."

# OpenRouter: any model ID it lists
uagent --provider openrouter --model <vendor>/<model> --effort high "..."

# Fireworks
uagent --provider fireworks --model accounts/fireworks/models/<model> --effort medium "..."

# Local Ollama, or Ollama on another machine
uagent --provider ollama --model <model> --effort low "..."
uagent --provider ollama --model <model> --effort low --base-url http://gpu-box:11434/v1 "..."

# Any server that speaks the Responses API
uagent --provider openai --model <model> --effort medium --base-url http://localhost:8000/v1 "..."

The openai provider always needs OPENAI_API_KEY or UAH_LLM_API_KEY; for a local server that ignores keys, any value works.

To make a backend the default, export the variables, for example export UAH_LLM_PROVIDER=openrouter UAH_LLM_MODEL=<vendor>/<model>. Flags win over the environment. uagent always passes the provider, model, and base URL to the runner explicitly, so a workspace .env cannot change them.

Guards and limits
Flag Default Environment Meaning
-C, --workspace . The agent's workspace and Bash working directory
-t, --timeout 30m Stop the run and all its tools after this long; 0 disables
--max-disk 5G Stop the run when tool output passes this size (500M, 2G); 0 disables
--allow-dotenv off Run even when the workspace .env sets risky variables, with a warning
--allow-state-in-workspace off Run even when the workspace holds the state directory, such as the home directory, with a warning
--state-dir ~/.local/state/uagent UAGENT_STATE_DIR Sessions, logs, and run records; must be outside the workspace. The first run moves the ~/.local/state/unreal-agent of earlier versions here
--runner ~/.local/bin, then PATH UAGENT_RUNNER The uah-core-runner executable
--session a new UUID Create or resume a named runner session
Output
Flag Stdout Stderr
none the final answer progress, then a summary
-q, --quiet the final answer the summary
--json the summary with the answer, as JSON progress, then the summary
--stream versioned JSONL events diagnostics only

--verbose adds the model's reasoning summaries to the progress, and --log-level debug|info|warn|error (default warn) controls diagnostic logs on stderr. --json and --stream cannot be combined. -v prints the version.

Exit code Meaning
0 The run finished and the runner reported no error
1 The run failed (the runner exited non-zero, reported an error, or the model failed), or uagent hit an unexpected error
2 Invalid usage, a preflight check blocked the run, or another run holds the session
3 The disk limit stopped the run
124 The timeout stopped the run
130 The run was interrupted

What uagent adds

Safety guards
Runner behavior Guard
Sessions stored in the workspace can be read back by the agent's own background searches and grow without limit (unreal-agent issue #3). Sessions, logs, and run records live in ~/.local/state/uagent. A state directory inside the workspace is refused, unless --allow-state-in-workspace (Request.AllowStateInWorkspace) allows it with a warning. --max-disk (default 5G) stops a run whose tool output grows past the limit.
The workspace .env is loaded and can redirect the model endpoint or credentials (issue #5). A .env that sets UAH_LLM_*, OPENAI_CODEX_*, CODEX_HOME, or *_PROXY blocks the run unless --allow-dotenv is given. Provider, model, and base URL are always set explicitly for the runner, so a .env cannot override them.
No overall time limit, and background tools can outlive a run. --timeout (default 30m) and Ctrl+C stop the runner gracefully: SIGINT first, so it records its state, then SIGTERM and SIGKILL if it does not stop within 5 s. A kill skips that wait, and a runner that outlives SIGKILL by another 5 s is abandoned so the run still ends. Tools and processes still running after the runner exits, for any reason, are killed, and tools left behind by a run that was itself killed are killed when its session next starts. Before it signals a tool's process group, the harness checks that the group's leader has the boot and start time the runner recorded with it, so a process ID reused after the tool exited, or after a reboot, is never signaled.
Session files have no lock, so two runs on one session corrupt it. Each run holds a lock on its session (sessions/<id>.lock). A second run on the same --session fails at once with exit code 2.
Codex tokens are never refreshed. An expired token stops the run before it starts; a token that expires within an hour produces a warning.
Ergonomics
  • A plain answer on stdout, with progress and a summary on stderr, so uagent composes with pipes and scripts.
  • A prompt from an argument or stdin, sensible defaults, and flags that validate their values.
  • A record of every run in ~/.local/state/uagent/runs/<run-id>/: request.json, the raw runner output in events.jsonl, stderr.log, and summary.json.
  • Statistics for comparing the runner with other agents: wall time, model time, tool busy time, tool time that overlapped model time, turns, tool calls and failures, parallelism, and tokens.

Use it from Go

The CLI is a thin layer over two packages, and a TUI or another tool can use them directly:

  • core is the pure model: Request, Result, the run events, StatsCollector, and the rules for findings and outcomes. It does no I/O.
  • harness runs the runner with every guard. New(Config) returns a Harness:
    • Run(ctx, request, sink) streams events to sink and returns the Result. Start does the same but returns a Run handle at once, with Wait, Interrupt (graceful), Kill, and Done.
    • A request carries either a Prompt or Messages, each with an ID. The runner echoes every message as a UserMessage event with the same ID, which confirms delivery.
    • A message with Role: core.RoleDeveloper is the harness's, not the user's, such as context it prepares for a new session. The model reads it as a developer message with the next user message, so Messages must include at least one user message. The runner echoes it as a DeveloperMessage event, which does not count as a user message.
    • Preflight checks a request without running it. LockSession is the per-session lock every run takes.
    • Config.Backend swaps how the agent runs. The default, RunnerBackend, spawns the runner (its Env adds variables to the runner's environment, such as SHELL); another backend can run the runner's packages in process and keep every guard, the lock, and the run records. Run.Process returns the backend's handle.
    • Runs lists every run record, including runs in progress; History lists finished runs; LoadRequest, LoadEvents, and Load reopen one.
    • DefaultStateDir is ~/.local/state/uagent ($XDG_STATE_HOME/uagent). MoveLegacyStateDir renames the unreal-agent state directory of earlier versions to it when it does not exist yet; the CLI calls it before a run that uses the default.
h := harness.New(harness.Config{RunnerPath: runner, StateDir: harness.DefaultStateDir()})
result, err := h.Run(ctx, core.Request{Prompt: "Summarize this project.", Provider: "openai-codex",
	Model: "gpt-6-sol", Effort: "high", Workspace: dir}, func(e core.Event) { /* update the UI */ })

Run and Start send RunStarted first and RunFinished last for every run that starts, and they are safe to call concurrently for different sessions. Programs in other languages use the stream instead:

The stream package writes run events as versioned JSONL for uagent --stream. It is the contract for programs that drive uagent from outside Go.

Development

go test ./...              # unit, golden, and end-to-end tests; no model or tokens needed
golangci-lint run ./...    # the aicore linter set plus sloglint, formatted with gofumpt
memoria check              # documentation freshness

The tests replay real runner output through a fake runner:

Real captured runner output, a fake runner that replays it with its original timing, and golden files, so uagent is tested end to end without a model or tokens.

CI runs the build, the race-enabled tests, and golangci-lint in ci.yml, and memoria check in the Memoria-managed memoria.yml. The architecture notes explain the layout, and the documentation guide explains how the READMEs are maintained.

License

uagent is licensed under the Apache License 2.0; keep the NOTICE when you redistribute it. It runs uah-core-runner (MIT) as a separate program and includes none of its code.

Directories

Path Synopsis
cmd
uagent command
Command uagent runs one uah-core-runner task with safety guards, shows progress, prints the final answer, and records per-run stats.
Command uagent runs one uah-core-runner task with safety guards, shows progress, prints the final answer, and records per-run stats.
Package core is the pure part of uagent: what a run is, what happens during it, and the rules that turn events into statistics and an outcome.
Package core is the pure part of uagent: what a run is, what happens during it, and the rules that turn events into statistics and an outcome.
Package harness runs uah-core-runner with uagent's guards: preflight checks, a session lock, a pinned environment, process-group supervision, a disk limit, and run records in the state directory.
Package harness runs uah-core-runner with uagent's guards: preflight checks, a session lock, a pinned environment, process-group supervision, a disk limit, and run records in the state directory.
internal
procstart
Package procstart identifies a process by the boot it runs in and the time it started, so that a recorded process ID can be told apart from a later process that reuses it.
Package procstart identifies a process by the boot it runs in and the time it started, so that a recorded process ID can be told apart from a later process that reuses it.
Package stream writes run events as versioned JSONL, one object per line.
Package stream writes run events as versioned JSONL, one object per line.
testing
fakerunner command
Command fakerunner stands in for uah-core-runner.
Command fakerunner stands in for uah-core-runner.
fixtures
Package fixtures holds deterministic test data: captured runner output and domain value builders.
Package fixtures holds deterministic test data: captured runner output and domain value builders.

Jump to

Keyboard shortcuts

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