Inspiration

An ant colony has no manager. No ant tells another ant where the food is. One ant wanders more or less at random, and if it finds something, it dribbles a chemical on the ground as it walks home. Other ants are slightly more likely to walk where the chemical is strong, and every ant that follows the trail and finds food strengthens it. Trails to nowhere get no reinforcement and evaporate. Come back an hour later and there is a clean highway between the nest and the food, and no ant ever decided to build it. The ground built it.

Investigators fail the same way ants would fail without that mechanism. Not because they cannot read — because the one line that matters is filed under someone else's topic. A firmware changelog does not get opened by the person investigating power budgets. The person investigating firmware has no reason to keep reading past the firmware section.

We wanted to know whether ant coordination does real work on evidence instead of food. And whether it could be made completely reproducible, because that is the part that decides whether anyone would ever trust it.

What it does

Maya is a fraud investigator at a mid-size insurer. One claim, forty documents behind it — interview notes, a maintenance log, a firmware changelog, a weather report, a diver's field notes — and three theories about what happened. Whichever theory she chases, she has to write down why. Reading all forty and holding the connections in her head takes a day. A chatbot gives her a confident paragraph she has no way to check.

Pheromone Trails gives her a map instead of an answer.

The forty documents become nodes in a graph and their relationships become edges. Maya picks a theory. A language model turns it into three different search angles on the same question. Five agents start on a neutral fragment — deliberately not on the answer — and each one walks the evidence through one of those angles. When an agent reaches a fragment that is genuinely relevant and that fits what it has already read, it strengthens the connection it just travelled. Every round, every trail evaporates a little, so paths that lead nowhere fade on their own.

No agent messages another. There is no coordinator, no planner, no orchestrator deciding who goes where. The only thing the five share is the map they are all writing on.

After eight rounds most of the map has faded to grey and a handful of connections have thickened into a corridor. It is extracted and highlighted with every source, every scoring factor and every contradiction shown — including the evidence that argues against Maya's theory, which deposits trail too, at a discount, so the map surfaces it instead of burying it.

Then she can drag a slider and replay the entire search backwards like video. Or delete the fragment at the centre of the corridor and re-run on the same seed, and watch the swarm route around it. That is how she knows she is looking at a real search and not an animation.

The find

Asked about a firmware regression, the swarm extracts a corridor that leaves the firmware cluster entirely and ends on battery pack voltage crossing its low cutoff.

It gets there through one fragment: a changelog line recording that the firmware release also raised the transmit duty cycle. That line is filed under firmware, so nobody investigating power ever opens it. Raising duty cycle raises current draw.

That crossing is in no agent's plan. Two agents hit the bridging fragment at tick 3 by random exploration, with a trail term of 0.055. By tick 7 the trail term on that same edge is 0.978. They came back because of what they had left behind. One tick later, one of them stepped across into the power chain.

Suppress that single fragment and re-run on the same seed. The corridor stops crossing, stays inside firmware, and surfaces the fragment that contradicts the firmware theory instead.

Five concurrent agents, byte-identical every time

Multi-agent systems usually give a different answer every run. That makes them impossible to test, miserable to debug, and dangerous to demo. We removed the cause instead of tolerating it.

The agents cannot write. ForageTick reads an agent's current fragment and its incident edges, scores the options, and returns exactly one proposal. ReduceTick is the single writer: it sorts all five proposals into canonical order — never arrival order — applies the deposits, runs one global evaporation pass, and writes a receipt. One writer, fixed order, so five agents running at once land bit for bit in the same place.

The exploration randomness is sha256(seed | tick | agent_id | candidate_id). Not Python's hash(), which is salted per process so the same seed diverges after a restart. Not a shared random stream, whose output would depend on which agent happened to run first.

Same seed, same run, forever, across process restarts. A judge can drag the scrubber through eight ticks of history and it replays exactly.

The colony outlives the run — and the analyst

A single eight-tick search is one pass. Ant coordination is a many-passes mechanism. So the trails do not die with the run.

Every run's useful deposits are committed into a commons: a second value carried on the same edges that decays slowly across runs instead of quickly within one. The next search starts on a map that already knows where the good routes were. Contributions are deferred to the start of the next run, so a run's own baselines and comparisons never get to read its own answer.

The part we are landing now is that the commons is genuinely shared, not private. The case graph hangs off root.shared, so every analyst's swarm forages the same graph and deposits into the same memory. Maya's search makes the next investigator's search start warmer. And it cost no branching at all: outside a served context — jac run, scripts, jac test — the runtime makes root.shared and root the same thing, so one code path is single-user cross-run memory on the CLI, multiplayer on a server, and isolated inside every test.

Every deposit carries who made it. Not a log line — a node in the graph recording the exact edge, the exact amount, the contributing analyst's root id, the provider, and the id of the receipt that justifies it.

Then somebody poisons it

Shared memory that anyone can write to is an attack surface. So we attacked it.

The Warden reconciles every session's deposits against the receipts of that session's own run. Because every legitimate run is reproducible from its own receipts, the check is exact. There is no threshold to tune, no anomaly score, no confidence band. A deposit either matches what its receipts say happened, or it does not:

  • Unbacked — the row cites no receipt at all. Exact. Actioned.
  • Divergent — the run exists and its receipts disagree with the deposit. Exact. Actioned.
  • Unverifiable — a receipt is cited but the run was pruned. Reported, and never actioned.

When it fires, it quarantines the session, subtracts that session's rows from the commons arithmetically — exact, not approximate, because the provenance is per edge and per amount — bumps the commons version, and writes a permanent quarantine record.

Revocation is wired into the same sweep. When the poisoning contributor is a real analyst root, revoke_contributor calls disallow_root to remove their write path to the shared case, so the attack cannot simply resume on the next run, and the quarantine record reads reverted+revoked.

The on-stage demo does not reach that branch, and we would rather say so than imply otherwise. InjectPoison is a demo affordance: it attributes its deposits to the synthetic id "rogue", which revoke_contributor refuses by design. So what the demo shows end to end is the exact arithmetic revert, with the record reading reverted. Revocation exists and fires for a real contributor root; it is not the thing the demo demonstrates end to end.

The security mechanism is not a feature we bolted on. It is the determinism work, pointed sideways. We built receipts so the demo could rewind, and receipts turned out to be an exact integrity primitive for shared memory.

Where Jac does the work

The product depends on Jac. It is not a wrapper over a Python service.

The graph is the program state. There is no database, no ORM, no migration and no serialisation code anywhere in the repository. Fragments are node archetypes, relationships are typed EvidenceLink edges, and pheromone, commons, commons_version and traversals are mutable attributes on the edge itself. Because the graph hangs off root.shared, it survives a server restart with every trail exactly where it was. We kill the server on stage and bring it back.

Walkers are the agents. In swarm.sv.jac, walker ForageTick spawns on a fragment, reads its incident edges with [here ->:EvidenceLink:->], and proposes. walker ReduceTick commits. Propose-and-reduce is not a pattern we imposed — it falls straight out of walker semantics.

walker:pub is the API. Eleven HTTP endpoints with zero route handlers and zero controller code. Declaring the walker creates the endpoint. Nine more come from def:pub.

by llm() is a typed function boundary, not a prompt string. Three call sites in framing.sv.jac with prompts in sem declarations: search framings return a typed FramingTriple, corpus scoring returns a typed FragmentScoreList at temperature 0, and the corridor narrative returns typed prose that is required to name its own weakest link. A glob rebind swaps the entire provider at runtime.

Permissions are the runtime's. root.shared and disallow_root are how multi-analyst memory and revocation are expressed. No permission table, no middleware.

The frontend is Jac too. .cl.jac client modules compile to a React bundle against the same server objects. 25 tests ship as a .test.jac annex of the module they test.

The model does one job, once

It decides what each fragment means against each framing — that "eighteen percent below specification after the housing change" is about power, not paperwork. Those judgements are cached into the graph, keyed by sha256(provider | model | framing | fragment).

The search itself makes zero model calls. Three batched calls up front, roughly ten seconds, and then it runs offline forever. That is what lets model understanding and byte-identical replay coexist instead of trading against each other. It is also why the demo survives dead wifi.

A missing API key is a state, not an exception. The provider registry reports available / no-key / unreachable, and with no key at all the whole system runs on a deterministic stemmed-keyword floor with a badge on screen saying exactly that. We ran the entire build day that way.

Real sources, not just our corpus

The honest weakness of everything above is that we wrote the forty fragments. Firecrawl attacks exactly that weakness, which is why it is not a bolt-on.

ingest.jac takes a real research question, searches with Firecrawl, scrapes the top sources to clean markdown, and then two typed by llm() calls turn documents into a graph. Extraction lifts each document into discrete evidence fragments, each with an excerpt in the source's own words, the claim it bears on, and a reliability score — unprompted, the model discounted Reddit threads to 0.66–0.70 and scored primary reporting higher. Hypothesis generation infers three competing explanations from what was actually gathered.

Links are deliberately not asked of a model. They are computed from stemmed token overlap using the same tokenizer the keyword scorer uses, so the graph and the scorer agree on what "related" means. Deterministic, and it costs no extra calls.

Verified live on "why did the Boeing 737 MAX MCAS system cause two crashes": 5 sources → 22 fragments → 47 links → 3 competing hypotheses — and the swarm runs on it. Corridor s08 → s12 → s03 → s04 → s05 → s02, coverage 16 of 22 fragments against 6.7 of 40 on the curated corpus, because the scraped graph is denser and the agents spread further. The engine changed by zero lines. It reads the same evidence contract and does not care where the evidence came from.

Firecrawl is normally an ingest step feeding a vector store. Here the scraped output becomes a graph that agents physically walk.

Measured results

jac run eval.jac — 3 hypotheses × 20 seeds = 60 trials per column.

Trail precision keyword floor Fireworks gpt-oss-120b
swarm (5 agents, trails on) 0.494 0.472
trio (5 agents, trails off) 0.431 0.489
solo (1 agent, equal total budget) 0.392 0.242
swarm − solo +0.103 +0.231
swarm − trio +0.064 −0.017
win / tie / loss vs solo 23 / 32 / 5 45 / 13 / 2
replay on the same seed identical identical
coverage 6.7 / 40 7.7 / 40

Honest limitations

This ranks path strength, not truth probability. It ranks routes inside a curated corpus and does not validate the underlying evidence.

Swarm versus the no-trails trio is a tie, and we say so. Within one eight-tick run the agents barely cross paths, so there is little trail for anyone to read — under the Fireworks scorer the trio actually edges the swarm by 0.017. Stigmergy is a many-passes mechanism and this measures one pass. That is precisely why the commons exists, and why the honest claim here is swarm over a single walker of equal budget.

The metric punishes the behaviour the system exists to produce. Trail precision counts a fragment as a hit only if it sits on the selected theory's planted chain. In the demo run the score is 0.333, because the cross-cluster find — the bridging changelog line, the pack voltage, the relay on the shared bus — sits on the other theory's chain and is scored wrong. Suppress the bridge and the score doubles to 0.667. The run that scores twice as well is the run that found nothing. We report it as measured rather than picking a seed that hides it.

Coverage is 6.7 of 40 fragments. The exploration weight (0.30) is swamped by a relevance term reaching 0.8, so agents converge on the same high-scoring fragments instead of spreading out. We chose not to retune a constant that every other number was measured against.

Determinism is bounded. Exact given a fixed score cache, not across cache rebuilds, because model scoring is not bit-reproducible even at temperature 0.

Multi-analyst memory is new. The shared commons on root.shared with disallow_root revocation is landing at the end of the build. Cross-run memory is measured; cross-user memory is demonstrated, not yet benchmarked across sessions.

Challenges we ran into

A silent zero. Without stemming, the framing token housings never matches the evidence token housing. Relevance collapsed to zero across the whole corpus, nothing ever deposited, and the swarm did nothing at all — while looking exactly like a broken algorithm rather than a broken tokenizer.

A corpus unreachable in one direction. Every link ran cause → effect and every anchor sat at the effect end, so a directed walk from an anchor never reached the head of a chain. Five chain members were simply unreachable, with no error anywhere.

One framing per theory made three of five agents inert. Deposit is proportional to relevance, so an agent carrying a framing aimed at a theory you did not select scores zero everywhere and contributes nothing. Fixed by giving every hypothesis three distinct angles.

A single seed is not interpretable. With a six-step corridor, precision quantises to sixths, so three modes tying at 3/6 means nothing at all. Hours went into chasing an "all modes are identical" bug that was this quantisation.

A trail you read yourself is not information. Agents were ping-ponging on their own deposits and coverage collapsed. The fix is one rule: pheromone scores zero if this agent has already visited the destination. A trail is a tip from someone else. It is never a reason to re-read your own notes.

What we learned

Caching at the semantic boundary is what makes language models and determinism compatible. Let the model decide meaning once, freeze the result, and run the mechanism on the frozen judgement. You get model understanding and exact replay instead of choosing between them.

And infrastructure built for one reason pays for another. We built receipts so a judge could rewind the demo. Receipts turned out to be the thing that makes poisoning shared analytic memory detectable exactly, with nothing to tune.

What's next

Coverage first — the exploration weight is the obvious lever and the 60-trial harness already exists to measure it.

Then a multi-session evaluation of the commons: quantifying how much warmer the second analyst's search actually starts, which is the number that turns a tie into a result. Then per-analyst views of the same shared graph, so two investigators can see whose trails they are standing on.

AI for Defense

Multi-source evidence fusion with full provenance and contradiction flagging is decision support. Reskinned to contested-sensor attribution, the system ranks which corroborated chains actually explain an anomaly, exposes every scoring factor, and detects and reverts corrupted contributions to shared analytic memory — revoking the contributor's write path whenever that contributor is a real analyst root.

Built With

  • agentic-ai
  • ai-agents
  • ai-safety
  • byllm
  • deterministic-ai
  • explainable-ai
  • firecrawl
  • fireworks-ai
  • graph-computing
  • human-centered-ai
  • information-retrieval
  • jac
  • knowledge-graphs
  • large-language-models
  • litellm
  • local-llms
  • multi-agent-systems
  • ollama
  • provenance
  • react
  • reproducible-ai
  • retrieval-systems
  • vite
  • web-scraping
Share this project:

Updates