LangChain Plugin

Records what each of a LangChain agent’s tool calls changed on disk, so a conversation shows the work as well as the words.

The LangChain receiver brings the conversation. This brings what the tools did to the workspace, which nothing on the wire says.

What it is

Two pieces, and neither holds any logic of its own:

Is
apache-skywalking-asz-langchain says when a tool call begins and ends
asz-changes scans the watched path and writes the difference

asz-changes is the same program the Claude Code plugin runs. LangChain has no way to tell a program outside the process that a tool started, and Claude Code does, which is the whole reason the Python half exists.

Installing

pip install apache-skywalking-asz-langchain
asz-langchain enable

and asz-changes on the machine, from the release archive, Homebrew or apt.

The application imports nothing and changes nothing. enable writes a loader into the environment’s site directory, so it runs at interpreter start and attaches the handler once the environment says to. It is a separate command because a wheel cannot put a file there: measured, pip places a declared data file in the environment’s root, where Python never reads it and the loader silently never runs.

asz-langchain status says whether it is on, and asz-langchain disable takes it out again.

Running

export ASZ_CHANGES=true
export ASZ_WATCH=/path/to/the/workspace
export ASZ_CHANGES_DATA=/var/lib/asz/changes

ASZ_CHANGES_BIN names the program when it is not on PATH. Without it and without PATH, the handler logs one line and the agent runs untouched: a missing record is a gap in observation, and stopping the agent would be a gap in the application.

Then read what it wrote with the changes adapter, pointed at the same directory:

adapters:
  - name: changes
    enabled: true
    source_root: /var/lib/asz/changes

Where the settings live

In a file, not in your code. asz-changes reads settings.yaml from the data directory it was given:

$ASZ_CHANGES_DATA/settings.yaml     the settings
$ASZ_CHANGES_DATA/output/           the records it writes

So with ASZ_CHANGES_DATA=/var/lib/asz/changes, the file is /var/lib/asz/changes/settings.yaml, and the changes adapter reads the records with source_root: /var/lib/asz/changes.

Under Claude Code there is no ASZ_CHANGES_DATA: the runtime gives the plugin its own directory, and the file is ~/.claude/plugins/data/file-changes-skywalking-ai-sessionizer/settings.yaml.

Choosing which tools are watched

Nothing is watched until you say so. The default names Bash, PowerShell and Monitor, which are Claude Code’s tools; a LangChain application has none of them, so the plugin records nothing until this is set.

That is the honest default. A LangChain application names its own tools, and asz has no way to know which of them write and which only read — there is no convention to follow and nothing to guess from a name.

So the tool names below are yours, not ours. Write the ones your application defines:

tools:
  scope: ["*"]
  exclude: ["re:^(read|get|list)_", "search_docs"]

An entry is an exact name, a regular expression when it begins re:, or * for every tool. The exclusions take the same three forms and are applied after, so an exclusion beats an exact entry in scope.

They are what makes * usable. The read-only classifier understands shell commands and nothing else, so for a runtime whose tools take arguments the exclusions are the only way to keep a scan off a tool that only reads. A regular expression that does not compile matches nothing rather than everything, so a setting that was got wrong observes too little and is noticed rather than scanning the workspace on every call.

Two ways to arrive at a setting, both fine:

  • name the tools that write — scope: [write_report, apply_patch] — which costs nothing and misses a tool you forget;
  • take everything and exclude what reads — scope: ["*"] — which misses nothing and scans more.

What it costs when it is off

Nothing measurable. The loader reads one environment variable and imports nothing else, because importing langchain_core takes about 0.2 s and the loader runs in every Python process in the environment, pip included. Measured against a real wheel installed into a fresh environment:

Interpreter start
plugin off 0.01 s
plugin on 0.21 s

Which conversation a record belongs to

The same name asz gives it: the project and the thread, slugged for a reader and digested so two owners cannot collide. The rule is written twice, once in Python and once in Go, and a test holds them to the same table — the receiver lands the conversation, this lands what changed, and they meet only in the name of the session directory.

A tool call in a run with no thread key records nothing. There is no conversation to file it under, and inventing one would be worse than the gap.

What it cannot see

  • Changes outside the watched path, including anything in another container or on another host.
  • Which tool changed what, when two run at once. The window is shared, and the record says so rather than guessing.
  • Anything at all after a long collector outage. Output expires on the retention window whether or not it was collected, the same as on the Claude Code path.