Skip to content

AKMON v0.5.0

Choose a tag to compare

@github-actions github-actions released this 05 Nov 03:10
· 2 commits to main since this release

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.whl is attached to
    this release. Install it with uv tool install or pip install, then
    follow the quickstart (docs/start/quickstart.md).
  • PyPI (new): the same wheel is also published to PyPI as the project
    okg, so pip install okg==0.5.0 and uv tool install okg==0.5.0 work.
    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 doctor warns
    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 manual okg db prune-source-blobs removed them (about
    0.5 GB a day on one large deployment). Set the window with
    source_blob_prune_grace_seconds in deployment.yaml (default 21600, that
    is six hours; 0 turns the cleanup off). The sync report shows the rows and
    bytes removed under record_set_blob_prune. A failed cleanup does not fail
    the sync; okg doctor warns source_record_set_blob_prune_failed for 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 excludes tests/** already left out documents under tests/, 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, in retry:, in error.retry_call and 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, min and max, and one total. aggregate over stored
    records now serves sum, min and max over 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 in not_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)

  • run counts 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.
  • run computes 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 as map(compute) outside run, with no
    outside call. Later steps read them as calc.<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 run step 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.
  • run accepts the call shapes models send. It now takes a small
    max_bytes (raised to the 4096 minimum, with a note), retain on a live step
    (ignored, with a note), a kept answer (l1.…) as a step input or each
    source, input: "$n" on graph records, and constants beside each'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-Question and X-OKG-Try, or okg/question and okg/try in the
    request's _meta). The server stores them on the call row (question_id,
    try_number, shown in okg.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 under agent_surface.surface_text in
    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 doctor checks 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_boundary and
    subject_prefix link 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 lint checks 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.replay replays 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.diagnose labels 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 run turns 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=1 keeps 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, min and max, 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. Use number(attrs.x) in map(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 run argument 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 a retry: call.

Upgrading from 0.4.0

  1. There is no new migration file in 0.5.0, but the schema step changes one
    table. Install the 0.5.0 wheel, then run okg up <deployment> --plan and
    okg up <deployment> as usual (or okg migrate --deployment <deployment> --apply). The step adds two nullable columns, question_id and
    try_number, to the MCP call log and its view okg.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-only alone
    fails on the new view columns until it has run.
  2. No setting needs to change. To tune the new record-list cleanup, set
    source_blob_prune_grace_seconds in deployment.yaml (default 21600; 0
    turns it off). To keep a test run's databases, set OKG_TEST_KEEP_DBS=1.
  3. 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 run okg db prune-source-blobs.
  4. If you compare results of search, aggregate or run against 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 run limits remain. A link key over a set plus other conditions, or
    written as input: {"ref": "$1"}, is still refused without an offered call.
    The negated form not (src in $1) can run out of the step budget on large
    sets. A kept run step 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 run can 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
    okg subcommands, 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 on question_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 under consistency.dangling_edges: prune,
    removed sources keeping their records, the first-ingest refs/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.