grain-code-agent-tools is a dev-only package that exposes a live Grain
application to a coding agent over nREPL. It turns Grain's registries, schema
registry, event store, read models, and processors into plain EDN inspection and
execution tools.
The package is intended for local development and debugging. It is not a production API.
Add the package to the application project:
obneyai/grain-code-agent-tools
{:git/url "https://github.com/ObneyAI/grain.git"
:git/sha "6120a4b3dceaff827bddd7cbf0703ead0131ab11"
:deps/root "projects/grain-code-agent-tools"}Require and install the tools after the Grain system has started:
(ns my-app.core
(:require [ai.obney.grain.code-agent-tools.interface :as code-agent-tools]
[integrant.core :as ig]))
(defn start
[]
(let [app (ig/init system)]
(code-agent-tools/install! {:system app
:context (::context app)
:mode :dev})
app))install! stores the runtime that all other tool calls use. The runtime should
include:
:system- the Integrant system map.:context- the Grain request context, usually containing:event-store,:cache,:tenant-id, and application services.:mode :dev- required when:modeis supplied. Any other mode throws.
From nREPL:
(require '[ai.obney.grain.code-agent-tools.interface :as tools])
(tools/runtime)
;; => {:mode :dev
;; :installed-at #inst "..."
;; :system/keys #{...}
;; :context/keys #{...}}Returns a sanitized catalog of live Grain registries:
(tools/catalog)
;; => {:commands {...}
;; :queries {...}
;; :read-models {...}
;; :processors {...}
;; :periodic-triggers {...}
;; :schemas {...}
;; :missing-schemas {...}}Each command, query, read model, processor, and periodic trigger includes its registered options, schema presence, source metadata where available, authorization presence, and consumed event schemas where applicable.
Use this first when entering an unfamiliar app:
(keys (:commands (tools/catalog)))
(get-in (tools/catalog) [:commands :example/create-counter])These functions expose Grain's schema registry. They are the main safety layer for an agent before invoking commands or queries.
(tools/schemas)
;; => {:example/create-counter [:map ...]
;; :example/counter-created [:map ...]
;; ...}
(tools/explain-schema :example/create-counter)
;; => {:schema/name :example/create-counter
;; :schema [:map ...]}
(tools/validate :example/create-counter {:name "Counter A"})
;; => {:valid? true, :schema [:map ...]}
(tools/validate :example/create-counter {:name 1})
;; => {:valid? false
;; :schema [:map ...]
;; :value {:name 1}
;; :explain/data {...}
;; :explain/humanized {...}}validate also supports Grain envelope validation. Use this when checking a
fully materialized command, query, or event map, including Grain metadata such
as ids and timestamps:
(tools/validate :command
:example/create-counter
{:command/name :example/create-counter
:command/id #uuid "00000000-0000-0000-0000-000000000010"
:command/timestamp (java.time.OffsetDateTime/now)
:name "Counter A"})
(tools/validate :query
:example/counters
{:query/name :example/counters
:query/id #uuid "00000000-0000-0000-0000-000000000011"
:query/timestamp (java.time.OffsetDateTime/now)})
(tools/validate :event
:example/counter-created
{:event/type :example/counter-created
:event/id #uuid "01900000-0000-7000-8000-000000000012"
:event/timestamp (java.time.OffsetDateTime/now)
:event/tags #{}
:counter-id #uuid "00000000-0000-0000-0000-000000000013"
:name "Counter A"})An agent should prefer this workflow:
- Use
catalogto discover the command/query/event name. - Use
explain-schemato inspect the expected shape. - Use
validateon the proposed payload. - Invoke only after validation succeeds.
Processes a command through the live command processor:
(tools/invoke-command! {:command/name :example/create-counter
:name "Counter A"})The tool adds :command/id and :command/timestamp when absent. It uses
:tenant-id from the command map or from the installed context.
This call can mutate application state by appending events.
Processes a query through the live query processor:
(tools/invoke-query {:query/name :example/counters})
;; => {:query/result [...] ...}The tool adds :query/id and :query/timestamp when absent. It uses
:tenant-id from the query map or from the installed context.
Reads events from the installed event store:
(tools/events {:types #{:example/counter-created}
:limit 10})Accepted keys:
:tenant-id- required when the installed context does not include one.:types- event type set.:tags- event tag set.:limit- max number of events returned.
Projects a registered read model:
(tools/projection :example/counters)Pass a scope map when the read model expects one or when tenant id must be supplied explicitly:
(tools/projection :example/counters {:tenant-id tenant-id})Returns runtime health and discovery information:
(tools/diagnostics)
;; => {:runtime {...}
;; :registries {:commands 3, :queries 2, ...}
;; :event-store {:present? true, :tenants {...}}
;; :cache {:present? true, :l1 {...}}
;; :control-plane {...}}For apps using the control plane, pass a tenant id to include routing diagnostics:
(tools/diagnostics {:tenant-id tenant-id
:staleness-threshold-ms 6000})A useful first pass in a live app is:
(def cat (tools/catalog))
(keys (:commands cat))
(keys (:queries cat))
(keys (:read-models cat))
(:missing-schemas cat)
(tools/diagnostics)Then inspect one workflow:
(get-in cat [:commands :example/create-counter])
(tools/explain-schema :example/create-counter)
(tools/validate :example/create-counter {:name "Counter A"})
(tools/invoke-command! {:command/name :example/create-counter
:name "Counter A"})
(tools/events {:types #{:example/counter-created} :limit 5})
(tools/projection :example/counters)Because the tools expose source metadata for registered handlers and reducers, an agent can move from runtime discovery to the exact source file/line that owns the behavior.
install!is dev-only. Passing any mode other than:devthrows.catalog,schemas,explain-schema,validate,events,projection,runtime, anddiagnosticsare inspection-oriented.invoke-command!is state-changing and should be treated like any other command execution path.- Commands, queries, event reads, and projections require a tenant id either in the installed context or in the call arguments.
- Returned data is sanitized for nREPL: vars, functions, classes, and nested values are represented as EDN-friendly data.