# Tool/action DAG — research path

**Status:** research path · not a ticket · awaiting first agent use case
**Opened:** 2026-05-07
**Promotion trigger:** when an actual agent (an LLM that takes
real-world actions through arborist's verified answers) is in flight
or about to land. Until then this stays a doc.
**Audience:** fox + future blackops shifts evaluating whether arborist
should host an action-provenance layer.

## Why this is a doc, not a ticket

Per `docs/TICKETS.md`: a doc proposes change AND awaits a decision
AND has scoped implementation cost → it's a ticket. Otherwise it's
reference. This proposal has the first two but not the third — the
implementation cost is conditional on a use case that doesn't exist
in tree today (`grep` for `tool_call`, `action_plan`, etc. returns
zero hits across `arborist/` and `docs/tickets/` as of 2026-05-07).
Five-step algorithm step 2 says: don't optimize a process that
shouldn't exist yet. So we capture the design now, ticket it later
when an agent shows up.

## Problem statement

Today arborist's run-DAG ends at `final_label`. Everything
downstream of "the LLM gave us a STRICT pointer-linked answer" is
invisible to the audit chain. When arborist serves a question-
answerer LLM upstream of human decision-making, that's fine — the
human is the side-effect surface and lives outside arborist's
audit scope.

When arborist serves an *agent* — an LLM that calls tools, makes
API requests, modifies state on the strength of a verified answer —
the audit chain has a downstream gap analogous to the one #000001
closed for retrieval keywords:

- `cache_key` (8-dim): no action footprint
- `run_dag_root`: no action footprint
- `audit_events`: no action footprint
- `providence_cache`: no action footprint

The map (verified answer) is committed; the territory reached
(tools dispatched, side effects produced) is not.

### Concrete failure shapes

**Case A — silent action.** An agent reads `STRICT` from arborist,
calls `delete_user(id=42)`, and the deletion is permanent. Audit
replay can recover the question, the sources, the verified answer —
but no record that anything was done with it.

**Case B — wrong tool.** LLM answers correctly, then dispatches
the wrong tool (typo'd argument, schema drift). The verified answer
was right; the action was wrong. Today there's no separation between
"the answer was right" and "the action that the answer authorized
was right."

**Case C — partial rollback.** Action sequence partially completes,
host process crashes, restart finds inconsistent world state. No
committed rollback plan to replay.

## Proposed DAG extension

Five new stages chained after `final_label`:

```
final_label
  → action_plan          # LLM-proposed sequence, committed BEFORE any execution
  → tool_call            # one node per dispatch; committed at moment of dispatch
  → tool_output          # one node per response; committed when host returns it
  → postcondition_check  # did the declared postcondition hold?
  → action_label         # EXECUTED / FAILED / ROLLED_BACK / QUARANTINED
```

`action_label` mirrors `audit_mode` — a binary verdict; soft signals
stay sidecars. Same verifier-stays-binary discipline as the existing
trichotomy.

## Three design options

### Option A — in-tree action DAG

Schema lives in arborist's SQLite. Host process registers a
tool-dispatch callback; arborist commits the call before the
callback runs and commits the output when it returns. Failures
(timeout, exception) commit a node with `tool_output.status='failed'`
so audit replay sees the attempt.

**Pros:** one audit chain, one Merkle root, one verifier path.

**Cons:** arborist's surface area expands to include a tool-dispatch
contract. Pass 1 of the Donto comparison framed arborist as
"upstream of an LLM" — Option A makes it both upstream and
downstream. Identity drift.

### Option B — sidecar package (recommended)

Separate `arborist-action` package consumes arborist's `run_dag_root`
and chains its own `action_root`. Two roots, audit chain spans both
via a typed cross-link (e.g., `action_root.parent_run_dag_root` is
a foreign key into the answer's run-DAG).

**Pros:**
- Preserves arborist's Pass-1 identity. The verified-answer cache
  stays a verified-answer cache; agent-side audit lives next door.
- Two-root model is the same shape the existing mesh code uses to
  chain across peers.
- Operators who don't want action-DAG don't install it. Operators
  who do install it get the cross-link "for free."
- Schema migrations live in the action package, not in arborist
  core — `chain-check-shards` and the existing audit chain stay
  unchanged.

**Cons:**
- Two roots to verify, two packages to keep in sync.
- Cross-link contract has its own drift surface (the foreign-key
  shape, what the action package commits to, what arborist
  commits to).

### Option C — out of scope

Document the boundary. Operators wanting action provenance build
it in their host process and emit their own audit chain. Arborist
stays a pure question→answer cache.

**Pros:** smallest blast radius. No new code, no new identity
question.

**Cons:** every operator that hits this need re-derives the same
shape. Provenance discipline becomes per-operator self-discipline,
which is exactly the gap arborist refuses to accept on the
question→answer side (cf. #000022 LossReport on the adapter side).

## Recommended landing shape (when promoted to ticket)

Option B, scoped to:

1. New package `arborist-action` (separate pyproject, separate
   import root) with its own SQLite schema and Merkle conventions
   matching `arborist/merkle.py`.
2. Cross-link contract: `action_root` payload includes
   `parent_run_dag_root` and `parent_cache_key`. Verifier is
   "those values resolve to a live providence-cache row whose
   `audit_mode` is in the policy-allowed set."
3. Tool dispatch happens in the host process. Action package
   commits `tool_call` *before* dispatch and `tool_output` when
   the host returns. Failures (timeout, exception) commit a
   `tool_output` row with `status='failed'`.
4. Rollback plans are committed at `action_plan` time, replay-able
   from the audit chain. Actually running rollback is host-process
   work.
5. `action_label` mirrors `audit_mode` — `EXECUTED` / `FAILED` /
   `ROLLED_BACK` / `QUARANTINED`. Binary verdict. Soft signals
   (e.g., "did the action have unintended side effects?") stay
   sidecars in the action package, never enter the proof path.

## Out of scope (whenever this ships)

- **Tool dispatch.** Arborist (core or action package) never calls
  a tool. It commits intent, call, and response. Host process
  executes.
- **Semantic validation of tool outputs.** Verifier stays binary.
  `postcondition_check` is structural ("did this declared invariant
  hold?"), not semantic ("was this the right action?").
- **Multi-step planning logic.** Whatever plan the LLM produced
  gets committed; the action package doesn't re-plan, doesn't
  optimize, doesn't constrain.
- **Rollback execution.** Plan commit only. Running it is host
  work.
- **The arborist 8-dim cache_key, the verifier, retrieval, schema
  / canonicalization / chunking versions.** Untouched.

## The identity question

The Donto comparison (Pass 1) was explicit:

> Arborist is upstream of an LLM. Donto sits next to humans
> curating contested knowledge.

Adding an action-DAG layer makes arborist (or its sidecar)
*also* downstream of an LLM — committing what the LLM did, not
just what it said. Three readings:

1. **Identity drift.** Arborist becomes a hybrid: cache for
   verified answers + ledger for agentic actions. Operators who
   wanted "pure verified-answer cache" now share a tree with
   action provenance even if they don't install the sidecar.
2. **Natural extension.** The Merkle-AGI principle that says
   "every cognition-affecting input must be committed" also says
   "every action taken under a verified answer must be committed."
   Same axiom, downstream side. Option B (sidecar) honors the
   axiom without forcing the identity shift on operators who
   don't want it.
3. **Premature.** No agent is hitting arborist today. Build
   action-DAG when the first real agent runs through, not before.
   Reading 3 is consistent with five-step algorithm step 2.

Reading 3 is the current default. Readings 1 and 2 reactivate when
the first real agent surfaces.

## Promotion criteria

This doc graduates to a ticket when **any** of:

- An operator-in-tree (or a clearly named upstream consumer) has
  an LLM in flight that takes real-world actions on the strength
  of arborist verified answers.
- A second LossReport-shaped audit gap surfaces (e.g., a Donto-style
  comparison naming a different downstream missing-provenance
  axis) and the cumulative case for "arborist needs a downstream
  provenance story" outweighs the identity-drift cost.
- A verified-answer cache gets used for an irreversible action
  in a real production incident, and post-mortem can't recover
  what happened. (Don't wait for this. The first two are cheaper
  signals.)

Until then: the doc captures the design so the work is ready when
the use case is.

## References

- `/home/fox/Downloads/arborist_vs_donto.pdf` §6.1 ("Pass 1 — Purpose
  and audience"). Names arborist's audience as "developer who
  wants `ask(question) → (answer, proof)`." This doc explicitly
  lives downstream of that contract.
- `docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md` —
  the same provenance-gap shape on the *upstream* (retrieval)
  side. Closing the analogous gap on the action side is the
  symmetric move.
- `docs/tickets/ticket-000022-adapter-loss-report.md` — same
  axiom ("every causally relevant transformation must surface
  what it dropped or did") applied to adapter normalization.
  Action-DAG would extend the axiom one stage further downstream.
- `docs/tickets/ticket-000012-selection-consensus-protocol.md` —
  v8 selection protocol. If action-DAG ships, accepted mutations
  in v8 fork-choice should be able to reference action histories
  as evidence (e.g., "this policy patch reduced rollback rate by
  X%"). Cross-link surface, not implementation dependency.
- `arborist/qa/dag.py` — the existing run-DAG construction; the
  action package would chain into the resulting `run_dag_root`
  via Option B's foreign key, not modify this module.
