v0.2.0-alpha.1
Pre-releaseBuildMax is an out-of-the-box, privately deployable Agent platform: a CLI/TUI,
a desktop app, and a team Portal on one shared Go Agent Runtime.
Highlights
- The local agent can work in the background:
Bash,Task, and the new
Monitortool detach into jobs,JobList,JobOutput, andJobStop
inspect them, and a completion or a monitor line can wake the conversation
in the TUI and in Desktop. Every job writes a durable, redacted event log. - The CLI and Desktop now have exactly two modes, decided by whether you are
signed in: local models fromsettings.yamlstraight to their provider, or
the models a deployment offers through its gateway. Neither mode covers for
the other. - Local inference through Ollama, in a local session and through a deployment
alike, with no provider key on either path. - Runs report what they cost: per-model
pricing, per-call cost in traces,
cache tokens on every surface that shows spend,buildmax statsfor a
session, and prompt caching as a policy rather than a boolean. - Teams activate published plugin releases, an agent names the plugins its
background runs load, and a worker materializes exactly those. Portal gains
a Plugins section under Space. - Identity and storage were rebuilt: opaque unprefixed entity identifiers,
numeric relational keys underneath them,DATETIME(6)timestamps, and an
API resource that names its own identifierid.
Upgrade notes
- An existing Alpha database and object store must be recreated. Opaque
entity identifiers, numeric relational keys, textpublic_idcolumns, and
DATETIME(6)timestamps all land in this release with no conversion
migration. Every existing identifier, access token, refresh session, worker
token, Portal link, bookmark, and stored object key is invalid. - API clients that read a resource's own identifier from a type-named field
(task_id,user_id,team_idon the resource itself) must readid.
Relationships keep their semantic names. - The deployment-wide
worker.tokenis gone. Remove the secret from your
manifests and upgrade the server before the worker image: a run dispatched
without a run token now fails rather than falling back. - Managed models are named by catalog name. Replace
llm.aliasesand
llm.default_aliaswithllm.default_model,worker.llm.aliaswith
worker.llm.model, and/api/teams/{team_id}/llm/...with/api/llm/...;
atransport: buildmaxentry dropsteam_idand names the catalog model. models[].transportandmodels[].server_urlare removed from
settings.yaml; a session's mode followsbuildmax login/logout, and
default_modelnames the entry a signed-out session starts with.- The
prompt_cacheboolean is removed. Writecache_control: {mode: off}
where it saidfalse; Anthropic agent turns now cache by default. - The server stops in order within
shutdown_grace(default 25s). Deployments
should set a matchingterminationGracePeriodSecondsandpreStoppause, as
the reference manifests do. - Native Desktop bundles are still not published by the release workflow. The
tagged source contains Desktop; GitHub Release artifacts contain the CLI and
server binaries.
Install
Download the archive for your platform below. Each one contains all three
binaries — buildmax, buildmax-server, buildmax-worker — plus
config-examples/, LICENSE, and NOTICE-THIRD-PARTY. Verify it against
checksums.txt first.
With a Go toolchain, for the CLI alone:
go install github.com/gougoujiang/buildmax/cmd/buildmax@v0.2.0-alpha.1
As a container, for linux/amd64 and linux/arm64:
docker pull ghcr.io/gougoujiang/buildmax:0.2.0-alpha.1
Copy config-examples/settings.example.yaml to ~/.buildmax/settings.yaml,
configure at least one model, then run buildmax for the TUI or
buildmax -p "your prompt" for a single non-interactive turn. Running the
server and Portal additionally needs MySQL and object storage; see the README.
The desktop app is not published here. It needs code signing and notarization
to launch on macOS, so build it locally with ./make build.
Before you deploy this
This is an alpha, and two defaults deserve to be read as warnings rather than
footnotes:
- Server authentication is bootstrap-level. There is no mail channel, so an
operator creates accounts and issues single-use login codes with
buildmax-server user createanduser login-code. Self-registration is
closed by default. There is no password, second factor, SSO, or recovery
flow; putting the Portal on a network you do not control requires wiring a
real identity provider first. - The bash sandbox is off by default, and worker hardening is incomplete.
SECURITY.md lists both, along with how to report a vulnerability privately.
Interfaces and deployment guidance may change before a stable release.
Added
-
A Portal agent names the plugins it loads for a background run, and loads only
those: nothing is inherited from what its team activated. The selection
versions with the rest of the definition, so an earlier revision still says
what that agent named, and restoring one brings its selection back. -
Background events can wake the conversation in the TUI:
run_in_background
calls acceptdeliver_resultto have the completion delivered as its own
turn, and aMonitorstarted withreactsends each delivered line back
for analysis. Delivered payloads are marked as untrusted observations,
recorded with non-user provenance, and never run user-prompt hooks. -
Background subagents in the TUI and Desktop:
Taskaccepts
run_in_backgroundto delegate investigation without blocking the
conversation. The final reply is read withJobOutput, the job stops with
JobStop, and traces link the subagent run to the tool call that launched
it. -
./make cache-qualifychecks prompt caching against a real provider. Every
other cache test runs against a fake upstream, which proves what BuildMax
sends and nothing about what a provider does with it — a request can be
perfectly shaped while the provider declines to cache it, for a minimum prefix
length, an unsupported model, or an expired retention window. The suite runs
first write, sequential read, changed prefix, long-history lookback,
streaming, concurrent cold starts, and retention, and prints what the provider
reported for each. Name the target withBUILDMAX_CACHE_QUALIFY_PROVIDER,
_MODEL,_API_KEY, and optionally_BASE_URL; it calls a paid provider, no
check runs it, and it skips when none is named. A model entry can also name an
integrationfor an OpenAI-compatible gateway whose cache behaviour has been
qualified — none has, so every value is currently refused. -
Desktop delivers background events into the conversation: a completion
requested withdeliver_resultor areactmonitor line runs as its own
turn when the owning session is on screen and idle, and is parked — not
lost — while that session is busy or another one is open. The transcript
labels delivered events as background observations, collapsed by default. -
Background jobs write a durable event log under
<traces>/jobs/: launch
provenance (owning session, parent run and tool call, sandbox fact),
monitor lines with drop accounting, and the terminal state. Logs are
redacted, bounded, and always end with how the job ended. -
./make kind seedfills the local kind cluster's model catalog from the
repository-rootsettings.local.yamland grants the deployment's teams an
alias for each model, so the CLI and Desktop can drive the managed transport
against real inference. The cluster's own Portal conversations and task runs
keep answering from the deterministic mock. -
Background commands in the TUI and Desktop:
Bashaccepts
run_in_backgroundto detach long builds, tests, or servers as local jobs,
and the newJobList,JobOutput, andJobStoptools inspect and stop
them. Jobs pass the normal permission and sandbox checks before detaching
and end with the application. -
Local models through Ollama:
provider: ollamaon a model entry calls a
local daemon's own API with noapi_keyat all. It sends the context window
on every call, so the daemon no longer applies its own default and quietly
truncates the system prompt and tool definitions out of a longer request —
the failure that made small local models look like they could not call tools.
buildmax init --ollamawrites the entry,buildmax models --locallists
what is installed and which models can call tools, andbuildmax doctor
reports a daemon that is not running or a model that is not pulled with the
command that fixes it. -
A deployment can serve a local Ollama model:
--provider ollamaon
buildmax-server model add, orprovider: ollamaunder
conversation.model, with no credential in either place. Real inference and
real tool calls reach the gateway, thellm_callledger, and quota without a
provider key or a bill. The daemon stays on the host — a pod cannot use the
host's GPU — and the deployment names an address that reaches it, which under
Docker Desktop ishost.docker.internal. -
New
Monitortool in the TUI and Desktop: watch logs, files, or CI by
running a command whose stdout lines become bounded events. Lines are
rate-limited and truncated, dropped lines are counted, and the watcher
passes the same permission and sandbox checks asBash. -
OpenAI Responses calls now carry a scoped
prompt_cache_key, and can ask for
24-hour retention withcache_control: {ttl: 24h}. The API caches on its own
either way, so the key does not turn caching on — it decides which prefixes
are looked up together, which matters because callers sharing a credential
otherwise share one bucket. The key is derived from the credential, the model,
the team on a managed call, and fingerprints of the system prompt and tool
definitions; it carries none of them in readable form and is never written to
a ledger, trace, or log. Retention vocabulary is per provider:5mand1h
are Anthropic's and24his OpenAI's, and asking for one where it is not
documented is refused at startup rather than sent and ignored. -
Add
./make models list,./make models info <model>, and
./make models checkto look up configured and OpenRouter-catalog model
details, and catch context_window drift, from the terminal instead of the
openrouter.ai models page. -
buildmax -p --output jsonand--output jsonlnow reporttrace_idand
trace_path, so a script can open the trace that run wrote instead of
guessing at the newest file in the session's trace directory. -
A run can now report what it cost. A model entry takes a
pricingblock —
currency plus four decimal rates per million tokens, for fresh input, cache
reads, cache writes, and output — and the CLI prints aCost(session)line,
--format jsoncarries the same figures, and the session file keeps a running
total. A managed deployment sets the same rates per catalog model with
--currency,--input-price,--cache-read-price,--cache-write-price,
and--output-priceonbuildmax-server model add; the rates in force are
copied onto each call's ledger row when it is accepted, so repricing a model
does not restate what a team already spent. Portal's run view shows the run's
cost and what caching saved against an uncached baseline. Cost is shown only
where every rate needed for it was recorded — anything else reads
unavailablerather than zero — and a saving is reported only when caching
actually saved: a run that wrote cache entries nothing read back paid more
than it would have uncached, and is shown as the cost it was. -
Run details now name which revision of an agent definition a run executed
under, and say when the definition has been edited since. Editing an agent
changes what its next run does, which is intended; until now nothing recorded
which text produced a given result. -
Run details now open with where the run came from. A background run records
the conversation message it was asked for in, and the Portal shows that
message next to the instruction the worker was actually given — so a
constraint missing from the instruction can be told apart from one that was
never asked for. It is shown even for a run that wrote no trace. -
buildmax stats [session-id]reports one session's spend with its cache
breakdown, how close it came to the context window, how many bytes each tool
put back into that window, the split between model time and tool time, and
what its delegated runs cost;--jsonemits the whole record./statsin
the TUI shows the same figures for the session on screen. -
A team can activate published plugin releases for its background runs, pinned
to an exact version and digest. A team either curates that list or leaves it
open, in which case naming a plugin in an agent activates it. Releases
contributing hooks or MCP servers cannot be activated yet.
buildmax plugin activations --team <id>reads what a team activated. -
Portal gains a Plugins section under Space: what this team has activated and
at which version, what each release contributes, which agents name it, whether
a newer release is available, and whether the team curates its list or opens
the whole catalog. Owners and admins can activate, update, and suspend from
there; any member can read it. -
A run trace now records what each model call cost, not just what the run did.
llm_endcarries that call's own token counts and its estimated cost;
run_endcarries the run's. The per-call figures matter because the running
totals cannot answer which turn was expensive, and subtracting consecutive
records to find out goes wrong the moment a call in between failed. It also
makes the shape of caching visible: the turn that writes a cache entry costs
more than it would have uncached, and only a later turn reading it back puts
the run ahead. Costs are absent when the model was unpriced, which is not the
same fact as a call that cost nothing, andcost_incompleteonrun_end
says a call did work that could not be priced. -
A background run now loads the plugins its agent names. The server resolves
the team's activations when the worker claims the run, and the worker fetches
exactly those releases, verifies each against its pinned digest before
extraction, and refuses to start rather than run without one it was told to
have.
Changed
-
An API resource now names its own identifier
idrather than repeating its
type — a task returns{"id": ..., "team_id": ...}where it used to return
{"task_id": ..., "team_id": ...}. Relationships are unchanged and keep their
semantic names, so only the field naming the resource itself moved. Users,
teams, artifacts, audit events, managed model catalog entries, system grants,
managed LLM call records, and webhook keys are affected; most other resources
already usedid. Agent and workflow revisions no longer return an identifier
at all: a revision is addressed by its parent plus its revision number, which
is what the restore route already used. Catalog plugins and plugin releases
likewise drop theirs, being addressed by name and by name plus version. -
The CLI and Desktop now run in one of two modes, decided by whether you are
signed in. Signed out, the models are the ones insettings.yamland each
call goes straight to its provider; signed in, they are the ones that
deployment offers and every prompt goes there.buildmax loginand
buildmax logoutswitch, and nothing else configures it —models[].transport
andmodels[].server_urlare gone, because a session is in one mode or the
other rather than holding both kinds of entry. A newdefault_modelkey names
which entry a session starts with while signed out; a deployment names its
own.buildmax models, the/modelpickers, and the TUI footer all say which
mode you are in, andbuildmax doctorreports it as a check of its own.
Desktop no longer opens on a sign-in form: local mode is a working state, so
the workbench opens directly and signing in is an action in the account menu.
Neither mode covers for the other — a deployment that is down, or a login that
has expired, stops the session and says so rather than quietly sending the
next prompt to a provider you did not choose for it. -
./make doctornow warns that Portal test dependencies and Playwright
browsers are unavailable when npm is missing, instead of reporting cached
browser state as ready for./make e2e. -
The server now stops in order on SIGINT or SIGTERM: it reports itself
unready so a load balancer stops sending it work, ends the streams watching a
run so the Portal reopens them elsewhere, refuses new conversation turns and
waits for the ones running, drains in-flight requests, and only then stops its
background loops. The whole budget isshutdown_graceinserver.yaml,
default 25s; the reference manifests set a matching
terminationGracePeriodSecondsand apreStoppause. -
buildmax --helpno longer lists cobra's auto-generatedcompletion
command.buildmax completion <shell>still prints the shell script for
anyone who wants it. -
BuildMax now identifies its outbound LLM requests with a versioned
User-Agentheader. -
worker.run_mode: local_processis now documented as what it is — a
single-machine topology where a task run is a child process of the server
under the same uid, sharing its trust domain — instead of being called a
development path the Compose deployment contradicts. The startup warning says
the same. Nothing about how a worker is launched changed; a deployment that
needs its server separated from model-chosen code still runs
worker.run_mode: k8s_job. -
A managed model is now named by its catalog name rather than by a team alias.
server.yamllosesllm.aliasesandllm.default_aliasand gains
llm.default_model, which names one of the modelsbuildmax-server model add
created, or is left empty to use the first enabled one.worker.llm.alias
becomesworker.llm.model. A name that matches no catalog row now stops the
server at startup, while an empty catalog does not — rows are added while the
server runs. Insettings.yaml, atransport: buildmaxentry dropsteam_id
and puts the catalog name inmodel; every model a deployment offers is
available to every user of it, sobuildmax models --team <id>becomes
buildmax models --server. The gateway routes move from
/api/teams/{team_id}/llm/...to/api/llm/..., and a managed call is
recorded against the person who made it rather than against a team. -
Entity relationships are stored as numeric keys rather than as repeated
identifier strings. Every reference that names exactly one kind of row is now
abigint, the public handle each row shows the outside world is a separate
binary(12)column, and translating between the two happens inside the store
and nowhere else. Nothing about the API changed — a handle is still what
every request, response, token, log line, and object key carries. What
changed is underneath: a team's task list is answered by reading an index
backwards instead of sorting rows, usage aggregation is answered from indexes
without reading rows at all, and identity no longer depends on the database's
text collation. References that cannot be one number — a polymorphic actor, a
provider's tool-call ID, an agent session naming a file — stay text
deliberately, and a test refuses a new one added without that reason. -
Login-chain, trace-file, and Desktop-project identifiers lost their
as_,
rt_, andp_prefixes and are now ordinary opaque IDs, leaving one
identifier format in the codebase. None of the three is read by a person or
dispatched on; they had kept a prefix only because they are not database
rows, which was where the previous change stopped rather than a reason to
keep one. Background jobs are the exception and keepjb_: a job ID reaches
the model as a bare string inside tool output, and free prose is the one
place a type prefix says something the surrounding context does not. -
Entity identifiers are now opaque and unprefixed:
ivyoh5qcfu6ypfkhyedq
rather thant_9f3k2m8x1qwe7rt4zy0p. Each is 96 bits of crypto-random data
written as 20 lowercase base32 characters, case-insensitive on input and safe
unchanged in a URL, a filename, a Kubernetes name, and a shell. The type
prefix is gone because nothing dispatched on it — a route, a JSON field, and a
column already name the type — and because it was the only thing standing
between a presentation choice and the database schema. Agent and workflow
revisions, catalog plugins, and plugin releases carry no identifier at all
now; they are addressed by their parent plus a revision number, by name, and
by name plus version. Login-chain, trace-file, and Desktop-project identifiers
keep their prefixes: none of them names a database row. This is a breaking
change with no compatibility path. Every existing identifier, access token,
refresh session, worker token, Portal link, bookmark, and stored object key is
invalid. Recreate the database and object store rather than upgrading. -
Sub-agent delegations to a read-only agent type such as
explorenow run in
parallel with each other and no longer prompt for approval, and a sub-agent's
own tool calls honouragent.max_parallel_toolsinstead of always running one
at a time. -
Prompt caching is now a policy rather than a boolean, and Anthropic agent
turns cache by default. A model entry takescache_control: {mode, ttl}—
mode: auto(the new default) asks on an agent turn, whose prefix goes out
again on the next iteration, and never on a one-shot call such as title
generation or compaction, where a cache write costs more than it can ever
save;offnever asks andforcealways does.ttlselects retention where
the provider documents it —5mor1hon Anthropic,24hon OpenAI — and
is refused at startup anywhere else rather than sent and ignored, as is
forceon a provider that takes no cache instructions at all. The
prompt_cacheboolean it replaces is removed rather than kept as a
shorthand; writecache_control: {mode: off}where it saidfalse. Managed
deployments get the same policy per catalog model through--cache-modeand
--cache-ttlonbuildmax-server model add. -
Prompt-cache token counts now reach every surface that shows what a run
spent. Providers already reported them and the managed ledger already stored
them, but they stopped there: run statistics, run traces, session totals, the
CLI's summary and--format jsonoutput, Desktop's run status, the team
run-ledger route, and Portal's run-spend view all dropped them, so a cached
run was indistinguishable from an uncached one. Cached counts remain a
breakdown of the prompt rather than an addition to it, and each surface shows
them only where a provider actually reported some — a provider that reports
nothing is not a provider that missed. -
Server
public_idcolumns now store the 20-character canonical text form
instead of raw bytes, so direct database queries show the same IDs the API
does. Breaking for existing server databases: there is no migration —
recreate the database (./make kind down && ./make kind up, or drop the
Compose MySQL volume). -
The deployment-wide
worker.tokenis gone. Every/api/worker/*route now
takes only the run token the server mints at dispatch, so a worker can read
and write its own run and nothing else.worker.tokenand
BUILDMAX_WORKER_TOKENare no longer read, and the secret has been dropped
from the Compose and Kubernetes manifests — remove it from yours. Upgrade the
server before the worker image: a run dispatched without a run token now
fails immediately instead of falling back. -
Stopping a server in
local_processworker mode no longer waits for the agent
run it dispatched. The scheduler stops claiming immediately, asks the worker it
spawned to stop — which reports what the run produced — and gives up on a
worker that will not go, within the sameshutdown_gracebudget. In
k8s_jobmode the Jobs already outlive the server, and still do. -
Every stored moment in time is now a
DATETIME(6)column and an RFC 3339
string in the API, replacing Unix seconds inbigintcolumns and JSON
numbers. Auditsinceanduntilquery parameters take RFC 3339 too. An
existing Alpha database is recreated rather than converted; no conversion
migration ships. -
A task run whose worker is shut down — a drained node, an evicted pod, a
restarted deployment — now stops, uploads what it produced, and reports
FAILEDwith a message naming the shutdown, instead of sitting inRUNNING
until the stale-run reaper closes it hours later. Its output and artifacts are
kept and shown the way a cancelled run's are.
Fixed
-
Editing an agent no longer fails with a duplicate-key error. The update wrote
a row rebuilt from the domain model, which no longer carries the database's
own key, so it was saved as a new agent rather than as a change to the
existing one; the unique index caught it and refused the edit. The update now
addresses the agent by its identifier. -
The Portal conversation list no longer fills up with machinery. A workflow
step and an issue agent run each create a conversation because a task requires
one, and those were listed alongside conversations people actually hold — on a
team that runs either, they pushed real conversations off the page. They are
still kept, and a link straight to one still opens it. -
A finished background task now always gets reported back to its conversation.
The reply used to be a one-shot attempt: a model call that failed, a busy
conversation, or a server restart between the task finishing and the reply
being written meant the conversation was simply never told. The report is now
recorded as owed and retried until it succeeds, or until it has failed enough
times to be given up on with the reason kept. The result itself was never at
risk — the task's card reads it directly. -
Closing the runtime now waits for the background job trace writer to drain,
so a job's final record always lands intraces/jobs/before exit. -
Refresh the built-in context_window fallback table: several ids had drifted
from what providers now report (some by a lot, e.g. deepseek-r1), one
Anthropic id had a hyphen where the real id uses a dot and never matched,
and OpenAI/Anthropic now cover their fast/mid/premium tiers instead of one
model each. -
The TUI
/modeland/taskspanels scroll. With more entries than fit the
terminal each listed the first few and a… N morerow, while the arrow keys
kept moving a selection through the ones it was not showing — so a model past
the fold could be switched to, and a job past it stopped withs, without
ever being seen. Both lists are now a window that follows the selection, and
/modelopens on the model in use rather than at the top. -
A Portal conversation now shows the background tasks it started. Each task
gets a card in the thread carrying its status, what the run produced, its
files, its run details, and stop or run-again, placed among the messages by
when it was created. The cards are read from the server, so they survive a
refresh and a dropped connection, and a task result is no longer displayed as
if the user had typed it. -
The Portal image now ships third-party license attributions: the licenses of
every npm production dependency are served at/third-party-notices.txt, and
the image carries the Apache-2.0 text under/usr/share/licenses. -
Pressing esc on the
/command popup now closes it for good. The popup was
rebuilt from the input on every message, so the next cursor blink brought it
straight back; it now stays closed until the typed command changes. -
Session token counts and cost now include the work a run delegated to a
subagent and what each context compaction cost; both were previously spent
and never counted, so long sessions and ones that usedTaskunder-reported
themselves. A subagent's trace is also filed under the session that started
it rather than under a discarded id, andtool_endrecords how a call failed. -
A finished background task now reports back whether or not anyone is watching.
The reply used to be sent through the creator's first open browser connection,
which meant it was skipped entirely when they had none and reached only one
tab when they had several. Every connection on the team is now told the task
changed, and the reply itself is written to the conversation independently of
any connection. -
A run trace now reaches disk one record at a time, so a run that is
interrupted — killed, crashed, or given up on mid-turn — keeps its final
records instead of losing up to 4KB of them. Previously the tail stayed
buffered until the run closed cleanly, which made the last recorded event
appear seconds before the last one that actually happened. Shutting the
runtime down also closes the trace of a run still in flight, marking it
abandoned rather than leaving it open with norun_end. -
A TUI panel such as
/modelor/toolsnow trims its list to what the
terminal can show. It listed a fixed number of rows whatever the height was,
so on a short terminal the panel pushed the input box and the footer off the
top of the screen, and nothing could scroll them back. Panel lines also no
longer wrap inside the panel border, which was quietly doubling the height of
the/tools,/skills, and/diffpanels. -
Closing a TUI panel such as
/modelno longer leaves its box drawn above the
input. The terminal renderer the CLI depends on stopped erasing the lines a
shrinking frame vacates, so every dismissed panel stayed on screen; the
dependency is pinned back to a working revision. -
The inbound webhook's
202response no longer labels a run identifier
task_id. It was returning the task run's ID under the task's name and no
task identifier at all, so a caller that wanted to follow the work had to
guess which of the two it had been handed. The body now carries both, matching
what creating a run through the API already returned:
{"task_id": "...", "task_run_id": "..."}.
Earlier releases are in
CHANGELOG.md.