luv

Workshop wiki

agent-in-the-world.org

An agent in the little world

An agent should be able to stand inside luvcraft the way a player does: see what is there, act on it with the same verbs, and leave a record others can read. The harness notes (#YY6EYZ) settled what any such surface must do. This page says how luv does it: with the CLIM vocabulary the game already speaks -- command tables, presentation types, views, output records -- and with the world itself as the place the agent's work shows up. The OpenAI websocket client (openai/agent.lisp) is the first provider behind it; nothing here depends on which provider it is.

The agent surface is a command table, a view, and a handle space #4Y8Q8B

The four harnesses converged on seams (#YY6EYZ), and CLIM already has a name for each of them. That is the whole design bet of this page:

harness seamCLIM concept
tool definition and JSON schemadefine-command with presentation-typed arguments
argument validation, "did you mean"accept-from-string, complete-input, presentation errors
typed result with several renderingsa presentation, presented under different views
short stable handlespresentation history, a handle presentation type
"what can I do with this?"find-applicable-translators on a presentation
history as data, re-rendered laterthe output-record history of the agent's stream

Nothing in the table is a new mechanism. The work is to choose the presentation types, write the views, and route a provider's tool calls through accept and execute-frame-command instead of around them. #0IF4TY through #1W8K03 take the rows in turn; #9K823O and #14ZP6S take the rendering from nxt; #UTQQV9 and #5JDNKF put the agent in the world.

defpackage #:luvcraft.agent package.lisp:11

An agent in the little world.

This package is where a language model meets luvcraft, and it meets it through CLIM: a tool is a command whose arguments are presentation types, a tool result is an object presented under a view the model reads, and the running transcript is a list of typed records that the HUD, a wall, and a plain ./sly session all draw from. The design is the wiki page "An agent in the little world" (#4Y8Q8B); the provider behind it is the OpenAI websocket client, but nothing here is about OpenAI.

defpackage#:luvcraft.agent
:use#:clim-lisp#:clim
:local-nicknames
#:luv#:luv
#:climi#:clim-internals
#:mcluv#:mcluv
#:luvcraft#:luvcraft
#:luvcraft.clim#:luvcraft.clim
#:world#:luvcraft.world
#:openai#:openai

Preserve Luvcraft's public vocabulary while making these names the exact shared objects. Remaining game files specialize and consume the common protocols without wrappers or a duplicate compatibility layer.

:shadowing-import-from#:luv.application-agent#:model-view#:trace-view#:cassette-view#:+model-view+#:+trace-view+#:+cassette-view+#:model-text#:handle#:handle-table#:make-handle-table#:intern-handle#:handle-object#:*handles*#:presentation-type-json-schema#:command-tool#:make-command-tool#:command-tool-command#:command-tool-runs-on-canvas-p#:command-provider-output#:command-result-presentation-type#:command-output-line-limit#:settle-command-result#:tool-call#:tool-call-name#:tool-call-command#:tool-call-tool#:tool-call-arguments#:tool-call-status#:tool-call-result#:tool-call-error#:tool-call-output#:tool-call-elapsed-seconds#:tool-call-arguments-text#:turn#:turn-prompt#:turn-thought#:turn-text#:turn-calls#:turn-status#:turn-error#:turn-response#:turn-started#:turn-finished#:turn-thread#:turn-calls-in-order#:transcript-lines#:print-transcript#:add-agent-observer#:remove-agent-observer#:notify-agent-observers#:note-tool-call#:note-tool-call-finished#:release-application-agent#:*current-agent*
:export

Views and handles

#:model-view#:trace-view#:cassette-view#:+model-view+#:+trace-view+#:+cassette-view+#:model-text#:handle#:handle-table#:make-handle-table#:intern-handle#:handle-object#:*handles*#:presentation-type-json-schema

Tools

#:command-tool#:make-command-tool#:command-tool-command#:command-tool-runs-on-canvas-p#:command-provider-output#:tool-approval#:tool-approval-state#:tool-approval-note#:approve-tool-approval#:deny-tool-approval#:steer-tool-approval

The transcript

#:tool-call#:tool-call-command#:tool-call-tool#:tool-call-arguments#:tool-call-status#:tool-call-result#:tool-call-error#:tool-call-output#:tool-call-elapsed-seconds#:turn#:turn-prompt#:turn-thought#:turn-text#:turn-calls#:turn-status#:world-agent#:make-world-agent#:world-agent-session#:world-agent-turns#:world-agent-current-turn#:world-agent-handles#:ask#:ask-and-wait#:transcript-lines#:print-transcript#:release-application-agent#:*agent*

Commands

#:luvcraft-agent#:com-where-am-i#:com-view-surroundings#:com-move-to#:com-place-block-at#:com-eval#:com-propose-block-box#:com-describe-handle#:com-block-at

The HUD

#:open-agent-hud#:close-agent-hud#:toggle-agent-hud

The wall

#:agent-terminal-display#:open-agent-wall#:com-open-agent-wall#:ansi-view#:+ansi-view+

Embodied agents

#:embodied-agent#:gnome#:cat#:embodied-agent-pending-approval#:*agents*#:agents-in-session#:find-agent#:spawn-agent#:find-cat#:spawn-cat#:gnome-say#:gnome-agent#:com-say#:com-spawn-agent#:com-spawn-cat#:*current-agent*#:world-agent-presence

A tool is a CLIM command whose arguments are presentation types #0IF4TY

command-menu.lisp already answers "what can I do?" for the player by walking the command tables, and it skips parameterized commands because M-x cannot yet prompt for their arguments. The agent's tool list is the same walk with the same gap. One

define-command
com-place-block:command-tableluvcraft-world
where'block-position
...

should yield a keystroke, an M-x entry that prompts through accept, and a JSON-schema tool whose arguments are parsed with accept-from-string per presentation type. Finishing parameterized M-x and building the agent surface are one piece of work.

Concretely: a command-tool is an openai:tool wrapping a command name; a generic presentation-type-json-schema with EQL methods on type names (integer, string, (member ...) as an enum, block-position as an object of three integers, handle as a pattern) derives the schema once from the command's parser table; and call-tool accepts each argument by its declared type and hands the finished command to the canvas thread (#T6PEVY). An argument the model gets wrong is a presentation-parse error rendered back to it, not a crash, which is nxt's "failures as values" (#KZ019W) without writing a second validator.

A tool result is a presentation, and the model reads it under a view #WS5OEX

Froth's lesson is that what the tool found, how much the model sees, and how it is spelled for a provider are three policies (#5GU1VI). In CLIM a tool body does not return a string; it =present=s objects to a recording stream. The three policies become three views over the same records:

Because rendering is present to a string stream, escaping and layout are in one place; there is no string concatenation anywhere between a tool and the model. And because the records are kept, the whole transcript can be redrawn under any view later without rerunning a thing, which is the "history is data first" point of #YY6EYZ with the output-record history as the data.

Handles are presentation history #1W8K03

Sheaf's six characters (#9P5YLW) and this wiki's figure IDs are the same idea: a short handle the model can cite by typing. In the agent stream, every presented object of interest -- a chunk, a block position, a session, a folded body -- is entered in a presentation history under a short id, and handle is a presentation type whose accept method resolves the id back to the live object. The model says #AB12 in a tool argument; accept gives the tool the object; an unknown or stale handle is a parse error the model can read. The pager (#FDMSXG) is then nothing special: accept a handle, present a slice of it under the model view.

The cassette is one presentation file #9K823O

Before nxt deleted its UI runtime (nxtui commit 3d692d0), a tool call was drawn as a cassette (#7FFNVT): a trace as XML (turn > thought | call[name, status, elapsed] > args, result[kind], affordances[key]), and one XSLT stylesheet, cassette/baltic-birch-v1.xsl, holding the whole visual language in about three hundred lines organized as

<xsl:template match"result[@kind='matches']">= is a declarative visitor case, and define-presentation-method present (object (type matches) stream (view cassette-view)) is the same case with a live object behind it. So the luv cassette is one file: a cassette-view, presentation methods keyed on result kind for the window and the chip, a spine keyed on call status, the per-tool colour as a generic on the command name, and the affordances strip as find-applicable-translators printed as a menu. That last point is the nicest: the affordances are not a list a tool author writes, they are whatever translators apply to the result's type.

defparameter *agent-hud-width* hud.lisp:14

The cassette HUD: the current turn drawn as cards in the corner (#9K823O, #14ZP6S).

A McCLIM frame composited over the view, the way the hotbar and the legend are. Every frame the overlay reads the turn and repaints when anything it shows has changed -- a call's status, its elapsed tenths of a second, a new line of thought -- so a running call's clock ticks and a finished one flips in place. The drawing itself is presentation methods under the cassette view, one per kind of record, so a wall can draw the same cards later from the same objects.

defparameter*agent-hud-width*640
defclass agent-terminal-display wall.lisp:20

The agent's wall: the transcript on a terminal wall, drawn the way the phone and the shell walls are drawn -- Ghostty cells as Slug glyph geometry, sharp at any distance (#GJUPMQ) -- rather than through a McCLIM mirror.

The wall is a terminal-display with no PTY. What it shows is VT bytes the agent writes into its Ghostty terminal: the prompt the player types at it, the thought as it streams, and each tool call as an ANSI cassette (#9K823O) once it has finished. The terminal does the scrolling and the wrapping; we do the colours.

Two threads meet here and one thing crosses between them, the way the Telegram wall already does it: the turn's thread formats text and posts it to an outbox; the canvas thread drains the outbox into the terminal at the frame boundary, where it also reads the terminal to draw it. Nothing writes the terminal from anywhere else.

defclassagent-terminal-display
agent:initarg:agent:accessoragent-wall-agent
outbox:initform
sb-concurrency:make-mailbox:name"agent wall outbox"
:readeragent-wall-outbox
draft:initform"":accessoragent-wall-draft:documentation"What the player has typed at the wall so far."
line-open-p:initformnil:accessoragent-wall-line-open-p:documentation"Whether streamed text has left the cursor mid-line."
printed:initform
make-hash-table:test#'eq
:readeragent-wall-printed:documentation"Per turn, (THOUGHT-LENGTH . TEXT-LENGTH) already written."
observer:initformnil:accessoragent-wall-observer
:documentation

A terminal wall showing one agent's transcript.

Draw and print: live cards and committed records #14ZP6S

The same nxtui tree (src/nxtrt/ui_runtime.hpp, src/nxtai/tool_ui.cpp) drew a batch of parallel tool calls with two verbs: draw(layout) replaces a live surface shown as a bottom HUD, and print(layout) commits a rendered block to durable scrollback above it. Each tool call was a child with its own slot; the batch drew a column of the children's slots; each child redrew its card every frame with elapsed milliseconds while running, flipped it to ok or error with a few sanitized lines of output and a memory chip when done, held it for 900 ms so a person could see the flip, and returned. When the batch finished the live column was cleared and the assistant's text was printed as a block.

Luv has both surfaces already. The HUD overlay (the legend and the tape prompt are McCLIM frames composited over the view) is the live surface: running calls are call-view objects, each presented every frame under #9K823O with its status. The terminal wall and the agent's output-record history are the committed surface. Keep the settle delay: a card that flips and vanishes in the same frame cannot be read. And keep the no-terminal fallback -- when nothing is drawing, one line per call on a plain stream -- so the agent is usable from ./sly alone.

defparameter *agent-hud-width* hud.lisp:14

The cassette HUD: the current turn drawn as cards in the corner (#9K823O, #14ZP6S).

A McCLIM frame composited over the view, the way the hotbar and the legend are. Every frame the overlay reads the turn and repaints when anything it shows has changed -- a call's status, its elapsed tenths of a second, a new line of thought -- so a running call's clock ticks and a finished one flips in place. The drawing itself is presentation methods under the cassette view, one per kind of record, so a wall can draw the same cards later from the same objects.

defparameter*agent-hud-width*640

A shell scope is a session object with a screen and an observation #UTQQV9

nxtui's demo/shell_scope_demo.cpp ran a shell as systemd-run --user --scope so it had a cgroup by construction, then forked four siblings into one scope: a PTY reader, a stdin pump, a 33 ms cgroup sampler keeping a sparkline series, and a 16 ms renderer drawing memory and CPU sparklines above the live PTY screen. The observation (unit, cgroup path, samples) rode along inside the tool result, which is how a finished cassette could show a memory chip (#KZ019W).

In luv the scope is a CLOS object rather than a coroutine zone: a shell-session owning a PTY (the world terminal already has one serialized PTY owner, #NMAD2U), a sampler thread, and its observation, all presentable. A shell a tool starts should become a terminal-material wall the agent stands beside and the player can walk up to. The concurrency story is the one mcclim/telegram.lisp already states for the Telegram console: a mailbox one way, a published immutable snapshot the other, and no lock anywhere. Swash's default applies too (#I0U0LI): a shell tool returns within a few seconds with either the exit and a screenful, or a handle to the still-running session and the command to follow it.

The agent has a body #5JDNKF

An agent that is only a websocket is a chat window with extra steps. The point of doing this inside luvcraft is that the agent can be somewhere: a presence object with a position and a facing, so look, walk and place are presentation-typed commands relative to it, and its transcript is the terminal wall it is standing at. The playful surface follows from that without new machinery:

defparameter *gnome-name* gnome.lisp:18

The gnome: an agent with a body (#5JDNKF), not a block (#V9DHV5).

A gnome occupies a discrete world cell but is not a block. The cell is semantic state -- where this agent stands -- while its animated, camera-facing figure is an independently rendered scene object. Focusing it is how you talk to it: the view leans in the way it does for a wall, the keys become a prompt line at the bottom of the screen, and RET asks. The gnome then thinks, and its thinking shows above its head as a bubble: a matte rounded cassette standing in the world, drawn through McCLIM the way the Telegram panel is (a world widget, in the scene, with relief). Each tool call is the next cassette, and pushes the ones before it upward. When the gnome talks -- the SAY command, which is a tool like any other -- it is not a cassette but a title at the bottom of the view, the way an NPC line is subtitled.

defparameter*gnome-name*

gnome

The turn runs beside the canvas, and commands cross by mailbox #T6PEVY

Commands run on the canvas thread; a provider turn runs on its own. call-tool must therefore never call execute-frame-command from the websocket thread. It accepts its arguments and posts the finished command through request-canvas-frame -- froth's prepare/commit split (#IOGTGD), with accept as the prepare. Most commands return their values in that one frame. A command may instead return a running semantic object and let settle-command-result wait for it on the provider's turn thread. Move To does exactly that: the canvas returns a body-move-action immediately, keeps advancing it at frame boundaries, and publishes its terminal state to a private mailbox. The tool call is deliberately blocking in this first version while the canvas never is. A later duration bound can return a handle to the same action without changing either the command or movement protocol.

defclass tool-approval approval.lisp:7

A parked tool call is a semantic object, not a closure waiting to happen. The canvas owns every transition; the provider thread only waits on the private mailbox through settle-command-result (#T6PEVY).

defclasstool-approval
agent:initarg:agent:readertool-approval-agent
presence:initarg:presence:readertool-approval-presence
session:initarg:session:readertool-approval-session
state:initform:proposed:accessortool-approval-state
note:initformnil:accessortool-approval-note
created-at:initform
get-internal-real-time
:readertool-approval-created-at
mailbox:initform
sb-concurrency:make-mailbox:name"tool approval"
:readertool-approval-mailbox
lock:initform
sb-thread:make-mutex:name"tool approval"
:readertool-approval-lock
:documentation

One inert proposed tool effect waiting for a player's decision.

A proposal is a temporary possible world #Y7X7WK

A tool approval should show its consequence, not merely ask whether a verb may run. Construction makes that unusually literal. tool-approval retains an inert semantic payload, its :proposed state, and a private decision mailbox; the provider's turn may wait in settle-command-result while the canvas keeps rendering. validate-tool-approval, commit-tool-approval and detach-tool-approval are the open CLOS protocol. Approval validates the frozen expectations again and commits, denial drops the temporary presence, and typed feedback finishes as :changes-requested so the model can propose a different world. There is no retained arbitrary thunk to become dangerous or opaque.

The construction specialization carries a block-change-set: dense integer coordinate and value columns at the semantic boundary, plus a small lookup for counterfactual neighbour queries. Its scene overlay uses the ordinary block mesh vertex product, atlas, material shader, depth and lighting vocabulary. Only the emission lane is raised, enough for the post stack to give the candidate a soft possible-world glow. In focused conversation the same approval supplies a slowly orbiting camera pose, and the dialogue line offers Ctrl-Y approve, Ctrl-N deny, or prose plus Enter to steer. What the player judges is therefore recognizably the finished result, not a wireframe bill of materials.

The first Propose Block Box primitive is intentionally additive and capped at 512 resident cells. It refuses to replace an occupied cell, freezes air as the expected value, and rechecks every site immediately before one with-world-change-transaction commit. Additions can be drawn exactly over the current scene. Removal and replacement previews need the next rendering step: candidate chunk products substituted for the live products in the affected window, so the possible world can hide geometry as truthfully as it adds it. That extension changes the preview realization, not the approval protocol or change-set object.

defun call-with-construction-proposal-capture construction.lisp:6

The construction proposal as a reproducible little scene. This is the first capture recipe attached to the possible-world design. #Y7X7WK

defuncall-with-construction-proposal-capture

Call function with a hidden session, gnome, and inert wall proposal.

let
sessionnil
approvalnil
unwind-protect
let*
camera
make-instance'luvcraft:fly-camera:position
luvcraft::make-vec35.5d05.0d01.5d0
:yaw0.15d0:pitch-0.25d0
setfsession
luvcraft:start-luvcraft:title"construction proposal capture":width960:height640:visible-pnil:frames-per-secondnil:worldworld:cameracamera:sky-clock

The lobby is useful in a game and accidental in a design plate.

dolist
overlay
copy-list
luvcraft:luvcraft-session-overlayssession
let*
gnome
agent:spawn-agent:sessionsession:x6:y2:z8
proposal
setfapproval
make-instance'agent::construction-approval:agentnil:presencegnome:sessionsession:change-setchange-set
funcallfunctionsessiongnomeproposal
whenapproval
agent:deny-tool-approvalapproval"Capture complete."
whensession

A glance is an image and the small truth that frames it #IFEAQ6

An embodied agent's View Surroundings is one observation at one canvas boundary, not a screenshot followed by unrelated queries. Its image comes from a camera above and behind the body, aimed forward over its shoulder; its text names the body and camera poses, the player relative to the body, the world revision, and one exact integer box whose block population was counted. The box is deliberately called the sampled neighbourhood, not the viewport: a perspective frustum sees sky and distant geometry and has no honest voxel bounding box without reading depth back too.

The observation keeps unavailable cells distinct from air (#YUBB7X). It selects each intersecting resident chunk once, borrows that chunk's palette and dense index column, and counts raw lanes inside the box. The retained result is a semantic object with an encoded PNG and a small material histogram; the model reads its compact textual presentation and the provider receives the same result's PNG as image content (#WS5OEX). A glance therefore remains cheap enough to be an ordinary verb and rich enough to orient action.

Facing belongs to the body rather than the observer. Movement updates a retained yaw and stopping preserves it, so both the over-the-shoulder camera and the analytic gnome or cat field read the same direction. The proxy quad may face the camera for conservative rasterization while the figure inside it looks where the agent is going; an avatar need not stare at every eye that can see it.

Evidence (2026-08-21). The first live gnome glance was an 819 by 614 clean PNG looking east over the gnome's back. Its paired reading counted 10,625 sites in (-570 -2 -532)..(-546 14 -508): 9,375 resident, including 6,868 air, and 1,250 unavailable. The two SDF pipelines validated after their instance records gained facing, and the provider boundary received the observation as text plus image content.

IDEA Authored edits form sparse swaths, not another terrain field #758F9X

Intent. Add a Survey Edits observation over the little world's sparse authored overlay. Nearby edited sites should be clustered into semi-contiguous swaths and each swath presented by its integer bounding box, edited-site count, box fill ratio, and final block-kind distribution (with explicit air retained as a value). This is archaeology of authored intent, not a census of the current materialized terrain.

Evidence. little-world-source-edits already owns one hash entry per final authored site and survives chunk departure. A neighbourhood query can filter those sparse keys before a bounded 26-neighbour or small-gap component walk; it need not scan chunks or retain an object per ordinary cell.

Done when. A wall, a diagonally joined ornament, and two genuinely separate works produce stable, inspectable swaths without treating generated blocks as edits or merging distant work merely because their bounding boxes overlap.

IDEA Terrain summits are born at peaks and merge at saddles #ANOLKV

Intent. Add a Survey Terrain observation derived from a resident height-field window. Besides elevation range, relief, and slope summaries, report the strongest summits by a discrete merge tree: descend through height levels, birth a component at each local maximum, and record the saddle where it joins a component with a higher peak. Peak height minus that saddle is a useful topological persistence -- the playful "geotopological peak" with a real definition.

Evidence. The block world is volumetric, while a landscape survey is a derived two-dimensional product: the highest occupied site per horizontal column plus an availability mask. Naming that product prevents caves, unloaded columns, and absent terrain from being silently flattened into a height. The descending component pass is then dense work over the finite window, not repeated sparse world lookup.

Done when. Synthetic plateaus, twin peaks joined by a known saddle, a peak touching the window boundary, and unavailable columns have exact results; a live survey names a few durable landforms rather than dumping every local maximum.

DONE Give embodied agents a truthful third-person glance #AHAI6I

The basic view-surroundings verb now realizes #IFEAQ6: it captures on the canvas boundary under a temporary over-the-shoulder pose, restores the player's camera, retains one semantic observation, and sends its clean PNG beside the compact textual census. Stable body yaw is shared by movement, where-am-i, capture, and the gnome and cat fields. Dense census tests keep air separate from unavailable space; the shader validator covers both oriented figures.

DONE Give construction tools a visible possible world #YXUKSU

Intent. Let an embodied agent propose a voxel change set which remains inert while the player sees the actual candidate materials in the world, flies around them in focus chat, and approves, denies, or steers in ordinary words.

Evidence. Propose Block Box installs a construction-approval and a GPU-owned construction-preview-overlay. The focused tests freeze and stale check four cells, verify that one proposed block is the ordinary 36-vertex block mesh with the proposal emission lane, prove a finished approval does not wait, and find the command in the gnome toolbox. On 2026-08-21 a live six-block stone-brick possible world rendered beside the gnome while the dialogue showed all three decisions and the focus camera orbited it; denial removed the GPU buffer without editing any cell.

Done when. A proposal never edits terrain before approval; its candidate is visually legible as the after-world; focus supplies decision affordances and a cinematic view; approve revalidates and commits once; deny and steering release the preview and wake the waiting tool call; and the behavior has focused tests plus a live capture.

DONE Wire three commands into an agent and draw their cassettes #YDAQNO

Intent. The first slice: a luvcraft.agent layer where a command-tool wraps a CLIM command (#0IF4TY), presentation-type-json-schema derives its schema, tool bodies' values are presented under +model-view+ (#WS5OEX), and a cassette-view draws each call while it runs and after (#9K823O, #14ZP6S).

Evidence. luvcraft/agent/ (2026-08-19). make-command-tool reads climi::*command-parser-table*; accept-from-string parses every argument and a bad one comes back as a parse error the model reads ("cheese is not a block kind; try one of grass, dirt, ..."). Results are presented under the model view and objects worth returning to carry #ABCD handles (describe-handle reads them; folded long text names its remainder by handle). Commands run on the canvas thread through request-canvas-frame; eval runs on the turn's thread. The cassette is one file of presentation methods keyed on status and command (hud.lisp), drawn both on a HUD pane and -- the thing that turned out to matter -- as bubbles in the world (#V9DHV5). A real turn against GPT-5.6 over the websocket placed blocks, surveyed its neighbours in one parallel batch of block-at calls, and, asked to remove a block without a removal tool, used eval with apropos to find edit-block-at on its own. The provider client had to move from response.append to response.create with previous_response_id along the way. Parameterized M-x is still open (#OL2JZ1).

The gnome is an agent occupying a cell, not a block #V9DHV5

The body (#5JDNKF) is deliberately skew to the block field. A gnome or cat belongs to *agents*, not *critters*, and Spawn Gnome or Spawn Cat chooses an empty supported cell ahead of the player without editing terrain. Its integer (x y z) is the public, intentional place; continuous position and velocity are the physical authority while it travels between places (#YPGXKI). The old gnome-block-kind and its atlas tiles no longer exist.

The visible bodies are analytic SDFs launched from conservative scene proxies: a red-hatted gnome and an orange tabby cat, each with its own live metabar vocabulary. Their world-space fields follow continuous body positions, so focus rays, lighting, and silhouettes move with the same body that collides with terrain. Focusing one with TAB steps the camera back to a conversational distance, aims it at the face with the focused field of view, and turns the keys into a prompt line; RET asks. Speech bubbles remain retained McCLIM world widgets above that analytic body.

What the gnome shows is the transcript in three registers:

The threads meet the same way as everywhere else on this page: the turn's observer posts notes to the gnome's mailbox; the gnome's own refresh-luvcraft-overlay, on the canvas thread, turns them into bubbles and lines. Repainting a mirror from any other thread races the frame and faults; every repaint here happens at the frame boundary.

defparameter *gnome-name* gnome.lisp:18

The gnome: an agent with a body (#5JDNKF), not a block (#V9DHV5).

A gnome occupies a discrete world cell but is not a block. The cell is semantic state -- where this agent stands -- while its animated, camera-facing figure is an independently rendered scene object. Focusing it is how you talk to it: the view leans in the way it does for a wall, the keys become a prompt line at the bottom of the screen, and RET asks. The gnome then thinks, and its thinking shows above its head as a bubble: a matte rounded cassette standing in the world, drawn through McCLIM the way the Telegram panel is (a world widget, in the scene, with relief). Each tool call is the next cassette, and pushes the ones before it upward. When the gnome talks -- the SAY command, which is a tool like any other -- it is not a cassette but a title at the bottom of the view, the way an NPC line is subtitled.

defparameter*gnome-name*

gnome

DONE Discrete destinations, continuously embodied #YPGXKI

Intent. An embodied agent acts in places rather than holding a WASD key: Move To (x y z) is one discrete, nearby destination and one terminal result. Intent does not flatten physics. The body has a continuous position and velocity, accelerates through the same collision motor as the player, falls under gravity, steps one-cell ledges, and only changes its reported cell as its base crosses the lattice boundary. Those are two explicit authorities rather than an interpolation pretending not to be state.

A bounded four-connected search names standable cells, routes around terrain, and permits one-cell steps up or down. The path is an inspectable part of the body-move-action. Stalling replans twice before failing; impossible, nonresident, or distant destinations fail as values. The player implements the same body protocol and can use the primitive internally, while held movement or jump input cancels it immediately rather than fighting an autopilot.

Evidence. simulation.lisp factors step-walking-body out of the player; locomotion.lisp owns path search, action state, completion mailbox, and the fixed-step motor; embodied-agent supplies the same body methods and advances from the overlay frame clock. com-move-to chooses the current command subject (agent presence or player), returns on the canvas, and settles beside it (#T6PEVY). On 2026-08-20 the live SDF cat travelled four terrain cells while all 60 film frames rendered; the waiting worker began in :running and returned :arrived after 2.458 simulated seconds at the requested cell.

Done when. A cat asked to move reaches a nearby supported place by a visible continuous route; its tool call waits without parking rendering; the same primitive moves a player body; manual player input takes authority back; and terrain routing and invalid destinations have tests.

defclass embodied-agent gnome.lisp:86
defclassembodied-agent
session:initarg:session:readergnome-session

The cell is the discrete public place; position and VELOCITY are the physical realization moving between such places. Keeping both makes the intentional and simulation authorities explicit (#YPGXKI).

x:initarg:x:accessorgnome-x
y:initarg:y:accessorgnome-y
z:initarg:z:accessorgnome-z
position:initarg:position:accessorembodied-agent-position
velocity:initform:accessorembodied-agent-velocity
facing-yaw:initarg:facing-yaw:initform0d0:accessorembodied-agent-facing-yaw:documentation"Retained body heading in radians, with +z as north."
grounded-p:initformt:accessorembodied-agent-grounded-p
movement-action:initformnil:accessorembodied-agent-movement-action
movement-accumulator:initform0d0:accessorembodied-agent-movement-accumulator
pending-approval:initformnil:accessorembodied-agent-pending-approval:documentation"The one possible-world tool effect awaiting the player."
agent:initformnil:accessorgnome-agent
agent-lock:initform
sb-thread:make-mutex:name"embodied agent provider"
:readerembodied-agent-agent-lock
agent-ready:initform
sb-thread:make-waitqueue:name"embodied agent provider ready"
:readerembodied-agent-agent-ready
agent-opening-owner:initformnil:accessorembodied-agent-agent-opening-owner
agent-opening-thread:initformnil:accessorembodied-agent-agent-opening-thread
pending-agent-prompts:initform'
:accessorembodied-agent-pending-agent-prompts
agent-released-p:initformnil:accessorembodied-agent-agent-released-p
body:initformnil:accessorgnome-body
bubbles:initform'
:accessorgnome-bubbles:documentation"Bubble overlays, newest first."
dialogue:initformnil:accessorgnome-dialogue:documentation"The HUD line: the prompt being typed, or what was said."
draft:initform"":accessorgnome-draft
said:initformnil:accessorgnome-said:documentation"The gnome's last line, or NIL."
said-at:initform0:accessorgnome-said-at
observer:initformnil:accessorgnome-observer
notes:initform
sb-concurrency:make-mailbox:name"gnome notes"
:readergnome-notes:documentation"Transcript events waiting for the canvas thread."
turn-finished-at:initformnil:accessorgnome-turn-finished-at
:documentation

One embodied agent at a discrete cell: its thread, figure, and voice.

NEXT Parameterized M-x prompts through presentation types #OL2JZ1

Intent. Finish what #0IF4TY promised: the command menu should offer commands with arguments, prompting through accept with the presentation types the agent already uses, so place-block-at is one definition reachable by key, M-x, and tool.

Evidence. The tool side already reads the command parser table. The M-x gap is command-menu-command-without-arguments-p in luvcraft/clim/command-menu.lisp.

Done when. M-x "Place Block At" prompts for a kind and three integers and places the block.