Skip to content

Build1 publisher2 min readPublished

Tagging tool calls with source at write time turns the reverse audit into one traversal

A Strands hook recorder writes each agent step into Neo4j through Neo4j Labs' agent-memory SDK. The audit query that finds every decision touching a bad source depends on a dict lookup keyed by tool name.

The Engineer · Build desk

Illustration accompanying Tagging tool calls with source at write time turns the reverse audit into one traversal

What happened

  • A dev.to post published under the AWS organisation builds an agent audit trail around two questions: replaying why a decision was made, and finding which decisions touched a data source that later turned out to be wrong.
  • Strands' hook system delivers BeforeInvocationEvent, AfterToolCallEvent and AfterInvocationEvent to a HookProvider registered on the agent, and the tool-call event carries the tool name and its input.
  • DecisionTraceRecorder, which the author wrote on top of those hooks and which is not part of Strands, opens a trace when an invocation starts, appends one step per tool call, and closes it with the outcome.
  • The post says that when a source turns out wrong, one graph traversal returns every decision that touched it at read time, where a flat log would have to scan every record.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The audit is only as precise as the tool name it is keyed on. A single tool that reads two external feeds gives you one answer for both, and no way to tell which one a decision rested on.
  • decision Choosing agent.state or Neo4j for the trace store settles whether finding every decision that touched a given source is a query you can run during an incident or a batch job you have to schedule.
  • cost Adoption costs a Neo4j deployment, a Labs SDK in the dependency tree, and the standing work of keeping the tool-to-source map current as tools are added. The tools themselves need no edits.

The edge gets written during the tool call. Strands fires AfterToolCallEvent, the recorder reads event.tool_use["name"], and it appends one record per call carrying the tool, the input, and "source": TOOL_SOURCE.get(name) [6][4]. The graph recorder passes the same association into the SDK as touched_entities on record_tool_call. The post's inline comment describes that as tagging the external source the tool touched so the audit can traverse to it [9].

Read time is cheap because the work happens at write time: nothing in the graph works out which feed a tool read, and a dict lookup keyed on the tool name decides it [6].

TOOL_SOURCE.get(name) returns None for any tool missing from the map [15]. The step is still recorded and the decision is still replayable, and the reverse traversal goes past it, because there is no source on the other end of the edge [15]. A flat store fails the other way round: the scan is slow, and you can still grep the stored input for a string nobody anticipated when the schema was written [6][10].

The comparison the post draws is asymptotic, and it gives no timings or trace counts [11][16]. Whether it transfers to your system depends on retention and scope. Retention has to be long enough that reading all the traces is a job you schedule. And the scan has to be unscoped: start_trace takes a session_id, "travel" in the sample [9], so a flat store can be partitioned by session, while a reverse audit starts out not knowing which sessions read the bad source. At that point you read every partition.

The schema restraint is the good craft here. The graph writes go through neo4j-agent-memory from Neo4j Labs, so the node labels, the writes and the audit traversal are the vendor's design and stay the vendor's to maintain [7]. The recording happens entirely outside the tools, and that is only possible because Strands already puts the tool name and the input on the event the recorder subscribes to [3][4][1].

The post's opening example is the reason any of this gets built. Ask the agent a week later why it recommended that flight and it gives a confident, plausible answer. The answer is made up, because the real chain was never kept [13]. The framing the post attributes to Neo4j is that "a flat log records what happened; a graph records why" [12].

What to watch

  • Whether Neo4j Labs promotes neo4j-agent-memory out of Labs, since a Labs SDK carries a different support commitment from a shipped product.
  • Published timings for the traversal against a flat scan at a stated trace count, which is what would let the comparison transfer to another workload.
  • How the pattern handles a tool that reads more than one external source, where one tool name has to map to several entities.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories