Repository navigation
AKMON v0.5.0
AKMON 0.5.0 follows 0.4.0. It stops the run logs from filling the database:
a source's large record list is stored once per sync instead of five times, and
old copies are deleted after each accepted sync. It makes the graph reader
easier for AI agents to use: run counts links, adds computed columns and
accepts more call shapes; a refusal shows the one call that fixes it; an empty
page says why it is empty; and stored numbers can be totalled. It speeds up two
slow paths, link rules that match by name and joins in the query interpreter.
It also adds the first tools for replaying benchmark questions against a graph
and testing wording changes on an experiment branch, and publishes the wheel to
PyPI. There are no new migration files; the upgrade adds two columns to the
call log. AKMON's Python package and command
are named okg, so every command below starts with okg.
Get it
- Python package: the wheel
okg-0.5.0-py3-none-any.whlis attached to
this release. Install it withuv tool installorpip install, then
follow the quickstart (docs/start/quickstart.md). - PyPI (new): the same wheel is also published to PyPI as the project
okg, sopip install okg==0.5.0anduv tool install okg==0.5.0work.
It is published after the release, by a separate workflow started by hand
that uploads only the wheel the release pipeline built, using PyPI trusted
publishing (PyPI trusts that one workflow, so no upload password or token
exists anywhere). Until that workflow has run, install from the attached
wheel. - Containers and Kubernetes: the Install section at the end lists the
runtime and PostgreSQL images and the Helm chart, each with its digest. The
PostgreSQL image is unchanged from 0.4.0.
What's new
Storage
- A source's record list is stored once per sync. A source keeps a "record
set": the list of record keys it owns and the graph rows each key produced,
used to retract rows a record no longer has. Before, a record set up to 1 MB
was copied into the source's saved position, its approval record, every
approval event and the sync report (saved twice). Now a set over 64 KB
(65,536 bytes) is stored once in its own table and the other places hold a
short reference. On a lab copy of a large deployment, run-log growth per sync
fell 85% with an identical graph.OKG_SOURCE_RUNNER_INLINE_RECORD_SET_MAX_BYTES
changes the 64 KB limit. If a stored set goes missing,okg doctorwarns
source_record_set_blob_missing, naming the source. - Old record lists are deleted after each accepted sync. After a sync is
accepted, the source's older record-set and cursor copies that no saved
position or held run names are deleted once they are older than a grace
window. Before, only the manualokg db prune-source-blobsremoved them (about
0.5 GB a day on one large deployment). Set the window with
source_blob_prune_grace_secondsindeployment.yaml(default 21600, that
is six hours;0turns the cleanup off). The sync report shows the rows and
bytes removed underrecord_set_blob_prune. A failed cleanup does not fail
the sync;okg doctorwarnssource_record_set_blob_prune_failedfor that
source until a later cleanup succeeds. Do not set a very short window while
inline or command-line runs of the same source can overlap: a sync that
outlives the window can lose its copies.
Reading the graph: what agents get
AKMON's MCP server is how an AI agent reads a graph. The server still lists
eight tools: inspect, search, expand, filter, map, aggregate,
query and run.
Finding things and saying why not
- Empty search and filter pages say why they are empty. Before, the reply
said only "0 rows". Now it names the cause: a record type the graph does not
have (with the real types listed), the condition that removed every match, a
query word that matches nothing, or the query itself. It also lists up to
three ids found with that part dropped, marked "loosened" and never counted. A
check that runs out of time says so and claims nothing. Under access rules, a
record the reader cannot see changes nothing in the reply. Pages with rows
are unchanged and cost no extra reads. - A grep list row shows every matched line, up to three. Searching for exact
text lists one row per file or record. Before, a row showed only the first
matching line, so the line that held the answer could be hidden. Now a row
shows all matching lines up to three and says how many more it left out. A
reply over its size limit drops these extra lines before it drops any row. A
typical grep page is about a third larger. - A search profile's excluded paths now apply to text chunks. A profile
that excludestests/**already left out documents undertests/, but the
chunks of those documents (which carry no path) were still found and shown.
A chunk is now left out when every document it belongs to is excluded, inside
the database query, so excluded chunks cannot crowd kept ones off a page and
counts match what is shown. Under access rules (audit or enforce mode) the
check runs after the query, so a page can come up short; it then says it is
cut. These searches cost somewhat more.
Refusals that show the fix
- A refusal names the one changed call that fixes it. When a read tool
turns down a call and one small change would make it work, the reply shows
that corrected call, ready to send, inretry:, inerror.retry_calland in
fix_hint. About twenty refusal kinds now do this. The server picks the call
without reading any data, offers none when the call has a second problem, and
marks a call that is one of several possible fixes as "a guess". A shown call
passes every check that reads no data; it is not proven to return rows.
Exact numbers and links
- Stored
sum,minandmax, and one total.aggregateover stored
records now servessum,minandmaxover up to 50,000 records, and
by=[]gives one total over everything. The reply names what each total
covers and which values were skipped, with record ids. A misspelled record
type is refused with the closest names instead of being called "a stored
set". This is not served under access rules yet. - One strict rule for what counts as a number. Live and stored totals, live
filters, and computed columns that read a field as a number now read text as
a number only when it is exactly one JSON number. Values such as" 10 ",
"+5","010",".5"and"1_000"are skipped and named in the reply (a
filter names them innot_comparable). To read them anyway, use
map(compute={"n": "number(attrs.x)"}). Numbers group by numeric value, so 1
and 1.0 are one group.
Several reads in one call (run)
runcounts links from an earlier step's records. A step can name the
records an earlier step selected as the start of a link count, for example
"how many of these commits changed each file":
{"aggregate": {"input": {"kind": "edges"}, "where": {"edge_type": "affects", "src": "$1"}, "by": ["dst"], "metrics": ["count"]}}. Before, a join reached
each file once, so a count after it said 1 per file, and naming the commits
as the links' start was refused. On a 3-million-link test graph the
most-changed-files question runs in 0.3 s.runcomputes columns on a step's rows. A step can add computed columns,
such as a ratio, to the rows an earlier step holds, using the same formula
language, limits and row states asmap(compute)outsiderun, with no
outside call. Later steps read them ascalc.<name>to filter, rank, total or
call by. Before, this was refused ("does not take compute"). The same change
fixes filters on an aggregate step's text group keys (for example
site = "X"), which matched 0 rows with no note. Computed columns over
stored graph records still wait for a later release.- A join step reads its start set and links once. A
runstep that follows
links from an earlier step's records finishes in about a second on the test
graph; for 91 commits it took 5.9 s and was refused for time before, and the
whole call now takes 1.4 s, with the same records and counts. runaccepts the call shapes models send. It now takes a small
max_bytes(raised to the 4096 minimum, with a note),retainon a live step
(ignored, with a note), a kept answer (l1.…) as a step input oreach
source,input: "$n"on graph records, and constants besideeach's varying
value. The reply says what it adjusted. A kept answer's upstream calls count
toward the run's 1,500-call limit. Of 28 recorded evaluation calls that 0.4.0
refused, 16 now run fully.
Benchmarks and tracing
- Each logged MCP call can carry a benchmark question and try. A test
harness may send the question id and try number with each request (headers
X-OKG-QuestionandX-OKG-Try, orokg/questionandokg/tryin the
request's_meta). The server stores them on the call row (question_id,
try_number, shown inokg.v_mcp_calls); concurrent calls keep their own
labels. A question is one printable-ASCII token of 1 to 200 characters with no
#or,; a try is 1 to 999999. A badly formed label refuses the call with a
named error. Calls logged before the upgrade keep an empty label. - A new experiment branch keeps the attribute values its base had. An
experiment branch is a separate copy of the graph used to try a change.
Before, attribute values set by observations (stored corrections layered over
the graph) were silently dropped from the branch on its first publish, so the
branch served the original value while the default branch served the observed
one. The branch now serves the observed value. Observations made later on the
branch show only on the branch. A refused branch read now names the branch. - Wording the server shows an agent can be set per deployment and per
branch. Tool descriptions, argument descriptions, refusal hints and the
instructions can come from a file named underagent_surface.surface_textin
the deployment manifest, with a different version per experiment branch.
Mistakes in the file are refused at start, a path must stay inside the
deployment directory,okg doctorchecks the served wording against the
file, and each benchmark replay records a fingerprint of the wording it saw.
A deployment without the file behaves exactly as before.
Ingest and publish
- Two link rules read each text once. The
word_boundaryand
subject_prefixlink matchers (a ticket that mentions a file path, a commit
subject that starts with a change id) used to compare every text with every
name. They now read each text once and find exactly the same links. At the
size of a stalled deployment (230,000 tickets, 56,000 file paths) a rule that
ran over two hours without finishing takes about 30 seconds on generated data
of that size. A graph read that times out now stops the run with an error;
before, it counted as "nothing to link". The one remaining compare-everything
step refuses above a set limit instead of running for hours. Four other link
builders still compare every pair. - Equality joins in the transform interpreter use an index. A join of two
tables on matching column values looked at every pair of rows. It now looks
keys up in an index and returns the same rows in the same order. A check that
joined claims to claim text took 13.6 s at 2,000 claims, 187 s at 8,000 and
3,061 s at 32,000; it now takes 0.66 s, 2.7 s and 10.6 s.
Benchmark tools (optimizer)
AKMON now ships a first set of tools for measuring a deployment against a fixed
question set and trying a change on an experiment branch. They run as Python
modules and do not change how a graph is built or served. A deployment that
does not use them is unaffected.
objectives.yaml. A deployment can state its goals in this file: quality
targets, cost limits, operator floors and ceilings, and three fixed rules
(provenance, versioning, access control). A scorer reports each target as met,
violated or unmeasured; a missing value never counts as zero.okg deployment lintchecks the file.- Question sets. Questions load from one format that records who wrote each
question, the facts a correct answer needs, its topics ("scope tags") and
whether it is held out. A load withholds held-out questions by default, and a
command prints how many questions cover each topic. - Replay runner.
python -m okg.optimizer.replayreplays a question set
against one fixed setup, judges the answers and compares runs that share the
same setup fingerprint. It reports how much scores vary between two runs of
one setup, enforces a hard spend cap over every model call, and ends a run as
incomplete, with a named reason, when a call fails or the cost per try passes
the multiple of the estimate its run file sets (budget.max_ratio). - Diagnosis.
python -m okg.optimizer.diagnoselabels each missed fact with
a cause, a quoted piece of evidence and a confidence (a language model writes
the labels), counts wasted tool calls by rule with no model call, and
measures agreement with hand labels. - Proposal.
python -m okg.optimizer.propose runturns a diagnosis into one
checked change, either new wording for what the server tells an agent or a
rule that links records already in the graph. It writes the change as
deployment files, publishes it on its own experiment branch, counts the new
links against the base branch, and restores the deployment exactly as it was.
Whether the change helps is decided separately.
Release and operations
- The wheel is published to PyPI as
okg(see Get it). The upload workflow
is started by hand with a release tag, runs only from the main branch, and
refuses a wheel that differs byte for byte from the one the release published. - Test runs delete the databases they create. A test run that uses Postgres
now deletes its per-worker databases and migration templates when it
finishes and prints how many it deleted and any deletion that failed.
OKG_TEST_KEEP_DBS=1keeps them for debugging. This affects only people who
run AKMON's tests. - Flaky test fixed. Offered-call tests in the MCP read-grammar suite no
longer fail at random, and check the whole refused call again. No product
code changed. - The test-lane budget check survives brief GitHub outages. The CI check
that compares each test lane's run time with its budget now waits longer
between retries, retries a reply cut short, and asks for smaller pages.
Breaking changes
These change how an existing installation behaves. A new install is not
affected. No command, setting or table was removed.
- Totals over text that is not exactly one JSON number change. Live
sum,minandmax, live filters and computed columns that read a field
as a number skip values such as" 10 ","+5"and"010"that 0.4.0 read as
numbers (see What's new). A total that included them is now smaller and the
reply names the skipped values. Usenumber(attrs.x)inmap(compute=...)
to read them. - A link rule's graph read that times out now fails the run. In 0.4.0 it
counted as "nothing to link" and the run went on. A run that depended on that
now stops with an error; fix the slow read or raise its limit. - Search profiles that exclude paths now hide the excluded files' text
chunks, which 0.4.0 still returned. Counts and pages shrink to match. - Grep list pages are larger (about a third on a typical page) because rows
show up to three matching lines. - A new
runargument shape or refusal hint can change a reply that an
evaluation script compares byte for byte: calls 0.4.0 refused now run, and
refusals can add aretry:call.
Upgrading from 0.4.0
- There is no new migration file in 0.5.0, but the schema step changes one
table. Install the 0.5.0 wheel, then runokg up <deployment> --planand
okg up <deployment>as usual (orokg migrate --deployment <deployment> --apply). The step adds two nullable columns,question_idand
try_number, to the MCP call log and its viewokg.v_mcp_calls
(compressed call-log chunks stay compressed); no data is deleted. Run the
full step the first time after upgrading:okg migrate --views-onlyalone
fails on the new view columns until it has run. - No setting needs to change. To tune the new record-list cleanup, set
source_blob_prune_grace_secondsindeployment.yaml(default 21600;0
turns it off). To keep a test run's databases, setOKG_TEST_KEEP_DBS=1. - Existing stored record lists stay where they are until a source's next
accepted sync. The first sync of a large source after the upgrade writes its
record set in the new form; the next accepted syncs delete the older copies
once they pass the grace window. To free the space at once, stop the worker
and runokg db prune-source-blobs. - If you compare results of
search,aggregateorrunagainst saved 0.4.0
output, expect the differences listed under Breaking changes.
Known issues
- Cleanup can race a long sync. The automatic cleanup protects a running
sync's copies only for the grace window. A sync that outlives the window can
lose its copies; keep the default six hours unless every run of a source is
shorter than your window. - Branch observations still do not reach the default branch on merge-back,
and a branch cannot override an inherited value without affecting the default
branch. Both come from observations not being scoped to a branch. - Other
runlimits remain. A link key over a set plus other conditions, or
written asinput: {"ref": "$1"}, is still refused without an offered call.
The negated formnot (src in $1)can run out of the step budget on large
sets. A keptrunstep does not keep a handle for its rows. A shown retry call
passes every check that reads no data but may still return no rows. - Stored totals are refused for large records. Records of about 25 KB with a
date condition are refused at 50,000 records. Stored totals are not served
under access rules. - Empty-page explanations have limits. Under access rules a "loosened" id
list is omitted for records the reader cannot see. A grep row's "N matches"
counts matching text units, not lines, so a row can say "1 match" next to
three lines. - Text-pattern counts in
runcan still time out. Counts of text patterns
inside one step are not helped by the join speed-up. - Experiment-branch wording and benchmark tools are early. The replay runner,
diagnosis and proposal tools run from Python modules, are not covered by
okgsubcommands, and have been exercised on a small number of deployments.
A scorer reports "fail" for the fixed rules until the replay runner records
them. A benchmark question label has no index onquestion_id. - PyPI is empty until the upload workflow runs.
pip install okg==0.5.0
works only after the separate publishing workflow has been started for this
release. - Everything listed as a known issue in the 0.4.0 notes that this release
does not mention is unchanged, including views that read outside the graph
and declare nothing, dangling links underconsistency.dangling_edges: prune,
removed sources keeping their records, the first-ingestrefs/heads/.invalid
failure, and the quickstart having been followed on podman.
Install
Everything below was built from the commit this tag names. The tags v0.5.0 (images) and 0.5.0 (chart) are never moved or overwritten; pin a digest to be certain.
| Artifact | Reference | Digest |
|---|---|---|
| Runtime image | ghcr.io/mitdbg/akmon:v0.5.0 |
sha256:944e74e8a27acd0889755398f40b840f80c4a4b04c28d32ff8e34e1d16cea303 |
| PostgreSQL image | ghcr.io/mitdbg/akmon-postgres:v0.5.0 |
sha256:53087d20398717e788ee998f22d4eb30450d7a06aa79cb5215ea42c13c36fa24 |
| Helm chart | oci://ghcr.io/mitdbg/charts/akmon version 0.5.0 |
sha256:72e84e38d2dbee0f207358400fdbe8dcb5e7c4890092faf8c0386e7cfb7bc3df |
helm install okg oci://ghcr.io/mitdbg/charts/akmon --version 0.5.0 -f values.yaml
The chart's appVersion is v0.5.0: with images.okg.tag and digest empty it runs ghcr.io/mitdbg/akmon:v0.5.0, and its PostgreSQL image is ghcr.io/mitdbg/akmon-postgres:v0.5.0.