REPL-Driven Ticket Triage: A Data-First Agent Tool Backend
Table of Contents
- 1. Thesis
- 2. Source landscape: mock-Python utils vs a live store
- 3. The backend is data, not code
- 4. Boundaries verified at the REPL
- 5. Configuration and analysis are the same surface
- 6. The agentic harness
- 7. The workflow, concretely
- 8. Open conjectures
- 9. Claims to test  refutation
- 10. Resources
1. Thesis
The best interface between an agent and a complex system is a REPL. A ticket
backlog is that problem worn as a queue: a store of work items, each with a
status you mutate and a shape you query, that an agent pulls from and pushes
back to. You do not learn the backlog's contract from docs — you evaluate
(next-ticket) and watch the map come back, then (set-status! …) and watch
one line change on disk.
This is the companion to REPL-Driven Feed Crawling and Flight Tracking. Same thesis, different domain. There the divergence between replicas of one system was the signal; here the interesting object is the tool backend itself — and the claim is that when you build it REPL-first, "the agent's tools", "the configuration you edit", and "the dataset you analyze" stop being three layers and become one.
2. Source landscape: mock-Python utils vs a live store
Anthropic's cookbooks ship a reference tool backend, tool_use/utils, used by the compaction and programmatic-tool-calling demos. It is a good baseline precisely because every axis is the opposite of the REPL-driven one:
| tool_use/utils (Python) | this backend (Clojure/bb/REPL) |
|---|---|
customer_service_api generates mock tickets |
wal-sh.site.beads reads the real issue store |
tools are @beta_tool decorated functions |
tools are EDN data (tool-defs) + a dispatch fn |
visualize.py renders responses |
the REPL is the visualizer (group-by, frequencies) |
| edit code → re-run the whole script | (next-ticket) / (agent/run …) incrementally in one session |
| domain is a fixture | domain is beads — a git-native issue tracker |
The Python version is the right shape for a tutorial — hermetic, reproducible, no external state. The REPL version is the right shape for doing the work: the backend is the thing you actually triage against, and you reshape it while it runs.
3. The backend is data, not code
The whole tool surface an agent sees is a literal value you can print, diff, and edit at the REPL. No decorator, no registration step:
(require '[wal-sh.site.beads :as b])
b/tool-defs
;;=> [{:name "get_next_ticket"
;; :description "Get the next ready (open, unblocked) ticket..."
;; :input_schema {:type "object" :properties {} :required []}}
;; {:name "update_ticket_status"
;; :input_schema {:properties {:id {...} :status {:enum ["open" "in_progress" "closed" "blocked"]}}
;; :required ["id" "status"]}}]
Because it is data, adding a tool is conj, and constraining one is
assoc-in — evaluated live, no restart. The dispatch side is one pure-ish
function, (handle-tool name input), returning the JSON string the Messages API
wants. That is the entire "SDK": a vector and a case.
4. Boundaries verified at the REPL
The point of a REPL-first backend is that you probe its edges by evaluating them, not by reading a spec. Every contract below was checked against the live backlog; the shapes are the tooling's, not the backlog's.
The schema an agent's tools resolve against (structure, no content):
(sort (keys (first (b/read-issues))))
;;=> (:acceptance_criteria :comment_count :created_at :created_by :dependencies
;; :dependency_count :dependent_count :description :id :issue_type
;; :owner :priority :status :title :updated_at)
The failure modes — each returns a value, never throws, so the agent loop can keep going:
;; empty queue: get_next_ticket yields an error object with a progress count
(let [h (b/queue-backend [])] (h "get_next_ticket" {}))
;;=> {"error":"No ready tickets in the queue","processed":0,"total":0}
(b/handle-tool "nope" {})
;;=> {"error":"unknown tool: nope"}
;; updating an id that doesn't exist mutates nothing and returns nil
(b/set-status! "does-not-exist-xyz" "closed")
;;=> nil
Two boundaries matter for correctness and were confirmed live:
- Readiness is dependency-aware.
readyexcludes open tickets whose dependencies aren't closed — 14 of 147 open tickets (~9%) were dependency-blocked, soget_next_ticketnever hands the agent work it can't start. set-status!is line-preserving. Setting one ticket's status rewrites only that line of the JSONL; every other line is byte-identical, so the git diff is a single line and the store stays reviewable. (Verified: 303 lines before = 303 after, exactly one changed.)
5. Configuration and analysis are the same surface
The claim that makes this more than "Python but parens": at the REPL the ticket store is simultaneously the config you edit and the dataset you analyze, with no API or visualization layer between.
Analyze — ordinary Clojure over the maps, no visualize.py:
(->> (b/read-issues) (map :priority) frequencies (into (sorted-map)))
;; a histogram across priority buckets
(->> (b/read-issues) (filter #(seq (:dependencies %))) count)
;; how many items carry a dependency edge (the blocked-work risk)
(->> (b/ready-queue) (map :issue_type) frequencies)
;; the shape of what's actually workable right now
Configure — the same values, mutated in place:
(b/set-status! "SYNTHETIC-001" "in_progress") ; advance one item
(b/claim-next!) ; pop the next ready + claim it
In the Python fixture these are different files — an API module to read, a
tools module to act, a visualize module to see. Here group-by is the
dashboard and assoc is the migration. The dataset, the configuration, and the
agent's tools are one store seen three ways.
6. The agentic harness
The same backend drives an autonomous loop. wal-sh.site.agent is a Messages-API
tool-use loop (a port of Anthropic's compaction cookbook) that takes any
tool-defs + handle-fn; pointing it at beads is one line:
(require '[wal-sh.site.agent :as a])
(a/run-tickets {:compact-threshold 6000 :max-turns 20}) ; dry-run backend
Run under the dry-run backend (queue-backend), the agent processes the real
ready queue while mutating nothing — the cursor advances in memory. That run is
written up empirically in Compaction Against a Beads Backend (same work, 37%
fewer tokens with client-side compaction). The REPL is where you watch it:
each turn's context size, each tool call, printed as it goes — the loop is not a
black box you submit, it's a form you step.
7. The workflow, concretely
A triage session is a sequence of evaluations, each informing the next — the literate-transcript form this site is built on:
(b/stats (b/read-issues))— how big is the problem.(->> … frequencies)— where is it concentrated (priority, type, blocked).(b/next-ticket)— pull one; read its shape; decide.(b/set-status! id "in_progress")— claim it; one line moves on disk.(a/run-tickets …)— or hand a batch to the agent and audit the transcript with chat.clj.
No step is a build. Every step is a value. The gap between "inspect" and "act" is the gap between two forms in the same buffer.
8. Open conjectures
- Data-first tools generalize. The
tool-defs+handle-toolshape is domain-agnostic; the same two-function backend should wrap logs, the search index, or the verdict ledger with no new machinery. Refutation: a domain whose tool surface can't be expressed as EDN + acase. - Line-preserving writes keep an agent's mutations reviewable. If every
agent-driven
set-status!is a one-line diff, a day of autonomous triage is a readable patch series, not an opaque state jump. Refutation: a mutation that necessarily rewrites unrelated lines. - The REPL beats a visualize module. For a store small enough to hold in a
process,
group-byat the prompt is faster to author and adapt than any rendering layer. Refutation: an analysis whose ergonomics genuinely need a dedicated view (large-scale, streaming, or shared dashboards).
9. Claims to test  refutation
- The tool backend is fully data (no code regen to change tools)
- a required
tool capability that can't be added by editing
tool-defs+handle-tool. - Every tool call returns a value, never throws
- an input that escapes the
case/ error-object contract and propagates an exception into the loop. - Configuration and analysis are one surface
- a triage task that forces a separate read-model and write-model rather than one store.
10. Resources
- Store + tools
src/wal_sh/site/beads.clj(read-issues,ready,ready-queue,next-ticket,set-status!,queue-backend,tool-defs,handle-tool).- Harness
src/wal_sh/site/agent.clj· run:gmake ticket-agent/gmake tickets.- Empirical companion
- Compaction Against a Beads Backend.
- The series
- Feed Crawling · Flight Tracking · Chat Mutation.
- Upstream baseline
- claude-cookbooks/tool_use/utils.
- Tracker
- beads (git-native issues).