5 Reference
Complete, neutral description of every module, binding, parameter, and struct exposed by Stone. Organized by module. Every binding’s documentation is co-located with its module, so require-ing a module is the way to discover the vocabulary it adds.
5.1 stone/edge
| (require stone/edge) | package: Stone |
Composition primitives. Every binding in this module produces or consumes a ashlar-meta. See Edge Primitives for the conceptual model.
struct
(struct ashlar-meta ( fn produces-all queries children lens name schema validate-walk middleware rebuilder) #:transparent) fn : procedure? produces-all : (listof symbol?) queries : (listof symbol?) children : (listof ashlar-meta?) lens : (or/c procedure? #f) name : symbol? schema : (or/c hash? #f) validate-walk : (or/c procedure? #f) middleware : list? rebuilder : (or/c procedure? #f)
produces-all — every node type this ashlar or its subtree can produce.
queries — node types read from the DAG that no earlier sibling produces.
children — child ashlars; '() for leaves.
lens — lens attached to the ashlar (set by ashlar-match when its extractor is a lens).
name — name used in logs and validation messages.
schema — JSON schema for structured outputs.
validate-walk — walk rule the validator dispatches through.
middleware — middleware list when the ashlar is an agent ashlar.
rebuilder — closure that can rebuild this ashlar with substituted children or middleware; used by ashlar-with-tool-stub.
procedure
(make-ashlar fn [ #:produces produces #:queries queries #:name name #:children children #:lens lens #:schema schema]) → ashlar-meta? fn : procedure? produces : (or/c symbol? #f) = #f queries : (listof symbol?) = '() name : (or/c symbol? #f) = #f children : (listof ashlar-meta?) = '() lens : (or/c procedure? #f) = #f schema : (or/c hash? #f) = #f
syntax
(~> ashlar ...)
procedure
(run-ashlar ashlar #:record record) → dag?
ashlar : ashlar-meta? record : record?
A record is mandatory — like run-mason, durability is intrinsic to a run verb; a missing record is Racket’s arity error. A pure, session-less pass over a hand-built DAG is direct application: (ashlar dag).
Resumable: replaying over a journal that already holds atomic completions skips the recorded leaves and re-runs from the crash point.
A pass scopes itself and records its own boundary. A pass scopes its resume cursors to the facts since the last conversation-completed boundary, and records a new one when it completes. So re-invoking run-ashlar on a completed record starts a fresh conversation — the next turn — instead of replay-skipping the previous turn’s completed ashlars. A suspended pass — one waiting on a request — records no boundary: answer that request (answer-request!) and re-invoke to resume the same conversation. A pass ending on a session-terminal-halt? also records none — the session is over, so re-invoking is a replay no-op rather than an append past the halt. A failure, and a Ctrl-C-cancelled pass, both record one (a bad turn does not end a session).
Looping is the runner’s job. There is no framework loop verb: an interactive session is a caller that repeats a run verb over one record and stops when the returned DAG satisfies session-terminal-halt?. A runner that only runs the topology repeats run-ashlar; run-tui instead invokes run-mason once per human line, so a mason decides at each boundary between ashlars as that pass lays them. See Run a durable, resumable session for the pattern, the record / journal / resume model, and stone/record for open-record.
syntax
(ashlar-loop body #:until predicate #:max max-iterations)
predicate = (any-expr) max-iterations = exact-nonnegative-integer?
To wrap a node-shaped predicate that only inspects the latest leaf, see on-latest.
syntax
(ashlar-match extractor maybe-name [val branch] ...)
maybe-name =
| #:name name
extractor is either a lens? or a procedure.
A lens? is applied to the latest leaf’s content (the value of (node-content (dag-cursor work-dag)) at the lens’s path).
A procedure is called with the work DAG and must return a value matching one of the val keys.
The branch whose val equals the extractor’s return value runs against the work DAG. If no branch matches, a 'match-failed failure node is appended.
Halt nodes. When the latest leaf is a halt node, ashlar-match bypasses the extractor and routes to the branch keyed by the halt’s halt-node-kind (e.g. 'declined, 'user-quit) — so a single match can catch a cancelled ask-human alongside its normal answer branches. With no matching kind branch, the halt passes through as the terminal node.
See also Edge Primitives for the conceptual model and on-latest for wrapping a node-shaped extractor.
procedure
(on-latest pred) → procedure?
pred : procedure?
procedure
(ashlar-map extractor body [#:name name]) → ashlar-meta?
extractor : procedure? body : ashlar-meta? name : (or/c symbol? #f) = #f
procedure
(ashlar-parallel [#:name name] lane ...) → ashlar-meta?
name : (or/c symbol? #f) = #f lane : ashlar-meta?
procedure
(ashlar-reduce ashlar [#:name name]) → ashlar-meta?
ashlar : ashlar-meta? name : (or/c symbol? #f) = #f
procedure
(make-ask-human #:format-fn format-fn #:name name #:produces produces [ #:queries queries]) → ashlar-meta? format-fn : (dag? . -> . string?) name : symbol? produces : symbol? queries : (listof symbol?) = '()
Answered — the DAG already carries the answer node (a prior suspend was resumed): carry it forward as this ashlar’s produces output (the answer node is that output; the answer is stored under both produces and 'text, so node-text reads it cleanly).
Fresh — append a request node ('ask, stone/dag), journal it, emit the 'ask crossing, and suspend the run (run-suspended, stone/threshold). No Racket thread is parked: the call returns a DAG cursored on the request node. The caller appends the answer node with answer-request! (stone/record) and re-invokes the run to resume.
There is no channels argument and no cancellation value; the threshold is the single opening the far side observes and answers through (see stone/threshold and Ask Human). Under a pure, session-less (ashlar dag) application there is no bound threshold, so an ask-human ashlar only runs inside run-ashlar #:record or run-tui.
procedure
(make-scoped-ashlar body #:walk walk [ #:children children #:produces produces #:queries queries #:name name]) → ashlar-meta? body : procedure? walk : procedure? children : (listof ashlar-meta?) = '() produces : (or/c symbol? #f) = #f queries : (listof symbol?) = '() name : (or/c symbol? #f) = #f
procedure
(ashlar-produces s) → (or/c symbol? #f)
s : any/c
procedure
(ashlar-queries s) → (listof symbol?)
s : any/c
procedure
(ashlar-produces-all s) → (listof symbol?)
s : any/c
5.2 stone/dag
| (require stone/dag) | package: Stone |
Content-addressed, append-only typed DAG. See The DAG as Ashlar State for the conceptual model.
5.2.1 Nodes
struct
(struct node (id parents content meta ts type) #:transparent) id : string? parents : (listof string?) content : any/c meta : hash? ts : real? type : symbol?
procedure
(make-typed-node parents type content [meta]) → node?
parents : (listof string?) type : symbol? content : any/c meta : hash? = (hash)
procedure
(typed-node d type content [meta]) → node?
d : dag? type : symbol? content : any/c meta : hash? = (hash)
procedure
(make-failure-node parents kind reason [meta]) → node?
parents : (listof string?) kind : symbol? reason : string? meta : hash? = (hash)
procedure
(failure-node? v) → boolean?
v : any/c
procedure
(failure-node d kind reason [meta]) → node?
d : dag? kind : symbol? reason : string? meta : hash? = (hash)
5.2.2 DAGs
struct
(struct dag (nodes leaves root parent label cursor-id) #:transparent) nodes : (hash/c string? node?) leaves : (listof string?) root : (or/c string? #f) parent : (or/c dag? #f) label : symbol? cursor-id : (or/c string? #f)
procedure
(dag-append d n) → dag?
d : dag? n : node?
procedure
(dag-append-typed d type content [meta]) → dag?
d : dag? type : symbol? content : any/c meta : hash? = (hash)
procedure
(dag-append-failure d kind reason [meta]) → dag?
d : dag? kind : symbol? reason : string? meta : hash? = (hash)
procedure
(dag-append-halt d kind reason [meta]) → dag?
d : dag? kind : symbol? reason : string? meta : hash? = (hash)
procedure
(dag-leaves d) → (listof string?)
d : dag?
procedure
(dag-cursor d) → (or/c node? #f)
d : dag?
procedure
(dag-cursor-id d) → (or/c string? #f)
d : dag?
procedure
(dag-set-cursor d n) → dag?
d : dag? n : node?
procedure
(dag-failed? d) → boolean?
d : dag?
procedure
(dag-halted? d) → boolean?
d : dag?
procedure
(dag-terminal? d) → boolean?
d : dag?
procedure
(request-node d request-kind payload [meta]) → node?
d : dag? request-kind : symbol? payload : any/c meta : hash? = (hash)
procedure
(request-node? n) → boolean?
n : any/c
procedure
d : dag?
procedure
d : dag?
procedure
(make-halt-node parents kind reason [meta]) → node?
parents : (listof string?) kind : symbol? reason : string? meta : hash? = (hash)
procedure
(halt-node? n) → boolean?
n : any/c
procedure
(halt-node-kind n) → symbol?
n : node?
procedure
(halt-node-reason n) → string?
n : node?
procedure
(dag-nearest-ancestor d type) → (or/c node? #f)
d : dag? type : symbol?
procedure
(dag-collect-until d #:type collect-type #:until sentinel-type) → (listof node?) d : dag? collect-type : symbol? sentinel-type : symbol?
Returns nodes oldest-first (the one closest to the sentinel comes first). Returns '() when no sentinel-type ancestor is reachable on the first-parent line — there is no enclosing scope, so there is nothing to collect within.
Same first-parent discipline as dag-nearest-ancestor: deterministic under loops and fan-outs, and siblings on other lanes are not visited.
procedure
(dag-query-all d type [#:scope scope]) → (listof node?)
d : dag? type : symbol? scope : (or/c 'conversation #f) = #f
procedure
(dag-select d nid) → (listof node?)
d : dag? nid : string?
procedure
(dag-select-window d nid n) → (listof node?)
d : dag? nid : string? n : exact-nonnegative-integer?
5.3 stone/messages
| (require stone/messages) | package: Stone |
A single turn in an LLM conversation. Self-contained: no link to a ashlar DAG. Used as the element type of the 'conversation list that make-agent-ashlar embeds in its result node.
struct
role : symbol? content : (or/c string? hash? (listof hash?)) tool-calls : list? call-id : (or/c string? #f) metadata : hash?
role — 'system, 'user, 'assistant, or 'tool.
content — free text, a parsed structured-output hash, or a list of multimodal content blocks.
tool-calls — list of tool-call records; assistant role only, '() otherwise.
call-id — tool-result correlation id; tool role only, #f otherwise.
metadata — provider-specific escape hatch. Convention: namespaced symbol keys (e.g. 'anthropic/cache-control, 'openai/logprobs, 'vllm/finish-reason) to prevent collision when multiple providers stash bits there.
procedure
(message-text m) → string?
m : (or/c message? #f)
5.4 stone/llm-ashlar
| (require stone/llm-ashlar) | package: Stone |
Bridge constructor that wraps a call-llm into a multi-turn agent ashlar. See Agents and Tools for the conceptual model.
procedure
(make-agent-ashlar [ #:call-llm call-llm] #:produces produces [ #:context ctx #:name name #:schema schema #:middleware middleware #:decide decide #:max-turns max-turns #:response-format response-format #:budget budget #:context-budget context-budget #:adversary adversary #:heal-with healer #:max-healing max-healing #:finalize finalize]) → ashlar-meta? call-llm : (or/c procedure? #f) = #f produces : symbol? ctx : context? = (context) name : (or/c symbol? #f) = #f schema : (or/c hash? #f) = #f middleware : (listof any/c) = '() decide : procedure? = continue-on-tool-use max-turns : exact-positive-integer? = 15 response-format : (or/c hash? #f) = #f budget : exact-nonnegative-integer? = 16384 context-budget : exact-nonnegative-integer? = 400000 adversary : (or/c ashlar-meta? #f) = #f healer : (or/c ashlar-meta? #f) = #f max-healing : exact-nonnegative-integer? = 3 finalize : (or/c (any/c . -> . node?) #f) = #f
#:call-llm — the model-bound call-llm built by make-openai-llm or make-anthropic-llm (the model is fixed inside it via #:model). Optional: when omitted, the ashlar falls back to the ambient default-call-llm. If neither #:call-llm nor default-call-llm is set, construction errors loudly.
#:produces — node type of the final result.
#:context — a context data structure (from stone/context-struct) describing how the outgoing messages are projected from the DAG: (context (system ...) (history 'type) (user 'type) #:compaction ashlar). The ashlar’s read-set is derived from the context’s lenses, never declared alongside it. Default is an empty context. (Upgrading from v0.2: see v0.2 to v0.3.)
#:name — defaults to produces, else a fresh agent-ashlar-* id.
#:schema — JSON schema attached to metadata. Usually auto-populated from #:response-format.
#:middleware — middleware onion wrapping each turn.
#:decide — loop decision function (context? (listof recommendation?) -> recommendation?). Must be a procedure. Default is continue-on-tool-use. For single-turn behavior, pass #:max-turns 1 with empty middleware.
#:max-turns — hard cap on turns inside the agent loop.
#:response-format — when truthy, the final draft must be a hash (parsed from JSON). Failing to parse produces a 'llm-parse-failed failure.
#:budget — max response tokens per turn (the response max-tokens handed to the call-llm).
#:context-budget — a provider-agnostic size hint the projected context is measured against. When the projection overflows and no #:compaction ashlar re-projects it small enough, the agent fails loud (a failure node becomes the leaf; the call-llm is never reached). Default is large.
#:adversary — quality-gate ashlar. Runs against the ashlar DAG when decide says 'continue. Failure node = rejection; non-failure = pass.
#:heal-with — healer ashlar. When adversary rejects, healer runs and its output enters the agent’s conversation.
#:max-healing — bounds heal cycles (reject → heal → retry), not adversary invocations. The adversary always votes at least once; the budget gates retries only. #:max-healing 0 is the gate idiom: adversary votes once; pass → done, reject → fail without retry. #:max-healing N allows up to N heal cycles, so the adversary may vote up to N+1 times. On the (+ N 1)th reject, returns a 'healing-exhausted failure node.
The threshold — the single designed opening through which the agent emits observation crossings (turns, and — via the streaming call-llm — tokens) and poses decisions (tool-call gates) — is not a construction argument. The agent reads (current-threshold) when it emits, a per-run resource bound by a run verb (run-ashlar with #:record, or run-mason) from the record’s threshold — symmetric with current-journal. Every session verb runs with a threshold present: pass your own via (open-record path #:threshold th) to own the bus and attach your own subscribers, or omit it and a fresh threshold (with a default logger) is created for you.
#:finalize — optional hook that receives the parsed final content and returns either a typed node or a failure node. The framework injects 'conversation into typed-node returns; failure-node returns pass through unchanged. Raises if the returned content already contains 'conversation. When absent, the framework builds a typed-node of type produces with the parsed content and the conversation merged in.
5.5 stone/context-struct
| (require stone/context-struct) | package: Stone |
The context data structure an agent ashlar’s #:context is built from (see make-agent-ashlar). A context is plain, inspectable data with two readings of the same value: statically, the validator walks it to derive the ashlar’s read-set (context-read-set); at run time, the agent loop interprets it to project the outgoing messages from the DAG. (Upgrading from v0.2: see v0.2 to v0.3.)
Naming. stone re-exports a different, unrelated context — the per-turn middleware struct (stone). When you require both, exclude that one: (require (except-in stone context context? on-latest) stone/context-struct).
procedure
(context [ #:compaction compaction] element ...) → context? compaction : (or/c ashlar-meta? #f) = #f element : context-element?
procedure
(system arg) → context-element?
arg : (or/c string? ctx-lens?)
procedure
(user arg) → context-element?
arg : (or/c symbol? ctx-lens?)
procedure
(history arg) → context-element?
arg : (or/c symbol? ctx-lens?)
struct
(struct context-element (role lens) #:transparent) role : (or/c 'system 'user 'history) lens : ctx-lens?
struct
(struct ctx-lens (projector reads literal) #:transparent) projector : procedure? reads : (listof symbol?) literal : any/c
procedure
(context-elements ctx) → (listof context-element?)
ctx : context?
procedure
(context-compaction ctx) → (or/c ashlar-meta? #f)
ctx : context?
procedure
(context-read-set ctx) → (listof symbol?)
ctx : context?
procedure
(seed-compaction-summary dag type content condensed) → dag? dag : dag? type : symbol? content : any/c condensed : (listof message?)
5.6 stone/threshold
| (require stone/threshold) | package: Stone |
The threshold is the single designed opening between a running ashlar and everything outside it — one per run. It has two faces: the inside (engine) emits observations and, for a request (ask-human, tool-approval), suspends the run; the outside — the far side, whatever it is: a TUI, a test, an automated approver — reads the crossing feed, steers, and answers a suspended request by appending a DAG node. Every crossing is provenance-tagged by its span, so one threshold serves a whole nested run. See Ask Human for the conceptual model and Run a durable, resumable session for how a run binds one.
A request is one-way out: posing it appends a request node to the DAG and raises run-suspended, which unwinds the run back to its caller — no Racket thread is parked, no resolver table, no attended/unattended policy. The answer re-enters as an ordinary child node (answer-request!, stone/record); the DAG’s own topology is the record.
procedure
procedure
(threshold? v) → boolean?
v : any/c
parameter
(current-threshold) → (or/c threshold? #f)
(current-threshold th) → void? th : (or/c threshold? #f)
struct
(struct crossing (span kind payload) #:transparent) span : pair? kind : symbol? payload : any/c
5.6.1 Outside: the far side
procedure
(threshold-subscribe th) → evt?
th : threshold?
procedure
(threshold-steer! th text) → void?
th : threshold? text : string?
procedure
(threshold-request-cancel! th) → void?
th : threshold?
procedure
th : threshold?
procedure
(threshold-clear-cancel! th) → void?
th : threshold?
5.6.2 Inside: the engine
These are called by the framework, not usually by user code.
procedure
(threshold-suspend! th req-id request-kind crossing-kind payload [ span]) → any th : threshold? req-id : any/c request-kind : symbol? crossing-kind : symbol? payload : any/c span : pair? = DECISION-SPAN
procedure
(threshold-emit! th c) → void?
th : threshold? c : crossing?
procedure
(threshold-drain! th) → (listof message?)
th : threshold?
procedure
(threshold-check-cancel! th) → void?
th : threshold?
struct
(struct run-cancelled () #:transparent)
struct
(struct run-suspended (request-id request-kind payload) #:transparent) request-id : any/c request-kind : symbol? payload : any/c
5.7 stone/record
| (require stone/record) | package: Stone |
A record binds what a durable run needs: the DAG it runs against, the plan masonry carves, the append-only journal it writes progress to, and the threshold the far side observes and answers through. See Run a durable, resumable session for the workflow.
Durability lives in the journal. The DAG and the plan are two disjoint projections of the same recorded facts — replay keeps the node facts for the DAG and the plan facts for the plan, each ignoring the other’s — so a run’s two histories cannot drift out of step.
struct
(struct record (dag plan journal threshold path facts) #:transparent) dag : dag? plan : plan? journal : journal? threshold : (or/c threshold? #f) path : (or/c path? string? #f) facts : (listof any/c)
procedure
(open-record path [#:threshold th]) → record?
path : (or/c path? string?) th : (or/c threshold? #f) = #f
Fresh (no file at path) — an empty DAG plus a new journal opened for append.
Resume (the file exists) — the DAG is rebuilt by replaying the journal, and the same file is reopened for append so new facts extend the log.
#:threshold defaults to #f; a runner such as the TUI passes a real one, and a batch test may pass one to observe crossings or answer a suspended request. Hand the result to run-ashlar #:record.
procedure
(open-memory-record [#:threshold th]) → record?
th : (or/c threshold? #f) = #f
procedure
(answer-request! r req-or-id raw) → void?
r : record? req-or-id : (or/c node? any/c) raw : any/c
The answer node’s type is derived from the request’s 'request-kind: an 'ask answer takes the ask-human’s own #:produces symbol (the answer node is that ashlar’s produces output); an 'approval answer is typed 'verdict. raw is either an explicit content hash (used as-is) or a bare value the helper shapes per kind — a string answer for an ask, or an 'allow/'deny symbol for a gate.
The caller re-invokes run-ashlar over the same record path to resume; the run rebuilds its DAG from the journal, so the appended answer node is visible. A suspended pass recorded no conversation boundary, so the re-invocation resumes the same conversation rather than starting the next one.
procedure
(halt-request! r req-or-id kind [reason]) → void?
r : record? req-or-id : (or/c node? any/c) kind : symbol? reason : string? = "session abandoned by the caller"
5.7.1 stone/journal
| (require stone/journal) | package: Stone |
The record’s on-disk form: an append-only JSONL log of the facts a run produces — where durability actually lives. Replaying it is exactly what a resume does: the node facts project into the DAG (the ashlar’s history) and the plan facts into the plan (masonry’s), each projection ignoring the other’s facts. Most users never touch this module directly — they use open-record and run-ashlar #:record. The facts (node-appended, plan-appended, session-started, ashlar-started, ashlar-completed, conversation-completed, lay-completed, current-head, …) are transparent structs so equality — the round-trip contract — is structural. A request and its answer are both ordinary node-appended facts — a request is a 'request-typed node, its answer a child node — so there is no separate decision fact type to track.
procedure
(open-journal path) → journal?
path : (or/c path? string?)
procedure
(journal-append! j fact) → void?
j : journal? fact : any/c
procedure
(close-journal j) → void?
j : journal?
parameter
(current-journal) → (or/c journal? #f)
(current-journal j) → void? j : (or/c journal? #f)
5.7.2 stone/tui-main
| (require stone/tui-main) | package: Stone |
procedure
(run-tui ashlar #:record path [ #:mason mason #:author? author? #:threshold th #:initial-state initial-state]) → any ashlar : ashlar-meta? path : (or/c path? string?) mason : mason? = (make-mason) author? : boolean? = #f th : threshold? = (make-threshold) initial-state : any/c = #f
5.8 stone/llm-types
| (require stone/llm-types) | package: Stone |
Shared response and exception types used by the call-llm layer. Tool authors and custom call-llm authors require this module; user ashlar code generally does not.
5.8.1 Responses
struct
(struct llm-response (text tool-calls usage))
text : string? tool-calls : (listof tool-call?) usage : hash?
5.8.2 Exceptions
struct
(struct exn:fail:repetition-tripped exn:fail (hit partial) #:extra-constructor-name make-exn:fail:repetition-tripped) hit : repetition-hit? partial : string?
struct
(struct exn:fail:repetition-exhausted exn:fail (attempts last-hit) #:extra-constructor-name make-exn:fail:repetition-exhausted) attempts : exact-nonnegative-integer? last-hit : (or/c repetition-hit? #f)
struct
(struct exn:fail:llm-http-error exn:fail (status body) #:extra-constructor-name make-exn:fail:llm-http-error) status : exact-nonnegative-integer? body : string?
struct
(struct exn:fail:llm-empty-response exn:fail () #:extra-constructor-name make-exn:fail:llm-empty-response)
5.9 stone/llm-client
| (require stone/llm-client) | package: Stone |
call-llm factories compatible with the make-agent-ashlar call-llm contract. Each factory returns a model-bound call-llm: the model is fixed at construction via #:model and travels inside the call-llm, so no per-call model argument is passed. See Provider constraints for per-provider knobs.
procedure
(make-openai-llm #:url url #:model model [ #:api-key api-key #:extra-body extra-body #:repetition-watch repetition-watch #:max-retries max-retries]) → procedure? url : string? model : string? api-key : string? = "" extra-body : hash? = (hasheq) repetition-watch : (or/c (-> repetition-watcher?) #f) = #f max-retries : exact-nonnegative-integer? = 3
#:url may be the base URL or the full /v1/chat/completions URL; /v1/chat/completions is appended when missing. #:api-key empty means no Authorization header. #:extra-body is closed over at construction time and shallow-merged into every request body; the reserved keys '(model messages max_tokens tools response_format stream) raise at call time if you try to override them.
#:repetition-watch is a zero-arg factory that returns a fresh repetition-watcher; when supplied, every call constructs independent watchers for the response and thinking channels and feeds them mid-stream. If a watcher trips, the SSE stream aborts; the call-llm appends an assistant turn carrying the partial output and a user turn explaining the model got stuck, then retries up to #:max-retries times. After exhaustion, raises exn:fail:repetition-exhausted. When #f (default), the retry/watch path is bypassed entirely.
procedure
(make-anthropic-llm #:api-key api-key #:model model [ #:url url #:extra-body extra-body #:repetition-watch repetition-watch #:max-retries max-retries]) → procedure? api-key : string? model : string? url : string? = "https://api.anthropic.com/v1/messages" extra-body : hash? = (hasheq) repetition-watch : (or/c (-> repetition-watcher?) #f) = #f max-retries : exact-nonnegative-integer? = 3
#:repetition-watch and #:max-retries are accepted for API symmetry with make-openai-llm but currently have no effect — they will be honored when Anthropic streaming lands.
procedure
(call-anthropic #:url url #:model model [ #:system system] #:messages messages [ #:max-tokens max-tokens #:api-key api-key #:tools tools #:response-format response-format #:extra-body extra-body]) → hash? url : string? model : string? system : string? = "" messages : list? max-tokens : exact-nonnegative-integer? = 4096 api-key : string? = "mock-key" tools : list? = '() response-format : (or/c hash? #f) = #f extra-body : hash? = (hasheq)
procedure
(call-openai #:url url #:model model [ #:system system] #:messages messages [ #:max-tokens max-tokens #:api-key api-key #:tools tools #:response-format response-format #:extra-body extra-body #:threshold threshold #:response-watcher response-watcher #:thinking-watcher thinking-watcher]) → hash? url : string? model : string? system : string? = "" messages : list? max-tokens : exact-nonnegative-integer? = 4096 api-key : string? = "" tools : list? = '() response-format : (or/c hash? #f) = #f extra-body : hash? = (hasheq) threshold : (or/c threshold? #f) = #f response-watcher : (or/c repetition-watcher? #f) = #f thinking-watcher : (or/c repetition-watcher? #f) = #f
#:response-watcher / #:thinking-watcher are optional repetition-watchers; if either trips during the SSE stream, the call raises exn:fail:repetition-tripped and closes the response. make-openai-llm’s retry layer constructs and supplies fresh watchers per attempt; supply them here directly only if you’re bypassing that layer.
procedure
c : procedure?
procedure
(extract-text response) → string?
response : hash?
5.9.1 Exceptions raised by the OpenAI call-llm
This module raises exn:fail:repetition-tripped and exn:fail:repetition-exhausted from stone/llm-types under the conditions documented at stone/llm-types. The retry layer in make-openai-llm catches exn:fail:repetition-tripped internally; user code only observes it if that retry layer is bypassed.
5.10 stone/repetition-watch
| (require stone/repetition-watch) | package: Stone |
Streaming-time detectors for degenerate LLM output: exact n-gram loops (verbatim repetition) and rambling without progress (near-repetition). Wired into call-openai via the #:response-watcher / #:thinking-watcher keywords; consumed by the call-llm-level retry loop in make-openai-llm via the #:repetition-watch keyword.
5.10.1 Hits
struct
(struct repetition-hit (kind detail position) #:extra-constructor-name make-repetition-hit) kind : symbol? detail : any/c position : exact-nonnegative-integer?
5.10.2 Karp–Rabin n-gram counter
Detects exact verbatim repetition. Maintains a rolling polynomial hash over the latest n bytes; trips when any window’s hash is observed threshold times (with sample-byte verification to filter hash collisions). Sticky after trip — once tripped, ngram-counter-add! is an O(1) no-op.
procedure
(make-ngram-counter [ #:n n #:threshold threshold]) → ngram-counter? n : exact-positive-integer? = 100 threshold : exact-positive-integer? = 4
procedure
(ngram-counter-add! c byte) → void?
c : ngram-counter? byte : byte?
procedure
(ngram-counter-tripped c) → (or/c #f repetition-hit?)
c : ngram-counter?
5.10.3 Compression-ratio detector
Detects rambling / near-repetition where the model produces grammatical-but-going-nowhere output. Maintains a circular byte buffer of size #:window; per tick, deflates the buffer (via file/gzip) and pushes compressed-size / window-size onto a fixed-length ratio history. Trips when the latest #:history-len ratios are all ≤ #:threshold. Sticky.
procedure
(make-compression-detector [ #:window window #:threshold threshold #:history-len history-len]) → compression-detector? window : exact-positive-integer? = 1024 threshold : real? = 0.18 history-len : exact-positive-integer? = 3
procedure
(compression-detector-add! d byte) → void?
d : compression-detector? byte : byte?
procedure
d : compression-detector?
Calls that complete in less than the emit interval will never engage the compression detector — by design, since short outputs are not a rambling failure mode. The n-gram detector remains active per-byte regardless.
procedure
(compression-detector-tripped d) → (or/c #f repetition-hit?)
d : compression-detector?
5.10.4 Combined watcher
Multiplexes both detectors behind a single API. This is what the streaming session and call-llm layer plug in.
procedure
(make-repetition-watcher [ #:ngram-counter ngram-counter #:compression compression #:on-trip on-trip]) → repetition-watcher?
ngram-counter : (or/c ngram-counter? #f) = (make-ngram-counter)
compression : (or/c compression-detector? #f) = (make-compression-detector) on-trip : (-> repetition-hit? any/c) = (λ (_) (void))
procedure
(repetition-watcher-add-bytes! w bs) → void?
w : repetition-watcher? bs : bytes?
procedure
w : repetition-watcher?
procedure
(repetition-watcher-tripped w) → (or/c #f repetition-hit?)
w : repetition-watcher?
5.11 stone/tools
| (require stone/tools) | package: Stone |
Middleware constructors for LLM tool calls. See Agents and Tools for the conceptual model. Human interaction is make-ask-human (see stone/edge and Ask Human), which solicits through the run’s threshold rather than through tool middleware.
5.11.1 Tool middleware
procedure
(make-tool name #:schema schema #:handler handler [ #:allowed-paths allowed-paths #:confirm? confirm?]) → any/c name : symbol? schema : hash? handler : (hash? . -> . any) allowed-paths : (or/c (listof string?) #f) = #f confirm? : boolean? = #f
procedure
(has-tool-call-for? ctx tool-name) → boolean?
ctx : any/c tool-name : symbol?
procedure
(extract-tool-calls response) → (listof hash?)
response : hash?
procedure
(tool-schema mw) → (or/c hash? #f)
mw : any/c
5.11.2 Built-in tool middleware
procedure
(write-file [ #:allowed-paths allowed-paths #:confirm? confirm?]) → any/c allowed-paths : (or/c (listof string?) #f) = #f confirm? : boolean? = #f
procedure
(edit-file [ #:allowed-paths allowed-paths #:confirm? confirm?]) → any/c allowed-paths : (or/c (listof string?) #f) = #f confirm? : boolean? = #f
procedure
(delete-file* [ #:allowed-paths allowed-paths #:confirm? confirm?]) → any/c allowed-paths : (or/c (listof string?) #f) = #f confirm? : boolean? = #t
procedure
(list-directory [#:allowed-paths allowed-paths]) → any/c
allowed-paths : (or/c (listof string?) #f) = #f
procedure
(file-exists* [#:allowed-paths allowed-paths]) → any/c
allowed-paths : (or/c (listof string?) #f) = #f
procedure
(run-command [ #:timeout default-timeout #:confirm? confirm?]) → any/c default-timeout : real? = 60000 confirm? : boolean? = #f
procedure
(start-command [#:timeout default-timeout]) → any/c
default-timeout : real? = 300000
procedure
(check-command) → any/c
procedure
(wait-commands) → any/c
5.12 stone/decisions
| (require stone/decisions) | package: Stone |
Ready-made decide functions for the #:decide kwarg of make-agent-ashlar. A decide function has the signature (context? (listof recommendation?) -> recommendation?) and is called after every turn to pick 'continue, 'loop, or 'halt.
procedure
(continue-on-tool-use ctx recs) → any/c
ctx : any/c recs : (listof any/c)
procedure
(tool-directed ctx recs) → any/c
ctx : any/c recs : (listof any/c)
5.13 stone/validate
| (require stone/validate) | package: Stone |
Static checks over a composed ashlar. See Validation for the conceptual model.
procedure
(validate-ashlar ashlar) → validation-result?
ashlar : ashlar-meta?
struct
(struct validation-result (errors) #:transparent) errors : (listof validation-error?)
struct
(struct validation-error (type ashlar-name queried-type message) #:transparent) type : symbol? ashlar-name : (or/c symbol? #f) queried-type : symbol? message : string?
procedure
(validation-ok? r) → boolean?
r : validation-result?
procedure
(validation-errors r) → (listof validation-error?)
r : validation-result?
procedure
(enumerate-ashlars ashlar) → (listof symbol?)
ashlar : ashlar-meta?
procedure
(enumerate-paths ashlar) → (listof (listof symbol?))
ashlar : ashlar-meta?
5.14 stone/test
| (require stone/test) | package: Stone |
Testing utilities for Stone ashlars. See Testing utilities for the conceptual model and Test ashlars that use tools for the how-to.
A tool-call record is an immutable hasheq with keys 'name (symbol), 'input (hash), 'result-text (string), and 'result-meta (hash). tool-calls and friends return lists of these records in call order.
5.14.1 The two forms
syntax
(with-live-call-llm #:call-llm call-llm maybe-strict maybe-timeout body ...)
maybe-strict =
| #:strict-tools? strict? maybe-timeout =
| #:timeout seconds
syntax
(with-mock-call-llm #:call-llm call-llm maybe-strict body ...)
(with-mock-call-llm #:responses responses maybe-strict body ...)
maybe-strict =
| #:strict-tools? strict?
procedure
(ashlar-with-tool-stub s tool-name stub-handler) → ashlar-meta? s : ashlar-meta? tool-name : symbol? stub-handler : (hash? . -> . any)
5.14.2 Assertions
procedure
(tool-calls) → (listof hash?)
procedure
(tool-calls-by-name name) → (listof hash?)
name : symbol?
procedure
name : symbol?
syntax
(check-tool-called? name)
(check-tool-called? name msg)
syntax
(check-tool-not-called? name)
(check-tool-not-called? name msg)
syntax
(check-tool-call-count name n)
(check-tool-call-count name n msg)
5.14.3 Stub helpers
procedure
(stub-answer s) → (hash? . -> . any)
s : string?
5.14.4 Response builders
procedure
(llm-tool-call tool-name [ #:id id #:question question #:input input]) → any/c tool-name : (or/c symbol? string?) id : string? = (fresh-call-id) question : (or/c string? #f) = #f input : (or/c hash? #f) = #f
5.14.5 Parameters
parameter
(current-test-call-llm) → (or/c procedure? #f)
(current-test-call-llm c) → void? c : (or/c procedure? #f)
= #f
5.15 stone/logging
| (require stone/logging) | package: Stone |
Structured logging on a dedicated Racket logger. See Observability for the design.
value
syntax
(log-stone-debug arg ...)
syntax
(log-stone-info arg ...)
syntax
(log-stone-warning arg ...)
syntax
(log-stone-error arg ...)
syntax
(log-stone-fatal arg ...)
procedure
(stone-event level event data) → void?
level : (or/c 'debug 'info 'warning 'error 'fatal) event : symbol? data : hash?
parameter
(current-trace-id) → (or/c string? #f)
(current-trace-id id) → void? id : (or/c string? #f)
= #f
parameter
(current-span-id) → (or/c string? #f)
(current-span-id id) → void? id : (or/c string? #f)
= #f
parameter
(current-parent-span-id) → (or/c string? #f)
(current-parent-span-id id) → void? id : (or/c string? #f)
= #f
procedure
(generate-id [prefix]) → string?
prefix : string? = ""
5.16 stone/trace
| (require stone/trace) | package: Stone |
Public API for reading and analyzing Stone’s trace.jsonl files. A trace is produced by attaching a logger to stone-logger (see Trace a run for emission); this module covers the consumption side.
The raco stone trace subcommands (tally, lifecycle, payload) are thin wrappers over the data API below — if you want to build a custom inspection tool, require this module and work with the primitives directly.
5.16.1 Loading
procedure
(load-trace path) → (listof hash?)
path : path-string?
5.16.2 Event accessors
Each accessor takes one event hash (as returned by load-trace) and returns a normalized field. Use these rather than hash-refing directly so callers stay robust against future trace-shape changes.
procedure
(event-data e) → hash?
e : hash?
procedure
(event-type e) → string?
e : hash?
procedure
(event-timestamp e) → real?
e : hash?
procedure
(event-ashlar-name e) → string?
e : hash?
procedure
(event-turn-number e) → (or/c exact-nonnegative-integer? #f)
e : hash?
5.16.3 Aggregators
procedure
(tally-events events)
→ (listof (cons/c string? exact-nonnegative-integer?)) events : (listof hash?)
procedure
(lifecycle-events events) → (listof hash?)
events : (listof hash?)
procedure
(find-payloads events [ #:ashlar ashlar #:turn turn]) → (listof hash?) events : (listof hash?) ashlar : (or/c string? #f) = #f turn : (or/c exact-nonnegative-integer? #f) = #f
Note that "api-call-payload" events are only emitted at the 'debug log level; calls captured at the default 'info level have headers ("api-call") but not full message contents.
5.16.4 Formatting
procedure
(format-lifecycle-line e) → string?
e : hash?
5.16.5 CLI entry points
These are the implementations of the raco stone trace subcommands; they’re provided so other tools (test runners, ad-hoc scripts) can invoke them programmatically with the same behavior as the CLI.
procedure
(stone-trace-cli) → any
procedure
path : path-string?
procedure
(run-lifecycle path) → any
path : path-string?
procedure
(run-payload path [ #:ashlar ashlar #:turn turn #:last? last?]) → any path : path-string? ashlar : (or/c string? #f) = #f turn : (or/c exact-nonnegative-integer? #f) = #f last? : boolean? = #f
By default picks the first matching payload; pass #:last? #t for the most recent. Equivalent to raco stone trace payload <path> with –ashlar, –turn, and –last flags.
5.17 stone/lens
| (require stone/lens) | package: Stone |
Lightweight path accessor used by ashlar-match extractors and validator lens checks. Re-exported from stone for convenience.
5.18 stone
| (require stone) | package: Stone |
Umbrella module. Re-exports every public binding from stone/edge, stone/dag, stone/decisions, and stone/lens, plus the runtime types documented here (default-call-llm, make-json-schema, and the middleware / context / recommendation support for custom multi-turn behavior).
For code meant to be compact, (require stone) pulls the core vocabulary. Reach for the per-module requires (stone/llm-ashlar, stone/llm-client, stone/tools, stone/test, stone/validate, stone/logging) for the modules the umbrella doesn’t cover.
5.18.1 Parameters
parameter
(default-call-llm) → (or/c procedure? #f)
(default-call-llm c) → void? c : (or/c procedure? #f)
= #f
5.18.2 Schema builder
procedure
(make-json-schema name properties required) → hash?
name : string? properties : hash? required : (listof (or/c symbol? string?))
(hasheq 'type "json_schema" 'json_schema (hasheq 'name name 'strict #t 'schema (hasheq 'type "object" 'properties properties 'required required 'additionalProperties #f)))
5.18.3 Middleware onion types
These are the types make-agent-ashlar uses internally. Most users never construct them directly — reach for them when building a custom decide function or a middleware that needs to inspect context state.
struct
(struct context ( system tools response-format budget meta dag threshold conversation) #:transparent) system : string? tools : (listof hash?) response-format : (or/c hash? #f) budget : exact-nonnegative-integer? meta : hash? dag : (or/c dag? #f) threshold : (or/c threshold? #f) conversation : (or/c dag? #f)
struct
(struct middleware (name guard handler) #:transparent) name : symbol? guard : (context? . -> . boolean?) handler : (context? (context? . -> . context?) . -> . context?)
struct
(struct recommendation (type source reason) #:transparent) type : (or/c 'continue 'halt 'loop) source : any/c reason : any/c
procedure
(make-context [ #:system system #:tools tools #:response-format response-format #:budget budget #:meta meta #:dag dag #:threshold threshold #:conversation conversation]) → context? system : string? = "" tools : list? = '() response-format : (or/c hash? #f) = #f budget : exact-nonnegative-integer? = 4096 meta : hash? = (hash) dag : (or/c dag? #f) = #f threshold : (or/c threshold? #f) = #f conversation : (or/c dag? #f) = #f
procedure
(context-set ctx [ #:system system #:tools tools #:response-format response-format #:budget budget #:meta meta #:dag dag #:threshold threshold #:conversation conversation]) → context? ctx : context? system : any/c = #f tools : any/c = (void) response-format : any/c = #f budget : any/c = #f meta : any/c = #f dag : any/c = #f threshold : any/c = (void) conversation : any/c = (void)
procedure
(make-middleware name guard handler) → middleware?
name : symbol? guard : (context? . -> . boolean?) handler : (context? (context? . -> . context?) . -> . context?)
5.19 Command-line interface
Stone’s command-line surface: the raco stone TUI launcher and the validate, trace, and install-skill subcommands.
5.19.1 raco stone
Synopsis: raco stone [flags...]
Loads a Stone configuration (if present), merges any command-line flag overrides, allocates (or resumes) a session journal, and launches the Stone TUI with the resulting agent ashlar.
Flag |
| Argument |
| Description |
–url |
| url |
| LLM API endpoint URL. Overrides url in config. |
–model |
| model |
| Model identifier. Overrides model in config; the CLI builds a model-bound call-llm from the resolved config and installs it as the ambient default-call-llm. |
–continue |
| [id] |
| Resume a recorded session (most-recent when no id). |
–url and –model are #:once-each. No positional arguments are accepted.
5.19.1.1 Configuration
On startup the CLI looks for .stone/settings.rkt (walking up from the current directory to $HOME). If found, it’s loaded with dynamic-require and expected to provide a zero-argument procedure named build-agent. The CLI invokes build-agent with no arguments; its return value is an ashlar-meta? — the topology to run. The CLI builds a single threshold for the run and hands it to run-tui, which binds it per run (as current-threshold) and subscribes to it. The agent reads current-threshold when it emits, so its observation crossings reach the TUI without the config threading a threshold — symmetric with current-journal.
#lang racket |
(require stone/llm-ashlar stone/llm-client stone/context-struct) |
(provide build-agent) |
(define call-llm (make-openai-llm #:url "http://localhost:8000" |
#:model "my-model")) |
(define (build-agent) |
(make-agent-ashlar #:call-llm call-llm |
#:produces 'response |
#:context (context (system "You are helpful.") |
(history 'response) |
(user 'prompt)))) |
5.19.1.2 Sessions
Each raco stone run is a durable session: its journal lives at .stone/sessions/<id>.jsonl, resolved next to the loaded .stone/settings.rkt (or .stone/sessions/ under the current directory when there is no config). A bare raco stone allocates a fresh session id; raco stone –continue reopens the most-recent session (or the one named by –continue <id>) and resumes it — the journal replays as scrollback and the run picks up where it stopped, as long as the topology is unchanged. List recorded sessions with raco stone sessions (below). See Run a durable, resumable session for the durability model.
5.19.1.3 Exit codes
Code |
| Meaning |
0 |
| Normal TUI exit, or fall-through when no agent is configured. |
The CLI does not call exit itself; exit status is whatever the TUI or the Racket runtime returns on termination.
5.19.2 raco stone sessions
Synopsis: raco stone sessions
Lists the recorded sessions under the current sessions directory (.stone/sessions/), newest activity first, as a fixed-width table with columns ID, STARTED, LAST-ACTIVITY, and LABEL (the first user prompt, truncated). Prints No recorded sessions. when the directory is empty or absent. Pair it with raco stone –continue <id> to resume a specific session.
5.19.3 raco stone validate
Synopsis: raco stone validate <ashlar-file>
Loads a Racket file, extracts the binding named ashlar, and runs validate-ashlar on it. Prints errors and warnings; sets exit code from the result.
5.19.3.1 Ashlar file requirements
The file is loaded via dynamic-require. It must:
Be a valid Racket module that racket can dynamic-require.
provide a binding named exactly ashlar.
Bind ashlar to a ashlar-meta? value.
If the file doesn’t export ashlar, the CLI prints Error: <file> does not export ’ashlar and exits 1.
5.19.3.2 Validation categories
Category |
| Error types |
| Affects exit code |
Hard errors |
| missing-producer, invalid-lens, fanout-not-reduced |
| yes — exit 1 |
Warnings |
| maybe-unavailable |
| no — exit 0 |
5.19.3.3 Output
Ashlar is valid. |
When hard errors are present, a header and one line per error, formatted as [<type>] <ashlar-name>: <message>:
Errors (2): |
[missing-producer] classify: classify queries 'ticket but no upstream Stone produces it |
[invalid-lens] dispatch: lens path '(category) not found in upstream schema properties |
When warnings are present, an analogous Warnings (N): block prints. Both blocks may appear in the same run; the hard-error block (if any) prints first.
5.19.3.4 Exit codes
Code |
| Meaning |
0 |
| Ashlar valid, or only maybe-unavailable warnings present. |
1 |
| Missing ashlar-file argument, file doesn't export ashlar, hard errors reported, or subcommand other than validate supplied. |
5.19.4 raco stone trace
Synopsis: raco stone trace <subcommand> <path> [opts...]
Inspect a trace.jsonl file produced by a Stone run with stone-logger attached. Three subcommands cover the default investigation flow:
Subcommand |
| Description |
tally <path> |
| Print total event count and per-event-type counts. First-pass overview of what happened during the run. |
lifecycle <path> |
| Print the filtered story of the run, one event per line: ashlar start/end, tool dispatch, api-call/response, streaming progress, and failure events. Excludes noise like middleware-run. |
payload <path> [opts] |
| Dump a single api-call-payload event: ashlar, turn, model, system prompt, and one line per message with role + content preview + tool-call count. |
5.19.4.1 payload options
Flag |
| Argument |
| Description |
–ashlar |
| name |
| Filter to payloads from the named ashlar. |
–turn |
| n |
| Filter to the named LLM turn. |
–last |
|
| Pick the most recent matching payload (default is the first). |
"api-call-payload" events are only emitted at the 'debug log level; if payload returns No api-call-payload found, re-run with STONE_LOG_LEVEL=debug in the environment to capture them.
The underlying data API is documented at stone/trace — if you want to script trace analysis beyond what these subcommands provide, require stone/trace and use the primitives directly.
5.19.4.2 Exit codes
Code |
| Meaning |
0 |
| Subcommand ran successfully. |
1 |
| Missing path, missing required option value, unknown subcommand, or unrecognized argument. |
5.19.5 raco stone install-skill
Synopsis: raco stone install-skill [–force] [–target-dir DIR]
Install the bundled Claude Code skills into ~/.claude/skills/ by default. Two skills ship with Stone and work as a pair: scope-ashlar (the guided interview that scopes an ashlar tree) and build-ashlar (which translates a scoped tree into a runnable Racket scaffold). build-ashlar also carries a scribblings snapshot for primitive lookup; scope-ashlar does not (it writes no Racket).
Flag |
| Description |
–force |
| Overwrite existing skill bodies. Without this, an already-installed skill body is left untouched. |
–target-dir DIR |
| Install under DIR instead of HOME/.claude/skills/. The skills end up at DIR/scope-ashlar/ and DIR/build-ashlar/. |
The command is safe to re-run. Without –force, an already-installed skill body is left untouched, and build-ashlar’s scribblings snapshot is refreshed so it tracks your current Stone version — so a bare re-run after upgrading Stone is the normal way to update the docs. With –force it wipes and re-ships the skill bodies as well.
5.19.5.1 Exit codes
Code |
| Meaning |
0 |
| Install, forced re-install, or bare scribblings refresh succeeded. |
1 |
| Unknown flag, bad argument, or filesystem error during install. |