On this page:
5.1 stone/  edge
ashlar-meta
make-ashlar
~>
run-ashlar
ashlar-loop
ashlar-match
on-latest
ashlar-map
ashlar-parallel
ashlar-reduce
make-ask-human
make-scoped-ashlar
ashlar-produces
ashlar-queries
ashlar-produces-all
5.2 stone/  dag
5.2.1 Nodes
node
make-typed-node
typed-node
node-get
node-get*
node-text
make-failure-node
failure-node?
failure-node
5.2.2 DAGs
dag
make-dag
dag-append
dag-append-typed
dag-append-failure
dag-append-halt
dag-leaves
dag-cursor
dag-cursor-id
dag-set-cursor
dag-nodes
dag-failed?
dag-halted?
dag-terminal?
request-node
request-node?
dag-awaiting-request?
session-terminal-halt?
make-halt-node
halt-node?
halt-node-kind
halt-node-reason
halt-node
dag-nearest-ancestor
dag-collect-until
dag-query-all
dag-select
dag-select-window
dag-merge
failure-log-handler
5.3 stone/  messages
message
message-text
5.4 stone/  llm-ashlar
make-agent-ashlar
5.5 stone/  context-struct
context
context?
system
user
history
raw-lens
on-latest
context-element
ctx-lens
context-elements
context-compaction
context-read-set
seed-compaction-summary
5.6 stone/  threshold
make-threshold
threshold?
current-threshold
crossing
5.6.1 Outside:   the far side
threshold-subscribe
threshold-steer!
threshold-request-cancel!
threshold-cancel-requested?
threshold-clear-cancel!
5.6.2 Inside:   the engine
threshold-suspend!
threshold-emit!
threshold-drain!
threshold-check-cancel!
run-cancelled
run-suspended
OBSERVATION-CAPACITY
5.7 stone/  record
record
open-record
open-memory-record
answer-request!
halt-request!
5.7.1 stone/  journal
open-journal
journal-read
journal-append!
close-journal
journal?
current-journal
5.7.2 stone/  tui-main
run-tui
5.8 stone/  llm-types
5.8.1 Responses
llm-response
tool-call
5.8.2 Exceptions
exn:  fail:  repetition-tripped
exn:  fail:  repetition-exhausted
exn:  fail:  llm-http-error
exn:  fail:  llm-empty-response
5.9 stone/  llm-client
make-openai-llm
make-anthropic-llm
call-anthropic
call-openai
call-llm-model
extract-text
5.9.1 Exceptions raised by the Open  AI call-llm
5.10 stone/  repetition-watch
5.10.1 Hits
repetition-hit
5.10.2 Karp–Rabin n-gram counter
make-ngram-counter
ngram-counter-add!
ngram-counter-tripped
5.10.3 Compression-ratio detector
make-compression-detector
compression-detector-add!
compression-detector-check
compression-detector-tripped
5.10.4 Combined watcher
make-repetition-watcher
repetition-watcher-add-bytes!
repetition-watcher-tick!
repetition-watcher-tripped
5.11 stone/  tools
5.11.1 Tool middleware
make-tool
has-tool-call-for?
extract-tool-calls
tool-schema
5.11.2 Built-in tool middleware
read-file
write-file
edit-file
delete-file*
list-directory
mkdir*
file-exists*
run-command
start-command
check-command
wait-commands
5.12 stone/  decisions
continue-on-tool-use
tool-directed
5.13 stone/  validate
validate-ashlar
validation-result
validation-error
validation-ok?
validation-errors
enumerate-ashlars
enumerate-paths
5.14 stone/  test
5.14.1 The two forms
with-live-call-llm
with-mock-call-llm
ashlar-with-tool-stub
5.14.2 Assertions
tool-calls
tool-calls-by-name
tool-call-count
check-tool-called?
check-tool-not-called?
check-tool-call-count
5.14.3 Stub helpers
stub-answer
stub-fn
5.14.4 Response builders
llm-text
llm-tool-call
llm-multi
5.14.5 Parameters
current-test-call-llm
5.15 stone/  logging
stone-logger
log-stone-debug
log-stone-info
log-stone-warning
log-stone-error
log-stone-fatal
stone-event
current-trace-id
current-span-id
current-parent-span-id
generate-id
5.16 stone/  trace
5.16.1 Loading
load-trace
5.16.2 Event accessors
event-data
event-type
event-timestamp
event-ashlar-name
event-turn-number
5.16.3 Aggregators
tally-events
lifecycle-events
find-payloads
5.16.4 Formatting
format-lifecycle-line
5.16.5 CLI entry points
stone-trace-cli
run-tally
run-lifecycle
run-payload
5.17 stone/  lens
lens
lens?
lens-path
5.18 stone
5.18.1 Parameters
default-call-llm
5.18.2 Schema builder
make-json-schema
5.18.3 Middleware onion types
context
middleware
recommendation
make-context
context-set
make-middleware
run-onion
recommend
5.19 Command-line interface
5.19.1 raco stone
5.19.1.1 Configuration
5.19.1.2 Sessions
5.19.1.3 Exit codes
5.19.2 raco stone sessions
5.19.3 raco stone validate
5.19.3.1 Ashlar file requirements
5.19.3.2 Validation categories
5.19.3.3 Output
5.19.3.4 Exit codes
5.19.4 raco stone trace
5.19.4.1 payload options
5.19.4.2 Exit codes
5.19.5 raco stone install-skill
5.19.5.1 Exit codes
9.3

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)
The structure wrapping every ashlar. prop:procedure at position 0 means a ashlar-meta is callable: applying one to a DAG runs fn and returns a DAG.

  • fn — runtime function, (dag? -> (values node? dag?)).

  • 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
Wrap a (dag? -> node?) function as a atomic ashlar. Returning a non-failure node appends it to the DAG; returning a failure node leaves the DAG unchanged. #:name defaults to #:produces, else a fresh ashlar-* id. Emits 'ashlar-start and 'ashlar-end events on each run.

syntax

(~> ashlar ...)

Sequence primitive. Runs each ashlar in order, threading the DAG. produces-all is the union of children’s produces; queries is the subset of child queries not satisfied by earlier siblings. A sequence of zero ashlars returns an 'empty-sequence failure; it stops at the first failing child.

procedure

(run-ashlar ashlar #:record record)  dag?

  ashlar : ashlar-meta?
  record : record?
Top-level durable verb: ONE conversation pass over the record’s DAG, journaling progress and binding the record’s threshold as (current-threshold). Returns the produced DAG.

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?
Bounded repetition. #:until accepts a procedure (dag? -> any/c). Invoked after each body iteration with the loop’s accumulated DAG. The loop terminates when the predicate returns a true value or when #:max iterations have run. A failure from body aborts the loop immediately. Exhausting #:max emits a 'loop-exhausted failure. Iteration N+1 sees every node iteration N produced.

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
Macro; branches collected statically.

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?
Returns a procedure that, given a DAG, applies pred to (dag-cursor dag). Use to wrap a node-shaped predicate or extractor as a dag-shaped one when only the latest leaf matters. When the DAG is empty, the wrapped predicate is called with #f; callers should only use on-latest where a leaf is guaranteed.

procedure

(ashlar-map extractor body [#:name name])  ashlar-meta?

  extractor : procedure?
  body : ashlar-meta?
  name : (or/c symbol? #f) = #f
Data-dependent fan-out. extractor is (node? -> list?); each lane runs against a snapshot DAG with a synthetic 'map-item node carrying the item. An empty extractor result produces 'map-empty. Must be followed by ashlar-reduce in any position where its last-result would be read.

procedure

(ashlar-parallel [#:name name] lane ...)  ashlar-meta?

  name : (or/c symbol? #f) = #f
  lane : ashlar-meta?
Static fan-out. Each lane runs against the pre-fan-out DAG. With zero lanes, produces 'parallel-empty. Must be followed by ashlar-reduce like ashlar-map.

procedure

(ashlar-reduce ashlar [#:name name])  ashlar-meta?

  ashlar : ashlar-meta?
  name : (or/c symbol? #f) = #f
Marks ashlar as a reducer. Runtime behavior is identical to ashlar; the wrapper exists as a topology marker for the validator and forwards schema and name from its child.

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?) = '()
Builds an ashlar that solicits a question — built by format-fn from the current DAG — through the run’s threshold (current-threshold). Reaching the ask site:

  • 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
The single factory underneath every ashlar in the framework. Takes a (dag? -> dag?) body plus a walk rule and returns a ashlar-meta. Most users never call this directly — reach for it when building a new composition primitive.

procedure

(ashlar-produces s)  (or/c symbol? #f)

  s : any/c
Last symbol in ashlar-meta-produces-all, or #f if s isn’t an ashlar.

procedure

(ashlar-queries s)  (listof symbol?)

  s : any/c
The ashlar’s queries list, or () if s isn’t a ashlar.

procedure

(ashlar-produces-all s)  (listof symbol?)

  s : any/c
The ashlar’s full produces-all list, or () if s isn’t an ashlar.

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?
Node in the DAG. id is a SHA-1 digest over sorted parents, type, and content. type is the node-type symbol (e.g., 'data, 'failure, or any domain symbol). Prefer make-typed-node or typed-node over the raw constructor.

procedure

(make-typed-node parents type content [meta])  node?

  parents : (listof string?)
  type : symbol?
  content : any/c
  meta : hash? = (hash)
Creates a typed node with the given type. Id is hashed from sorted parents, type, and content.

procedure

(typed-node d type content [meta])  node?

  d : dag?
  type : symbol?
  content : any/c
  meta : hash? = (hash)
Convenience wrapper over make-typed-node that defaults parents to (dag-leaves d) — the ~99% case. This is the body-site constructor: make-ashlar bodies build with it and never name the leaves. The optional meta rides on the node’s metadata.

procedure

(node-get n field [default])  any/c

  n : (or/c node? #f)
  field : symbol?
  default : any/c = #f
Pulls field out of n’s content hash. Returns default when n is #f, when content isn’t a hash, or when the key is missing.

procedure

(node-get* n path ...)  any/c

  n : (or/c node? #f)
  path : symbol?
Nested hash walk. Returns #f at any level that isn’t a hash or doesn’t contain the next path segment.

procedure

(node-text n)  string?

  n : (or/c node? #f)
Returns the text representation of n: the raw string content, or the 'text field of a hash content, or (format "~a" content) as a fallback. Returns "" when n is #f.

procedure

(make-failure-node parents kind reason [meta])  node?

  parents : (listof string?)
  kind : symbol?
  reason : string?
  meta : hash? = (hash)
Creates a typed node with type 'failure and content (hasheq 'kind kind 'reason reason). Invokes the failure-log-handler parameter; stone/logging installs a handler that emits an 'error-level event.

procedure

(failure-node? v)  boolean?

  v : any/c
#t iff v is a node? whose node-type is 'failure.

procedure

(failure-node d kind reason [meta])  node?

  d : dag?
  kind : symbol?
  reason : string?
  meta : hash? = (hash)
The body-site failure constructor (mirrors typed-node): builds a failure node onto (dag-leaves d) and returns it, for ashlar bodies that return a failure node — the body never names the leaves. Fires the failure-log-handler. Use dag-append-failure for the returns-a-dag, one-call form.

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)
nodes maps ids to nodes. leaves is the list of the DAG’s childless node ids — the open ends new nodes attach to. cursor-id is the read/replay position (see dag-cursor). parent points at the enclosing DAG for composites’ sub-DAGs; label is the scope label (e.g. 'root, 'loop-body).

procedure

(make-dag [#:parent parent #:label label])  dag?

  parent : (or/c dag? #f) = #f
  label : symbol? = 'root
Empty DAG. The framework passes #:parent and #:label when building sub-DAGs; most user code calls (make-dag) for a fresh ashlar root.

procedure

(dag-append d n)  dag?

  d : dag?
  n : node?
Returns a new DAG with n in nodes, n’s parents removed from leaves, and n’s id appended to the end of the leaves list.

procedure

(dag-append-typed d type content [meta])  dag?

  d : dag?
  type : symbol?
  content : any/c
  meta : hash? = (hash)
Builds a typed node onto d’s current dag-leaves and appends it, in one call — for the two-step append sites, where the caller does not name the leaves. Returns the new DAG.

procedure

(dag-append-failure d kind reason [meta])  dag?

  d : dag?
  kind : symbol?
  reason : string?
  meta : hash? = (hash)
Like dag-append-typed, but appends a make-failure-node onto the leaves (and fires the failure-log-handler).

procedure

(dag-append-halt d kind reason [meta])  dag?

  d : dag?
  kind : symbol?
  reason : string?
  meta : hash? = (hash)
Like dag-append-typed, but appends a make-halt-node onto the leaves. A halt is a deliberate, non-error ending, so no log handler fires.

procedure

(dag-leaves d)  (listof string?)

  d : dag?
Accessor for the leaves — the ids of the DAG’s childless nodes.

procedure

(dag-cursor d)  (or/c node? #f)

  d : dag?
Returns the node at the DAG’s read/replay cursor (cursor-id), or #f if the DAG has no nodes. This is the node an ashlar reads back from — normally the latest, but a resume can move it.

procedure

(dag-cursor-id d)  (or/c string? #f)

  d : dag?
The id at the read/replay cursor, or #f. dag-cursor resolves it against dag-nodes.

procedure

(dag-set-cursor d n)  dag?

  d : dag?
  n : node?
Returns a copy of d with the read/replay cursor moved to n.

procedure

(dag-nodes d)  (hash/c string? node?)

  d : dag?
Accessor for the node table.

procedure

(dag-failed? d)  boolean?

  d : dag?
#t when the most recent leaf is a failure node.

procedure

(dag-halted? d)  boolean?

  d : dag?
#t when the most recent leaf is a halt node — a deliberate, non-error ending. Unlike a failure, a halt is not an error, so no failure-log handler fires.

procedure

(dag-terminal? d)  boolean?

  d : dag?
#t when the most recent leaf ends the composition — a failure (dag-failed?), a halt (dag-halted?), or a suspended request awaiting its answer (dag-awaiting-request?). All three short-circuit a ~> sequence; only dag-failed? signals an error.

procedure

(request-node d request-kind payload [meta])  node?

  d : dag?
  request-kind : symbol?
  payload : any/c
  meta : hash? = (hash)
Builds a request node onto (dag-leaves d) — the durable "a request is posed, the ashlar is suspended" marker. A request (ask-human, tool-approval) is an ordinary node, not a fact side-channel: its answer re-enters as a child node, so the DAG’s own topology is the record. request-kind is 'ask or 'approval; payload is the request’s data (a question, or a tool call’s name/args/call-id). A request node is terminal (dag-terminal?), so a suspended sub-ashlar short-circuits ~> / ashlar-loop just like a halt, and the run returns to its caller cursored on it. See Ask Human.

procedure

(request-node? n)  boolean?

  n : any/c
#t when n is a request node.

procedure

(dag-awaiting-request? d)  boolean?

  d : dag?
#t when d’s cursor rests on a request node — the ashlar posed a request and suspended, awaiting its answer.

procedure

(session-terminal-halt? d)  boolean?

  d : dag?
#t when d’s cursor rests on a halt node whose kind is 'user-quit or 'input-ended — the two halts that end the whole session, not merely one turn. This is the predicate a runner loops on: a caller repeating run-ashlar takes another turn until this holds. A 'declined or 'agent-decided halt, and an ordinary failure, end only the current pass, so a bad turn never kills a long session.

procedure

(make-halt-node parents kind reason [meta])  node?

  parents : (listof string?)
  kind : symbol?
  reason : string?
  meta : hash? = (hash)
Create a halt node signaling a deliberate stop. kind is one of 'agent-decided, 'declined, 'user-quit, or 'input-ended; the latter two are session-terminal (session-terminal-halt? — they end the whole session, so a runner looping run-ashlar stops). A pending request has no auto-deny or auto-quit of its own — an unanswered request just leaves the run suspended (dag-awaiting-request?); a 'user-quit halt is appended explicitly, by halt-request! (the caller abandoning a suspended session) or by an ashlar’s own code mapping an answer to quit (see Ask Human).

procedure

(halt-node? n)  boolean?

  n : any/c
#t when n is a halt node.

procedure

(halt-node-kind n)  symbol?

  n : node?
The halt node’s kind. ashlar-match can catch a halt by matching on this kind.

procedure

(halt-node-reason n)  string?

  n : node?
The halt node’s human-readable reason.

procedure

(halt-node d kind reason [meta])  node?

  d : dag?
  kind : symbol?
  reason : string?
  meta : hash? = (hash)
The body-site halt constructor (mirrors typed-node): builds a halt node onto (dag-leaves d) and returns it, for ashlar bodies that return a halt node — the body never names the leaves. A halt is a deliberate, non-error ending, so no log handler fires. Use dag-append-halt for the returns-a-dag, one-call form.

procedure

(dag-nearest-ancestor d type)  (or/c node? #f)

  d : dag?
  type : symbol?
Walks first-parent pointers from the DAG’s most recently appended leaf until it finds a node whose node-type is type. Returns the node, or #f if none is reachable on the first-parent line. When the DAG has no leaves, returns #f.

procedure

(dag-collect-until d    
  #:type collect-type    
  #:until sentinel-type)  (listof node?)
  d : dag?
  collect-type : symbol?
  sentinel-type : symbol?
Walks first-parent pointers from the most recently appended leaf, collecting nodes whose node-type is collect-type, stopping at the first ancestor whose node-type is sentinel-type. The sentinel node itself is the boundary and is excluded from the result.

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
Returns every node of the given type, sorted oldest-first by timestamp. Single in-scope scan over (dag-nodes d). The #:scope argument is retained as a no-op for source compatibility; passing 'conversation yields the same result as omitting it.

procedure

(dag-select d nid)  (listof node?)

  d : dag?
  nid : string?
Walks first-parent pointers from nid back to the root. Returns the nodes oldest-first. Used internally to rebuild a linear conversation from a branching DAG.

procedure

(dag-select-window d nid n)  (listof node?)

  d : dag?
  nid : string?
  n : exact-nonnegative-integer?
Returns the last n nodes of (dag-select d nid). If the linear history is shorter than n, returns the whole history.

procedure

(dag-merge dags)  dag?

  dags : (listof dag?)
Unions the node tables, concatenates and de-duplicates the leaves, and uses the root of the first input.

procedure

(failure-log-handler)

  (parameter/c (or/c (symbol? string? hash? . -> . any) #f))
Default #f. When set, make-failure-node calls (handler kind reason meta) before returning the node. stone/logging installs a handler at module load that emits an 'error-level 'stone-event.

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

(struct message (role content tool-calls call-id metadata))

  role : symbol?
  content : (or/c string? hash? (listof hash?))
  tool-calls : list?
  call-id : (or/c string? #f)
  metadata : hash?
A turn in a conversation.

  • 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)
Extract a plain-string view of m’s content, tolerating every shape content can take. When m is #f, returns "". Plays the same role for messages that node-text plays for typed nodes: when new content shapes are added, this helper grows so consumer code does not have to.

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
Runs a multi-turn agent with its own internal loop and conversation list. The agent threads a (listof message) through each turn, preserving full structured content (including tool call blocks) across turns. Produces one typed node at the end; the agent’s conversation is embedded in the result node’s content under the key 'conversation.

  • #: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?
Builds a context from an ordered list of elements (each a system, user, or history). At most one 'user element is allowed, and it must be last (so the projected message tail is role 'user — the conversation-tail invariant); violations raise. #:compaction names an optional DAG→DAG ashlar run when the projected context overflows the #:context-budget hint; it lands a summary node of the history element’s type. Build it with seed-compaction-summary.

procedure

(context? v)  boolean?

  v : any/c
Recognizes a context.

procedure

(system arg)  context-element?

  arg : (or/c string? ctx-lens?)
A 'system element. (system str) sets a literal system prompt (folded into the topology hash). (system lens) computes it via a lens.

procedure

(user arg)  context-element?

  arg : (or/c symbol? ctx-lens?)
A 'user element — the latest user turn. (user 'type) is sugar for "the latest node of 'type, text-extracted"; (user lens) computes it via a lens.

procedure

(history arg)  context-element?

  arg : (or/c symbol? ctx-lens?)
A 'history element — the ashlar’s prior turns of 'type, expanded into role-tagged conversation. (history 'type) reads 'type nodes; (history lens) computes the node list via a lens.

procedure

(raw-lens projector #:queries reads)  ctx-lens?

  projector : (dag? . -> . any/c)
  reads : (listof symbol?)
A lens with an explicit read-set. projector computes the element’s value from the DAG; #:queries is the read-set the validator checks (omitting it is a construction error — an opaque lens can’t be validated). Use it when an element must combine or transform several node types.

procedure

(on-latest type extract)  ctx-lens?

  type : symbol?
  extract : (node? . -> . any/c)
A lens projecting extract applied to the latest node of 'type (via dag-nearest-ancestor). Reads 'type. A bare (user 'type) / (history 'type) desugars to (on-latest type node-text).

struct

(struct context-element (role lens)
    #:transparent)
  role : (or/c 'system 'user 'history)
  lens : ctx-lens?
One element of a context: a role plus a lens.

struct

(struct ctx-lens (projector reads literal)
    #:transparent)
  projector : procedure?
  reads : (listof symbol?)
  literal : any/c
A projection over the DAG. reads is its read-set; literal carries a folded-in constant for a literal lens.

procedure

(context-elements ctx)  (listof context-element?)

  ctx : context?
The context’s ordered element list.

procedure

(context-compaction ctx)  (or/c ashlar-meta? #f)

  ctx : context?
The context’s compaction ashlar, or #f.

procedure

(context-read-set ctx)  (listof symbol?)

  ctx : context?
The deduped union of every element’s lens read-set — the ashlar’s derived #:queries.

procedure

(seed-compaction-summary dag    
  type    
  content    
  condensed)  dag?
  dag : dag?
  type : symbol?
  content : any/c
  condensed : (listof message?)
Builds a #:compaction summary: condensed becomes a fresh 'turn chain rooted at dag’s current leaf, and a summary node of type carrying content is parented on that chain’s leaf, recording the pre-compaction leaf id under node-meta 'compacted-from. The condensed turns are the new current array (the reader walks the summary’s 'turn lineage); the frozen prior array stays retained in the DAG.

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

(make-threshold)  threshold?

Builds a fresh threshold. Takes no keyword arguments — there is no attended/unattended policy to configure; an unanswered request simply leaves the run suspended.

procedure

(threshold? v)  boolean?

  v : any/c
Recognizes a threshold.

parameter

(current-threshold)  (or/c threshold? #f)

(current-threshold th)  void?
  th : (or/c threshold? #f)
The threshold in effect for the current run, bound by a run verb (run-ashlar #:record, run-mason) from the record’s threshold. Engine-side constructs such as make-ask-human solicit through it. #f means no run is in flight.

struct

(struct crossing (span kind payload)
    #:transparent)
  span : pair?
  kind : symbol?
  payload : any/c
One thing passing through the threshold. span is a (cons ashlar-name span-id) provenance tag; kind is a symbol ('token, 'turn, 'steer, 'ask, 'gate, …); payload is plain data. A request crossing ('ask / 'gate) carries an 'id in its payload — the request node’s content-addressed id, which answer-request! answers by.

5.6.1 Outside: the far sideπŸ”—β„Ή

procedure

(threshold-subscribe th)  evt?

  th : threshold?
Returns a fresh subscription — a synchronizable source that yields every subsequent crossing. (sync sub) blocks until one arrives.

procedure

(threshold-steer! th text)  void?

  th : threshold?
  text : string?
Inject a user turn into a live run — the far side speaking into the conversation between the agent’s turns.

procedure

(threshold-request-cancel! th)  void?

  th : threshold?
Ask the in-flight turn to abort cooperatively (Ctrl-C in the TUI). The streaming loop polls the flag between chunks and drops the partial turn; completed turns stay journaled, so the run stays resumable. This is orthogonal to a request suspend — it cancels an in-flight turn, not a pending ask/gate.

procedure

(threshold-cancel-requested? th)  boolean?

  th : threshold?
Whether a cancel has been requested and not yet consumed.

procedure

(threshold-clear-cancel! th)  void?

  th : threshold?
Clear a pending cancel request (consumed).

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
Emit the request crossing (carrying req-id as the payload’s 'id, so the far side can render it and later answer by that id) and raise run-suspended. Never returns. The caller must have already appended and journaled the request node — this only emits and raises; it does not append. Called from a request site (make-ask-human, a tool-approval gate) with the request node already built.

procedure

(threshold-emit! th c)  void?

  th : threshold?
  c : crossing?
Push an observation crossing (non-blocking) to every subscriber.

procedure

(threshold-drain! th)  (listof message?)

  th : threshold?
Take and clear the queued steer texts (the engine’s steering pickup), as user-role messages.

procedure

(threshold-check-cancel! th)  void?

  th : threshold?
The streaming loop’s cancel poll: if a cancel was requested, consume it and raise run-cancelled so the in-flight turn is abandoned. A #f threshold (bare/ephemeral run) is a no-op.

struct

(struct run-cancelled ()
    #:transparent)
The cooperative-cancel signal raised inside the streaming loop when the far side requested a cancel. Deliberately not an exn:fail, so it slips past generic handlers to unwind the in-flight turn; the run boundary catches it and returns cleanly.

struct

(struct run-suspended (request-id request-kind payload)
    #:transparent)
  request-id : any/c
  request-kind : symbol?
  payload : any/c
The suspend signal, raised inside an ashlar when it poses a fresh request ('ask or 'approval) with no answer yet. Like run-cancelled, deliberately not an exn:fail, so generic handlers let it unwind to the pass boundary (run-topology-pass), which resolves the pass to the DAG cursored on the already-appended (and journaled) request node. No Racket thread is parked: the run returns suspended, the caller appends the answer node (answer-request!, stone/record) and re-invokes the run to resume. request-id is the request node’s content-addressed id; request-kind and payload mirror what was posed, for observers that catch the raise directly rather than reading the crossing.

Per-subscriber channel capacity. A healthy reader never reaches it; only a stalled reader starts dropping crossings — observation is best-effort and never stalls the engine. Durability is the journal’s job, not the observation lane’s.

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)
The durable-run binding. The dag field is the ashlar’s history and the plan field is masonry’s — both projected from the same facts. path is where the journal lives — the identity of the run on disk (#f for an in-memory record). facts is the journal as read once at open time — the facts a resume rebuilt both structures from, or '() for a fresh run.

procedure

(open-record path [#:threshold th])  record?

  path : (or/c path? string?)
  th : (or/c threshold? #f) = #f
Open (or reopen) the record at path — the single entry point for both a fresh run and a resume:

  • 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
An always-journaled record with no filesystem path: its journal is an in-memory buffer, so it can never resume across a process restart — always a fresh, empty DAG, same as open-record on a never-seen path. Useful for tests and short-lived runs that want journaling (so requests can suspend and be answered) without a file on disk.

procedure

(answer-request! r req-or-id raw)  void?

  r : record?
  req-or-id : (or/c node? any/c)
  raw : any/c
Answer a suspended request: append the answer node as a child of the request node and journal it, so a resumed run finds it already in the DAG (by topology) and carries it forward — never re-posing. req-or-id is either the request node itself (the suspended run’s dag-cursor) or its content-addressed id string (off the request crossing’s 'id); an id is looked up in the record’s journaled DAG.

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"
Abandon a suspended session: append a session-terminal halt node (e.g. 'user-quit) as a child of the pending request node, instead of an answer. A relaunched run-ashlar sees the terminal head and is a replay no-op — it never appends past the halt — and the runner’s loop stops on session-terminal-halt? instead of taking another turn. This is how quitting on a pending request works now that there is no cancelled value — the caller (or the TUI’s own key handler) decides to abandon and appends the halt itself.

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?)
Open (creating if absent) the journal file at path for append.

procedure

(journal-read path)  (listof any/c)

  path : (or/c path? string?)
Read every recorded fact from the journal at path, in order.

procedure

(journal-append! j fact)  void?

  j : journal?
  fact : any/c
Append one fact to the journal.

procedure

(close-journal j)  void?

  j : journal?
Close the journal’s file handle.

procedure

(journal? v)  boolean?

  v : any/c
Recognizes a journal.

parameter

(current-journal)  (or/c journal? #f)

(current-journal j)  void?
  j : (or/c journal? #f)
The journal in effect for the current run, bound by run-ashlar #:record. #f means an ephemeral run — no journaling, byte-identical to a bare run.

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
Run ashlar as a durable interactive session under the terminal UI. Opens the record at path with th, subscribes once, seeds scrollback from the journal, and opens idle. The TUI is the runner: it owns the loop. Each human line is appended as a 'prompt node and invokes (run-mason mason #:ashlar ashlar #:record r) once on a background thread. That call lays the whole topology in one pass, and the mason decides at each boundary between ashlars: whether the ashlar about to be laid there stands (keep) or a carved one is laid in its place (change). When a request (ask-human, tool-approval) suspends the pass, the background thread returns the suspended DAG and plan, and the TUI answers the request (answer-request!) and re-invokes run-mason to resume that same pass. When the pass completes, the TUI returns to idle to await the next line. This continues until a session-terminal-halt? (the user quits). #:mason defaults to (make-mason), whose chisel keeps, so a session with no mason supplied never carves. #:author? is forwarded to every run-mason invocation: leave it #f for compose-mode carving (the mason composes named palette ashlars) and pass #t for author mode (the mason may also introduce fresh closures / Racket source). When the caller built the agent with a threshold (so the agent emits to it), it MUST pass that same threshold here. See Use the TUI, which spells out which boundaries the mason decides at and which it leaves alone.

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?
Stone’s internal representation of an LLM response. text is the assistant’s text output; tool-calls is the list of tool invocations requested by the model; usage carries provider-specific token-usage stats and is opaque to Stone.

struct

(struct tool-call (id name input))

  id : string?
  name : string?
  input : hash?
A tool invocation requested by the LLM. id is an opaque identifier that round-trips through the tool result; name selects which tool to invoke; input is the hash of arguments.

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?
Raised by call-openai when a streaming session’s watcher trips. hit is the repetition-hit from the detector; partial is the channel text streamed before the trip (response-text if the response watcher tripped; thinking-text otherwise). make-openai-llm’s retry layer catches this internally; user code only sees this exception if the retry layer itself is bypassed.

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)
Raised by make-openai-llm when the retry budget is exhausted. attempts is the configured #:max-retries value — the retry budget that was exhausted, not the actual call count. last-hit is the most recent repetition-hit, or #f if none was captured. Caught by the agent layer and converted to a 'failure-typed node with content kind 'output-degenerate.

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?
Raised by the streaming LLM call-llm when the server returns a non-2xx HTTP response. Without this, an HTTP 400 (e.g. malformed conversation) would come back as an empty SSE stream and the agent loop would silently treat it as a successful empty response, burning turns until #:max-turns exhausts. status is the HTTP status code; body is the response body (typically a JSON error).

struct

(struct exn:fail:llm-empty-response exn:fail ()
    #:extra-constructor-name make-exn:fail:llm-empty-response)
Raised by the agent layer when the model produces no text AND no tool calls. The agent loop has nothing to act on; without this guard, the loop would silently advance through empty turns until #:max-turns exhausts. The agent’s outer with-handlers converts this to an 'agent-empty-response failure node.

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
Returns a model-bound call-llm with signature (messages system max-tokens tools response-format [threshold]) -> hash?. #:model fixes the model this call-llm sends on every request. POSTs an OpenAI-compatible chat-completion request and normalizes the response to Anthropic content-block shape. Always streams via SSE. When threshold is provided (not #f), per-token 'token crossings are also emitted through it (see stone/threshold).

#: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
Returns a model-bound call-llm with the same signature as make-openai-llm’s, with the model fixed at construction via #:model. POSTs an Anthropic messages request with anthropic-version: 2023-06-01. The threshold argument is accepted but ignored — Anthropic doesn’t stream yet, so it emits no token crossings. Reserved keys '(model system messages max_tokens tools response_format) raise at call time if overridden through #:extra-body.

#: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)
Low-level POST against an Anthropic-compatible messages endpoint. Returns the parsed JSON response. make-anthropic-llm wraps this.

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
Low-level POST against an OpenAI-compatible chat-completions endpoint. Converts Anthropic tool schemas to OpenAI function schemas, POSTs the request, and normalizes the response to Anthropic content-block shape. Always streams via SSE, parsing <think>...</think> markers into 'thinking token crossings when a #:threshold is supplied. make-openai-llm wraps this.

#: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

(call-llm-model c)  (or/c string? #f)

  c : procedure?
Returns the model a call-llm is bound to, or #f for a plain-lambda call-llm (such as a mock or scripted caller that carries no bound model). Used for logging which model a call used; it never drives a call, since the model is already inside the call-llm.

procedure

(extract-text response)  string?

  response : hash?
Returns the text of the first content block whose type is "text" in a normalized response, or "" if there is none.

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?
Reports what tripped the watcher. kind is 'ngram (verbatim n-gram repeat) or 'compression (sliding-window deflate ratio dropped below threshold for a streak of ticks). detail is the offending bytes? window for 'ngram hits, or a (listof real?) ratio history (leaf = newest) for 'compression hits. position is the total bytes seen at trip time.

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
Constructs a counter with the given window size and trip threshold. Defaults are tuned to ignore structural repetition in enumerated JSON output (e.g. descriptions in a list of behaviors) while still catching verbatim loops; the compression detector covers near-repetition that escapes this stricter bar.

procedure

(ngram-counter-add! c byte)  void?

  c : ngram-counter?
  byte : byte?
Feeds one byte to the counter.

procedure

(ngram-counter-tripped c)  (or/c #f repetition-hit?)

  c : ngram-counter?
Returns the repetition-hit if tripped, otherwise #f.

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
Constructs a detector with the given window size, ratio threshold, and history length.

procedure

(compression-detector-add! d byte)  void?

  d : compression-detector?
  byte : byte?
Feeds one byte to the detector. Cheap; never runs deflate.

procedure

(compression-detector-check d)  void?

  d : compression-detector?
Runs deflate on the current window, computes the ratio, and updates history. Call this on each streaming-progress tick (every current-streaming-emit-interval ms by default — i.e., once every 5 seconds in production).

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?
Returns the repetition-hit if tripped, otherwise #f.

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))
Constructs a watcher. Pass #f for either detector to disable that channel. The on-trip callback is invoked at most once, on the first trip from either detector.

procedure

(repetition-watcher-add-bytes! w bs)  void?

  w : repetition-watcher?
  bs : bytes?
Feeds bytes to whichever detectors are non-#f. After feeding, runs the trip check; if newly tripped, fires on-trip.

procedure

(repetition-watcher-tick! w)  void?

  w : repetition-watcher?
Runs compression-detector-check (no-op if compression is disabled). Called by maybe-emit-progress! inside the streaming session at the every-5s tempo.

procedure

(repetition-watcher-tripped w)  (or/c #f repetition-hit?)

  w : repetition-watcher?
Returns the captured repetition-hit if either detector has tripped, otherwise #f.

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
Returns a middleware that injects schema into the context’s tools list and dispatches matching tool_use blocks to handler. handler must return (values string? hash?) — the display text shown to the next turn and a structured meta hash. #:allowed-paths rejects calls whose path argument isn’t under one of these prefixes. #:confirm? #t causes the tool to refuse to run and recommend a 'loop with category 'approval-needed.

procedure

(has-tool-call-for? ctx tool-name)  boolean?

  ctx : any/c
  tool-name : symbol?
#t when the last response’s content contains a tool_use block whose name equals (symbol->string tool-name).

procedure

(extract-tool-calls response)  (listof hash?)

  response : hash?
Every content block in response whose type is "tool_use".

procedure

(tool-schema mw)  (or/c hash? #f)

  mw : any/c
Returns the JSON-schema hash that was passed to make-tool when mw was built, or #f if mw was not registered. Backed by a process-wide weak table keyed on middleware identity.

5.11.2 Built-in tool middlewareπŸ”—β„Ή

procedure

(read-file [#:allowed-paths allowed-paths])  any/c

  allowed-paths : (or/c (listof string?) #f) = #f
Creates a read_file tool middleware. Returns file contents; supports optional offset and limit tool arguments for a line-range slice.

procedure

(write-file [#:allowed-paths allowed-paths    
  #:confirm? confirm?])  any/c
  allowed-paths : (or/c (listof string?) #f) = #f
  confirm? : boolean? = #f
Creates a write_file tool middleware. Creates parent directories as needed and overwrites the target file.

procedure

(edit-file [#:allowed-paths allowed-paths    
  #:confirm? confirm?])  any/c
  allowed-paths : (or/c (listof string?) #f) = #f
  confirm? : boolean? = #f
Creates an edit_file tool middleware. Refuses the call if old_string isn’t found or isn’t unique in the target file.

procedure

(delete-file* [#:allowed-paths allowed-paths    
  #:confirm? confirm?])  any/c
  allowed-paths : (or/c (listof string?) #f) = #f
  confirm? : boolean? = #t
Creates a delete_file tool middleware. Note that #:confirm? defaults to #t.

procedure

(list-directory [#:allowed-paths allowed-paths])  any/c

  allowed-paths : (or/c (listof string?) #f) = #f
Creates a list_directory tool middleware.

procedure

(mkdir* [#:allowed-paths allowed-paths])  any/c

  allowed-paths : (or/c (listof string?) #f) = #f
Creates an mkdir tool middleware that creates a directory and any missing parents.

procedure

(file-exists* [#:allowed-paths allowed-paths])  any/c

  allowed-paths : (or/c (listof string?) #f) = #f
Creates a file_exists tool middleware returning "true" or "false".

procedure

(run-command [#:timeout default-timeout    
  #:confirm? confirm?])  any/c
  default-timeout : real? = 60000
  confirm? : boolean? = #f
Creates a run_command tool middleware. Runs a shell command synchronously under /bin/sh -c, waits up to timeout milliseconds, and returns the concatenated exit code, stdout, and stderr. Timed-out commands are killed.

procedure

(start-command [#:timeout default-timeout])  any/c

  default-timeout : real? = 300000
Creates a start_command tool middleware. Spawns the command in the background and returns a handle id.

procedure

(check-command)  any/c

Creates a check_command tool middleware. Given a handle id, returns "running" or the final exit/stdout/stderr.

procedure

(wait-commands)  any/c

Creates a wait_commands tool middleware. Given a list of handle ids, waits (up to each handle’s stored timeout) for them all to finish and returns a combined report.

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)
Default #:decide value on make-agent-ashlar. Halts if any middleware emitted a 'halt recommendation; loops while the recommendation list is non-empty; continues otherwise. Reach for this when the agent’s job is "think, call tools as needed, then reply" and any outstanding tool activity is the signal to keep going.

procedure

(tool-directed ctx recs)  any/c

  ctx : any/c
  recs : (listof any/c)
Stricter variant: loops only when a middleware explicitly recommends 'loop. An empty recommendation list means the agent is done — it’s the tool middlewares’ responsibility to ask for another turn. Use this when you want the tool layer to drive the conversation deliberately, e.g. a workflow where "no tool fired" means "time to finalize the answer."

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?
Walks the ashlar tree, dispatching on each ashlar’s validate-walk rule. Collects a 'missing-producer error when an ashlar queries a node type no upstream sibling produces, a 'maybe-unavailable warning when a type is produced by some match branches but not all, an 'invalid-lens error when a ashlar-match has a lens whose path isn’t in the upstream schema, and a 'fanout-not-reduced error when a fan-out isn’t immediately followed by a ashlar-reduce.

struct

(struct validation-result (errors)
    #:transparent)
  errors : (listof validation-error?)
Container for the errors and warnings collected by validate-ashlar. Prefer validation-ok? and validation-errors for inspection.

struct

(struct validation-error (type ashlar-name queried-type message)
    #:transparent)
  type : symbol?
  ashlar-name : (or/c symbol? #f)
  queried-type : symbol?
  message : string?
type is one of 'missing-producer, 'maybe-unavailable, 'invalid-lens, or 'fanout-not-reduced. ashlar-name identifies the offending ashlar. queried-type is the node type or lens path segment at fault. message is a human-readable description.

procedure

(validation-ok? r)  boolean?

  r : validation-result?
#t when r has no hard errors. 'maybe-unavailable warnings don’t make the result not-ok.

Returns the error list inside r.

procedure

(enumerate-ashlars ashlar)  (listof symbol?)

  ashlar : ashlar-meta?
Every named ashlar in the tree, depth-first pre-order. Unnamed ashlars are skipped. See Validate a topology for the orphan-ashlar guard pattern built on this.

procedure

(enumerate-paths ashlar)  (listof (listof symbol?))

  ashlar : ashlar-meta?
Every distinct execution path through the ashlar as a list of ashlar names. A match with n branches contributes n paths; a sequence concatenates its children’s paths; loops and reduce pass through their child’s paths.

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
Starts a recorder on its own threshold, parameterizes current-test-call-llm, and evaluates body. When #:strict-tools? #t, every recorded tool call must have been installed via ashlar-with-tool-stub; otherwise the form raises on exit. #:timeout runs body on a background thread; exceeding seconds breaks the thread and raises.

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?
Same setup as with-live-call-llm but synchronous with no timeout. Exactly one of #:call-llm or #:responses must be supplied. #:responses installs an internal call-llm that pops one response per call from the list; running the queue to empty raises.

procedure

(ashlar-with-tool-stub s    
  tool-name    
  stub-handler)  ashlar-meta?
  s : ashlar-meta?
  tool-name : symbol?
  stub-handler : (hash? . -> . any)
Returns a new ashlar identical to s except the tool middleware named tool-name is replaced by a fresh middleware built with the stored schema and stub-handler. Recurses through composition children. Raises when tool-name isn’t found, when multiple middlewares share the name, or when an ashlar has no rebuilder.

5.14.2 AssertionsπŸ”—β„Ή

procedure

(tool-calls)  (listof hash?)

Every recorded tool-call record in call order. Drains pending log events before reading. Returns '() when called outside both forms.

procedure

(tool-calls-by-name name)  (listof hash?)

  name : symbol?
Filters (tool-calls) down to records whose 'name key is eq? to name.

procedure

(tool-call-count name)  exact-nonnegative-integer?

  name : symbol?

syntax

(check-tool-called? name)

(check-tool-called? name msg)
Rackunit-style assertion. Passes when (tool-call-count name) > 0.

Rackunit-style assertion. Passes when (tool-call-count name) is zero.

syntax

(check-tool-call-count name n)

(check-tool-call-count name n msg)
Rackunit-style assertion. Passes when (tool-call-count name) equals n.

5.14.3 Stub helpersπŸ”—β„Ή

procedure

(stub-answer s)  (hash? . -> . any)

  s : string?
Returns a constant tool handler that ignores its input and returns (values s (hasheq 'stub #t)).

procedure

(stub-fn f)  (hash? . -> . any)

  f : (hash? . -> . any)
Identity passthrough; returns f unchanged. Exists so tests can opt into the explicit (stub-fn ...) form alongside (stub-answer ...) for documentation value.

5.14.4 Response buildersπŸ”—β„Ή

procedure

(llm-text s)  any/c

  s : string?
An llm-response with content s and no tool calls.

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
An llm-response carrying exactly one tool-call block and an empty text body. #:question is sugar: when non-false and #:input is false, builds (hasheq 'question question) as the input.

procedure

(llm-multi text-or-response resp ...)  any/c

  text-or-response : (or/c string? any/c)
  resp : any/c
Variadic combiner. If the first argument is a string, it becomes the text of the new response and tool calls are collected from the remaining arguments. If it’s an llm-response, its text is dropped and its tool calls plus every subsequent response’s tool calls are concatenated.

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
Bound by with-live-call-llm and with-mock-call-llm to the call-llm the form holds. Read inside body to pass the call-llm to make-agent-ashlar.

5.15 stone/loggingπŸ”—β„Ή

 (require stone/logging) package: Stone

Structured logging on a dedicated Racket logger. See Observability for the design.

Racket logger named ashlar, created with #:parent #f so events do not propagate to the root logger. Attach a log receiver to this logger to collect events.

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 ...)

Level-specific macros generated by define-logger for stone-logger. See Racket’s define-logger for argument shapes. Prefer stone-event for structured events.

procedure

(stone-event level event data)  void?

  level : (or/c 'debug 'info 'warning 'error 'fatal)
  event : symbol?
  data : hash?
Adds event, trace-id, span-id, parent-span-id, and timestamp to data, then logs via stone-logger only when level is enabled.

parameter

(current-trace-id)  (or/c string? #f)

(current-trace-id id)  void?
  id : (or/c string? #f)
 = #f
Identifies a full ashlar run; set once at the top and inherited by every ashlar below.

parameter

(current-span-id)  (or/c string? #f)

(current-span-id id)  void?
  id : (or/c string? #f)
 = #f
Identifies the currently executing ashlar. make-ashlar’s wrapper parameterizes this to a fresh span-* id for each invocation.

parameter

(current-parent-span-id)  (or/c string? #f)

(current-parent-span-id id)  void?
  id : (or/c string? #f)
 = #f
Holds the enclosing ashlar’s span id. make-ashlar’s wrapper sets this from the caller’s current-span-id before allocating a new span.

procedure

(generate-id [prefix])  string?

  prefix : string? = ""
Returns "<prefix><ms>-<random>" where <ms> is the truncated millisecond clock and <random> is a 5-digit number. Not cryptographically strong; intended for log correlation.

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?
Read a JSONL trace file into a list of event hashes, in file order. Each event is a hash with keys 'level, 'message, 'topic, and 'data; the event-specific payload lives under 'data. Use the accessors below to read fields without digging.

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?
Return the event-specific payload hash. Most events carry their fields here; the other accessors all read from this.

procedure

(event-type e)  string?

  e : hash?
The event-type string (e.g. "api-call", "tool-dispatch"). Empty string if absent.

procedure

(event-timestamp e)  real?

  e : hash?
Millisecond-epoch timestamp. 0 if absent.

procedure

(event-ashlar-name e)  string?

  e : hash?
Best-effort ashlar/agent/tool name. Falls back through 'ashlar-name, 'agent-ashlar-name, and 'tool-name because different event types use different keys. Empty string if none of those are present.

procedure

(event-turn-number e)  (or/c exact-nonnegative-integer? #f)

  e : hash?
LLM turn number for api-call/api-response events. #f for events that don’t carry one.

5.16.3 AggregatorsπŸ”—β„Ή

procedure

(tally-events events)

  (listof (cons/c string? exact-nonnegative-integer?))
  events : (listof hash?)
Return an alist of (cons event-type count) sorted by count descending. The shape used by raco stone trace tally and useful for a first-pass overview of what happened.

procedure

(lifecycle-events events)  (listof hash?)

  events : (listof hash?)
Filter events down to the ones that tell the story of how the orchestrator progressed: ashlar lifecycle, tool dispatch, api-call/response, streaming progress, and known failure events. Excludes noise like middleware-run. The set is defined inside stone/trace and not configurable; if you need different filtering, use filter with your own predicate.

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
Return "api-call-payload" events in file order, optionally filtered by ashlar name and/or turn number. Useful when you want to inspect the exact messages an ashlar sent to its call-llm.

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?
Render one lifecycle event as a single human-readable line with a HH:MM:SS local-time prefix, the event type, the ashlar name, and event-type-specific extras (e.g., turn=5 msgs=19 for "api-call"). Used by raco stone trace lifecycle.

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

Dispatch the trace subcommand based on current-command-line-arguments. Reads the next argv token as the sub-subcommand name ("tally", "lifecycle", "payload") and forwards remaining args to the matching runner.

procedure

(run-tally path)  any

  path : path-string?
Print the total event count and the type tally to current output. Equivalent to raco stone trace tally <path>.

procedure

(run-lifecycle path)  any

  path : path-string?
Walk lifecycle events in chronological order, one format-lifecycle-line per output line. Equivalent to raco stone trace lifecycle <path>.

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
Print a single "api-call-payload" event: ashlar, turn, model, system prompt (first 400 chars), and one line per message with role + content preview + tool-call count.

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.

procedure

(lens path ...)  any/c

  path : symbol?
Returns a lens that, applied to a node, walks the node’s content hash through the given path. Used as the extractor in (ashlar-match (lens 'field) ...) to branch on a field value.

procedure

(lens? v)  boolean?

  v : any/c
#t iff v was built by lens.

procedure

(lens-path l)  (listof symbol?)

  l : any/c
Returns the path list stored in l, or #f if l isn’t a lens.

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
Ambient, model-bound call-llm, inherited by make-agent-ashlar when no #:call-llm is supplied. Set it to a caller built by make-openai-llm or make-anthropic-llm (each of which fixes its model at construction via #:model). When neither this parameter nor #:call-llm is set, make-agent-ashlar errors loudly.

5.18.2 Schema builderπŸ”—β„Ή

procedure

(make-json-schema name properties required)  hash?

  name : string?
  properties : hash?
  required : (listof (or/c symbol? string?))
Returns a hash shaped as
(hasheq 'type "json_schema"
        'json_schema
        (hasheq 'name name
                'strict #t
                'schema (hasheq 'type "object"
                                'properties properties
                                'required required
                                'additionalProperties #f)))
suitable for the #:response-format argument of make-agent-ashlar or a call-llm directly. Symbols in required are coerced to strings.

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)
Per-turn state threaded through the middleware onion. This is the runtime message-assembly context — distinct from the context data structure of stone/context-struct that make-agent-ashlar takes as #:context. dag is the effective DAG the turn runs against; threshold is the run’s threshold (see stone/threshold). conversation is the agent’s conversation as a conv-dag — a chain of 'turn nodes, or #f outside an agent onion; the request messages are projected from it at call time, so there is no separate flat message buffer. meta holds 'trace-id, 'span-id, 'agent-name, 'turn-number, 'last-response, 'recommendations.

struct

(struct middleware (name guard handler)
    #:transparent)
  name : symbol?
  guard : (context? . -> . boolean?)
  handler : (context? (context? . -> . context?) . -> . context?)
Middleware record.

struct

(struct recommendation (type source reason)
    #:transparent)
  type : (or/c 'continue 'halt 'loop)
  source : any/c
  reason : any/c
Recommendation emitted by middleware or a decide function.

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
Default constructor.

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)
Returns a new context with the specified fields changed. Most fields use (or new old), so passing #f means "keep," not "clear." #:tools, #:threshold, and #:conversation use private sentinels instead (the (void) default above), so an explicitly passed value — #f included — is set rather than treated as "keep": an empty tool list, "no threshold," and "no conversation" are all meaningful values that must survive a context-set that does not mention them.

procedure

(make-middleware name guard handler)  middleware?

  name : symbol?
  guard : (context? . -> . boolean?)
  handler : (context? (context? . -> . context?) . -> . context?)
Constructor.

procedure

(run-onion middleware-list ctx call)  context?

  middleware-list : (listof middleware?)
  ctx : context?
  call : (context? . -> . context?)
Executes the middleware chain around call. For each middleware whose guard returns #t, the handler is invoked with (ctx inner-call); otherwise the handler is bypassed and the inner call runs directly. Emits 'middleware-run debug events.

procedure

(recommend ctx type source reason)  context?

  ctx : context?
  type : (or/c 'continue 'halt 'loop)
  source : any/c
  reason : any/c
Appends a recommendation to the 'recommendations list under (context-meta ctx).

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πŸ”—β„Ή

On success:

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.