Skip to content

Machine learning

Models small enough to train inside a box, in plain Kotlin, while you watch. Flow matching carries one 2D or 3D point cloud onto another and hands back the deformation between them; the multi-output regression head it trains is usable on its own. See Flow Matching.

112 source file(s), 2616 public symbol(s), 1044 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

AgentEvalCorpus.kt

The agent eval corpus: tasks mined from the operation transcripts under dev_docs/agents/transcripts/ rather than written by hand.

InFieldAgentEvals is seventeen tasks, so a pass rate over it moves 6% per task. The transcripts hold about a thousand boxes that someone wrote and ran as real work, so each runnable one is a task:

  • goal: make a box of this name, under these parents, that does what the box's own leading comment says, and whose output includes what the real box printed or displayed;
  • setup: the box's ancestors in its transcript, created with their code and run in order;
  • solution: the box's real code (NewBoxRequest + RunBoxRequest/LaunchBoxRequest), so every task carries a known-good answer to measure an agent against;
  • checks: derived from what the solution said when run, twice: only output that came out the same both times becomes a check.

Mining is two stages. mine is static, YAML only, and rejects a box with a reason (never executed by its transcript, needs a renderer / audio / the clock / files, prints nothing). Stage two runs each surviving candidate's solution twice in a live Field and hands the pair to calibrate, which either accepts it as a CorpusTask or rejects it (solution-fails, no-stable-output). The calibrated corpus is checked in as CHECKOUT_PATH, so an eval run does not re-mine.

The split is fixed by the task id (splitOf): about a third of the tasks are heldout, never shown to AgentLessons.distil when the protocol is followed (distil from a train run, measure on heldout).

src/jvmMain/kotlin/afield/ai/agent/AgentEvalCorpus.kt — read the source

AgentEvalCorpus

object AgentEvalCorpus

Mining, calibration, and the checked-in form of the corpus.

Declaration Summary
const val CHECKOUT_PATH
const val TRANSCRIPTS_PATH
const val MAX_LIVE_SAMPLE A live corpus run may sample at most this many tasks (the hand suite's cap is AgentEvalRunner.MAX_LIVE_TASKS).
val SEARCH_TOOLS The tools that read the repository, where every mined solution sits: withheld by a sealed run.
val SEALED_TOOLS What a sealed run withholds: the search tools, and the two that read arbitrary files from outside a box (EvaluateKotlinExpressionRequest evaluates in the server JVM; CodeUseQueryRequest returns source). The 10-26 sealed arms still saw their solution through these in 1 and 2 of 20 tasks. Box code can read files too, so withholding is not a proof: leak checks every result afterwards, and a sealed pass that saw its solution is scored leaked, not pass.
fun fingerprint(solutionScript: String): List<String> The solution's most distinctive code lines: the five longest non-comment lines over 25 characters.
fun leak(solutionScript: String, result: AgentTaskResult): String? The tool whose result held a line of the task's own solution, or null: did the turn look the answer up?
val DENY: List<Pair<String, Regex>> Code that needs something a headless scratch document does not give it, or whose output cannot be the same twice. Matched as a regex anywhere in the box's code or its ancestors'.
const val CALIBRATION_TIMEOUT_SECONDS How long a calibration run of a solution may take. Runaway code the deny list cannot see (recursion with no base case, a loop whose exit never comes) ends here as a timeout rejection, or — once the Runloop watchdog interrupts box code — as interrupted, rather than as a hung calibration.
const val MAX_CODE_CHARS
data class Candidate(val id: String, val source: String, val box: String, val title: String, val code: String, val verb: String, val parents: List<String>, val setup: List<SetupBox>, val prose: String)
data class SetupBox(val box: String, val code: String, val verb: String, val parents: List<String>)
data class Rejection(val source: String, val box: String, val reason: String)
data class Mined(val candidates: List<Candidate>, val rejections: List<Rejection>, val transcripts: Int, val codeBoxes: Int)
data class CorpusTask(val id: String, val source: String, val split: String, val kind: String, val request: AgentTaskRequest, val solution: String, val hasProse: Boolean)
data class Corpus(val version: Int = 1, val minedFrom: String = TRANSCRIPTS_PATH, val transcripts: Int = 0, val codeBoxes: Int = 0, val tasks: List<CorpusTask> = emptyList(), val rejections: List<Rejection> = emptyList())
val json
fun transcriptsDir(): File
fun mine(dir: File = transcriptsDir()): Mined
fun mineTranscript(source: String, yamlText: String): Mined
fun denyReason(code: String): String?
fun stripComments(code: String): String Code without its comments: a comment that mentions the renderer should not deny a box that does not use it.
fun prose(code: String): String The box's own description of itself: its leading comment, first two sentences, plain.
fun splitOf(id: String): String
fun solution(c: Candidate, timeout: Double = 90.0): String The scripted solution: the box made with its real code under its parents, then run as the transcript did.
fun setup(c: Candidate): String
fun calibrationRequest(c: Candidate) A calibration request: setup and solution, no checks (verdict unchecked); the tool log carries the output.
data class Observed(val success: Boolean, val stage: String, val stdout: List<String>, val displays: Map<String, String>, val error: String) What one run of the target box said, read from its Run/Launch tool record.
fun observed(result: AgentTaskResult, box: String): Observed?
fun failureCategory(o: Observed): String? A failed calibration run that ran away rather than failed: timeout (no verdict in time) or interrupted (the watchdog).
fun checkable(line: String)
fun stableLines(a: List<String>, b: List<String>): List<String> Lines that came out both times, in first-run order, trimmed; long ones cut to 100 chars.
fun stableTextNodes(a: Map<String, String>, b: Map<String, String>): List<String> Text nodes (between tags) that appeared in both displays.
sealed interface Calibrated
data class Accepted(val task: CorpusTask) : Calibrated
data class Refused(val rejection: Rejection) : Calibrated
fun calibrate(c: Candidate, first: AgentTaskResult, second: AgentTaskResult): Calibrated Turn two scripted runs of c's solution into a task: checks on the output that was the same both times. Up to two printed lines (first and last checkable), else one displayed text node, else — weakest — the first tag of the display.
fun goal(c: Candidate, kind: String, expect: List<String>): String
fun sourceFile(): File?
fun load(): Corpus
fun write(corpus: Corpus, target: File = File(CHECKOUT_PATH).absoluteFile)
fun select(corpus: Corpus, split: String, sample: Int, seed: Long, ids: List<String> = emptyList()): List<CorpusTask> The tasks a run asks for: split all / train / heldout, then sample of them (0 = all) drawn by seed. A seeded shuffle, so --sample 20 --seed 1 names the same tasks on every machine.

AgentLessons.kt

Lessons AI4 distils from its own eval runs, kept in a reviewable Markdown file and added to the instructions of every turn that has the graph tools.

src/jvmMain/kotlin/afield/ai/agent/AgentLessons.kt — read the source

AgentLessons

object AgentLessons

What AI4 has learned from its own evals, as a file a person reviews with git diff.

Declaration Summary
const val MAX_INSTRUCTION_CHARS
const val MAX_DISTILLED
const val RESOURCE
const val CHECKOUT_PATH The tracked source of RESOURCE, relative to a checkout (Field runs from its checkout).
fun enabledByDefault(): Boolean
fun resolve(setting: String): Boolean on / off pin; anything else is enabledByDefault.
fun sourceFile(): File? The writable file: AFIELD_AI4_LESSONS_FILE, else the checkout's tracked copy when it exists.
fun rawText(): String? The file's text: the source file when there is one (so a distil is live at once), else the packaged copy.
data class Parsed(val version: Int, val header: String, val curated: List<String>, val distilled: List<String>, val provenance: String)
fun parse(text: String): Parsed
fun instructionText(): String? The block added to AI4's instructions, or null when there are no lessons. Whole bullets only, curated first, until MAX_INSTRUCTION_CHARS.
fun isMisuse(call: AgentToolCallRecord): Boolean A failed call that is a mistake about the tools rather than the box's own error. Run/Launch failures that reached execution (a stage) and compiles that produced diagnostics are the code's errors, which is what the task is about.
fun misuseCount(result: AgentTaskResult): Int
fun normalize(text: String, documentRef: String): String Replace what differs between runs by placeholders: the document's name as file refs spell it (documentRef, the scratch directory's uuid) becomes <document>, other uuids <uid>.
data class Lesson(val tool: String, val message: String, val count: Int, val tasks: List<String>, val repeatedUnchanged: Int, val change: String)
fun distil(results: List<AgentTaskResult>): List<Lesson>
fun render(existing: String?, lessons: List<Lesson>, provenance: String): String existing with its ## Distilled section replaced by lessons, its version bumped, and a provenance line. The curated section and the header comment are kept as they are.
const val DEFAULT_HEADER
data class DistilOutcome(val written: Boolean, val path: String, val version: Int, val lessons: List<Lesson>, val message: String)
fun readReport(path: String): AgentEvalReport A saved AgentEvalReport (the JSON agent-eval returned), for re-distilling without re-running.
fun distilAndWrite(report: AgentEvalReport): DistilOutcome Distil report and write the file. Refuses when there is no writable source file.

AgentTask.kt

Headless agent tasks: a goal, a document and a set of checks, run through AI4's own turn and judged from what the document's boxes did.

AgentTaskRequest describes the task and its AgentChecks, AgentTaskRunner runs it, and AgentTaskResult carries the verdict, each check's AgentCheckOutcome and the tool calls the agent made (AgentToolCallRecord).

src/jvmMain/kotlin/afield/ai/agent/AgentTask.kt — read the source

Top-level functions

Declaration Summary
fun remoteProviderRefusal(mode: String, provider: String, allowRemote: Boolean): String? The refusal for a live task that would reach a remote provider without opt-in, or null when it may run. Pure so the gate is testable without a provider.
fun toolCallTally(calls: List<AgentToolCallRecord>): Map<String, ToolCallTally>

Top-level values

Declaration Summary
val HEADLESS_TASK_INSTRUCTIONS: String What a headless task adds to the ordinary AI4 chat instructions (afield.ai.conversation.buildDynamicInstructions, which already describes the graph tools via afield.ai.conversation.fieldGraphToolGuidance). Only the facts that differ from a person typing in the chat: nobody will answer a question, and the turn is judged by checks.

AgentTaskRequest

data class AgentTaskRequest(val goal: String, val checks: List<AgentCheck> = emptyList(), val setup: String = "", val focusBox: String = "", val maxToolRounds: Int = 24, val mode: String = "live", val script: String = "", val model: String = "", val extraInstructions: String = "", val taskId: String = "", val chatBox: String = "", val lessons: String = "", val blind: Boolean = false, val programs: String = "", val context: String = "", val followUps: List<String> = emptyList(), val withhold: List<String> = emptyList(), val repoRoot: String = "", val repoRead: List<String> = emptyList(), val repoWrite: List<String> = emptyList(), val repoCommands: List<String> = emptyList(), val repoCommandMaxSec: Int = 900, val allowRemote: Boolean = true)

A whole task handed to Field's own agent: a goal, a document, a tool-round budget, and checks that decide — from the document and what its boxes said, not from the agent's final text — whether the goal was met.

Declaration Summary
val goal: String
val checks: List<AgentCheck> Checks that decide the verdict. Empty = verdict unchecked.
val setup: String Tool calls applied before the agent's turn (not counted against its budget or in its log).
val focusBox: String Box the conversation is about (uid or name); optional.
val maxToolRounds: Int
val mode: String live = the configured provider; scripted = replay script.
val script: String JSON script for scripted mode; see ScriptedLlmBridge.parse.
val model: String Model override for live mode; blank = the configured flagship.
val extraInstructions: String Appended to the harness instructions.
val taskId: String
val chatBox: String Run the task as this box's AI4 panel turn (uid or unique name): it goes through the real ai-assist handler with kind general, so the exchange is saved in the box's ai4_chat_history and shows in the normal chat UI. Live mode only. The per-call tool log is then not available here (the panel keeps it); the checks still decide the verdict.
val lessons: String on / off pins the eval-distilled lessons in the instructions (AgentLessons); blank = default.
val blind: Boolean Withhold ViewBoxOutputRequest — the counterfactual for a visual task: can it be done without looking?
val programs: String off withholds RunToolProgramRequest (the tool-program A/B arm); on or blank leaves it to afield.ai.tools.program.ToolPrograms.enabled.
val context: String legacy sends this task's instructions the pre-2026-10-02 way, inside the stored message, instead of as ephemeral per-request context (afield.ai.conversation.TurnContext): the context-pipeline A/B arm.
val followUps: List<String> Further user turns, sent in order in the same conversation after the goal's turn finishes. The checks run after the last. Any non-empty list makes the task multi-turn.
val withhold: List<String> Tool simple names withheld from this task's turn (as programs off withholds one); see AgentEvalRequest.sealed.
val repoRoot: String A checkout the repo tools (afield.ai.tools.RepoFileTools) work in: blank = no repo tools. repoRead and repoWrite are globs over root-relative paths; the job may read only the first and write only the second.
val repoRead: List<String>
val repoWrite: List<String>
val repoCommands: List<String> Names from afield.ai.tools.RepoCommandTemplates this job may run with RepoRunCommandRequest, in repoRoot (empty = no command tool); repoCommandMaxSec caps any one run's wall clock.
val repoCommandMaxSec: Int
val allowRemote: Boolean Whether a live turn may go to a provider other than local (OpenAI, OpenRouter, Anthropic), which sends the goal, the document's code and every tool result — docs pages included — off this machine. In-Field callers (the panel, box tasks) default to yes, since the user picked the provider. The backdoor sends false unless --allow-remote-provider or AFIELD_AGENT_TASK_ALLOW_REMOTE=1, so an unattended run cannot fall through to a remote model it was not told to use (remoteProviderRefusal).

AgentCheck

data class AgentCheck(val kind: String, val box: String = "", val text: String = "", val property: String = "code", val parent: String = "", val child: String = "", val ints: List<Long> = emptyList(), val fraction: Double = 0.0)

One predicate over the document. Kept as data (not a lambda) so a task can arrive over HTTP or the backdoor, and so a result can say exactly what was checked.

Declaration Summary
val kind: String
val box: String
val text: String
val property: String
val parent: String
val child: String
val ints: List<Long>
val fraction: Double

AgentCheckOutcome

data class AgentCheckOutcome(val check: AgentCheck, val passed: Boolean, val detail: String)

AgentToolCallRecord

data class AgentToolCallRecord(val index: Int, val tool: String, val arguments: String, val ok: Boolean, val result: String, val parent: String = "")

Declaration Summary
val index: Int
val tool: String
val arguments: String
val ok: Boolean The tool's own verdict: false when it threw or returned success: false / error: true.
val result: String
val parent: String The call this one ran inside — a tool program's — or blank for a call the model made itself.

AgentTaskResult

data class AgentTaskResult(val taskId: String, val goal: String, val mode: String, val provider: String, val model: String, val verdict: String, val checks: List<AgentCheckOutcome>, val toolCalls: List<AgentToolCallRecord>, val finalText: String, val rounds: Int, val maxToolRounds: Int, val hitToolRoundCap: Boolean, val elapsedMs: Long, val inputTokens: Long? = null, val outputTokens: Long? = null, val cacheReadTokens: Long? = null, val cacheWriteTokens: Long? = null, val documentName: String, val createdBoxes: List<String> = emptyList(), val setupCalls: List<AgentToolCallRecord> = emptyList(), val error: String = "", val chatBox: String = "", val misuseCalls: Int = 0, val imagesSeen: Int = 0, val lessons: Boolean = false, val documentRef: String = "", val programs: Boolean = false, val programCalls: Int = 0, val nestedCalls: Int = 0, val turns: Int = 1, val contextPlacement: String = "", val leakedVia: String = "", val repoWritten: List<String> = emptyList(), val repoRuns: List<String> = emptyList(), val toolTally: Map<String, ToolCallTally> = emptyMap(), val offMachine: Boolean = false)

Declaration Summary
val taskId: String
val goal: String
val mode: String
val provider: String
val model: String
val verdict: String pass, fail, budget-exhausted, error, unchecked, leaked (a sealed corpus pass that read its solution), or running when observation ended before a live panel turn.
val checks: List<AgentCheckOutcome>
val toolCalls: List<AgentToolCallRecord>
val finalText: String
val rounds: Int
val maxToolRounds: Int
val hitToolRoundCap: Boolean
val elapsedMs: Long
val inputTokens: Long?
val outputTokens: Long?
val cacheReadTokens: Long? Input tokens the provider served from its prompt cache (see afield.ai.models.LlmSendResult.cacheReadTokens).
val cacheWriteTokens: Long?
val documentName: String
val createdBoxes: List<String>
val setupCalls: List<AgentToolCallRecord>
val error: String
val chatBox: String Set when the task ran as a box's panel turn (AgentTaskRequest.chatBox).
val misuseCalls: Int Failed tool calls that were mistakes about the tools, not the box's own errors (AgentLessons.isMisuse).
val imagesSeen: Int Tool results that attached a picture the model was shown.
val lessons: Boolean Whether the eval-distilled lessons were in this turn's instructions.
val documentRef: String The document as file refs spell it (afield://<documentRef>/<uid>/code): its directory's name.
val programs: Boolean Whether RunToolProgramRequest was offered, how often the model called it, and the calls those programs made.
val programCalls: Int
val nestedCalls: Int
val turns: Int User turns in the conversation: 1 + AgentTaskRequest.followUps.
val contextPlacement: String Where the last turn's instructions went: instructions, system+prefix, or inline (the legacy arm).
val leakedVia: String A mined-corpus task only: the tool whose result held a line of the task's own solution (see AgentEvalCorpus.leak).
val repoWritten: List<String> Root-relative paths the repo tools wrote, in order (a path written twice appears twice).
val repoRuns: List<String> The job's command runs, name args -> exit code (or timeout), in order.
val toolTally: Map<String, ToolCallTally> Per tool name: calls and failed calls (toolCallTally), so an all-errored tool shows without reading toolCalls.
val offMachine: Boolean True when the turn went to a provider other than local, i.e. its content left this machine.

ToolCallTally

data class ToolCallTally(val calls: Int, val failed: Int)

AgentTaskRunner

object AgentTaskRunner

Declaration Summary
fun run(request: AgentTaskRequest, info: SessionInformation, host: FieldGraphHost = LiveFieldGraphHost, contextFor: (SessionInformation, FieldGraphTools, String?) -> ToolExecutionContext = { i, graph, focus -> Ai4BackdoorTools.buildContext(i, focus).copy(fieldGraph = graph) }, bridgeFor: (AgentTaskRequest) -> LlmBridge = ::defaultBridge): AgentTaskResult Run request against info. Blocking; call it off the Runloop (the tools dispatch onto it).
fun defaultBridge(request: AgentTaskRequest): LlmBridge

AgentTaskTranscript.kt

Writes an agent task into a box's AI4 chat transcript, so a finished task can be read and continued in that box's chat panel.

src/jvmMain/kotlin/afield/ai/agent/AgentTaskTranscript.kt — read the source

AgentTaskTranscript

object AgentTaskTranscript

Writes an agent task into a box's AI4 chat transcript (ai4_chat_history), in the transcript's own markup (<<you>>, <<ai>>, <<progress>>, <<model:…>>), and pushes it to the box's ai-chat panel. The panel then renders the task like any other turn, and the person can continue the conversation from there.

Declaration Summary
val SCRIPTED_IDENTITY
fun AgentCheck.spec(): String
fun verdictBlock(result: AgentTaskResult): String The verdict block: a one-line summary (✓ box-exists:Primes ✓ … — PASS), then one line per check with its detail.
fun appendVerdict(info: SessionInformation, box: Box, result: AgentTaskResult)
fun appendScriptedTurn(info: SessionInformation, box: Box, goal: String, result: AgentTaskResult)
fun appendError(info: SessionInformation, box: Box, goal: String, message: String) A task that could not start: the person sees why in the panel, not only in a return value.

BoxAgentTask.kt

src/jvmMain/kotlin/afield/ai/agent/BoxAgentTask.kt — read the source

Top-level functions

Declaration Summary
fun Box.askFieldAgent(goal: String, checks: List<String> = emptyList(), mode: String = "auto", script: String = "", maxToolRounds: Int = 16, chatBox: Any? = null): CompletableFuture<AgentTaskResult> Hand goal to Field's own agent as an AI4 chat turn.
fun fieldAgentEvalScript(id: String): String The scripted solution of an eval task, for askFieldAgent's script — a no-credentials fallback.

BoxOutputCapture.kt

Pictures of a box's output area for the agent, taken from a headless Playwright page on the box's document, with simple statistics about what the picture contains.

BoxOutputCapture owns the pages and their release; BoxOutputShot is one capture, and PictureStats summarises its pixels: how much differs from the background, and the dominant colours.

src/jvmMain/kotlin/afield/ai/agent/BoxOutputCapture.kt — read the source

BoxOutputShot

data class BoxOutputShot(val success: Boolean, val message: String, val image: BufferedImage? = null, val path: String = "", val channels: List<String> = emptyList(), val viewerStates: List<Map<String, Any?>> = emptyList())

One captured picture of a box's output area, or the reason there is none.

Declaration Summary
val success: Boolean
val message: String
val image: BufferedImage?
val path: String
val channels: List<String>
val viewerStates: List<Map<String, Any?>>

PictureStats

data class PictureStats(val width: Int, val height: Int, val inkFraction: Double, val dominant: List<Pair<String, Double>>)

Measurements of a picture that a check, a log line, or a model can use without looking.

Declaration Summary
val width: Int
val height: Int
val inkFraction: Double
val dominant: List<Pair<String, Double>> Up to six #rrggbb → share, most common first (bin averages).
fun describeDominant(): String #ffffff 62%, #1f77b4 12%, …

BoxOutputCapture

object BoxOutputCapture

The server-owned browser page AI4 looks through: a picture of one box's output area, taken headless, whether or not anyone has Field open.

Declaration Summary
const val MAX_PAGES
const val IDLE_RELEASE_MS
val captureDir: File Where captures are kept as evidence: AFIELD_AI4_CAPTURE_DIR, else ~/.afield/logs/ai4_box_captures.
fun openPages(): Int How many capture pages are open — for tests of the release rules and for status.
fun isHeld(info: SessionInformation): Boolean
fun capture(info: SessionInformation, uid: String, settleMs: Int = 300): BoxOutputShot Capture uid's output area in info. Blocking; call it off the Runloop and off the Playwright thread (it marshals onto the latter, and the page needs the former to answer its websocket).
fun release(info: SessionInformation) Close info's capture page, if one is open. Safe from any thread.

FieldGraphTools.kt

The box graph and execution tools of Field's own agent: list, create, delete, edit, connect, run, launch, stop and read the output of boxes in the current document.

These are the operations an external agent reaches through tools/field_backdoor.py (new-box, set, connect, run, launch, stop, wait-output), registered as native tools in the AI4 registry. Each one calls the same server function the websocket and backdoor handlers call (createAgentBox, executeAllRequest, launchAllRequest, stopBoxRequest, and the action table's -connect, -disconnect and -delete-box), so box execution is not reimplemented here.

A box that fails to compile or throws comes back as success: false with its diagnostics, never as a silent success. FieldGraphHost is how these tools reach the server: LiveFieldGraphHost is the real one, and tests such as InFieldAgentHarnessTest supply a scripted host.

src/jvmMain/kotlin/afield/ai/agent/FieldGraphTools.kt — read the source

Used in: AI4's built-in tools

Top-level values

Declaration Summary
val fieldGraphToolNames: List<String> The tool names registerFieldGraphTools adds, for instructions and tests.

ListBoxesRequest

data class ListBoxesRequest(val unused: Int = 0)

NewBoxRequest

data class NewBoxRequest(@JsonPropertyDescription("Box name, shown on the canvas. Prefer unique names; tools accept a unique name wherever they take a box.") val name: String, @JsonPropertyDescription("Initial Kotlin source for the box's `code` property. The box is the implicit receiver: use println(...) and display(\"channel\", \"<html>\").") val code: String = "", @JsonPropertyDescription("Optional parent box (uid or unique name). The new box becomes its child and inherits from it.") val parent: String = "")

Declaration Summary
val name: String
val code: String
val parent: String

DeleteBoxRequest

data class DeleteBoxRequest(val box: String)

SetBoxPropertyRequest

data class SetBoxPropertyRequest(val box: String, @JsonPropertyDescription("Property name; `code` for Kotlin source, `name` to rename.") val property: String = "code", val value: String)

Declaration Summary
val box: String
val property: String
val value: String

PatchBoxTextRequest

data class PatchBoxTextRequest(val box: String, val property: String = "code", val find: String = "", val replace: String = "", val occurrence: Int = 1, val allowMultiple: Boolean = false, val startLine: Int = 0, val endLine: Int = 0, val replacement: String = "", val insert: Boolean = false)

Declaration Summary
val box: String
val property: String
val find: String
val replace: String
val occurrence: Int
val allowMultiple: Boolean
val startLine: Int
val endLine: Int
val replacement: String
val insert: Boolean

GetBoxPropertyRequest

data class GetBoxPropertyRequest(val box: String, val property: String = "code")

ConnectBoxesRequest

data class ConnectBoxesRequest(val parent: String, val child: String)

DisconnectBoxesRequest

data class DisconnectBoxesRequest(val parent: String, val child: String)

RunBoxRequest

data class RunBoxRequest(val box: String, @JsonPropertyDescription("Optional replacement source; omitted or blank = run the box's current code.") val code: String? = null, val timeoutSeconds: Double = 30.0)

Declaration Summary
val box: String
val code: String?
val timeoutSeconds: Double

LaunchBoxRequest

data class LaunchBoxRequest(val box: String, val code: String? = null, val timeoutSeconds: Double = 30.0, @JsonPropertyDescription("Seconds to keep collecting output after setup completes.") val settleSeconds: Double = 0.5)

Declaration Summary
val box: String
val code: String?
val timeoutSeconds: Double
val settleSeconds: Double

StopBoxRequest

data class StopBoxRequest(val box: String)

ReadBoxOutputRequest

data class ReadBoxOutputRequest(val box: String, @JsonPropertyDescription("Return entries after this marker; use the marker from a previous RunBox/LaunchBox/ReadBoxOutput result.") val since: Long = 0, @JsonPropertyDescription("If non-empty, wait (up to waitSeconds) until some output contains this text.") val contains: String = "", val waitSeconds: Double = 0.0, val maxEntries: Int = 60)

Declaration Summary
val box: String
val since: Long
val contains: String
val waitSeconds: Double
val maxEntries: Int

ViewBoxOutputRequest

data class ViewBoxOutputRequest(val box: String, @JsonPropertyDescription("Milliseconds to wait before capturing, for a late script or an animation; clamped to 0..5000. 0 = a short default.") val settleMs: Int = 0, @JsonPropertyDescription("What you are looking for. Travels with the picture as its caption.") val note: String = "")

Declaration Summary
val box: String
val settleMs: Int
val note: String

FieldBoxSummary

data class FieldBoxSummary(val uid: String, val name: String, val parents: List<String>, val children: List<String>, val running: Boolean, val codeChars: Int, val lastRunSuccess: Boolean?, val lastRunError: String = "")

Declaration Summary
val uid: String
val name: String
val parents: List<String>
val children: List<String>
val running: Boolean
val codeChars: Int
val lastRunSuccess: Boolean?
val lastRunError: String

FieldOutputEntry

data class FieldOutputEntry(val seq: Long, val kind: String, val text: String, val channel: String = "", val line: Int = 0)

FieldGraphResult

data class FieldGraphResult(val success: Boolean, val message: String, val uid: String = "", val name: String = "", val property: String = "", val value: String? = null, val stage: String = "", val error: String = "", val running: Boolean? = null, val stdout: List<String> = emptyList(), val errors: List<String> = emptyList(), val displays: Map<String, String> = emptyMap(), val entries: List<FieldOutputEntry> = emptyList(), val boxes: List<FieldBoxSummary> = emptyList(), val outputMarker: Long = 0, val found: Boolean? = null, val timedOut: Boolean = false)

One result shape for every graph tool, so the model always finds success and message.

Declaration Summary
val success: Boolean
val message: String
val uid: String
val name: String
val property: String
val value: String?
val stage: String
val error: String
val running: Boolean?
val stdout: List<String>
val errors: List<String>
val displays: Map<String, String>
val entries: List<FieldOutputEntry>
val boxes: List<FieldBoxSummary>
val outputMarker: Long
val found: Boolean?
val timedOut: Boolean

BoxOutputView

data class BoxOutputView(val success: Boolean, val message: String, val uid: String = "", val name: String = "", val channels: List<String> = emptyList(), val width: Int = 0, val height: Int = 0, val inkFraction: Double = 0.0, val dominantColours: List<String> = emptyList(), val path: String = "", val imageAttached: Boolean = false, val viewerStates: List<Map<String, Any?>> = emptyList(), @get:com.fasterxml.jackson.annotation.JsonIgnore val dataUrl: String = "", @get:com.fasterxml.jackson.annotation.JsonIgnore val note: String = "") : afield.ai.tools.CarriesToolImages, afield.ai.tools.SummarisesForToolLog

What ViewBoxOutputRequest returns: the picture (as a tool image, not in the JSON) plus what was measured from it, so the model gets numbers alongside the pixels and the tool log shows whether a picture was attached at all.

Declaration Summary
val success: Boolean
val message: String
val uid: String
val name: String
val channels: List<String>
val width: Int
val height: Int
val inkFraction: Double Share of pixels that are not the background colour.
val dominantColours: List<String> #rrggbb share%, most common first.
val path: String Where the full-resolution PNG was kept, as evidence.
val imageAttached: Boolean
val viewerStates: List<Map<String, Any?>> Geometry state actually shown in the independent capture browser.
val dataUrl: String
val note: String
override val toolImages: List<afield.ai.tools.ToolImage>
override fun toolLogSummary(): String

FieldGraphHost

interface FieldGraphHost

Where the graph tools meet Field. LiveFieldGraphHost is the real server; tests supply a host whose owner thread is the calling thread and whose "execution" is scripted, because no jvmTest can run the K2 REPL.

Declaration Summary
fun <T> onOwner(info: SessionInformation, action: () -> T): T Run action where box graph mutations belong, with info bound as the current session.
fun newBox(info: SessionInformation, name: String, frame: Rect, properties: Map<String, String>): Box
fun delete(info: SessionInformation, box: Box)
fun run(info: SessionInformation, box: Box, exec: Execute, reply: (Message) -> Unit) Start a whole-property run; the verdict arrives through reply as execution-verdict.
fun launch(info: SessionInformation, box: Box, exec: Execute, reply: (Message) -> Unit)
fun stop(info: SessionInformation, box: Box)
fun captureOutput(info: SessionInformation, box: Box, settleMs: Int): BoxOutputShot A picture of box's output area. The default host cannot take one and says so.

LiveFieldGraphHost

object LiveFieldGraphHost : FieldGraphHost

Declaration Summary
override fun <T> onOwner(info: SessionInformation, action: () -> T): T
override fun newBox(info: SessionInformation, name: String, frame: Rect, properties: Map<String, String>): Box
override fun delete(info: SessionInformation, box: Box)
override fun run(info: SessionInformation, box: Box, exec: Execute, reply: (Message) -> Unit)
override fun launch(info: SessionInformation, box: Box, exec: Execute, reply: (Message) -> Unit)
override fun stop(info: SessionInformation, box: Box)
override fun captureOutput(info: SessionInformation, box: Box, settleMs: Int): BoxOutputShot

FieldGraphTools

class FieldGraphTools(val info: SessionInformation, private val host: FieldGraphHost = LiveFieldGraphHost, val replyOwned: Set<Pair<String, String>> = emptySet(), val lessons: Boolean = AgentLessons.enabledByDefault(), val seesOutput: Boolean = true, val origin: String = "")

Declaration Summary
val info: SessionInformation
val replyOwned: Set<Pair<String, String>> (uid, property) pairs whose text this turn's reply will replace — the editor chat's current box and property, whose rewritten code is the reply itself. Writing those through a tool would be silently overwritten when the reply lands, so the tools refuse and say where the edit goes.
val lessons: Boolean Whether this turn's instructions carry the eval-distilled lessons (AgentLessons). Defaults to on unless AFIELD_AI4_LESSONS=off; an eval can pin it per task to measure it.
val seesOutput: Boolean Whether ViewBoxOutputRequest is offered. Only an eval's counterfactual turns it off (blind): the same visual task with the model unable to look.
val origin: String The OperationTranscript origin this turn's edits are recorded under (ai4:<turn>), so a rewound turn's edits can be undone without touching anyone else's. Blank: recorded like a person's.
fun kit(q: InspectKitRequest): KitToolResult InspectKitRequest: which kit this document runs and what differs from it (KitTools.kt).
val created: MutableList<String> Uids of boxes this instance created, in order — the task runner reports them.
fun resolve(token: String): Box A box by uid or unique name; ambiguity is refused, never guessed.
fun isRunning(box: Box): Boolean
fun listBoxes(): FieldGraphResult
fun newBox(req: NewBoxRequest): FieldGraphResult
fun deleteBox(req: DeleteBoxRequest): FieldGraphResult
fun setProperty(req: SetBoxPropertyRequest): FieldGraphResult
fun patchText(req: PatchBoxTextRequest): FieldGraphResult
fun getProperty(req: GetBoxPropertyRequest): FieldGraphResult
fun symbolAt(req: SymbolAtRequest): SymbolAtResult SymbolAtRequest: the box is found where the graph lives; resolving runs off it, as the editor hover does.
fun connect(req: ConnectBoxesRequest): FieldGraphResult
fun disconnect(req: DisconnectBoxesRequest): FieldGraphResult
fun run(req: RunBoxRequest): FieldGraphResult
fun launch(req: LaunchBoxRequest): FieldGraphResult
fun stop(req: StopBoxRequest): FieldGraphResult
fun captureOutput(box: Box, settleMs: Int = 300): BoxOutputShot A picture of box's output area, through the host. Used by viewOutput and by picture checks.
fun viewOutput(req: ViewBoxOutputRequest): BoxOutputView
fun readOutput(req: ReadBoxOutputRequest): FieldGraphResult

GeometryReviewTools.kt

AI4 tools for the geometry review inbox: listing a document's Three.js and canvas review views, reading and answering their notes, reporting findings, styling parts and inspecting a view.

Each request class here is one tool, described to the model by its @JsonClassDescription; every call returns a GeometryToolResult, which can attach saved pictures as images. The tools read and write the same notes the viewer shows, scoped to the current document.

src/jvmMain/kotlin/afield/ai/agent/GeometryReviewTools.kt — read the source

Used in: AI4's built-in tools, Point to geometry, keep the context

ListGeometryViewsRequest

data class ListGeometryViewsRequest(val unused: Int = 0)

List the review views this document owns. This is where a viewId and the current expectedRevision come from, and the other tools here need both. Views belonging to other documents are not listed.

StyleGeometryPartsRequest

data class StyleGeometryPartsRequest(val viewId: String, val expectedRevision: String, val parts: List<GeometryPartColor>, val unlit: Boolean = false, val xrayOpacity: Double? = null, val replace: Boolean = false)

Colour, highlight or ghost named parts of a solid viewer. The style map is stored on the source box, so it survives a rebuild that keeps the same part IDs; the geometry, the camera, the selection, the notes and the build's currency are all left alone.

Declaration Summary
val parts: List<GeometryPartColor> The parts to restyle, at most 10000, each named once and each present in the view as it stands.
val unlit: Boolean Draw them at their own colour, with neither shading nor tone mapping.
val xrayOpacity: Double? How solid these parts stay while the rest fades, from 0 to 1, or null to follow the viewer's slider.
val replace: Boolean Replace the view's whole style map rather than adding to it; an empty replacement restores defaults.

GeometryPartColor

data class GeometryPartColor(val partId: String, val color: List<Double>)

One part's colour, as RGB from 0 to 1.

ReadGeometryReviewsRequest

data class ReadGeometryReviewsRequest(val viewId: String, val includeResolved: Boolean = false, val offset: Int = 0, val limit: Int = 10)

A page of a view's notes, as summaries rather than whole notes: id, kind, status and the revision each was made against. Reading leaves every status where it was. A note that came from a build attempt says whether that attempt ran against earlier inputs, in which case it may describe a design that is gone.

ReadGeometryNoteRequest

data class ReadGeometryNoteRequest(val viewId: String, val noteId: String, val includeImages: Boolean = true)

One note whole: its text, the feature and view it points at, its replies and its resolution history, with the pictures saved when it was made attached as images. Those pictures are of the revision that was annotated, not of the geometry as it stands now; a picture whose bytes do not match its recorded hash is reported as a warning rather than attached.

ReplyGeometryNoteRequest

data class ReplyGeometryNoteRequest(val viewId: String, val noteId: String, val text: String, val actor: String, val checkedRevision: String, val expectedVersion: Int)

Add a reply to a note without closing it: what was checked, and against which revision. The note version from the latest read has to still be current, so two writers cannot answer the same note blind. ReviewGeometryNoteRequest is the one that changes a note's status.

ReviewGeometryNoteRequest

data class ReviewGeometryNoteRequest(val viewId: String, val noteId: String, val status: String, val summary: String, val actor: String, val checkedRevision: String, val expectedVersion: Int)

Resolve a note, or reopen one, with the revision the decision was checked against. The original note, its pictures, its replies and its history are kept either way. A note having been read, or the geometry having been rebuilt, is not a reason to resolve it.

ReportGeometryFindingRequest

data class ReportGeometryFindingRequest(val viewId: String, val expectedRevision: String, val stateJson: String, val key: String, val kind: String, val stage: String, val runId: String, val producer: String, val text: String, val consequence: String, val prompt: String)

Put a finding in front of a person, as a note in the same inbox their own annotations go to. It captures its own pictures in a separate viewer, so it never moves the camera the person is looking through, and it checks the revision again on the document thread: a capture that finished late cannot become a finding against newer geometry. Every field below is required, and two reports agreeing on key, stage, runId and producer are the same occurrence and reuse one thread.

Declaration Summary
val viewId: String The view whose inbox the note goes to, from ListGeometryViewsRequest.
val expectedRevision: String The revision this finding is about, which has to still be the view's when the capture lands.
val stateJson: String A complete state from InspectGeometryViewRequest, and one with a point or region selected.
val key: String What was checked and on what, as something stable across runs: wall-thickness/lid.
val kind: String error for a failed check, warning for a suspected problem, question for a decision needed.
val stage: String The build stage the check belongs to.
val runId: String The build or diagnostic attempt it came from.
val producer: String What made the finding.
val text: String The finding itself, 1 to 16000 characters.
val consequence: String What happens if it is left as it is, to 4000 characters.
val prompt: String What is wanted back, to 4000 characters; a repair that needs no permission should say so.

InspectGeometryViewRequest

data class InspectGeometryViewRequest(val viewId: String, val expectedRevision: String, val noteId: String = "", val stateJson: String = "", val explode: Double? = null, val hidden: List<String>? = null, val xray: Boolean? = null, val xrayOpacity: Double? = null, val sectionNormal: List<Double>? = null, val sectionFraction: Double = 0.5, val pick: GeometryPick? = null, val region: GeometryRegion? = null, val camera: List<Double>? = null, val target: List<Double>? = null, val includeMetadata: Boolean = false)

Look at the geometry as it stands: a fresh picture from a capture browser of its own, a compact part hierarchy, and a state complete enough to frame the next inspection the same way. The person's camera and selection are untouched. Only a solid viewer can be inspected this way; a custom canvas is refused, with the output box to use ViewBoxOutputRequest on instead.

Declaration Summary
val viewId: String The view to capture, which has to be a solid viewer rather than a custom canvas.
val expectedRevision: String The revision from ListGeometryViewsRequest. Geometry that changed under it fails the call.
val noteId: String A note to frame the view the way it was annotated, instead of stateJson, and never both.
val stateJson: String The complete state a previous inspection returned, to 128000 characters, carrying at least its part IDs, camera and target. A partial or invented state is refused; to change only the framing, pass camera and target instead.
val explode: Double? How far to pull the parts apart, as a finite distance of zero or more.
val hidden: List<String>? Parts to leave out of the picture, at most 10000 of them.
val xray: Boolean? Fade the surfaces that are not the subject.
val xrayOpacity: Double? How solid the faded surfaces stay, from 0.03 to 1.
val sectionNormal: List<Double>? The normal of a cutting plane, as a finite 3-vector that is not all zeroes.
val sectionFraction: Double Where along that normal to cut, from 0 to 1 across the geometry.
val pick: GeometryPick? A point to pick the nearest visible feature at, instead of a region.
val region: GeometryRegion? A rectangle to select, instead of a point.
val camera: List<Double>? Where to put the camera, as a finite 3-vector in viewer coordinates, Y up.
val target: List<Double>? What to point it at, in the same coordinates; it has to differ from camera.
val includeMetadata: Boolean Add the source provenance and the full part tree, which most inspections do not need.

GeometryPick

data class GeometryPick(val x: Double, val y: Double, val kind: String = "face")

A point to pick at, in normalized view coordinates, and what kind of feature to pick there.

GeometryRegion

data class GeometryRegion(val cx: Double, val cy: Double, val width: Double, val height: Double, val angle: Double = 0.0)

A rectangle to select, centred at cx, cy in normalized view coordinates and rotated by angle.

GeometryToolResult

data class GeometryToolResult(val success: Boolean, val message: String, val details: Map<String, Any?> = emptyMap(), val warnings: List<String> = emptyList(), @get:JsonIgnore override val toolImages: List<ToolImage> = emptyList()) : CarriesToolImages, SummarisesForToolLog

What every geometry review tool answers with, pictures included.

Declaration Summary
val success: Boolean False when the request was refused or the view was unavailable, with the reason in message.
val message: String One sentence about what happened.
val details: Map<String, Any?> Whatever the tool reports: the views, a page of notes, a captured observation, a note's new version.
val warnings: List<String> Anything the reader should know about the answer, such as a picture that could not be verified.
override val toolImages: List<ToolImage> The pictures themselves, kept out of the JSON the model reads as text.
val imageCount: Int How many pictures came back.
override fun toolLogSummary(): String The verdict, the picture count and the message, which is what the tool log keeps.

GeometryViewCapture.kt

Independent viewer captures for the geometry review tools: a picture of a three.js view taken without moving the one the user is looking at.

Each capture opens its own browser context on that view's page, applies the request's view state (explode, hidden parts, x-ray, section plane, camera, picks and regions) through the page's inspector API, waits for a frame to render, screenshots, and returns the image alongside the resulting state, source list and part tree. The geometry's revision digest is read before and after, so a capture is always of the revision the request named and a change in between is reported rather than photographed. Captures collect in the box-output capture directory, where the hundred newest are kept.

src/jvmMain/kotlin/afield/ai/agent/GeometryViewCapture.kt — read the source

InFieldAgentEvals.kt

A small eval suite for Field's own agent: seventeen tasks (five graded by pixels, three made of many similar calls that a tool program does in one, two that are several turns of one conversation), each a goal, the setup it starts from, checks that decide it, and a scripted solution.

The scripted solutions are real Kotlin for real Field. Replayed through ScriptedLlmBridge against a live Field (agent-eval --mode scripted) they test the tools, the execution path and the checkers end to end with no model, and a scripted pass is the floor any model run should reach. Replayed in jvmTest against a fake host, they test the harness logic. A live run hands the same goals and checks to the configured provider and ignores the solutions.

Box names are fixed, so run each task in a fresh document (AgentEvalRunner does).

src/jvmMain/kotlin/afield/ai/agent/InFieldAgentEvals.kt — read the source

InFieldAgentEvals

object InFieldAgentEvals

The eval tasks, their scripted solutions, and the helpers that build them.

Declaration Summary
data class EvalTask(val id: String, val request: AgentTaskRequest, val solution: String)
fun call(tool: String, vararg args: Pair<String, Any?>) A step for steps: a tool call.
fun steps(vararg items: Any): String Build a script / setup JSON from calls and (optionally) a final answer.
val PRIMES_CODE
val BROKEN_CODE
val FIXED_CODE
val SOURCE_CODE
val READER_CODE
val TABLE_CODE
val TICKER_CODE
val DIVIDER_CODE
val DIVIDER_FIXED
val SWATCH_CODE A swatch whose colour is decided by the cascade: a stylesheet !important red, a more specific green, an inline purple — and then a script that sets an inline !important blue, which beats them all. Reading the source invites "red"; the picture is blue.
val ANSWER_CODE
val SUN_CODE
val BARS_CODE Percent heights inside a parent of auto height compute to auto: five empty bars, zero pixels tall.
val BARS_FIXED
val THEME_CODE Look → revise → look. Chart's container has class bars, and another box, Theme, displays a stylesheet that makes .bars flex-direction: row-reverse — output panels share one page, so a style in one panel reaches every other. The bars therefore appear in the reverse of the list's order, and sorting the list ascending, the obvious first move, draws them tallest-first. Chart's own code does not say so; the picture does. The stylesheet lives in Theme for that reason: in Chart's own code, reading it would stand in for looking at the output. The scripted solution makes the mistake first, sees it, and fixes it.
val ROW_CODE
val ROW_FIRST_TRY
val ROW_FIXED
val CELL_BAD
fun cellCode(n: Int)
val CELL_NAMES
val FAILING_CELLS
val REPORT_CODE
fun sqCode(k: Int) A value nobody computes in their head: the hash of a string built from k, so the sweep has to run.
val SWEEP_OUTPUTS What sqCode prints for k = 1..8, as Kotlin computes it.
val RESULTS_CODE
val TODO_NOTES
fun noteCode(n: Int)
fun noteName(n: Int)
val COUNTER_3
val COUNTER_5
val COUNTER_SQUARES
val TOTAL_CODE
val POWERS_3
val POWERS_5
val POWERS_5_CUBES
val CHART_CODE
val CHART_FIRST_TRY The first try: sorted ascending, tallest red — drawn tallest-first because of row-reverse.
val CHART_FIXED
val tasks: List<EvalTask>
val manyCallTaskIds Tasks made of many similar calls: what tool programs are for.
val multiTurnTaskIds Tasks that are several user turns of one conversation: what the context pipeline is for.
val visualTaskIds Tasks whose checks read pixels; they need a Field that can capture box output.
fun byId(id: String): EvalTask

AgentEvalRequest

data class AgentEvalRequest(val ids: List<String> = emptyList(), val mode: String = "scripted", val model: String = "", val maxToolRounds: Int = 24, val lessons: String = "", val distil: Boolean = false, val distilFrom: String = "", val blind: Boolean = false, val programs: String = "", val context: String = "", val corpus: String = "", val sample: Int = 0, val seed: Long = 0, val calibrate: Boolean = false, val sealed: Boolean = false)

Declaration Summary
val ids: List<String> Task ids; empty = all.
val mode: String
val model: String
val maxToolRounds: Int
val lessons: String on / off pins the distilled lessons in every task's instructions; blank = default (AgentLessons).
val distil: Boolean After the run, distil its tool misuse into the lessons file (AgentLessons.distilAndWrite).
val distilFrom: String Path to a saved AgentEvalReport: run nothing, distil that report instead (implies distil). How a change to AgentLessons.distil is applied to a run already paid for.
val blind: Boolean Withhold ViewBoxOutputRequest from every task (the counterfactual for the visual tasks).
val programs: String off withholds RunToolProgramRequest from every task (the tool-program A/B arm); see AgentTaskRequest.programs.
val context: String legacy puts every task's instructions back inside the stored message; see AgentTaskRequest.context.
val corpus: String Run the mined corpus (AgentEvalCorpus) instead of the hand suite: all, train or heldout. Blank = the hand suite. ids then filter corpus ids (<transcript>/<box>).
val sample: Int With corpus: run a seeded sample of this many tasks (0 = every task in the split).
val seed: Long
val calibrate: Boolean Re-mine the corpus: mine the transcripts, run every candidate's solution twice (scripted), derive checks from the output that matched, and rewrite AgentEvalCorpus.CHECKOUT_PATH. sample > 0 calibrates only that many candidates (and does not write).
val sealed: Boolean Withhold the tools that read the repository (AgentEvalCorpus.SEALED_TOOLS) from every task. A mined task's solution is checked in — the transcript it came from — so with search on, a model can find the answer instead of writing it; the first live corpus runs did, in at least 3 and 5 of 20 tasks.

AgentEvalReport

data class AgentEvalReport(val mode: String, val provider: String, val model: String, val population: Int, val passed: Int, val results: List<AgentTaskResult>, val elapsedMs: Long, val inputTokens: Long?, val outputTokens: Long?, val error: String = "", val cacheReadTokens: Long? = null, val cacheWriteTokens: Long? = null, val misuseCalls: Int = 0, val imagesSeen: Int = 0, val rounds: Int = 0, val lessons: Boolean = false, val lessonsVersion: Int = 0, val distilled: List<String> = emptyList(), val distilMessage: String = "", val documentsBefore: Int = -1, val documentsAfter: Int = -1, val capturePagesOpenAfter: Int = -1, val programs: Boolean = false, val programCalls: Int = 0, val nestedCalls: Int = 0, val context: String = "", val corpus: String = "", val corpusMessage: String = "", val rejections: Map<String, Int> = emptyMap(), val sealed: Boolean = false, val leaked: Int = 0)

Declaration Summary
val mode: String
val provider: String
val model: String
val population: Int
val passed: Int
val results: List<AgentTaskResult>
val elapsedMs: Long
val inputTokens: Long?
val outputTokens: Long?
val error: String
val cacheReadTokens: Long? Sums of AgentTaskResult.cacheReadTokens / AgentTaskResult.cacheWriteTokens; null when no task reported them.
val cacheWriteTokens: Long?
val misuseCalls: Int Sum of AgentTaskResult.misuseCalls.
val imagesSeen: Int Sum of AgentTaskResult.imagesSeen.
val rounds: Int
val lessons: Boolean Whether the lessons were on, and which version of the file was read.
val lessonsVersion: Int
val distilled: List<String> Set when the request asked to distil: what was written.
val distilMessage: String
val documentsBefore: Int Leak evidence, filled by the server's entry points (afield.app.server.runAgentEvalHeadless): open documents before and after the run, and AI4 capture pages still open after it. -1 = not measured.
val documentsAfter: Int
val capturePagesOpenAfter: Int
val programs: Boolean Whether tool programs were offered; sums of AgentTaskResult.programCalls and AgentTaskResult.nestedCalls.
val programCalls: Int
val nestedCalls: Int
val context: String legacy when the run put instructions inside the stored message, else blank.
val corpus: String The corpus split run (all / train / heldout), or blank for the hand suite; calibrate for a re-mine.
val corpusMessage: String A calibration's summary: what was mined, accepted and written.
val rejections: Map<String, Int> A calibration's rejections, counted by reason (static and calibration stages together).
val sealed: Boolean Whether the repository search tools were withheld (AgentEvalRequest.sealed).
val leaked: Int Corpus live runs: tasks whose tool results held a line of their own solution (AgentEvalCorpus.leak).

AgentEvalRunner

object AgentEvalRunner

Declaration Summary
const val MAX_LIVE_TASKS Hard caps for a live eval: one run of at most this many tasks, at most this many rounds each.
const val MAX_LIVE_ROUNDS
fun run(request: AgentEvalRequest, runTask: (AgentTaskRequest) -> AgentTaskResult): AgentEvalReport Run the suite, one task at a time. runTask runs one request in a fresh document and returns its result; the server passes one that opens a headless scratch document per task.
fun calibrate(request: AgentEvalRequest, runTask: (AgentTaskRequest) -> AgentTaskResult): AgentEvalReport Re-mine the corpus (AgentEvalCorpus): every statically mined candidate's solution runs twice, scripted, with no checks; the runs' output decides its checks. The report's results are the second runs, passed the tasks accepted.

KitTools.kt

The AI4 tool that reports which kit a document runs and which of its kit-provided properties and assets have drifted from the kit.

InspectKitRequest is the tool and KitToolResult its answer. It reads only; migrating a document to a newer kit is done by a person with tools/field_kit.py.

src/jvmMain/kotlin/afield/ai/agent/KitTools.kt — read the source

InspectKitRequest

data class InspectKitRequest(@JsonPropertyDescription("A kit directory to compare with, instead of the one the document records; empty for the recorded one.") val kit: String = "")

Declaration Summary
val kit: String

KitToolResult

data class KitToolResult(val success: Boolean, val message: String, val details: Map<String, Any?> = emptyMap()) : SummarisesForToolLog

Declaration Summary
override fun toolLogSummary()

StudyBuildAwaits.kt

Waiting for study build jobs without the model: a box chat that ended its turn on AwaitStudyBuildsRequest is resumed by StudyBuildAwaits when its jobs are done or its deadline passes.

AiChatSuspension and AiChatAwaitedJob are the wait as ai-chat-history reports it to the panel.

src/jvmMain/kotlin/afield/ai/agent/StudyBuildAwaits.kt — read the source

AiChatAwaitedJob

data class AiChatAwaitedJob(val boxId: String, val name: String, val jobId: String, val stage: String = "", val label: String = "", val status: String = "")

A job a chat waits for, as ai-chat-history reports it. status is the last one seen, or "" before the first poll.

Declaration Summary
val label: String

AiChatSuspension

data class AiChatSuspension(val since: String, val resumesBy: String, val jobs: List<AiChatAwaitedJob>)

What a box chat is waiting for (AiBoxChatReply.suspendedOn): since when, until when at the latest, and the jobs.

StudyBuildAwaits

class StudyBuildAwaits(private val graphFor: (SessionInformation) -> FieldGraphTools = { FieldGraphTools.forTurn(it) }, private val resume: (SessionInformation, Box, AiAssist) -> Boolean = ::resumeThroughChat, val pollMs: Long = 5_000, val runnerGraceMs: Long = 120_000, private val clock: () -> Long = System::currentTimeMillis, private val isOpen: (SessionInformation) -> Boolean = { info -> afield.app.server.documents.values.any { it === info } }) : AutoCloseable

Box chats whose turn ended with AwaitStudyBuildsRequest, waiting for their build jobs without the model.

Declaration Summary
val pollMs: Long
val runnerGraceMs: Long
data class SavedSuspension(val id: String, val property: String = "code", val maxToolRounds: Int = 24, val since: Long, val deadline: Long, val jobs: List<SavedJob>, val row: String) What PROPERTY holds. row is the transcript block the suspended turn ended with.
data class SavedJob(val boxId: String, val name: String, val jobId: String, val stage: String = "", val label: String = "", val directory: String = "") One awaited job as saved: directory is the runner's, read when the runner is not running.
fun suspend(info: SessionInformation, box: Box, suspension: TurnSuspension, property: String, maxToolRounds: Int): SavedSuspension Save suspension as box's wait and start watching it. A wait already armed for the box keeps its jobs: the new one waits for both sets, until the later deadline.
fun rearm(info: SessionInformation) Arm every wait saved in info's boxes that is not armed yet; called when a document has loaded.
fun cancel(info: SessionInformation, box: Box): SavedSuspension? Stop waiting (/stop-waiting, a cleared or rewound chat). The builds keep running. Returns what was waited for.
fun suspensionOf(info: SessionInformation, box: Box): AiChatSuspension? What box's chat waits for, for ai-chat-history; null when it is not waiting.
fun shown(info: SessionInformation, box: Box, markdown: String): String markdown as a panel should show it: while box waits, its waiting row open, like live activity.
override fun close()

StudyBuildTools.kt

AI4 tools over a box's study builds: list, inspect, start, wait for, cancel, await and prune build jobs.

Each request class is one tool and returns a StudyBuildToolResult. AwaitStudyBuildsRequest is the exception: its StudyBuildAwaitResult can end the turn with a suspension, which StudyBuildAwaits then holds until the jobs finish.

src/jvmMain/kotlin/afield/ai/agent/StudyBuildTools.kt — read the source

Used in: AI4's built-in tools, Studies, Let AI4 Wait for Long Builds

ListStudyBuildsRequest

data class ListStudyBuildsRequest(val unused: Int = 0)

List the document's build runners. This is where a boxId and name come from; every other tool here needs both, and a document with no runners answers with an empty list rather than a failure.

InspectStudyBuildsRequest

data class InspectStudyBuildsRequest(val boxId: String, val name: String, val jobId: String = "", val offset: Int = 0, val limit: Int = 10)

One runner's stages and a page of its attempt history, or one attempt named by jobId. Running, succeeded and current are three different things here: a stage's old success stops being current as soon as its inputs change. StartStudyBuildRequest needs the stage fingerprint this reports.

WaitStudyBuildRequest

data class WaitStudyBuildRequest(val boxId: String, val name: String, val jobId: String, val waitSeconds: Double = 600.0, val afterUpdated: String = "")

Watch a job that is already running, reporting its progress into the chat without spending a model turn on each poll. The wait ending is not the build failing: the result says whether the job is terminal. Cancelling the wait leaves the build running.

Declaration Summary
val waitSeconds: Double How long to watch, 0 to 900 seconds. Reaching it ends the observation and not the build.
val afterUpdated: String A progress timestamp to return early on: the wait ends as soon as the job's own differs from it.

StartStudyBuildRequest

data class StartStudyBuildRequest(val boxId: String, val name: String, val stageId: String, val expectedFingerprint: String, val waitSeconds: Double = 0.0, @JsonPropertyDescription("Optional parameters for this one attempt that the stage documents, as a JSON object in a string, such as a job to resume, e.g. `{\"resume\": \"<jobId>\"}` or `{\"budgetSeconds\": 60}`. Empty for none. Recorded in the attempt and part of its fingerprint; a later start does not reuse it, so pass it again when it still applies. Anything but a JSON object fails with success=false.") val request: String = "")

Start one stage from the inputs it has now. The expected fingerprint comes from the latest InspectStudyBuildsRequest, and a stage whose dependencies are missing or stale, whose gate is closed, or that is already building, is refused. A positive waitSeconds starts the job and watches it in the same call; zero returns the job handle for WaitStudyBuildRequest or AwaitStudyBuildsRequest.

Declaration Summary
val request: String Parameters for this one attempt, as a JSON object in a string: a job to resume, a budget. Anything that is not a JSON object fails the call. It goes into the attempt's fingerprint and is recorded with it, and it is not carried over, so a later start that still needs it passes it again.

CancelStudyBuildRequest

data class CancelStudyBuildRequest(val boxId: String, val name: String, val jobId: String)

Ask a running build to stop. Success means the cancellation was recorded, not that the worker has stopped: it ends at its next cancellation check, so poll the same job until it is terminal. The stage keeps whatever result it had already published, and the attempt history survives.

AwaitStudyBuildsRequest

data class AwaitStudyBuildsRequest(val boxId: String, val name: String, val jobIds: List<String>, val maxHours: Double = 4.0)

End the turn and have Field wait for up to twenty jobs without the model, then start the conversation again with their outcomes. This is the alternative to repeated WaitStudyBuildRequest calls for a build measured in hours: each wait costs a model round and this costs none. Jobs that have all already finished are answered at once and the turn carries on.

PruneStudyBuildsRequest

data class PruneStudyBuildsRequest(@JsonPropertyDescription("Job IDs to keep even when nothing else needs them, such as a result to compare against later.") val pins: List<String> = emptyList(), @JsonPropertyDescription("false (the default) reports what would be deleted and deletes nothing; true deletes it.") val apply: Boolean = false)

Delete attempt history nothing in this document still needs, across all of its runners. What is kept is each stage's current result, latest attempt and latest success, anything running, anything pinned, and whatever those were built from, gated on or resumed from. A deleted attempt cannot be inspected or resumed from again, so apply is false until asked for.

Declaration Summary
val pins: List<String> Attempts to keep when nothing else needs them; a pin that names no attempt is reported back.
val apply: Boolean False reports what would go and deletes nothing.

StudyBuildToolResult

data class StudyBuildToolResult(val success: Boolean, val message: String, val details: Map<String, Any?> = emptyMap()) : SummarisesForToolLog

What every build tool but the await answers with: a verdict, a sentence, and whatever the tool reports.

Declaration Summary
override fun toolLogSummary() The verdict and the message, which is what the tool log keeps rather than the details.

StudyBuildAwaitResult

data class StudyBuildAwaitResult(val success: Boolean, val message: String, val details: Map<String, Any?> = emptyMap(), @get:JsonIgnore override val suspension: TurnSuspension? = null) : SummarisesForToolLog, SuspendsTurn

AwaitStudyBuildsRequest's result. A set suspension ends the turn; the model reads only the rest.

Declaration Summary
override fun toolLogSummary() The verdict, whether the turn was suspended, and the message.

SymbolAtTool.kt

src/jvmMain/kotlin/afield/ai/agent/SymbolAtTool.kt — read the source

SymbolAtRequest

data class SymbolAtRequest(@param:JsonPropertyDescription("The box, by name or uid.") val box: String, @param:JsonPropertyDescription("The identifier to resolve, e.g. `color` or `prop1`; for `a.b.c` give the last part, `c`.") val symbol: String, @param:JsonPropertyDescription("Optional text that occurs where the name is used, e.g. `line.color =`; the name is resolved inside or right after it. Empty = no constraint.") val near: String = "", @param:JsonPropertyDescription("Optional 1-based line of the box's code to resolve the name on. 0 = not given.") val line: Int = 0, @param:JsonPropertyDescription("Which occurrence to use when the name (or `near`) appears more than once. 1 = the first.") val occurrence: Int = 1, @param:JsonPropertyDescription("Optional code to resolve in instead of the box's saved code, e.g. a draft you have not written yet. Empty = the box's code.") val code: String = "")

What a name means at one place in a box's code, as the compiler sees it there: the editor's hover, every completion candidate the compiler offers for the name (all overloads, with signatures and return types), the live value if a run left one in this box or an ancestor, where the box itself declares it, and the indexed Field declarations of that name with their KDoc and source.

Declaration Summary
val box: String
val symbol: String
val near: String
val line: Int
val occurrence: Int
val code: String

SymbolCandidate

data class SymbolCandidate(val text: String, val signature: String, val type: String, val deprecated: String = "")

SymbolRuntimeValue

data class SymbolRuntimeValue(val box: String, val type: String, val value: String, val declaredAt: String)

SymbolAtResult

data class SymbolAtResult(val success: Boolean, val message: String, val box: String = "", val symbol: String = "", val line: Int = 0, val column: Int = 0, val lineText: String = "", val hover: String = "", val candidates: List<SymbolCandidate> = emptyList(), val runtimeValues: List<SymbolRuntimeValue> = emptyList(), val declaredInBox: List<String> = emptyList(), val fieldDeclarations: List<WorkspaceSymbolMatch> = emptyList())

Declaration Summary
val success: Boolean
val message: String
val box: String
val symbol: String
val line: Int
val column: Int
val lineText: String
val hover: String The editor hover for this position, as text.
val candidates: List<SymbolCandidate> Every completion candidate the compiler offers for this name at this position.
val runtimeValues: List<SymbolRuntimeValue> Live values left by the last run, in this box first and then its ancestors.
val declaredInBox: List<String> N: text for each line of this box's code that declares the name.
val fieldDeclarations: List<WorkspaceSymbolMatch> Indexed Field declarations with this name (KDoc, source line, body), matched by name.

AI4.kt

Re-exports the AI4 assistant's public types and helpers under one package, afield.ai.compat, so code that imports from this one package keeps compiling.

Every declaration here is a typealias or a one-line forwarding val or fun into the package that now owns it: logging sinks from afield.ai.diagnostics, language specs such as KOTLIN_SCRIPT and the GLSL and Slang stage specs from afield.runtime.languages, tool request and result schemas from afield.ai.tools, and the completion helpers from afield.ai.completion. New code should import from those packages directly.

The file also declares two @RunsAt(installHandlers) hooks, ai4_install and docs_search_install, which call installAi4 and installDocsSearch (the latter installs the docs_search_input message handler).

src/jvmMain/kotlin/afield/ai/compat/AI4.kt — read the source

Top-level functions

Declaration Summary
fun wrapScript(script: String)
fun unwrapScriptLenient(wrapped: String)
fun glslSpecForProperty(property: String)
fun shaderSpecForProperty(property: String)
fun virtualFilesToAttachments(files: List<afield.document.workspace.VirtualTextFile>)
fun validateNoMarkers(code: String)
fun ai4_install()
fun docs_search_install()

LogSink

typealias LogSink = afield.ai.diagnostics.LogSink

Re-exports the AI4 assistant's public types and helpers under one package, afield.ai.compat, so code that imports from this one package keeps compiling.

NotifyLog

typealias NotifyLog = afield.ai.diagnostics.NotifyLog

TraceLogger

typealias TraceLogger = afield.ai.diagnostics.TraceLogger

LanguageSpec

typealias LanguageSpec = afield.runtime.languages.LanguageSpec

MalformedAI

typealias MalformedAI = afield.runtime.languages.MalformedAI

Commentary

typealias Commentary = afield.ai.tools.Commentary

ContextAttachment

typealias ContextAttachment = afield.ai.tools.ContextAttachment

ListAttachmentsRequest

typealias ListAttachmentsRequest = afield.ai.tools.ListAttachmentsRequest

ListAttachmentsResult

typealias ListAttachmentsResult = afield.ai.tools.ListAttachmentsResult

AttachmentInfo

typealias AttachmentInfo = afield.ai.tools.AttachmentInfo

AttachmentRequest

typealias AttachmentRequest = afield.ai.tools.AttachmentRequest

AttachmentResponse

typealias AttachmentResponse = afield.ai.tools.AttachmentResponse

ListDefinitionsRequest

typealias ListDefinitionsRequest = afield.ai.tools.ListDefinitionsRequest

ListDefinitionsResult

typealias ListDefinitionsResult = afield.ai.tools.ListDefinitionsResult

DefinitionInfo

typealias DefinitionInfo = afield.ai.tools.DefinitionInfo

ListContentsRequest

typealias ListContentsRequest = afield.ai.tools.ListContentsRequest

ListContentsResult

typealias ListContentsResult = afield.ai.tools.ListContentsResult

ListContentEntry

typealias ListContentEntry = afield.ai.tools.ListContentEntry

ListKotlinContextVariablesRequest

typealias ListKotlinContextVariablesRequest = afield.ai.tools.ListKotlinContextVariablesRequest

KotlinContextVariableInfo

typealias KotlinContextVariableInfo = afield.ai.tools.KotlinContextVariableInfo

ListKotlinContextVariablesResult

typealias ListKotlinContextVariablesResult = afield.ai.tools.ListKotlinContextVariablesResult

EvaluateKotlinExpressionRequest

typealias EvaluateKotlinExpressionRequest = afield.ai.tools.EvaluateKotlinExpressionRequest

EvaluateKotlinExpressionResult

typealias EvaluateKotlinExpressionResult = afield.ai.tools.EvaluateKotlinExpressionResult

EditFileRequest

typealias EditFileRequest = afield.ai.tools.EditFileRequest

EditFileResponse

typealias EditFileResponse = afield.ai.tools.EditFileResponse

CompileKotlinRequest

typealias CompileKotlinRequest = afield.ai.tools.CompileKotlinRequest

CompileKotlinResult

typealias CompileKotlinResult = afield.ai.tools.CompileKotlinResult

ReloadShadersRequest

typealias ReloadShadersRequest = afield.ai.tools.ReloadShadersRequest

ReloadShadersResult

typealias ReloadShadersResult = afield.ai.tools.ReloadShadersResult

ScratchpadReadRequest

typealias ScratchpadReadRequest = afield.ai.tools.ScratchpadReadRequest

ScratchpadUpdateRequest

typealias ScratchpadUpdateRequest = afield.ai.tools.ScratchpadUpdateRequest

ScratchpadResult

typealias ScratchpadResult = afield.ai.tools.ScratchpadResult

ChecklistReadRequest

typealias ChecklistReadRequest = afield.ai.tools.ChecklistReadRequest

ChecklistUpdateRequest

typealias ChecklistUpdateRequest = afield.ai.tools.ChecklistUpdateRequest

ChecklistItemInput

typealias ChecklistItemInput = afield.ai.tools.ChecklistItemInput

ChecklistItem

typealias ChecklistItem = afield.ai.tools.ChecklistItem

ChecklistResult

typealias ChecklistResult = afield.ai.tools.ChecklistResult

AgentMemoryState

typealias AgentMemoryState = afield.ai.tools.AgentMemoryState

WorkspaceSymbolSearch

typealias WorkspaceSymbolSearch = afield.ai.tools.WorkspaceSymbolSearch

WorkspaceSymbolSearchResult

typealias WorkspaceSymbolSearchResult = afield.ai.tools.WorkspaceSymbolSearchResult

WorkspaceSymbolMatch

typealias WorkspaceSymbolMatch = afield.ai.tools.WorkspaceSymbolMatch

GetUtcTime

typealias GetUtcTime = afield.ai.tools.GetUtcTime

Remember

typealias Remember = afield.ai.tools.Remember

Recall

typealias Recall = afield.ai.tools.Recall

ToolExecutionContext

typealias ToolExecutionContext = afield.ai.tools.ToolExecutionContext

OpenAiIdeCodexBridge

typealias OpenAiIdeCodexBridge = afield.ai.models.OpenAiIdeCodexBridge

IdeChatHelpers

typealias IdeChatHelpers = afield.ai.conversation.IdeChatHelpers

CompletionCandidate

typealias CompletionCandidate = afield.ai.completion.CompletionCandidate

DocumentGuideTools

typealias DocumentGuideTools = afield.ai.tools.DocumentGuideTools

DocumentGuideListRequest

typealias DocumentGuideListRequest = afield.ai.tools.DocumentGuideListRequest

DocumentGuideReadRequest

typealias DocumentGuideReadRequest = afield.ai.tools.DocumentGuideReadRequest

DocumentGuideSearchRequest

typealias DocumentGuideSearchRequest = afield.ai.tools.DocumentGuideSearchRequest

DocumentGuideCreateRequest

typealias DocumentGuideCreateRequest = afield.ai.tools.DocumentGuideCreateRequest

DocumentGuidePatchRequest

typealias DocumentGuidePatchRequest = afield.ai.tools.DocumentGuidePatchRequest

DocumentGuideAppendRequest

typealias DocumentGuideAppendRequest = afield.ai.tools.DocumentGuideAppendRequest

DocumentGuideToolResult

typealias DocumentGuideToolResult = afield.ai.tools.DocumentGuideToolResult

Ai4AutoBoxNamer.kt

src/jvmMain/kotlin/afield/ai/completion/Ai4AutoBoxNamer.kt — read the source

Ai4CompletionDoc.kt

Drafts documentation, with a language model, for a completion item whose declaration has no KDoc.

Ai4CompletionDoc.resolveDoc finds the item's definition through the Kotlin source index and returns nothing when that definition already carries a KDoc. Otherwise it sends the declaration's source to a fast model and caches the draft in GENERATED_COMPLETION_DOCS_FILE, keyed by symbol, file, line and a hash of the declaration source, so an edited declaration gets a fresh draft. Ai4CompletionDoc.dropStaleGeneratedDocs evicts drafts whose source has changed or gone; it is covered by GeneratedCompletionDocSweepTest. Drafts the docs revision pass declined, listed in GENERATED_COMPLETION_DOCS_DECLINED_FILE, are neither shown nor regenerated.

The model is AFIELD_COMPLETION_DOC_MODEL, defaulting to the fast model.

src/jvmMain/kotlin/afield/ai/completion/Ai4CompletionDoc.kt — read the source

Top-level functions

Declaration Summary
fun generatedCompletionDocsPath(): Path The drafts cache: GENERATED_COMPLETION_DOCS_ENV when set, else GENERATED_COMPLETION_DOCS_FILE in the filestore.

Top-level values

Declaration Summary
const val GENERATED_COMPLETION_DOCS_FILE The generated-completion-doc cache's file name, spelled once.
const val GENERATED_COMPLETION_DOCS_DECLINED_FILE Keys of drafts the docs revision pass looked at and chose not to use, written beside the cache by tools/docs_notes.py (DRAFT_DECLINED_FILE). Completion neither shows nor regenerates them.
const val GENERATED_COMPLETION_DOCS_ENV Points the drafts cache at one file, leaving the rest of the filestore where it is. The docs runner sets it to a fresh copy of a committed cache, so its captures of the drafts read the same input on every machine; tools/docs_notes.py (DRAFT_CACHE_ENV) honours it too.

Ai4CompletionDoc

object Ai4CompletionDoc

Declaration Summary
fun resolveDoc(fileKey: String, label: String, displayText: String, insertText: String, detail: String, existingDoc: String, allCode: String, line: Int, column: Int, documentationSourceName: String = "", documentationSourcePath: String = "", documentationSourceLine: Int = -1): String
fun dropStaleGeneratedDocs(cachePath: Path? = null): StaleSweep Drop cache records the generation path can no longer reach, and report what went.
data class StaleSweep(val considered: Int = 0, val kept: Int = 0, val unverifiable: Int = 0, val dropped: List<String> = emptyList(), val skipped: String? = null) What dropStaleGeneratedDocs did. dropped carries one readable line per removed record.
fun isInOurTree(sourcePath: String): Boolean Whether sourcePath — as the source index records it — is a declaration in this repository's tree (any src/ source set, src/personal included) rather than a dependency's sources jar. The index stores our files repo-relative and library sources as jar: URLs.

Ai4GlslAutocomplete.kt

Language-model autocomplete for shader properties (fragment, vertex, compute and the other shader stages).

Ai4GlslAutocomplete.complete marks the cursor in the box's shader code, wraps it for the stage's language, adds context from the shader include library, and asks a fast model for three candidates, each with a one-line note. It returns them as CompletionCandidates. One model thread is kept per file key, so later requests in the same editor reuse the conversation.

The model is AFIELD_SHADER_AUTOCOMPLETE_MODEL (or AFIELD_GLSL_AUTOCOMPLETE_MODEL), defaulting to the fast model.

src/jvmMain/kotlin/afield/ai/completion/Ai4GlslAutocomplete.kt — read the source

CompletionCandidate

data class CompletionCandidate(val text: String, val documentation: String)

Declaration Summary
val text: String
val documentation: String

Ai4GlslAutocomplete

object Ai4GlslAutocomplete

Declaration Summary
fun complete(fileKey: String, property: String, code: String, context: Map<String, String>, line: Int, column: Int, position: Int, maxOutputTokens: Long = 400): List<CompletionCandidate>

Ai4KotlinAutocomplete.kt

Language-model autocomplete for box Kotlin code.

Ai4KotlinAutocomplete.complete marks the cursor in the code, wraps it as a Kotlin script and asks a fast model for three short candidates, each with a one-line note, returned as KotlinCompletionCandidates. One model thread is kept per file key, so later requests in the same editor reuse the conversation. The Kotlin counterpart of Ai4GlslAutocomplete.

The model is AFIELD_KOTLIN_AUTOCOMPLETE_MODEL, defaulting to the fast model.

src/jvmMain/kotlin/afield/ai/completion/Ai4KotlinAutocomplete.kt — read the source

KotlinCompletionCandidate

data class KotlinCompletionCandidate(val text: String, val documentation: String)

Declaration Summary
val text: String
val documentation: String

Ai4KotlinAutocomplete

object Ai4KotlinAutocomplete

Declaration Summary
fun complete(fileKey: String, code: String, line: Int, column: Int, position: Int, maxOutputTokens: Long = 320): List<KotlinCompletionCandidate>

AiBoxConversation.kt

Where an AI4 chat transcript is stored: on the box for a box's chat, and in a file beside the document for the document chat.

AiBoxConversation reads and writes the ai4_chat_history property and the turn's running flag; the provider's own thread is only a cache that can be reseeded from that transcript. DocumentChatStore keeps the document-level chat in <document>/ai4_document_chat.md.

src/jvmMain/kotlin/afield/ai/conversation/AiBoxConversation.kt — read the source

AiBoxConversation

object AiBoxConversation

The document owns the transcript; provider threads are only disposable continuation caches.

Declaration Summary
const val PROPERTY
const val RUNNING_PROPERTY
fun read(box: Box): String
fun write(box: Box, transcript: String)
fun running(box: Box, value: Boolean)
fun recoverInterrupted(box: Box) Called only when there is no live turn for the box in this server.
fun resumeContext(transcript: String, limit: Int = 64_000): String Full images stay in the saved transcript, but base64 must not consume the model's text budget.

DocumentChatStore

object DocumentChatStore

The document chat's transcript (the chat on the code-use page, about a whole document rather than one box).

Declaration Summary
const val FILE
fun read(info: afield.app.server.SessionInformation?): String
fun appendTurn(info: afield.app.server.SessionInformation?, user: String, reply: String)

AiChatFenceCatalog.kt

The fenced blocks AI4 chat draws as pictures rather than printing as code, read on the JVM from the same catalog the browser renderers read.

src/commonMain/resources/ai4-fence-renderers.json holds one entry per language, each naming the renderer module and export that draws it in the chat panel alongside the syntax guidance and the worked example the model is shown. There are two today, mermaid and math. The JVM reads that file out of its resources and turns it into the paragraph of fence instructions added to the chat system prompt, so a language the model is told about always has a renderer behind it and a language the browser cannot draw is never offered. Only chat mode gets that paragraph; the other instruction modes describe no fences.

The catalog is checked on first read for a known schema version, one entry per language, a lowercase language name and no blank field. A malformed or absent catalog stops the read and says what is wrong with it, instead of leaving the panel one renderer short of what it advertised.

src/jvmMain/kotlin/afield/ai/conversation/AiChatFenceCatalog.kt — read the source

AiLiveConversation.kt

The running transcript of one AI4 chat turn, assembled in the order things happened.

A turn interleaves three kinds of entry: text streamed from the model, the activity lines that say which tool is running, and anything the person typed while the turn was still going. Each is appended as it arrives, and the whole sequence renders into the <<ai>>, <<you>> and <<progress>> markers that a box's ai4_chat_history property stores and aiChatHistoryHtml turns into the chat panel.

Two rules keep that order readable while the turn is live. Consecutive text deltas merge into one block instead of one block per delta, and a new activity line replaces the previous one only where that one was still transient, so a finished tool's result or failure is never overwritten by the next thing to start. The newest activity line of a live turn is marked <<progress-open>> and renders expanded; the earlier ones stay collapsed.

At the end the bridge hands back the turn's final text, which usually repeats what was already streamed. Rendering drops the streamed copy when the final text starts with it and nothing but completion activity follows it, so the reader is left with one answer rather than two.

src/jvmMain/kotlin/afield/ai/conversation/AiLiveConversation.kt — read the source

AiRateLimitRecovery.kt

src/jvmMain/kotlin/afield/ai/conversation/AiRateLimitRecovery.kt — read the source

AiRequestLedger.kt

The on-disk accounting record for every AI4 model request: which endpoint answered, how many tokens it billed, how long it took and how it ended, with none of the prompt and none of the reply.

The OpenAI Responses bridge and the Anthropic messages bridge both write through it, so a session's whole model spend lands in one place whatever provider served it. Lines are appended as JSON under ~/.afield/logs/ai_usage, redirected by the afield.ai.usageLog.path system property or the AFIELD_AI_USAGE_LOG_PATH environment variable. A file name carries the date, the process id and a uuid minted per JVM, so two Field processes logging at once never share a file or contend for a lock.

Four events describe one request. attempt_started opens each attempt; response_usage records the billed token counts, normalised so that input means the whole prompt for either provider and cached input is part of it; retry_wait marks a backoff between attempts; and attempt_finished carries the elapsed time, the error type, the HTTP status and whichever rate-limit headers the provider returned. Usage is recorded before the response is validated, because a stream that failed partway can still have billed.

Writing is best effort. A directory that cannot be written prints one warning to stderr and the request carries on, since losing accounting is not a reason to lose the answer.

src/jvmMain/kotlin/afield/ai/conversation/AiRequestLedger.kt — read the source

AiSessionTree.kt

A box's AI4 conversation recorded as a tree of turns beside its markdown transcript, so a turn can be rewound to or edited and re-sent without losing the abandoned branch.

AiSessionTree reads and writes the ai4_session JSONL sidecar and reconciles it with ai4_chat_history, which remains what the panel shows. AiImageBlobs moves inline images out to <document>/ai4_blobs/ as content-addressed references, and FieldBuildInfo identifies the Field build a turn's checkpoint records.

src/jvmMain/kotlin/afield/ai/conversation/AiSessionTree.kt — read the source

AiSessionTree

object AiSessionTree

A box's AI conversation as a tree, not a list

Declaration Summary
const val PROPERTY
class Session internal constructor(val lines: MutableList<JsonObject>)
fun load(box: Box): Session
fun clear(box: Box)
data class TurnMeta(val kind: String, val property: String, val context: ContextReport? = null, val model: afield.ai.conversation.AiChatModelIdentity? = null, val inputTokens: Long? = null, val outputTokens: Long? = null, val cacheReadTokens: Long? = null, val rounds: Int = -1, val memory: AgentMemoryState.Snapshot? = null, val world: Map<String, Long> = emptyMap(), val opFrom: Int = -1, val opTo: Int = -1, val origin: String = "", val createdBoxes: List<String> = emptyList(), val createdBefore: Map<String, String> = emptyMap(), val createdAfter: Map<String, String> = emptyMap(), val branchBefore: Map<String, String> = emptyMap(), val branchAfter: Map<String, String> = emptyMap()) What a turn knew about itself when it finished; the installer fills it from the turn's result.
fun recordTurn(box: Box, before: String, after: String, meta: TurnMeta): String? Record one finished turn: the transcript went from before (when the turn started) to after. The head is first reconciled with before, so anything that changed the markdown in between is kept as its own node. Returns the new turn's id, or null when the turn added nothing.
data class UserTurn(val ordinal: Int, val text: String, val nodeId: String, val offsetInNode: Int) One user turn on the active path, as the panel numbers them (0-based, in order of appearance).
fun userTurns(session: Session): List<UserTurn> The user turns on the head's path, matching the panel's <<you>> blocks one for one.
data class Rewind(val markdown: String, val restoredMemory: AgentMemoryState.Snapshot?, val abandoned: List<JsonObject>, val userText: String, val branchStates: Map<String, String> = emptyMap())
fun rewind(box: Box, ordinal: Int, expectedText: String?): Rewind Move the head to just before user turn ordinal on the active path, keeping everything after it as a branch. expectedText must match that turn's request (the panel's copy and the server's must agree). Inside an imported block the earlier part becomes a new root, because an import has no finer nodes.
fun currentBranchStates(boxes: Collection<Box>): Map<String, String> The branch states at the head of the most recently active conversation among boxes (the one whose head node is newest), as AgentExtensionRegistry.resumeBranchState restores them when a document opens.
fun lastKnownStates(box: Box): Map<String, String> The extension branch states as this conversation last left them: in the order things happened, each turn's after and each restore's state line, the latest per key. What a restore checks the current state against (AgentExtensionRegistry.restoreBranchStates).
fun noteStates(box: Box, states: Map<String, String>) Record the branch states a rewind or switch left the document in, for the next lastKnownStates.
fun fork(box: Box, id: String, name: String): String Add a named, empty child of an exact saved state. Does not move the active head or restore anything.
fun switchTo(box: Box, index: Int): Rewind Make branch tip index (as branches numbers them) the head.
fun branches(box: Box): String The branches as /branches lists them: tip, turn count, last request, whether it is the head.
fun opOf(node: JsonObject): Triple<Int, Int, String>? The op record of a turn node, for undoing its edits.
fun createdAlongHead(box: Box): List<String> Every box the turns on the current path created.
fun nodesInOrder(box: Box, ids: Collection<String>): List<JsonObject> ids (turn node ids) as nodes, in the order they were recorded.
fun createdBoxesOf(node: JsonObject): List<String>

AiImageBlobs

object AiImageBlobs

Images in session-tree markdown, stored once per document rather than once per line and per backup.

Declaration Summary
fun extract(markdown: String, box: Box): String
fun inline(markdown: String, box: Box): String

FieldBuildInfo

object FieldBuildInfo

Which Field this is: the checkout's commit, whether the tree was dirty, and a hash of the diff — what a capability checkpoint records so a transcript says which code its tools ran on. Read once, in the background, from git in the working directory (a packaged build may ship field-build-info.properties instead, which wins). Not a Gradle-generated resource: one that changes on every commit would make every test task out of date.

Declaration Summary
data class Info(val sha: String, val dirty: Boolean, val diffSha: String, val source: String)
fun current(): Info

AiStatePruning.kt

Housekeeping for the box states AI4 conversations save under <document>/ai4_blobs/state/.

AiStatePruning.sweepOrphans removes states no session tree refers to, and Box.pruneAiStates trims old states along each branch on request, reporting by default rather than deleting.

src/jvmMain/kotlin/afield/ai/conversation/AiStatePruning.kt — read the source

Top-level functions

Declaration Summary
fun Box.pruneAiStates(keepPerBranch: Int = 3, dryRun: Boolean = true): AiStatePruning.Report? Free disk taken by this document's saved AI4 box states, keeping the latest keepPerBranch along every branch of every conversation and the state at every fork. A turn whose state is removed can still be rewound to, but its box is not restored. Reports what it would remove unless dryRun is false.

AiStatePruning

object AiStatePruning

Keeping <document>/ai4_blobs/state/ (the box states AI4 conversations save after each request that changes them) from growing without bound, without deleting a state a turn can still be restored to.

Declaration Summary
data class Report(val kept: Int, val removed: List<File>, val dryRun: Boolean) What a sweep or prune found: the files it removed (or would remove), and the bytes they hold.
const val GRACE_MILLIS A state file younger than this may belong to a request still writing its turn, so it is never swept.
fun referenced(sessions: List<AiSessionTree.Session>): Set<String> Every state hash any of sessions refers to.
fun retained(sessions: List<AiSessionTree.Session>, keepPerBranch: Int): Set<String> The hashes Box.pruneAiStates keeps: along the path to every tip, the last keepPerBranch distinct after states of each extension key, and every state recorded at a node with more than one child.
fun sweep(folder: File, keep: Set<String>, dryRun: Boolean, graceMillis: Long = GRACE_MILLIS, now: Long = System.currentTimeMillis()): Report The state files in folder whose hash is not in keep, older than graceMillis; deleted unless dryRun.
fun sweepOrphans(box: Box, dryRun: Boolean = false): Report? Delete the states in box's document that no session tree refers to.

AiToastPolicy.kt

Which AI4 log lines become a toast on the canvas, and which stay in the log.

A turn logs a great deal, and almost none of it is addressed to the person watching. Only events carrying the ai.user channel are candidates, which is what userNotice writes; everything else a bridge or a tool logs is diagnostics, and the log viewer is where it belongs.

Candidates are then thinned twice. The same text repeated inside fifteen seconds is dropped, and an ordinary notice arriving within five seconds of the last one is dropped as well, so a burst of activity shows one line rather than six. A notice also carrying warning or error skips the five-second gate and is held back only by the repeat test.

toolActivity is the other half of the policy. It maps a handful of tool names to one short phrase each, "Checking the view…" or "Updating the document…", and says nothing for the rest: reads, discovery and unfamiliar tools are already visible in the chat panel and the tool pool, and a tool's implementation name is not a description of what was asked for.

src/jvmMain/kotlin/afield/ai/conversation/AiToastPolicy.kt — read the source

AiToolFailure.kt

What a chat turn does when a tool throws. Three answers live here: which failures end the turn rather than becoming the call's output, what the model reads in place of a result, and what is written back for a call the turn ended before answering.

The provider's thread has to hold an output for every call it made, so a turn that stops mid-round still writes one for each call left outstanding; without it, every later turn in that chat is refused for a function call with no output.

src/jvmMain/kotlin/afield/ai/conversation/AiToolFailure.kt — read the source

AiTurnCancellation.kt

Telling a stopped AI4 turn apart from one that failed.

Stopping a turn arrives as a CancellationException or an InterruptedException, but rarely at the top of the throwable: a future, a coroutine or a reflective tool call wraps it, sometimes several layers down, so the type that was thrown says nothing by itself. The one function here walks the cause chain for either of those two, guarding against a cycle on the way.

It is asked wherever the agent loop would otherwise swallow an exception and carry on. Saving and restoring a conversation's branch state both do that, and both rethrow instead when this answers yes, so a turn the person stopped is not written into the transcript as a step that failed.

src/jvmMain/kotlin/afield/ai/conversation/AiTurnCancellation.kt — read the source

AiTurnInbox.kt

The queue for messages a person sends to a box's chat while its AI turn is still running.

The bridge hands waiting messages to the model at the next tool-round boundary, and whatever is left when the turn ends becomes the box's next turn. See AiTurnInbox.

src/jvmMain/kotlin/afield/ai/conversation/AiTurnInbox.kt — read the source

AiTurnInbox

class AiTurnInbox

Messages a person sends to a box's chat while that box's AI turn is still running.

Declaration Summary
data class Pending(val shown: String, val forModel: String, val images: List<String> = emptyList(), val assist: AiAssist? = null, val session: io.ktor.websocket.WebSocketSession? = null, val fromField: Boolean = false)
var onDelivered: ((List<Pending>) -> Unit)? Called, on the bridge's thread, with the messages it has just handed to the model.
var onQueued: (() -> Unit)? Called by whoever queued a message, after offer and outside any lock, so the turn can show it.
fun offer(message: Pending): Boolean Queue message; false once the turn has ended and taken what was left.
fun queued(): List<Pending>
fun queuedMarkdown(): String The waiting messages as transcript blocks, to show after the live turn; empty when none wait.
fun drainForModel(): List<Pending> Take everything waiting, for the bridge to send now, and report it delivered.
fun close(): List<Pending> The turn has ended: refuse further messages and return those never delivered, for the next turn.

AiWorldUndo.kt

Which boxes an abandoned turn created can be deleted without taking anyone's later work with them.

The world undo after a rewind (ai-chat-rewind-world) undoes the abandoned turns' property edits strictly (an edit changed since is skipped) and deletes the boxes they created. A person may since have typed into one of those boxes, moved it, or wired it to their own; the panel promises that anything changed since is left alone. So every turn records a fingerprint of each box its conversation has created, as the turn starts and as it ends (AiSessionTree.TurnMeta.createdBefore / createdAfter), and a box is deleted only when it is exactly as the last abandoned turn left it, and nobody changed it between abandoned turns either.

A fingerprint covers the box's name, frame, links to other boxes and every property that is saved with the document, apart from the chat's own (ai4_*). Values are written out typed and quoted, lists in their own order (their order can be a polygon's vertices or an authored sequence) and only sets and maps sorted, so two different values never print alike. The one normalisation is for LOADER_DEFAULTS, properties that loading a document fills in when absent, so an untouched box still matches its own fingerprint after its document is reopened.

src/jvmMain/kotlin/afield/ai/conversation/AiWorldUndo.kt — read the source

AiWorldUndo

object AiWorldUndo

Fingerprints the boxes a chat turn created and decides which of them a world undo may delete.

Declaration Summary
fun fingerprint(box: Box): String A 16-hex-digit SHA-256 prefix over box's name, frame, links and saved properties, excluding ai4_*.
fun fingerprints(boxes: Map<String, Box>, uids: Collection<String>): Map<String, String> Fingerprints of the live boxes among uids.
data class Plan(val delete: List<Box>, val keep: List<Pair<Box, String>>) The outcome of plan: boxes to delete, and boxes to keep paired with the reason they stay.
fun plan(abandoned: List<JsonObject>, boxes: Map<String, Box>): Plan For abandoned (turn nodes, in the order they happened), the boxes they created that may be deleted, and those that must stay, each with the reason.

ContextPipeline.kt

The parts an AI4 turn's prompt is built from, tagged with how long each is kept and whether it changes between turns.

A TurnContext holds a turn's ContextFragments and the request that follows them; each fragment carries a FragmentRetention (stored with the message, or sent for this turn only) and a Stability (cacheable across turns, or not). The provider bridges decide where each kind goes, and TurnContext.legacyPrompt rebuilds the single-string prompt for bridges that send one. ContextReport is what /context shows about a turn, and ContextPipeline.enabledByDefault reads the afield.ai4.ephemeralInstructions switch.

src/jvmMain/kotlin/afield/ai/conversation/ContextPipeline.kt — read the source

Top-level functions

Declaration Summary
fun keptFor(placement: String, retention: String, stability: String): Kept Sent with this turn's requests and then dropped.
fun ContextReport.toMarkdown(): String A context report as the markdown /context shows: one row per fragment, largest parts easy to spot.

FragmentRetention

enum class FragmentRetention

What a turn tells the model, as parts rather than one string

Constants: DURABLE, EPHEMERAL.

Stability

enum class Stability

Constants: STABLE, VOLATILE.

ContextFragment

data class ContextFragment(val source: String, val text: String, val retention: FragmentRetention = FragmentRetention.EPHEMERAL, val stability: Stability = Stability.STABLE)

Declaration Summary
val source: String Where it came from, as /context shows it: core.contract, tools.graph, guides.catalogue, lessons, ...
val text: String The text, including the separator that joins it to the fragment before (so a join is a plain concat).
val retention: FragmentRetention
val stability: Stability
val chars: Int
val approxTokens: Int A rough token count (four characters a token); for showing proportions, never for billing.
val sha8: String

TurnContext

data class TurnContext(val fragments: List<ContextFragment>, val rest: String)

One turn's prompt in parts: the fragments of instructions and request, then rest — what follows them in the user's message (the prompt itself, the code an editor turn rewrites).

Declaration Summary
fun legacyPrompt(): String Exactly what a turn sent before fragments existed: every fragment, then a blank line, then rest.
fun durablePrompt(): String The user message to store: the durable fragments and rest.
fun ephemeral(stability: Stability): List<ContextFragment>
fun ephemeralText(stability: Stability): String The ephemeral fragments of one stability, joined, with the leading separator dropped.
fun allEphemeralText(): String Stable first, then volatile: the order that keeps a cached prefix valid longest.
fun instructionHash(): String A hash of the ephemeral instructions, for checkpoints and for telling two turns' instructions apart.
fun report(placement: String): ContextReport

ContextFragmentInfo

data class ContextFragmentInfo(val source: String, val retention: String, val stability: String, val chars: Int, val approxTokens: Int, val sha8: String)

What /context shows and the tool I/O log records: fragment metadata, never the text.

Declaration Summary
val source: String
val retention: String
val stability: String
val chars: Int
val approxTokens: Int
val sha8: String

ContextReport

data class ContextReport(val placement: String, val fragments: List<ContextFragmentInfo>, val restChars: Int, val instructionHash: String, val cacheReadTokens: Long? = null, val cacheWriteTokens: Long? = null, val inputTokens: Long? = null, val tools: List<String> = emptyList(), val dynamicTools: List<DynamicToolInfo> = emptyList())

Declaration Summary
val placement: String How the bridge placed the ephemeral parts: instructions (OpenAI), system+prefix (Anthropic), inline (legacy).
val fragments: List<ContextFragmentInfo>
val restChars: Int
val instructionHash: String
val cacheReadTokens: Long?
val cacheWriteTokens: Long?
val inputTokens: Long?
val tools: List<String> The tools whose schemas the model was sent, by name.
val dynamicTools: List<DynamicToolInfo> Box-authored tools in the conversation, with the box that registered each and its source revision then.

DynamicToolInfo

data class DynamicToolInfo(val name: String, val box: String, val sourceRevision: String)

ContextPipeline

object ContextPipeline

AFIELD_AI4_EPHEMERAL_INSTRUCTIONS=off (or -Dafield.ai4.ephemeralInstructions=off) puts every turn back on TurnContext.legacyPrompt; an agent task's context=legacy does the same for one task (the A/B arm).

Declaration Summary
val enabledByDefault: Boolean

Kept

enum class Kept(val label: String)

Where a fragment ends up, given where the bridge put it (ContextReport.placement). The fragment classes describe intent; the provider decides retention, and these differ: OpenAI's conversations live on its side, so anything sent inside one stays there.

Constants: EVERY_TURN, ONCE, STORED, EVERY_REQUEST, THIS_TURN.

IdeChatHelpers.kt

The entry points an editor or chat panel calls to run one AI4 turn against a box's code.

IdeChatHelpers wraps an LlmBridge. IdeChatHelpers.chatFile, IdeChatHelpers.chatInline and IdeChatHelpers.chatError build the turn's instructions and tool context, run it through the bridge, and return an IdeChatHelpers.HighLevelResult with the edited code, commentary and tool transcript; IdeChatHelpers.chatGeneral runs a chat turn and returns the bridge's own result.

src/jvmMain/kotlin/afield/ai/conversation/IdeChatHelpers.kt — read the source

IdeChatHelpers

class IdeChatHelpers(private val bridge: LlmBridge, private val log: LogSink = StdoutLog, private val ephemeralInstructions: Boolean = ContextPipeline.enabledByDefault)

Runs AI4 editor and chat turns through one LlmBridge.

Declaration Summary
data class HighLevelResult(val editedCode: String, val commentary: List<String>, val rawResponseId: String, val threadState: OpenAiIdeCodexBridge.ThreadState, val toolTranscript: List<OpenAiIdeCodexBridge.ToolTranscriptEntry> = emptyList(), val contextReport: ContextReport? = null)
fun chatFile(state: OpenAiIdeCodexBridge.ThreadState, code: String, lang: LanguageSpec, userPrompt: String, attachments: List<ContextAttachment> = emptyList(), resolveAttachment: ((String) -> AttachmentResponse?)? = null, listDefinitions: (() -> ListDefinitionsResult)? = null, listFileContents: (() -> ListContentsResult)? = null, listKotlinContextVariables: ((ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult)? = null, evaluateKotlinExpression: ((EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult)? = null, editOtherFile: ((EditFileRequest) -> EditFileResponse)? = null, compileKotlin: ((CompileKotlinRequest) -> CompileKotlinResult)? = null, reloadShaders: ((ReloadShadersRequest) -> ReloadShadersResult)? = null, captureRenderWindow: ((CaptureRenderWindowRequest) -> CaptureRenderWindowResult)? = null, listMacros: ((MacroListRequest) -> MacroListResult)? = null, readMacro: ((MacroReadRequest) -> MacroReadResult)? = null, writeMacro: ((MacroWriteRequest) -> MacroWriteResult)? = null, runMacro: ((MacroRunRequest) -> MacroRunResult)? = null, codeUseQuery: ((CodeUseQueryRequest) -> CodeUseQueryResult)? = null, semanticRetrievalSearch: ((SemanticRetrievalSearchRequest) -> SemanticRetrievalToolResult)? = null, semanticRetrievalStatus: (() -> SemanticRetrievalToolResult)? = null, semanticRetrievalRebuild: (() -> SemanticRetrievalToolResult)? = null, agentMemory: AgentMemoryState? = null, documentGuides: DocumentGuideTools? = null, workspaceSymbolSearch: ((WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult)? = null, workspaceSearch: ((WorkspaceSearchRequest) -> WorkspaceSearchResult)? = null, focus: ToolFocus? = null, dynamicTools: List<BoxTool> = emptyList(), fieldGraph: afield.ai.agent.FieldGraphTools? = null, inbox: AiTurnInbox? = null, vectorStoreIds: List<String> = emptyList(), onTextDelta: ((String) -> Unit)? = null, onProgress: ((OpenAiIdeCodexBridge.ProgressUpdate) -> Unit)? = null): HighLevelResult
fun chatGeneral(state: OpenAiIdeCodexBridge.ThreadState, code: String, lang: LanguageSpec, userPrompt: String, imageUrls: List<String> = emptyList(), attachments: List<ContextAttachment> = emptyList(), resolveAttachment: ((String) -> AttachmentResponse?)? = null, listDefinitions: (() -> ListDefinitionsResult)? = null, listFileContents: (() -> ListContentsResult)? = null, listKotlinContextVariables: ((ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult)? = null, evaluateKotlinExpression: ((EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult)? = null, editOtherFile: ((EditFileRequest) -> EditFileResponse)? = null, compileKotlin: ((CompileKotlinRequest) -> CompileKotlinResult)? = null, reloadShaders: ((ReloadShadersRequest) -> ReloadShadersResult)? = null, captureRenderWindow: ((CaptureRenderWindowRequest) -> CaptureRenderWindowResult)? = null, listMacros: ((MacroListRequest) -> MacroListResult)? = null, readMacro: ((MacroReadRequest) -> MacroReadResult)? = null, writeMacro: ((MacroWriteRequest) -> MacroWriteResult)? = null, runMacro: ((MacroRunRequest) -> MacroRunResult)? = null, codeUseQuery: ((CodeUseQueryRequest) -> CodeUseQueryResult)? = null, semanticRetrievalSearch: ((SemanticRetrievalSearchRequest) -> SemanticRetrievalToolResult)? = null, semanticRetrievalStatus: (() -> SemanticRetrievalToolResult)? = null, semanticRetrievalRebuild: (() -> SemanticRetrievalToolResult)? = null, agentMemory: AgentMemoryState? = null, documentGuides: DocumentGuideTools? = null, workspaceSymbolSearch: ((WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult)? = null, workspaceSearch: ((WorkspaceSearchRequest) -> WorkspaceSearchResult)? = null, focus: ToolFocus? = null, dynamicTools: List<BoxTool> = emptyList(), fieldGraph: afield.ai.agent.FieldGraphTools? = null, inbox: AiTurnInbox? = null, vectorStoreIds: List<String> = emptyList(), onTextDelta: ((String) -> Unit)? = null, onProgress: ((OpenAiIdeCodexBridge.ProgressUpdate) -> Unit)? = null, maxToolRounds: Int = 24, suspendable: Boolean = false): OpenAiIdeCodexBridge.SendResult
fun chatGeneral(state: OpenAiIdeCodexBridge.ThreadState, code: String, lang: LanguageSpec, userPrompt: String, ctx: ToolExecutionContext, imageUrls: List<String> = emptyList(), vectorStoreIds: List<String> = emptyList(), maxToolRounds: Int = 24, extraInstructions: String = "", onTextDelta: ((String) -> Unit)? = null, onProgress: ((OpenAiIdeCodexBridge.ProgressUpdate) -> Unit)? = null): OpenAiIdeCodexBridge.SendResult The document/general chat turn, given an already-built tool context. Every AI4 prose turn goes through here: the box panel's general kind and the document chat (via the overload above), and AgentTaskRunner, which is a headless entry into this same turn rather than a harness of its own.
fun chatInline(state: OpenAiIdeCodexBridge.ThreadState, code: String, lang: LanguageSpec, lineNumber: Int, userPrompt: String, attachments: List<ContextAttachment> = emptyList(), resolveAttachment: ((String) -> AttachmentResponse?)? = null, listDefinitions: (() -> ListDefinitionsResult)? = null, listFileContents: (() -> ListContentsResult)? = null, listKotlinContextVariables: ((ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult)? = null, evaluateKotlinExpression: ((EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult)? = null, editOtherFile: ((EditFileRequest) -> EditFileResponse)? = null, compileKotlin: ((CompileKotlinRequest) -> CompileKotlinResult)? = null, reloadShaders: ((ReloadShadersRequest) -> ReloadShadersResult)? = null, captureRenderWindow: ((CaptureRenderWindowRequest) -> CaptureRenderWindowResult)? = null, listMacros: ((MacroListRequest) -> MacroListResult)? = null, readMacro: ((MacroReadRequest) -> MacroReadResult)? = null, writeMacro: ((MacroWriteRequest) -> MacroWriteResult)? = null, runMacro: ((MacroRunRequest) -> MacroRunResult)? = null, codeUseQuery: ((CodeUseQueryRequest) -> CodeUseQueryResult)? = null, semanticRetrievalSearch: ((SemanticRetrievalSearchRequest) -> SemanticRetrievalToolResult)? = null, semanticRetrievalStatus: (() -> SemanticRetrievalToolResult)? = null, semanticRetrievalRebuild: (() -> SemanticRetrievalToolResult)? = null, agentMemory: AgentMemoryState? = null, documentGuides: DocumentGuideTools? = null, workspaceSymbolSearch: ((WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult)? = null, workspaceSearch: ((WorkspaceSearchRequest) -> WorkspaceSearchResult)? = null, focus: ToolFocus? = null, dynamicTools: List<BoxTool> = emptyList(), fieldGraph: afield.ai.agent.FieldGraphTools? = null, inbox: AiTurnInbox? = null, vectorStoreIds: List<String> = emptyList(), onTextDelta: ((String) -> Unit)? = null, onProgress: ((OpenAiIdeCodexBridge.ProgressUpdate) -> Unit)? = null): HighLevelResult
fun chatError(state: OpenAiIdeCodexBridge.ThreadState, code: String, lang: LanguageSpec, errorMessage: String, attachments: List<ContextAttachment> = emptyList(), resolveAttachment: ((String) -> AttachmentResponse?)? = null, listDefinitions: (() -> ListDefinitionsResult)? = null, listFileContents: (() -> ListContentsResult)? = null, listKotlinContextVariables: ((ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult)? = null, evaluateKotlinExpression: ((EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult)? = null, editOtherFile: ((EditFileRequest) -> EditFileResponse)? = null, compileKotlin: ((CompileKotlinRequest) -> CompileKotlinResult)? = null, reloadShaders: ((ReloadShadersRequest) -> ReloadShadersResult)? = null, captureRenderWindow: ((CaptureRenderWindowRequest) -> CaptureRenderWindowResult)? = null, listMacros: ((MacroListRequest) -> MacroListResult)? = null, readMacro: ((MacroReadRequest) -> MacroReadResult)? = null, writeMacro: ((MacroWriteRequest) -> MacroWriteResult)? = null, runMacro: ((MacroRunRequest) -> MacroRunResult)? = null, codeUseQuery: ((CodeUseQueryRequest) -> CodeUseQueryResult)? = null, semanticRetrievalSearch: ((SemanticRetrievalSearchRequest) -> SemanticRetrievalToolResult)? = null, semanticRetrievalStatus: (() -> SemanticRetrievalToolResult)? = null, semanticRetrievalRebuild: (() -> SemanticRetrievalToolResult)? = null, agentMemory: AgentMemoryState? = null, documentGuides: DocumentGuideTools? = null, workspaceSymbolSearch: ((WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult)? = null, workspaceSearch: ((WorkspaceSearchRequest) -> WorkspaceSearchResult)? = null, focus: ToolFocus? = null, dynamicTools: List<BoxTool> = emptyList(), fieldGraph: afield.ai.agent.FieldGraphTools? = null, inbox: AiTurnInbox? = null, vectorStoreIds: List<String> = emptyList(), onTextDelta: ((String) -> Unit)? = null, onProgress: ((OpenAiIdeCodexBridge.ProgressUpdate) -> Unit)? = null): HighLevelResult

Instructions.kt

The instruction text AI4 sends the model with each turn, per kind of turn.

InstructionMode names the kinds (general, inline, error, chat). buildInstructionFragments assembles a turn's instructions as ContextFragments from the base contract, the tool guidance and the document's context, and buildDynamicInstructions joins them into one string. generalInstructions, inlineInstructions, errorInstructions and generalChatInstructions are the per-mode texts.

src/jvmMain/kotlin/afield/ai/conversation/Instructions.kt — read the source

Top-level functions

Declaration Summary
fun generalInstructions(lang: LanguageSpec): String
fun generalChatInstructions(lang: LanguageSpec, prompt: String): String
fun generalChatContract(lang: LanguageSpec): String The chat contract without the request: the part that is the same on every turn.
fun inlineInstructions(lang: LanguageSpec, prompt: String, line: Int): String
fun errorInstructions(lang: LanguageSpec, errorMessage: String): String
fun buildDynamicInstructions(mode: InstructionMode, lang: LanguageSpec, userPrompt: String?, errorMessage: String?, lineNumber: Int?, ctx: ToolExecutionContext): String The whole instruction block as one string: buildInstructionFragments, joined.
fun buildInstructionFragments(mode: InstructionMode, lang: LanguageSpec, userPrompt: String?, errorMessage: String?, lineNumber: Int?, ctx: ToolExecutionContext): List<ContextFragment> A turn's instructions as ContextFragments, in order; joined, they are buildDynamicInstructions. See TurnContext for what the bridges do with each kind. The request itself is durable only where this is the one place it appears (inline and error turns); a chat turn's request follows in the message anyway.
fun fieldGraphToolGuidance(editorTurn: Boolean, seesOutput: Boolean = true): String What the chat is told about the native box graph and execution tools. One text for every AI4 path that wires ToolExecutionContext.fieldGraph — the box panel, the document chat and headless AgentTasks — so the tools cannot be described two ways.

InstructionMode

enum class InstructionMode

Constants: General, Inline, Error, Chat.

TurnSuspension.kt

How a tool result ends a model's turn early so the conversation can wait for study builds without spending model calls.

A tool result implementing SuspendsTurn carries a TurnSuspension: the AwaitedBuild jobs and a deadline. The bridge stops after the current round and keeps that round's tool outputs as a SuspendedRound, which the resuming turn sends ahead of its own prompt.

src/jvmMain/kotlin/afield/ai/conversation/TurnSuspension.kt — read the source

SuspendsTurn

interface SuspendsTurn

A tool result that ends the model's turn when suspension is set: the bridge finishes the round it is in, makes no further model request, and returns the suspension in OpenAiIdeCodexBridge.SendResult. Someone else resumes the conversation later (StudyBuildAwaits for AwaitStudyBuildsRequest), so the time spent waiting costs no model calls.

Declaration Summary
val suspension: TurnSuspension?

TurnSuspension

data class TurnSuspension(val jobs: List<AwaitedBuild> = emptyList(), val deadlineMs: Long = 0L, val question: AskedQuestion? = null)

What a suspended turn waits for: study-build jobs and how long it may wait for them, or the person's answer to a question (AskUserQuestionRequest), which has no deadline: their next message is the answer.

Declaration Summary
val jobs: List<AwaitedBuild>
val deadlineMs: Long Epoch milliseconds after which the conversation resumes whatever the jobs' state; 0 with no jobs.
val question: AskedQuestion?
fun merge(other: TurnSuspension): TurnSuspension One round may await more than once (two runners): wait for every job, until the later deadline. The first question stands.
fun describe(): String For logs: what the turn waits for.

AskedQuestion

data class AskedQuestion(val question: String, val choices: List<String> = emptyList())

A question the agent asked the person, and the answers it suggests (possibly none).

AwaitedBuild

data class AwaitedBuild(val boxId: String, val name: String, val jobId: String, val stage: String = "", val label: String = "", val directory: String = "")

One awaited attempt: the runner (its box and name), the job, and what to call it in the chat.

Declaration Summary
val boxId: String
val name: String
val jobId: String
val stage: String
val label: String
val directory: String The runner's directory, so a job can still be read from disk when the runner is not running.

SuspendedRound

data class SuspendedRound(val outputs: List<PendingToolOutput>)

The tool outputs of the round that suspended a turn, which the provider has not seen yet. The next turn sends them first: on OpenAI's chained responses its previous_response_id is still the response that made these calls, and every call must have an output before anything else can follow it; on Anthropic, whose history this bridge keeps, the tool_use blocks need their tool_results in the next user message.

PendingToolOutput

data class PendingToolOutput(val callId: String, val name: String, val outputJson: String, val isError: Boolean = false)

A finished call's output as the provider will receive it. isError marks a failed call (Anthropic).

ChatArchive.kt

The chat archive: a saved AI4 conversation, read from disk as a tree

An AI4 chat is saved as two box properties (AiBoxConversation, AiSessionTree): - ai4_chat_history, the transcript the panel shows: the current branch only, images inline. - ai4_session, the session tree: every turn as a node with a parent, including turns rewound away, with a timestamp, the model, token usage and images as ai4img: references into <document>/ai4_blobs/.

This reads both from a document folder without opening the document. No Field server or box is involved, so it is safe to point at a document someone is working in. The tree is the source, and the transcript is used to check it: transcript text past the tree's head (a verdict appended by an agent task, an interrupted turn) becomes an unrecorded node, and a transcript that no longer starts with the head (a hand edit) becomes an unrecorded root. That is what AiSessionTree would record the next time it reconciles the box.

Addresses

Every block has an address, <nodeId>.b<index>: the session-tree node that added it, and its position in that node's markdown. Node ids are written once and never reused, so an address stays valid as the chat grows, and a block in an abandoned branch has one just as a block on the current branch does. Summaries written over a chat (lenses) cite these addresses.

src/jvmMain/kotlin/afield/ai/conversation/chatlog/ChatArchive.kt — read the source

ChatArchive

object ChatArchive

Reads the saved AI4 chats of a document folder from disk, without opening the document.

Declaration Summary
const val HISTORY
const val SESSION
class StoredChat(val documentDir: File, val boxId: String, val boxName: String?, val history: String, val session: String) A chat as stored: one box's transcript and session tree.
fun chats(documentDir: File): List<StoredChat> Every box in documentDir with a saved AI4 chat.
fun tree(chat: StoredChat, families: AiChatToolFamilies = ChatToolFamilies.default): ChatTree
fun lensTree(chat: StoredChat, nodeId: String, title: String, markdown: String, families: AiChatToolFamilies = ChatToolFamilies.default): ChatTree A lens read as a chat: one node, nodeId, holding the lens's blocks, so a lens can be outlined, read, cited and written over like any chat. Its pictures are the chat's, so chat is the chat at the bottom of the chain.

ChatCall

data class ChatCall(val call: AiChatToolCall, val family: String)

A tool call with the family AiChatToolFamilies puts it in.

ChatImageEntry

data class ChatImageEntry(val image: AiChatImage, val resolved: Boolean?)

Declaration Summary
val image: AiChatImage
val resolved: Boolean? For an ai4img: reference, whether its blob is on disk; true for an inline image; null otherwise.

ChatBlockEntry

data class ChatBlockEntry(val address: String, val node: String, val turn: Int? = null, val role: String, val kind: String, val title: String, val call: ChatCall? = null, val calls: List<ChatCall> = emptyList(), val images: List<ChatImageEntry> = emptyList(), val chars: Int, val model: afield.ai.conversation.AiChatModelIdentity? = null, val sources: List<String> = emptyList(), @kotlinx.serialization.Transient val text: String = "")

One block of a chat with what the archive knows about it. kind is user, reply, tool (one call; a tool program's own calls are in calls), build (a study-build status line), failure (the turn failed), status (progress with no tool) or queued.

Declaration Summary
val address: String
val node: String
val turn: Int? For a user block on the current branch, its turn number from 1.
val role: String
val kind: String
val title: String
val call: ChatCall?
val calls: List<ChatCall>
val images: List<ChatImageEntry>
val chars: Int
val model: afield.ai.conversation.AiChatModelIdentity? The model a reply records (<<model:…>>), so a re-rendered block keeps its badge.
val sources: List<String> For a block of a lens, the stretches it stands for (<<source:…>>).
val text: String The block's markdown, images as references; left out of the index file.
val allCalls: List<ChatCall> The call itself and a program's calls: every tool call this block records.
val failed: Boolean

ChatNode

data class ChatNode(val id: String, val parent: String?, val type: String, val ts: Long? = null, val onHead: Boolean, val user: String? = null, val model: String? = null, val inputTokens: Long? = null, val outputTokens: Long? = null, val rounds: Int? = null, val blocks: List<ChatBlockEntry>, val chat: String? = null)

A session-tree node. type is turn, append, import or, for transcript text the tree has not recorded yet, unrecorded. onHead says whether it is on the current branch.

Declaration Summary
val id: String
val parent: String?
val type: String
val ts: Long?
val onHead: Boolean
val user: String?
val model: String?
val inputTokens: Long?
val outputTokens: Long?
val rounds: Int?
val blocks: List<ChatBlockEntry>
val chat: String? In a series (ChatlogStore), the chat this node comes from.

ChatTree

data class ChatTree(val document: String, val box: String, val boxName: String?, val head: String?, val nodes: List<ChatNode>)

Declaration Summary
val document: String
val box: String
val boxName: String?
val head: String?
val nodes: List<ChatNode>
fun node(id: String): ChatNode?
fun children(id: String?): List<ChatNode>
val roots: List<ChatNode>
val tips: List<ChatNode> Tips of every branch: nodes nothing else continues from.
val headPath: List<ChatNode> Nodes along the current branch, root first.
fun block(address: String): ChatBlockEntry?
fun pathTo(id: String): List<ChatNode> Nodes from the root to id, root first.
fun span(from: String, to: String): List<ChatBlockEntry>? The blocks from from to to inclusive, in reading order along the branch that leads to to: so a range may start on the shared trunk and end on an abandoned branch. Null when either end does not exist or from does not come before to on that branch.
val headBlocks: List<ChatBlockEntry> The current branch's blocks in reading order.

ChatToolFamilies

object ChatToolFamilies

The family table the archive uses unless given another: the ai4-chat-tool-families.json resource.

Declaration Summary
val default: AiChatToolFamilies by lazy

ChatArchiveCli.kt

Chat archive statistics from the command line

ChatArchiveCli reads saved AI4 chats from document folders through ChatArchive and prints a ChatStats row per chat (shape, tool calls by family, images, tokens), as a table or JSON, or prints the outline and read views an agent would see (ChatLogViews). It never opens a document in Field.

src/jvmMain/kotlin/afield/ai/conversation/chatlog/ChatArchiveCli.kt — read the source

ChatStats

data class ChatStats(val document: String, val box: String, val boxName: String?, val nodes: Int, val offHeadNodes: Int, val branches: Int, val unrecorded: Boolean, val userTurns: Int, val abandonedUserTurns: Int, val replies: Int, val toolCalls: Int, val programCalls: Int, val failedCalls: Int, val failures: Int, val families: Map<String, Int>, val toolImages: Int, val attachments: Int, val missingImages: Int, val inputTokens: Long, val outputTokens: Long, val toolMs: Long, val firstTs: Long?, val lastTs: Long?)

Counts over one chat tree: its shape, its tool calls by family, its pictures, its cost.

Declaration Summary
val document: String
val box: String
val boxName: String?
val nodes: Int
val offHeadNodes: Int
val branches: Int
val unrecorded: Boolean
val userTurns: Int
val abandonedUserTurns: Int
val replies: Int
val toolCalls: Int
val programCalls: Int
val failedCalls: Int
val failures: Int
val families: Map<String, Int>
val toolImages: Int
val attachments: Int
val missingImages: Int
val inputTokens: Long
val outputTokens: Long
val toolMs: Long
val firstTs: Long?
val lastTs: Long?

ChatArchiveCli

object ChatArchiveCli

afield.ai.conversation.chatlog.ChatArchiveCli: read saved AI4 chats from document folders and report on them. It reads only; it never opens a document in Field.

Declaration Summary
fun main(args: Array<String>)
fun table(stats: List<ChatStats>): String

ChatLensViewer.kt

The lens viewer

A lens beside the chat it summarizes, both drawn by the chat panel's renderer, with a band from each lens block to the stretch of the chat it stands for (chatLens.js). A box shows one with

showChatLens("house-machine-10", "storyline")
in a box whose document has run registerChatlogTools: the viewer reads chats and lenses through that document's ChatlogStore, over the routes in ChatlogRoutes.kt, which answer only for documents that registered one.

src/jvmMain/kotlin/afield/ai/conversation/chatlog/ChatLensViewer.kt — read the source

Top-level functions

Declaration Summary
fun Box.showChatLens(chat: String, lens: String, height: Int = 900, width: Int? = null) Show lens beside chat in this box's lens output channel. chat may itself be a lens (<chat>#<lens>): the viewer then shows every level of the chain, each beside the one it summarizes.

ChatLensViewer

object ChatLensViewer

Holds each registering document's ChatlogStore and builds the payload the lens viewer page fetches.

Declaration Summary
fun register(document: String, store: ChatlogStore) Called by registerChatlogTools: document is the registering document's folder.
fun payload(document: String, chat: String, lens: String): JsonObject What the viewer page fetches: one column per level of the chain, top first. lens over chat gives the lens and the chat; over <chat>#<lens> it gives three columns, and so on down to the chat itself. Every block carries its address, and lens blocks their citations, so the page can draw a band between each pair of neighbouring columns.

ChatLogViews.kt

What an agent reads of a chat

Text views of a ChatTree for AI4 agents that write lenses (registerChatlogTools). Every view names blocks by address, so what the agent reads is what it cites. Tool activity reads as round lines in the panel's own words (AiChatRound); images read as their captions and ai4img: references, never as bytes.

src/jvmMain/kotlin/afield/ai/conversation/chatlog/ChatLogViews.kt — read the source

ChatLogViews

object ChatLogViews

The outline and read views of a ChatTree, as text an agent can cite by block address.

Declaration Summary
fun outline(tree: ChatTree, families: AiChatToolFamilies = ChatToolFamilies.default): String The map of a chat: every user turn with its time, the request in full, and what followed: replies as their first line, rounds as round lines, images as captions. Then each abandoned branch the same way.
data class Page(val text: String, val next: String?) One page of read: the text, and where to continue if it stopped early.
fun read(tree: ChatTree, from: String?, to: String?, detail: String = "text", maxChars: Int = 20_000, families: AiChatToolFamilies = ChatToolFamilies.default): Page The chat as text from from to to (the whole current branch when both are null), each block headed by its address. text gives requests and replies in full and rounds as round lines; full adds every tool block's own text. Stops before maxChars and says where to continue.
data class Hit(val address: String, val kind: String, val snippet: String)
fun search(tree: ChatTree, query: String, family: String? = null, limit: Int = 40): List<Hit> Blocks on any branch whose text contains query (case-insensitive), optionally only tool calls of family.
data class ImageEntry(val address: String, val ref: String?, val caption: String?, val conclusion: String?)
fun images(tree: ChatTree, from: String? = null, to: String? = null): List<ImageEntry> The images from from to to (the whole tree when both are null): where each sits, its reference, what the tool was looking for, and the agent's next words about it (the rest of its block, or the next reply).
fun lensProblems(tree: ChatTree, lens: String): List<String> A lens's problems. Every block must cite what it stands for with a <<source:…>> line directly under its <<you>>/<<ai>>/<<summary>> line, and every citation must resolve to a stretch of the chat. A source line anywhere else (above the marker, at the end of the block before) is refused, because it would cite nothing and show as text.
fun lensStats(tree: ChatTree, lens: String): Map<String, Any> How much of the chat a lens covers, and how much shorter it is than what it cites.
fun withImageUrls(markdown: String, url: (String) -> String): String markdown with every ai4img: reference replaced by the URL url gives its blob name (<sha>.<ext>).
fun addressedTranscript(blocks: List<ChatBlockEntry>): String blocks as chat markup again, each with an <<at:address>> line, so the chat panel's renderer draws them as the panel would and tags every block with where it came from (the lens viewer's line ends).

ChatlogRoutes.kt

The lens viewer's two read routes (ChatLensViewer). Both answer only for a document that registered a chat archive, and only to this machine and this site: chats can hold private work, and the archive may be another workspace than the server's. Pictures are served only by blob name (<sha16>.<ext>) from the chat's own ai4_blobs/.

src/jvmMain/kotlin/afield/ai/conversation/chatlog/ChatlogRoutes.kt — read the source

Top-level functions

Declaration Summary
fun Route.installChatlogRoutes() Mounts /api/chatlog/lens, which answers ChatLensViewer.payload as JSON, and /api/chatlog/blob, which serves one picture from a chat's own blob directory. Called once while the server builds its routing.

ChatlogTools.kt

Chat-log tools: AI4 agents that read chats and write lenses

A box hands its document's AI4 agents a chat archive by running, once:

registerChatlogTools(File("/path/to/workspace"))
Agents then see chatlog.list, chatlog.outline, chatlog.read, chatlog.search, chatlog.images and lens.write, lens.read, lens.list. The archive is only read (see ChatArchive); the chats can live in another workspace, including one a person is working in.

A lens is a shorter chat written over a longer one: the same <<you>>/<<ai>> markup, so the chat panel renders it, with a <<source:a..b>> line at the top of each block saying which stretch of the chat it stands for. Lenses are saved in lensDir, by default this document's lenses/ folder, as <chat>/<name>.md beside a <name>.json record of the brief that made it.

Chats are named <document> when the document has one chat, otherwise <document>/<box name or id>. A lens is itself a chat, named <chat>#<lens>: the tools outline, read and cite it like any other, so a lens can be written over a lens (house-machine-10#episodes), and the viewer shows the whole chain.

A series runs several chats end to end, as one chat: series:<glob> takes every chat whose name matches (series:house-machine-*), and series:<glob>@<lens> takes each one's lens of that name instead, skipping chats that have none. A series is outlined, read and cited like a chat (its addresses are its chats' own), so a lens over a series (series:house-machine-*@storyline#arc) tells the story across them.

src/jvmMain/kotlin/afield/ai/conversation/chatlog/ChatlogTools.kt — read the source

Top-level functions

Declaration Summary
fun Box.registerChatlogTools(vararg roots: File, lensDir: File? = null)

ChatlogStore

class ChatlogStore(val roots: List<File>, val lensDir: File)

Finds chats under roots, keeps each tree until its files change, and keeps lenses under lensDir.

Declaration Summary
fun all(): List<Pair<String, ChatTree>>
fun tree(id: String): ChatTree The chat id; for <chat>#<lens> (any depth), that lens read as a chat; for series:…, a series.
fun seriesMembers(id: String): List<String> The chats a series takes, in order: series:<glob> or series:<glob>@<lens>.
fun chain(id: String): List<String> The chats of a chain from the top: a#b#c gives a#b#c, a#b, a.
fun dataUrl(id: String, ref: String): String? An ai4img: reference as a data URL, from the chat's document blobs.
fun writeLens(id: String, name: String, markdown: String, brief: String, stats: Map<String, Any>): File
fun readLens(id: String, name: String): String?
fun lensMarkdown(id: String, name: String): String? The lens itself, or null.
fun lensRecord(id: String, name: String): kotlinx.serialization.json.JsonObject? The record writeLens saved beside a lens (brief, model, stats), or null.
fun blobDir(id: String): File Where a chat's ai4img: pictures are.
fun blobFile(id: String, blob: String): File? A picture by blob name for id; in a series, from whichever member chat holds it.
fun lenses(id: String): List<String>

Logging.kt

The channel-tagged log events the AI tooling writes, and the sinks that receive them.

A LogEvent carries a channel (a comma-separated set of tags such as info,ai.toolio), a message, and optional block indent and boundary markers. LogDispatcher fans each event out to every installed LogSink; LogDispatcher.withSink attaches one for the length of a block.

Sinks

ChannelMatch is the rule all channel selectors share: a selector matches when each of its tags is among the channel's.

File traces

TraceLogger appends sectioned, truncated text to a trace file when afield.ai4.trace (or AFIELD_AI4_TRACE) is set; 1/true selects ~/.afield/logs/ai4_trace.log. AiToolIoLogger writes one JSONL file per tool session under ~/.afield/logs/ai4_tool_io, on unless afield.ai4.toolIoLog (or AFIELD_AI4_TOOL_IO_LOG) is 0, false or off.

src/jvmMain/kotlin/afield/ai/diagnostics/Logging.kt — read the source

Used in: Logging: channels, sinks and the console, Visualizers, Displays, and Little Instruments, Agent transcript gallery

Top-level functions

Declaration Summary
fun LogSink.log(channel: String, message: String, indent: String = "", blockId: String? = null, boundary: BlockBoundary? = null)
inline fun <R> LogDispatcher.withSink(sink: LogSink, block: () -> R): R Runs block with sink attached, and detaches it again however block ends — normally, by an early return, or on a throw.

LogEvent

data class LogEvent(val channel: String, val message: String, val timestamp: Instant = Instant.now(), val indent: String = "", val blockId: String? = null, val boundary: BlockBoundary? = null)

One log line: its channel tags, message, time, and the block indent and boundary it sits in.

Declaration Summary
val channel: String
val message: String
val timestamp: Instant
val indent: String
val blockId: String?
val boundary: BlockBoundary?

BlockBoundary

enum class BlockBoundary

Constants: BEGIN, END.

LogSink

fun interface LogSink

Declaration Summary
fun log(event: LogEvent)

LogDispatcher

object LogDispatcher

Declaration Summary
fun setSinks(newSinks: List<LogSink>)
fun addSink(sink: LogSink)
fun removeSink(sink: LogSink)
fun log(event: LogEvent)
fun log(channel: String, message: String, indent: String = "", blockId: String? = null, boundary: BlockBoundary? = null)

ChannelMatch

object ChannelMatch

The one rule every channel selector follows — the console mute set, the pinned console and the LogStream filter: a channel is a comma-separated set of tags, and a selector matches when every one of its tags is among the channel's. So boxes matches info,boxes and boxes,debug; info,boxes matches boxes,info and info,boxes,slow; kotlin does not match kotlinjs. Tags compare exactly, case included, as written at the call site.

Declaration Summary
fun tags(channel: String): Set<String>
fun matches(channel: String, selector: String): Boolean

ConsoleMute

object ConsoleMute

Channels suppressed from the console — both the plain StdoutLog and the interactive PinnedConsole honor this. Muted events still reach every other sink (e.g. the LogStream web widget), so nothing is lost; they're just too chatty for the terminal. Default mutes the chatty background channels (info,boxes, info,persist, info,ai.boxnames, debug,boxes). Override with -Dafield.log.mute=... or AFIELD_LOG_MUTE=... (semicolon-separated selectors, matched by ChannelMatch: boxes mutes every channel tagged boxes); set to a blank/empty value to mute nothing.

Declaration Summary
val channels: Set<String>
fun isMuted(channel: String): Boolean

StdoutLog

object StdoutLog : LogSink

Declaration Summary
override fun log(event: LogEvent)

NotifyLog

class NotifyLog(private val label: String = "AI4", private val prefix: String = "AI4", private val maxLen: Int = 200, private val dedupeWindowMs: Long = 1000) : LogSink

Declaration Summary
override fun log(event: LogEvent)

SilentLog

object SilentLog : LogSink

Declaration Summary
override fun log(event: LogEvent)

TraceLogger

class TraceLogger private constructor(private val path: Path, private val maxChars: Int)

Declaration Summary
fun log(section: String, payload: String)

AiToolIoLogger

class AiToolIoLogger private constructor(private val root: Path, private val maxChars: Int, private val log: LogSink)

Declaration Summary
data class Session(val id: String, val path: Path)
fun begin(fields: Map<String, Any?>): Session
fun event(session: Session, event: String, fields: Map<String, Any?> = emptyMap())

Clip.kt

CLIP, as a source of directions in meaning.

CLIP embeds images and text into one 512-dimensional space. Two separate ONNX graphs — text_model.onnx and vision_model.onnx — each emit an already-projected 512-vector, so no head has to be reimplemented here.

That gives three uses:

  1. Zero-shot classification with no training. Encode a handful of prompts once, cosine them against the live frame, take the best. See ClipVector.classify.
  2. Identity by similarity. A stored vector plus a threshold is a recogniser.
  3. A knob whose units are meaning. clipEncode("storm") - clipEncode("calm") is a direction, and projecting the current frame onto it is a continuous parameter that tracks a semantic axis rather than a pixel statistic. See ClipVector.axisTo.

Encode prompts once, into a val, and the text tower stays out of the per-frame path.

Cosine values are not comparable across modalities

This is why ClipVector.classify ranks prompts rather than thresholding a bare cosine. Measured on this checkpoint:

  • text against text sits in a narrow high cone — cos("storm","calm") = 0.881, cos("a photo of a cat","a photo of a dog") = 0.931. Nothing is ever near 0.
  • image against text sits far lower — around 0.21 for every prompt tried, because the two towers occupy offset regions of the sphere (the "modality gap").

So 0.21 does not mean "unrelated" and 0.88 does not mean "the same". Only rank and difference carry information: compare one image against several prompts, or track how a projection moves over time. An absolute threshold on a raw cosine does not transfer between prompts or scenes.

Cost

Text is ≈5ms per prompt, after the one-off cost of loading the encoder. Vision is ≈36ms at 224x224 on a CPU provider, far cheaper than DepthAnything, but still not free, so drive it from a worker if it is per-frame.

The text tower is ClipText and the vision tower is ClipImage.

src/jvmMain/kotlin/afield/ai/inference/onnx/Clip.kt — read the source

Used in: Depth and CLIP: neural vision in a box

Top-level functions

Declaration Summary
fun clipEncode(text: String): ClipVector Encode a prompt to a unit-length 512-vector.
fun clipEncodeImage(image: java.awt.image.BufferedImage): ClipVector Encode a picture to a unit-length 512-vector, in the same space as clipEncode's prompts.
fun clipEncodeImage(file: java.io.File): ClipVector clipEncodeImage for an image file: anything javax.imageio reads (JPEG, PNG, BMP, GIF).

Clip

object Clip

Shared constants and helpers for the two CLIP towers, ClipText and ClipImage.

Declaration Summary
const val DIMENSION The dimensionality of the shared embedding space for this checkpoint.
const val IMAGE_SIZE Side length the vision tower requires; ViT-B/32's position embeddings are fixed at 7x7+1.
val CLIP_MEAN CLIP's own channel means — not ImageNet's, which DepthAnything uses.
val CLIP_STD CLIP's own channel standard deviations.
const val LOGIT_SCALE The trained temperature for CLIP logits, logit_scale.exp().
val TEXT_MODEL Text tower: input_ids to a projected 512-vector.
val VISION_MODEL Vision tower: pixel_values to a projected 512-vector.
val TOKENIZER The BPE tokenizer, as a plain file rather than a model.

ClipVector

class ClipVector(val values: FloatArray)

A point, or a direction, in CLIP's 512-dimensional space.

Declaration Summary
val norm: Float Euclidean length. 1.0, to within float error, for anything an encoder returned.
fun normalized(): ClipVector Unit-length copy, or a copy of this if the length is too small to divide by.
fun dot(other: ClipVector): Float Inner product. Equal to the cosine when both sides are unit length.
fun cosine(other: ClipVector): Float Cosine similarity, normalising both sides first.
operator fun plus(other: ClipVector)
operator fun minus(other: ClipVector)
operator fun times(scale: Float)
infix fun axisTo(other: ClipVector): ClipVector The unit direction from this towards other — a semantic axis.
fun project(axis: ClipVector): Float Scalar projection of this onto axis, assuming axis is unit length.
fun classify(candidates: Map<String, ClipVector>, logitScale: Float = Clip.LOGIT_SCALE): List<Pair<String, Float>> Softmax over labelled candidates — the correct way to ask "which of these?".
fun best(candidates: Map<String, ClipVector>): Pair<String, Float>? The best-matching label, or null for no candidates. Shorthand for classify.first().
fun toBase64(): String Little-endian fp32, base64 encoded — 2732 characters.
override fun toString(): String

ClipText

object ClipText

The text tower.

Declaration Summary
fun tokenize(text: String): LongArray Token ids the tokenizer produces for text, including CLIP's start and end markers.
fun encode(text: String): ClipVector Encode one prompt to a unit-length 512-vector.
fun encodeAll(texts: List<String>): Map<String, ClipVector> encode for several prompts, sharing the loaded model. Sequential — the model is the cost.

ClipImage

class ClipImage(val provider: OnnxRuntime.OnnxProvider = OnnxRuntime.OnnxProvider.AUTO)

The vision tower.

Declaration Summary
val provider: OnnxRuntime.OnnxProvider
val activeProvider: String The provider actually in force.
var lastInferenceMs: Long Wall time of the last encode, in milliseconds.
fun encode(rgba: ByteBuffer, srcW: Int, srcH: Int): ClipVector Encode one frame to a unit-length 512-vector.

ClipSmokeTest.kt

src/jvmMain/kotlin/afield/ai/inference/onnx/ClipSmokeTest.kt — read the source

ClipSmokeTest

object ClipSmokeTest

CLIP end to end: both towers, the tokenizer, and the vector algebra on top of them.

Declaration Summary
fun main(args: Array<String>)

DepthAnything.kt

Monocular depth, from any image, with no depth camera.

Depth Anything V2 Small is the least demanding useful vision model there is: one image in, one single-channel map out, no anchors, no non-maximum suppression, no decode step and no state between frames, so what it exercises is the ONNX plumbing that harder models reuse.

val depth = DepthAnything(width = 266, height = 266)
depth.infer(webcamRgbaBuffer, srcW = 640, srcH = 480)
// depth.depthFp16 now holds width*height fp16 values in [0,1]

The output is inverse relative depth, in arbitrary units

predicted_depth is not metres and not normalised. On the synthetic frame OnnxSmokeTest uses, the raw range at 518x518 is [1.0353, 2.8187] and at 266x266 it is [1.2826, 1.9854] — both asserted there against a Python onnxruntime reference. The range moves with the content and the resolution, and larger means nearer.

So a usable texture needs a normalisation window, and choosing it per-frame from that frame's own min and max makes the image flicker: a hand entering shot rescales the entire background. DepthAnything instead tracks the window with an exponential moving average (DepthAnything.adaptation) and normalises against the smoothed window, which is the cheap version of the one-euro filter that anything body-tracked eventually needs. DepthStats reports both the raw and the smoothed window so a box can watch that decision rather than guess at it.

Buffers keep their identity

DepthAnything.depthFp16 and DepthAnything.depthRaw are allocated once and refilled in place. That is not just to avoid garbage: afield.graphics.texture.StreamingTexture captures its source buffer at construction and re-reads it every frame, so the buffer has to keep the same identity for the texture to keep working. Uploading to Vulkan is then:

val tex = StreamingTexture(
    depth.depthFp16, depth.width, depth.height,
    imageFormat = VK10.VK_FORMAT_R16_SFLOAT
)

fp16 is the output format because a single-channel depth map has nowhere near 16 bits of real precision in it, and R16_SFLOAT halves the upload against R32_SFLOAT for no visible loss.

Cost

Inference is not frame-rate on a CPU execution provider: ≈388ms at 518x518 and ≈101ms at 266x266 on a 10-core Apple Silicon machine, and see OnnxRuntime for why CoreML does not fix that. Cost is roughly quadratic in resolution, which makes DepthAnything.width and DepthAnything.height the main lever. Anything interactive should drive this through DepthAnythingWorker rather than calling DepthAnything.infer on the render thread.

src/jvmMain/kotlin/afield/ai/inference/onnx/DepthAnything.kt — read the source

Used in: Depth and CLIP: neural vision in a box

DepthAnything

class DepthAnything(val width: Int = 266, val height: Int = 266, val model: OnnxModel = FP32, val provider: OnnxRuntime.OnnxProvider = OnnxRuntime.OnnxProvider.AUTO, var adaptation: Float = 0.1f, var invert: Boolean = false)

Monocular depth estimation with Depth Anything V2 Small, normalised into a reusable fp16 buffer.

Declaration Summary
val width: Int
val height: Int
val model: OnnxModel
val provider: OnnxRuntime.OnnxProvider
var adaptation: Float
var invert: Boolean
val activeProvider: String The provider actually in force, which may not be the one requested.
val depthRaw: FloatBuffer The most recent depth map in the model's own units. Valid until the next infer.
val depthFp16: ByteBuffer The most recent depth map as fp16 in [0,1], ready to upload as VK_FORMAT_R16_SFLOAT.
var framesInferred: Long How many frames have been through infer.
var lastInferenceMs: Long Wall time of the last infer call, in milliseconds, inference and conversion together.
fun infer(rgba: ByteBuffer, srcW: Int, srcH: Int): DepthStats Run one frame.
fun resetAdaptation() Forget the normalisation window, so the next frame sets it from its own range.

DepthStats

data class DepthStats(val rawMin: Float, val rawMax: Float, val windowMin: Float, val windowMax: Float)

The normalisation window one DepthAnything.infer used.

Declaration Summary
val rawMin: Float
val rawMax: Float
val windowMin: Float
val windowMax: Float
val settled: Boolean True once the smoothed window has caught up with this frame's raw range.

DepthAnythingWorker.kt

DepthAnything on its own thread, so a 100–400ms inference cannot stall the renderer.

Depth Anything Small costs ≈101ms at 266x266 and ≈388ms at 518x518 on a CPU execution provider; calling DepthAnything.infer from a render loop turns 60fps into 2.6fps. So the renderer gets staleness instead of latency: the most recent depth map that happens to be finished, which at 266x266 is six frames old at 60fps and looks fine for displacement, parallax and depth-keyed compositing, and looks like lag for anything a hand is steering directly.

// once
val depth = DepthAnythingWorker(width = 266, height = 266)
val tex = StreamingTexture(depth.depthFp16, depth.width, depth.height,
                           imageFormat = VK10.VK_FORMAT_R16_SFLOAT)
// per frame
depth.submit(webcam.frameBuffer, 640, 480)   // returns immediately, drops old frames
if (depth.consume()) tex.recordUpload(commandBuffer)

Frames are dropped, not queued

DepthAnythingWorker.submit overwrites any frame that has not been picked up yet. A queue would be wrong here: it would grow without bound whenever inference is slower than capture, which is always, and every frame it held would be more stale than the one behind it. Dropping means the model is always working on the newest frame available when it became free.

Why consume copies

DepthAnythingWorker.depthFp16 has a fixed identity because StreamingTexture captures its source buffer once, at construction. The worker therefore cannot write into it directly — the renderer could be reading it mid-write and would show a torn map with a visible seam. Instead the worker publishes into an internal buffer, and DepthAnythingWorker.consume copies that into DepthAnythingWorker.depthFp16 under the lock, on the calling thread. The copy is ~537KB at 518x518, tens of microseconds, and it is the only work the render thread does.

src/jvmMain/kotlin/afield/ai/inference/onnx/DepthAnythingWorker.kt — read the source

DepthAnythingWorker

class DepthAnythingWorker(val width: Int = 266, val height: Int = 266, model: OnnxModel = DepthAnything.FP32, provider: OnnxRuntime.OnnxProvider = OnnxRuntime.OnnxProvider.AUTO, invert: Boolean = false, adaptation: Float = 0.1f) : AutoCloseable

Runs DepthAnything on a background thread and hands the renderer the newest finished depth map.

Declaration Summary
val width: Int
val height: Int
val activeProvider: String The provider actually in force.
val depthFp16: ByteBuffer The most recently consumed depth map, fp16 in [0,1], width*height*2 bytes.
var framesSubmitted: Long Frames handed to submit, including ones later dropped.
var framesInferred: Long Frames actually inferred. The difference from framesSubmitted is the drop count.
var framesDropped: Long Frames overwritten in staging because inference was still busy when a newer one arrived.
var lastInferenceMs: Long Wall time of the most recent inference, in milliseconds.
var lastStats: DepthStats? Normalisation window of the most recent inference, or null before the first one completes.
var lastError: Throwable? Whatever the worker thread last failed with, if anything.
fun submit(rgba: ByteBuffer, srcW: Int, srcH: Int) Offer a frame. Returns immediately.
fun consume(): Boolean Publish a finished depth map into depthFp16, if one is waiting.
override fun close() Stop the worker thread and release the model's buffers. The cached session is left alone.

Lpips.kt

LPIPS: how different two pictures look, as a learned perceptual distance.

0 is identical, and larger is more different; for natural images most distances fall between 0 and about 0.8. It is Zhang et al.'s "Learned Perceptual Image Patch Similarity" (CVPR 2018, richzhang/PerceptualSimilarity, BSD-2), version 0.1 with the AlexNet backbone: both images go through AlexNet's five convolution stages, each stage's activations are normalised per position, and the squared differences are weighted by a linear layer trained on human judgements of which of two distortions looks closer to the original. So it compares edges, blobs and textures where they are, not pixel colours.

val d = Lpips.distance(imgFromFile("a.png"), imgFromFile("b.png"))
val ds = Lpips.distances(candidates, target)   // many against one, the target computed once

Both images must be the same size, at least 32 pixels on each side, RGB or grey in 0..1.

Size matters

AlexNet's first stage has stride 4 and the network halves twice more, so at 32x32 the three deepest stages are 1x1: they still see the image, but only as a whole. Measured on fixed pairs (see tools/export_lpips_onnx.py), a yellow disc against a colour ramp is 0.42 at 32 pixels, 0.68 at 64 and 0.83 at 128; the same pair, the same verdict, a compressed scale. Compare distances made at one size with each other, not across sizes.

Where the model comes from

Field exports its own AlexNet LPIPS ONNX file: tools/export_lpips_onnx.py builds it from the lpips package on PyPI (0.1.4, which ships the trained linear weights) and torchvision's ImageNet AlexNet, checks the ONNX graph against the PyTorch module (agreement 1.2e-7 on twelve pairs), and installs a 9.9MB file in the OnnxModel cache. Lpips.ensureModel runs that script through uv the first time it is needed — a one-off of about a minute that downloads PyTorch if uv has not cached it — and checks the result against Lpips.MODEL's pinned sha256, which two exports with the pinned package versions reproduced byte for byte.

Its gradient

Lpips.distanceAndGradientPlanar also returns d distance / d candidate pixel, from a second exported graph (Lpips.GRAD_MODEL) that the same script builds and checks against PyTorch's autograd. That is what lets a fit descend on LPIPS: afield.lang.stacksl.cpu.StackSLFit seeds its per-pixel tapes with it.

src/jvmMain/kotlin/afield/ai/inference/onnx/Lpips.kt — read the source

Lpips

object Lpips

LPIPS perceptual distance between images, with an optional gradient, from an exported ONNX model.

Declaration Summary
val MODEL The exported model: candidate [N,3,H,W], reference [N or 1,3,H,W] in, distanceN`` out.
val GRAD_MODEL The gradient model: the same inputs, distanceN` andgradient [N,3,H,W]out, the gradient being ddistancei/ dcandidateion the0..1` pixels.
const val MIN_SIDE: Int Smallest side the network accepts: AlexNet's deepest stage is 1x1 at 32.
val EXPORT_ARGS: List<String> The command ensureModel runs, with the package versions the pinned digest was made with.
val GRAD_EXPORT_ARGS: List<String> The command ensureGradModel runs: EXPORT_ARGS' environment, the gradient graph only.
fun ensureModel(): Path The model file, exporting it first if it is not in the cache, and checked against MODEL's sha256 once per process.
fun ensureGradModel(): Path The gradient model file, exported on first use and checked against GRAD_MODEL's sha256, exactly as ensureModel does for the forward model.
val session: OnnxRuntime.LoadedSession The cached session.
val gradSession: OnnxRuntime.LoadedSession The cached session of the gradient model.
fun distance(a: Img, b: Img): Double LPIPS distance between a and b, which must be the same size.
fun distances(candidates: List<Img>, reference: Img): DoubleArray LPIPS distance from each of candidates to reference; all must be the same size.
fun distancesPlanar(candidates: FloatArray, n: Int, width: Int, height: Int, reference: FloatArray): DoubleArray The same, on planar data already laid out as the network wants it: candidates is n x 3 x height x width floats in 0..1, reference is one 3 x height x width image. For callers scoring thousands of renders, who should not pay for an Img per render.
fun distanceAndGradientPlanar(candidate: FloatArray, width: Int, height: Int, reference: FloatArray): Pair<Double, FloatArray> The LPIPS distance from candidate to reference and its gradient with respect to every candidate value: planar 3 x height x width floats in 0..1, as distancesPlanar takes them. The gradient has the candidate's layout.
fun distancesAndGradientsPlanar(candidates: FloatArray, n: Int, width: Int, height: Int, references: FloatArray, m: Int = 1): Pair<DoubleArray, FloatArray> Batched distanceAndGradientPlanar: n candidates against references of batch m, which is 1 (one broadcast reference) or n (one each, as the regions of a tiled image are). The gradients come back concatenated, n x 3 x height x width.
fun conform(img: Img, width: Int, height: Int): Img img resampled to width x height and expanded to RGB: the usual way to bring two pictures to one size before comparing them.

LpipsSmokeTest.kt

src/jvmMain/kotlin/afield/ai/inference/onnx/LpipsSmokeTest.kt — read the source

LpipsSmokeTest

object LpipsSmokeTest

Lpips, end to end on the CPU, against PyTorch.

Declaration Summary
fun main(args: Array<String>)

OnnxRuntime.kt

ONNX models, running inside Field.

ONNX Runtime reaches the JVM through Microsoft's own JNI artifact — com.microsoft.onnxruntime:onnxruntime, already on the classpath because afield.ai.retrieval.SemanticRetrievalService embeds text. OnnxRuntime caches each session in a JVM singleton, so re-running a box does not rebuild it.

Session construction is the expensive half of ONNX — hundreds of milliseconds to several seconds, because the graph is parsed, fused and specialised for the execution provider. Frame-time inference is the cheap half. A box that says depthAnything() and gets re-evaluated on every keystroke must not pay session construction on every keystroke, and a recurrent model (RVM, Silero) must not lose its hidden state either.

Field recompiles the box, not the application. The session map lives in an object loaded by the application class loader, so the map outlives every recompilation of the code that reads it — the same discipline afield.audio.synth.FaustEngine uses for compiled DSP. Key a session by something stable (its OnnxRuntime.SessionSpec, i.e. file plus provider plus thread counts) and hot-reload becomes free. Build an OrtSession yourself in box code and every keystroke costs a rebuild.

Measured costs

Apple Silicon, 10 cores, Depth Anything V2 Small at 518x518. These numbers set the defaults:

  • CoreML does not help this model. The provider claims 43 of 848 nodes, across 19 partitions; the boundary crossings cost about what the accelerated nodes save (≈470ms vs ≈388ms for plain CPU — i.e. CoreML is slower). OnnxRuntime.OnnxProvider.AUTO therefore resolves to CPU. CoreML is still worth requesting for transformer encoders, where the partition count is small.
  • fp16 weights are slower than fp32 on CPU, ≈462ms vs ≈388ms, because every operator upcasts. fp16's win is download size (50MB vs 99MB), not speed. Pick it to save disk, not time.
  • Pinning intra-op threads costs throughput. setIntraOpNumThreads(4) measured ≈457ms against ≈388ms for the runtime's own default. OnnxRuntime.SessionSpec.intraOpThreads defaults to 0, meaning "let ORT decide". Set it only when you have measured that the render thread is actually starving.

The general shape: nothing on a CPU execution provider is frame-rate. 388ms is 2.6fps. Inference belongs on its own thread with the renderer reading the most recent finished result — see DepthAnythingWorker — and resolution is a far bigger lever than any provider flag, because cost is roughly quadratic in it.

What is not here

The Java API (1.27.0) has no IoBinding. Outputs are therefore always copies, and zero-copy CUDA/Vulkan interop is not reachable from Java today without dropping to Panama downcalls against the same loaded library. That is fine for everything here: a 518x518 fp32 depth map is 1.07MB, and the copy is invisible beside a 388ms inference.

Model download and caching is OnnxModel; DepthAnything is a capability built on top.

src/jvmMain/kotlin/afield/ai/inference/onnx/OnnxRuntime.kt — read the source

Used in: Depth and CLIP: neural vision in a box

OnnxRuntime

object OnnxRuntime

The process-wide ONNX Runtime environment and its cache of sessions keyed by OnnxRuntime.SessionSpec.

Declaration Summary
val env: OrtEnvironment by lazy The process-global environment.
val liveSessionCount: Int How many sessions are currently cached; useful in tests to prove reuse actually happens.
enum class OnnxProvider Which execution provider to ask for.
data class SessionSpec(val model: Path, val provider: OnnxProvider = OnnxProvider.AUTO, val intraOpThreads: Int = 0, val interOpThreads: Int = 0) Everything that changes the identity of a session, and therefore the cache key.
class LoadedSession internal constructor(val spec: SessionSpec, val session: OrtSession, val provider: String) A session plus the provider it actually got, as opposed to the one that was asked for.
fun session(spec: SessionSpec): LoadedSession The cached session for spec, building it on first request.
fun session(model: Path, provider: OnnxProvider = OnnxProvider.AUTO): LoadedSession Convenience for the common case: a model file and nothing else to say about it.
fun evict(spec: SessionSpec): Boolean Drop and close one cached session.
fun closeAll() Close every cached session. For shutdown and for tests that want a cold measurement.

OnnxModel

data class OnnxModel(val id: String, val url: String, val fileName: String, val sha256: String = "", val note: String = "")

A model that can be fetched once and then found again.

Declaration Summary
val id: String
val url: String
val fileName: String
val sha256: String
val note: String
val localPath: Path Where this model lives once fetched, whether or not it has been.
val isPresent: Boolean True when the weights are already on disk, so ensureLocal will not touch the network.
fun ensureLocal(): Path The local file, downloading it if absent.
fun session(provider: OnnxRuntime.OnnxProvider = OnnxRuntime.OnnxProvider.AUTO): OnnxRuntime.LoadedSession The cached session for this model, fetching the weights first if needed.

OnnxSmokeTest.kt

src/jvmMain/kotlin/afield/ai/inference/onnx/OnnxSmokeTest.kt — read the source

OnnxSmokeTest

object OnnxSmokeTest

The ONNX binding, end to end, on the CPU.

Declaration Summary
fun main(args: Array<String>)

AI4FilestoreCli.kt

A command-line tool that uploads files to OpenAI through the content-hash cache and chats over them.

main reads --file, --dir, --vfs-root and --vfs-file targets (run it with --help for the full list), uploads each through OpenAiFileStoreCache so unchanged content keeps its file ID, and creates a vector store and file batch for the results when there are any. It then opens an interactive chat thread through afield.ai.models.OpenAiIdeCodexBridge, with file search when a vector store was made and workspace symbol search when the Kotlin source index has definitions. The cache directory defaults to FilestoreDir; the API key comes from --api-key or OPENAI_API_KEY.

src/jvmMain/kotlin/afield/ai/knowledge/AI4FilestoreCli.kt — read the source

Top-level functions

Declaration Summary
fun main(args: Array<String>)

VectorStoreCreateResponse

data class VectorStoreCreateResponse(val id: String)

VectorStoreFileBatchCreateResponse

data class VectorStoreFileBatchCreateResponse(val id: String)

VectorStoreFileBatchGetResponse

data class VectorStoreFileBatchGetResponse(val id: String, val status: String? = null, @JsonProperty("last_error") val lastError: LastError? = null)

Declaration Summary
val id: String
val status: String?
val lastError: LastError?

LastError

data class LastError(val message: String? = null)

AI4_filestore.kt

A local cache that maps file contents to OpenAI file IDs, so the same bytes are uploaded once per API key.

OpenAiFileStoreCache keys each record by a SHA-256 of the content and a peppered hash of the API key (the key itself is never stored), keeps the database as JSON, and writes it atomically under a read-write lock. uploadIfNeeded returns an existing file ID when the content is already known, and DuplicatePolicy decides what a second upload of known content does. OpenAiFileUploader is the interface an upload goes through; OkHttpOpenAiFileUploader is a minimal multipart client for /v1/files, and uploadVirtualFiles pushes Field virtual files through the same path.

OpenAiKnowledgeStore is a second, registry-style store over the Files and Vector Stores REST endpoints. The command-line driver for the cache is AI4FilestoreCli.kt.

src/jvmMain/kotlin/afield/ai/knowledge/AI4_filestore.kt — read the source

Top-level functions

Declaration Summary
fun uploadVirtualFiles(cache: OpenAiFileStoreCache, uploader: OpenAiFileUploader, openAiApiKey: String, purpose: String, files: List<VirtualTextFile>, duplicatePolicy: DuplicatePolicy = DuplicatePolicy.REUSE_EXISTING): List<VirtualUploadResult>

Sha256

object Sha256

SHA-256 digests as lowercase hex strings.

Declaration Summary
fun hex(bytes: ByteArray): String
fun hexUtf8(s: String): String

DuplicatePolicy

enum class DuplicatePolicy

Constants: REUSE_EXISTING, APPEND, OVERWRITE, ERROR.

CachePutResult

data class CachePutResult(val contentHash: String, val keyHash: String, val record: FileRecord, val action: String)

If contentHash already exists for this keyHash, throw.

Declaration Summary
val contentHash: String
val keyHash: String
val record: FileRecord
val action: String

CacheLookupResult

data class CacheLookupResult(val contentHash: String, val keyHash: String, val records: List<FileRecord>)

Declaration Summary
val contentHash: String, val keyHash: String, val records: List<FileRecord>

FileRecord

data class FileRecord(val fileId: String, val fileName: String, val purpose: String, val byteCount: Long, val createdAtEpochMs: Long = Instant.now().toEpochMilli())

Declaration Summary
val fileId: String
val fileName: String
val purpose: String
val byteCount: Long
val createdAtEpochMs: Long

FileStoreCacheDb

data class FileStoreCacheDb(val version: Int = 1, val pepperId: String = "v1", val byKey: MutableMap<String, MutableMap<String, MutableList<FileRecord>>> = mutableMapOf())

Declaration Summary
val version: Int
val pepperId: String Optional "pepper id" so you can rotate peppers if you want. Keep it stable if you want stable key hashes.
val byKey: MutableMap<String, MutableMap<String, MutableList<FileRecord>>> byKeykeyHash = list of file records (most recent last).

OpenAiFileStoreCache

class OpenAiFileStoreCache(private val dbPath: Path, private val pepper: String = "afield-openai-keyhash-pepper:v1", private val mapper: ObjectMapper = defaultMapper())

Declaration Summary
fun keyHash(openAiApiKey: String): String
fun contentHash(contentBytes: ByteArray): String
fun lookup(openAiApiKey: String, contentBytes: ByteArray): CacheLookupResult
fun fileIdsForContent(openAiApiKey: String, contentBytes: ByteArray): List<String>
fun newestFileIdForContent(openAiApiKey: String, contentBytes: ByteArray): String?
fun put(openAiApiKey: String, contentBytes: ByteArray, fileId: String, fileName: String, purpose: String, duplicatePolicy: DuplicatePolicy = DuplicatePolicy.REUSE_EXISTING): CachePutResult Record a mapping (content -> fileId) without uploading anything.
fun uploadIfNeeded(openAiApiKey: String, fileName: String, contentBytes: ByteArray, purpose: String, uploader: OpenAiFileUploader, duplicatePolicy: DuplicatePolicy = DuplicatePolicy.REUSE_EXISTING): CachePutResult High-level helper: - if content already uploaded for this key (per cache), return existing fileId - otherwise uploads then caches the returned fileId
fun removeByContent(openAiApiKey: String, contentBytes: ByteArray): Boolean
fun snapshot(): FileStoreCacheDb
fun flush()

OpenAiFileUploader

fun interface OpenAiFileUploader

Declaration Summary
fun upload(openAiApiKey: String, fileName: String, contentBytes: ByteArray, purpose: String): String Upload bytes as a File and return the OpenAI file id (e.g. "file-...").

OkHttpOpenAiFileUploader

class OkHttpOpenAiFileUploader(private val http: OkHttpClient = OkHttpClient(), private val baseUrl: String = "https://api.openai.com/v1", private val mapper: ObjectMapper = OpenAiFileStoreCache.defaultMapper()) : OpenAiFileUploader

Minimal multipart uploader for /v1/files.

Declaration Summary
override fun upload(openAiApiKey: String, fileName: String, contentBytes: ByteArray, purpose: String): String

VirtualUploadResult

data class VirtualUploadResult(val file: VirtualTextFile, val result: CachePutResult)

Declaration Summary
val file: VirtualTextFile
val result: CachePutResult

CanonicalDoc

data class CanonicalDoc(val fileName: String, val content: String, val contentType: String = "text/markdown; charset=utf-8")

Declaration Summary
val fileName: String, val content: String, val contentType: String

CanonicalBundle

data class CanonicalBundle(val name: String, val version: String, val docs: List<CanonicalDoc>)

Declaration Summary
val name: String, val version: String, val docs: List<CanonicalDoc>

OpenAiLocalRegistry

data class OpenAiLocalRegistry(val schemaVersion: Int = 1, val saltB64: String, val keys: MutableMap<String, KeyState> = mutableMapOf())

Declaration Summary
val schemaVersion: Int

KeyState

data class KeyState(val filesBySha256: MutableMap<String, UploadedFileRecord> = mutableMapOf(), val vectorStoresByBundle: MutableMap<String, VectorStoreRecord> = mutableMapOf())

Declaration Summary
val filesBySha256: MutableMap<String, UploadedFileRecord>
val vectorStoresByBundle: MutableMap<String, VectorStoreRecord>

UploadedFileRecord

data class UploadedFileRecord(val fileId: String, val fileName: String, val purpose: String, val bytes: Int, val createdAtEpochSec: Long)

Declaration Summary
val fileId: String, val fileName: String, val purpose: String, val bytes: Int, val createdAtEpochSec: Long

VectorStoreRecord

data class VectorStoreRecord(val vectorStoreId: String, val createdAtEpochSec: Long)

Declaration Summary
val vectorStoreId: String, val createdAtEpochSec: Long

OpenAiKnowledgeStore

class OpenAiKnowledgeStore(private val cachePath: Path, private val apiBase: URI = URI.create("https://api.openai.com/v1"))

Declaration Summary
fun sha256Hex(bytes: ByteArray): String
fun sha256Hex(text: String): String
fun hashApiKey(apiKey: String): String
fun ensureFileUploaded(apiKey: String, fileName: String, content: ByteArray, purpose: String = "user_data", ifAlreadyUploaded: AlreadyUploaded = AlreadyUploaded.ReturnExisting): UploadedFileRecord If content already uploaded for this key (by sha256), returns existing file_id. Otherwise uploads to OpenAI Files API and records mapping.
fun lookupFileIdForContent(apiKey: String, content: ByteArray): String? Look up a cached file_id for this (key, content) without uploading.
enum class AlreadyUploaded
fun ensureCanonicalBundleReady(apiKey: String, bundle: CanonicalBundle, purposeForDocs: String = "user_data"): String Ensures a vector store exists for (key, bundleName@bundleVersion) and that it contains all docs in the bundle (uploaded as files).
data class VectorStoreFileItem(@JsonProperty("file_id") val fileId: String? = null)
data class VectorStoreFileBatchCreateResponse(val id: String)
data class VectorStoreFileBatchGetResponse(val id: String, val status: String? = null, @JsonProperty("last_error") val lastError: LastError? = null)
data class LastError(val message: String? = null)

FilestoreDir.kt

Where Field's derived completion indexes live, resolved in one place for every index that needs it.

KotlinSourceIndex, afield.ai.tools.GlslIncludeLibraryIndex and the generated-completion-doc cache all read their directory from FilestoreDir. The index is per checkout because KotlinSourceIndex.runSync indexes src/{jvmMain,personal,generatedLinalg}/kotlin relative to user.dir: a single machine-wide directory would let two checkouts, a docs build or a sibling worktree overwrite each other's index. Completion ranking reads that index (a candidate's category comes from KotlinSourceIndex.getCompletionCategoryForSymbol), so a shared directory would also make a test measure whichever Field ran last rather than its own input.

Resolution order, highest first:

  1. -Dafield.filestore.dir=<path>, which the build forwards to Gradle test workers along with the rest of the afield. namespace, so a test can pin a fixture index.
  2. AFIELD_FILESTORE_DIR=<path>, the same for a shell or a CI job.
  3. <user.dir>/.field/filestore, the per-checkout default.

On first use of (3) in a checkout that has none, the legacy ~/.afield/filestore is copied in if it exists. Those caches are expensive to rebuild: filestore_cache.json maps content hashes to OpenAI file IDs, and discarding it re-uploads every source file. The copy happens once, is logged, and is best-effort; a failure leaves an empty directory, which means a slow first run, not a wrong answer. FilestoreDirTest exercises the default, the explicit pin and the empty-directory case. See dev_docs/HERMETIC_COMPLETION_INPUTS.md.

src/jvmMain/kotlin/afield/ai/knowledge/FilestoreDir.kt — read the source

FilestoreDir

object FilestoreDir

Resolves the filestore directory: system property, then environment variable, then the checkout default.

Declaration Summary
const val PROPERTY
const val ENV
val legacyGlobal: Path The machine-wide ~/.afield/filestore, which FilestoreDir copies from and never deletes.
val path: Path The directory itself. Read late rather than captured in a val, so that a test pinning it after some object's <clinit> has run is still obeyed; a value captured at class-load time would keep answering with whatever was resolved first.
fun resolve(name: String): Path
fun ensure(): Path Create it if it is not there, and return it. Callers that write should use this.
fun <T> withDirectory(dir: Path, block: () -> T): T Run block with the filestore pinned to dir — how a test hands the completion path a known index instead of whatever the machine last wrote.
fun describe(): String What was resolved and why, for a banner or a bug report.

deepsets.kt

A learned two-body interaction: the acceleration on a particle as a function of the neighbours around it, fitted by autodiff from examples of any rule that can already be evaluated.

flowMatch learns v(x, t): where a particle should go given only where it is. That can carry a cloud onto a shape, and it is blind to the other particles. An interaction is the other kind of rule:

a_i = phi( v_i, SUM_j psi(x_j - x_i, v_j - v_i), MEAN_j psi(...) )

two small networks and a reduction over neighbours. This is the Deep Sets form, and its aggregation is permutation-invariant over any number of neighbours, so one set of weights works for a particle with three neighbours and a particle with three hundred. Classical boids is one particular choice of psi and phi, so the family contains it.

Why both a sum and a mean

Reynolds separation is a sum: being crowded by twenty neighbours pushes harder than being crowded by two. Alignment and cohesion are means, since they are averages of neighbour velocity and position. A mean-only aggregator cannot represent separation at any width, and a sum-only one makes alignment scale with crowding, so phi is fed both, at a cost of messageWidth extra inputs. DeepSetsWeights.sumScale keeps the summed branch in a sane range.

The feature map is where the physics goes

psi is not fed the raw offset. It is fed interactionFeatures: the offset and velocity difference, plus the unit direction scaled by a small radial basis, u*p and u*p^2, where p is a bounded proximity that rises as neighbours get close.

Separation goes as 1/r over a four-hundred-fold range of r, and a tanh network cannot form the product of a direction and a steep radial profile out of Cartesian components at any practical width. Alignment and cohesion are plain means, and linear in the inputs, so they survive the raw encoding. Measured by ablation against the flocking rule: fed raw Cartesian offsets the model reproduces those two to a relative error of 0.036 and separation only to 0.78, which is barely better than predicting the average.

A radial interaction is a direction times a profile, so handing the network u already multiplied by a couple of profile terms states the form of a two-body law rather than transcribing boids. The profile's coefficients, the velocity dependence, and how the summed and meaned branches combine are all still learned.

Trained by afield.math.autodiff.ADTape, not by Smile

Smile's MLP cannot express this: the gradient has to flow back through a sum over a variable-length neighbour set, and psi is applied once per neighbour with shared weights. autodiff.kt is reverse-mode over a tape, which handles it without knowing anything about networks, since the tape records whatever the forward pass did. Cost is one forward and one backward sweep per batch whatever the neighbour counts are.

The weights come out as MlpWeights

Both halves are packaged in the same flat layout the flow-matching work already uses, so MlpWeights.glsl emits them for the GPU unchanged and the parity machinery in four.neural-field-parity.smoke applies to them without modification.

src/jvmMain/kotlin/afield/ai/learning/deepsets.kt — read the source

Used in: Neural particle systems

Top-level functions

Declaration Summary
fun interactionFeatures(dx: Float, dy: Float, dvx: Float, dvy: Float, radius: Float, velocityScale: Float, minSeparation: Float, into: FloatArray = FloatArray(DeepSetsWeights.PSI_INPUTS)): FloatArray The per-neighbour features psi reads — see the discussion on DeepSetsWeights.
fun trainDeepSets(radius: Float, velocityScale: Float, accelScale: Float, sumScale: Float, minSeparation: Float, options: DeepSetsOptions = DeepSetsOptions(), examples: (Int) -> List<InteractionExample>): DeepSetsWeights Fits a DeepSetsWeights to whatever rule examples embodies, by reverse-mode autodiff.
fun DeepSetsWeights.glsl(prefix: String = "learned"): String GLSL for the whole learned interaction: both networks, the feature map, and the combination.
fun invariantFeatures(dx: Float, dy: Float, dvx: Float, dvy: Float, radius: Float, velocityScale: Float, minSeparation: Float, into: FloatArray = FloatArray(EquivariantWeights.PSI_INVARIANTS)): FloatArray The rotation-invariant description of one neighbour: [r/R, p, p^2, \|dv\|/V, (u . dv)/V].
fun trainEquivariant(radius: Float, velocityScale: Float, accelScale: Float, sumScale: Float, minSeparation: Float, options: DeepSetsOptions = DeepSetsOptions(), examples: (Int) -> List<InteractionExample>): EquivariantWeights Fits an EquivariantWeights to whatever rule examples embodies.
fun EquivariantWeights.glsl(prefix: String = "learned"): String GLSL for the equivariant interaction: both networks, the invariant features, and the combination — the shader half of EquivariantWeights.

DeepSetsWeights

class DeepSetsWeights(val psi: MlpWeights, val phi: MlpWeights, val messageWidth: Int, val radius: Float, val velocityScale: Float, val accelScale: Float, val sumScale: Float, val minSeparation: Float)

A trained interaction in the plain Cartesian form: psi and phi, the width of the message between them, and the five scales that normalise a neighbourhood into the networks and the answer back out to world units. EquivariantWeights is the same model constrained so that rotating a neighbourhood rotates the answer with it.

Declaration Summary
val psi: MlpWeights Applied per neighbour. Inputs (dx, dy, dvx, dvy) normalised; outputs messageWidth.
val phi: MlpWeights Applied once. Inputs (vx, vy, sum…, mean…) normalised; outputs a normalised acceleration.
val messageWidth: Int How wide a single neighbour's message is.
val radius: Float Interaction radius, in world units. Offsets are divided by this.
val velocityScale: Float Velocity differences are divided by this.
val accelScale: Float The output is multiplied by this to get a world-units acceleration.
val sumScale: Float The summed branch is divided by this, so a typical crowd lands near 1.
val minSeparation: Float Closest approach used by the proximity feature — the same floor the rule itself uses.
val parameterCount: Int How many weights and biases in total.
fun evaluate(vx: Float, vy: Float, neighbours: List<FloatArray>): Pair<Float, Float> The acceleration on a particle with velocity (vx, vy) given its neighbours, in fp32 and in the same order a shader would accumulate it — the reference the GPU is held to.
override fun toString(): String Both network shapes and the parameter total. The GLSL emitter writes this line into the shader as a comment, so a generated source names the weights it was generated from.

InteractionExample

class InteractionExample(val velocity: FloatArray, val neighbours: List<FloatArray>, val target: FloatArray)

One training example: a neighbourhood and the acceleration the rule being imitated produces.

Declaration Summary
val velocity: FloatArray The centre particle's velocity, in world units.
val neighbours: List<FloatArray> (dx, dy, dvx, dvy) per neighbour, in world units.
val target: FloatArray The target acceleration, in world units.

DeepSetsOptions

data class DeepSetsOptions(val steps: Int = 4000, val batch: Int = 16, val messageWidth: Int = 6, val psiHidden: IntArray = intArrayOf(16, 16), val phiHidden: IntArray = intArrayOf(16, 16), val learningRate: Double = 3e-3, val finalLearningRate: Double = 2e-4, val seed: Long = 1L, val onProgress: ((Int, Double) -> Unit)? = null, val progressEvery: Int = 200)

The knobs trainDeepSets takes.

Declaration Summary
val steps: Int Adam steps to take. Each one calls examples for a fresh batch.
val batch: Int How many examples a batch is expected to hold. trainDeepSets divides its loss by the size of the list examples actually returned and only checks this is positive; trainEquivariant divides by this number, so a batch of a different size there rescales the gradient.
val messageWidth: Int How many numbers psi emits per neighbour, and so how wide the summed and the meaned branch phi reads each are. trainEquivariant reads it as a count of vector channels instead.
val psiHidden: IntArray Hidden layer widths of the per-neighbour network. tanh between them, linear out.
val phiHidden: IntArray Hidden layer widths of the combining network.
val learningRate: Double Adam's rate at the first step.
val finalLearningRate: Double Decayed linearly to this by the last step. Same reasoning as FlowMatchOptions.
val seed: Long Seeds the Xavier initialisation, and nothing else: examples draws its own randomness.
val onProgress: ((Int, Double) -> Unit)? Called with the step number and that step's loss, or null to train silently.
val progressEvery: Int How often onProgress fires, in steps. The last step reports only if it lands on one.

EquivariantWeights

class EquivariantWeights(val psi: MlpWeights, val phi: MlpWeights, val channels: Int, val radius: Float, val velocityScale: Float, val accelScale: Float, val sumScale: Float, val minSeparation: Float)

A learned interaction that is rotationally equivariant by construction — a(R x) = R a(x) holds exactly, not approximately.

Declaration Summary
val psi: MlpWeights Invariants to 2 * channels scalars: a_k then b_k.
val phi: MlpWeights Aggregate invariants to 2 * channels + 1 scalars: the sum weights, mean weights, and v.
val channels: Int How many vector channels are aggregated.
val radius: Float Interaction radius, in world units. Neighbour distances are divided by this.
val velocityScale: Float Divides velocity differences, and the centre particle's own speed, into the invariants.
val accelScale: Float The output is multiplied by this to get a world-units acceleration.
val sumScale: Float The summed channels are divided by this, so a typical crowd lands near 1.
val minSeparation: Float Closest approach used by the proximity invariant, the same floor the rule itself uses.
val parameterCount: Int How many weights and biases in total.
fun evaluate(vx: Float, vy: Float, neighbours: List<FloatArray>): Pair<Float, Float> The acceleration, in world units. Equivariant to fp32.
override fun toString(): String Both network shapes, the channel count and the parameter total. The GLSL emitter writes this line into the shader as a comment.

flowmatching.kt

Flow matching over 2D and 3D point clouds — flowMatch trains a velocity field that carries one distribution onto another, and the field it returns can be sampled, integrated part-way, or drawn.

The idea is small enough to state completely. Pick a point x0 from where you are starting and a point x1 from where you want to end up, and declare that the straight line between them is the path a particle should take: at time t it is at xt = (1-t)·x0 + t·x1, travelling at x1 - x0. Now fit a network v(x, t) to that velocity by least squares over many random (x0, x1, t) triples. No two straight lines agree where they cross, so the fit cannot reproduce any single one of them; what it converges to is the average velocity through each point at each time, and integrating that average from t = 0 to t = 1 transports the whole starting distribution onto the whole target one. That is the entire trick, and it is why there is no adversary, no sampler in the training loop, and nothing to diverge — the loss is a plain regression MSE.

val target = flowSamples(myLine, 600, jitter = 4.0)   // sampled FLine + noise
val field = flowMatch(target, options = FlowMatchOptions(steps = 6000))
val cloud = field.sample(600)                         // fresh points from the same shape
val threads = field.paths(80)                         // the trajectories, as FLines

With a from cloud supplied it becomes a morph instead of a generator, and FlowMatchField.transport runs the ODE only part-way: transport(a, to = 0.5) is a genuine half-way shape rather than a vertex-wise average of two point lists.

val morph = flowMatch(to = flowSamples(lineB, 600), from = flowSamples(lineA, 600))
val halfway = morph.transport(flowSamples(lineA, 600), to = 0.5)

Coordinates

Training happens in a normalised frame — the target cloud (both clouds, for a morph) is centred and isotropically scaled to roughly unit variance — because a Gaussian source has to be commensurate with the data for any of this to work, and FLine coordinates are usually in the hundreds. The scaling is a single factor rather than per-axis, so it cannot squash a shape's aspect ratio. FlowMatchField converts at its boundary, so everything you pass in and get back is in the coordinates you started with.

2D and 3D are distinguished by the data, not by a flag: a cloud whose z never varies is trained as a 2D problem and its points come back on that same z plane. Pass FlowMatchOptions.dimension to override. It matters — a 2D shape trained as 3D gets a Gaussian's worth of spread in z that the field then has to learn to collapse.

What this is not

  • Not a big model. The default is two hidden layers of 64 leaky units trained by RMSProp, which is right for a few thousand points in two or three dimensions and nothing like enough for images. Training is single-threaded and per-sample (Smile's MLP has no batched matmul), so cost scales with steps × batch; see the timings in FlowMatchingTest.
  • Not able to change topology. Integrating a velocity field is a continuous invertible map, so it cannot tear a loop open or join two strands into one — a flow can only deform. With FlowCoupling.PAIRED this stops being an abstraction and starts being the dominant error term: asking a straight segment to become a closed ring makes the mean landing error fifteen times worse than asking a small circle to do it, because the two ends of the segment are pulled to the same place and their paths have to cross. Give a closed target a closed source.
  • Not exact. The field is an average over crossing paths, so samples land near the target distribution, not on it. Sharp corners smooth off, and thin structure needs more steps and more hidden before it resolves. FlowMatchField.loss is the training MSE, which measures the velocity fit and not the quality of the samples — judge those by looking, or with chamferDistance.
  • Not ordered. FlowMatchField.sample returns an unordered cloud. Recovering an FLine from it is a separate problem; FlowMatchField.transport sidesteps it by moving points you already have, which keeps whatever order they came in.
  • Not conditional. There is no class label or text prompt input — one field carries one distribution onto one other.

src/jvmMain/kotlin/afield/ai/learning/flowmatching.kt — read the source

Used in: Flow Matching, Neural particle systems, A Generator, Graded by Datalog

Top-level functions

Declaration Summary
fun flowMatch(to: List<Vec3>, from: List<Vec3>? = null, options: FlowMatchOptions = FlowMatchOptions()): FlowMatchField Trains a velocity field that carries from onto to, and returns it.
fun flowMatch(line: FLine, samples: Int = 600, jitter: Double? = null, options: FlowMatchOptions = FlowMatchOptions()): FlowMatchField Trains a field that generates points shaped like line — the common case, spelled out.
fun flowSamples(line: FLine, count: Int = 600, jitter: Double = 0.0, seed: Long? = null): List<Vec3> Samples count points evenly along line's arc length, displaced by Gaussian noise of standard deviation jitter.
fun contourComponents(lines: List<FLine>, total: Int = 3500, minimum: Int = 24, jitter: Double = 0.0, seed: Long? = null): List<List<Vec3>> Splits lines into their closed subpaths and samples each one in arc-length order, in proportion to its length — the components mixtureSource wants, prepared the way that matters.
fun mixtureSource(components: List<List<Vec3>>, shrink: Double = 1.0, jitter: Double = 0.0, alignPhase: Boolean = true, seed: Long? = null): FlowPairing Builds a source cloud whose topology matches the target's: one closed curve per component, in point-for-point correspondence with the component it has to become.
fun chamferDistance(a: List<Vec3>, b: List<Vec3>): Double Symmetric mean nearest-neighbour distance between two point clouds — how far apart two shapes are, in input coordinates.

VectorMLP

class VectorMLP(val inputs: Int, val outputs: Int, hidden: IntArray = intArrayOf(96, 96), val activation: String = "tanh") : MultilayerPerceptron(*mlpLayers(inputs, hidden, outputs, activation))

A multi-output regression MLP, which is the one thing Smile's own smile.regression.MLP will not do — it fixes the output layer at a single neuron and predict returns a Double.

Declaration Summary
val inputs: Int Width of the input vector.
val outputs: Int Width of the predicted vector.
val activation: String The hidden activation, kept because exportWeights has to name it and Smile does not.
fun predict(x: DoubleArray, into: DoubleArray = DoubleArray(outputs)): DoubleArray Runs the network forward, writing outputs values into into, which is returned.
fun accumulate(x: DoubleArray, y: DoubleArray): Double Adds one sample's gradient to the batch, returning that sample's squared error.
fun step(batchSize: Int) Applies the accumulated gradient of a batch of batchSize samples and advances the clock.
fun configure(learningRate: Double, momentum: Double, rho: Double, epsilon: Double = 1e-7) Configures RMSProp, momentum and learning rate together, the combination flowMatch uses.
fun configure(learningRate: TimeFunction, momentum: Double, rho: Double, epsilon: Double = 1e-7) As configure, but with a learning-rate schedule — TimeFunction.linear(from, steps, to) and friends, indexed by the step count.
fun parameterCount(): Int How many parameters — weights plus biases — the network is fitting.
fun exportWeights(): MlpWeights A flat snapshot of every weight and bias, in the layout MlpWeights documents — which is what afield.ai.learning.glsl turns into a shader.

FlowCoupling

enum class FlowCoupling

How flowMatch decides which x0 travels to which x1.

Constants: INDEPENDENT, MINIBATCH_OT, PAIRED.

FlowIntegrator

enum class FlowIntegrator

Which integrator FlowMatchField uses to follow the velocity field.

Constants: EULER, MIDPOINT.

FlowMatchOptions

data class FlowMatchOptions(val steps: Int = 3000, val batch: Int = 48, val hidden: IntArray = intArrayOf(64, 64), val activation: String = "leaky", val learningRate: Double = 1e-2, val finalLearningRate: Double? = 3e-4, val momentum: Double = 0.0, val rho: Double = 0.9, val timeFeatures: Int = 4, val coupling: FlowCoupling = FlowCoupling.INDEPENDENT, val jitter: Double = 0.0, val seed: Long? = null, val dimension: Int? = null, val onProgress: ((Int, Double) -> Unit)? = null, val progressEvery: Int = 100)

The knobs flowMatch takes, bundled so that a call that only wants to change one of them does not have to name the rest positionally.

Declaration Summary
val steps: Int Gradient steps to take. The dominant cost and the dominant quality lever.
val batch: Int Samples per gradient step.
val hidden: IntArray Hidden layer widths.
val activation: String Hidden activation: leaky, relu, tanh, sigmoid or linear.
val learningRate: Double RMSProp learning rate at the first step.
val finalLearningRate: Double? Learning rate at the last step, decayed linearly from learningRate; null to hold it constant.
val momentum: Double Momentum, or 0 to disable. flowMatch refuses it unless rho is 0.
val rho: Double RMSProp decay, or 0 to fall back to plain SGD. See momentum before changing either.
val timeFeatures: Int How many sin/cos octaves of the time input the network sees, on top of raw t.
val coupling: FlowCoupling How x0 is paired with x1.
val jitter: Double Standard deviation of noise added to each drawn data point, in input coordinates.
val seed: Long? Seed for the training draws. null takes an unpredictable one.
val dimension: Int? Override the inferred 2-or-3 dimensionality.
val onProgress: ((Int, Double) -> Unit)? Called every progressEvery steps with the step index and the running mean loss.
val progressEvery: Int How often onProgress fires.
override fun equals(other: Any?): Boolean Two option sets are equal when they print the same. hidden is an IntArray, which compares by identity, so the equality a data class generates would call two identical settings different.
override fun hashCode(): Int Hashes the same string equals compares, so equal option sets land in the same bucket.

FlowPairing

class FlowPairing(val from: List<Vec3>, val to: List<Vec3>)

A source cloud and a target cloud of the same length, in correspondence — what FlowCoupling.PAIRED wants, and what mixtureSource builds.

Declaration Summary
val from: List<Vec3> Where the particles start. The i-th point travels to to[i](workspace.md#stdlib-kt--i).
val to: List<Vec3> Where they end up.
val size: Int How many pairs.

FlowMatchField

class FlowMatchField internal constructor(val dimension: Int, private val net: VectorMLP, private val timeFeatures: Int, private val frame: FlowFrame, private val sourcePoints: Array<DoubleArray>?, val losses: DoubleArray)

A trained velocity field, as returned by flowMatch — the thing you integrate.

Declaration Summary
val dimension: Int 2 or 3 — whether z is part of the problem.
val losses: DoubleArray The mean squared velocity error of every training step, in order.
val loss: Double The training loss of the final step.
fun loss(count: Int): Double Mean training loss over the last count steps — steadier to read than loss alone.
val hasSourceCloud: Boolean Whether this field was trained to morph from a supplied cloud rather than from a Gaussian.
val parameterCount: Int How many weights and biases the field is made of.
val network: MlpWeights The trained network itself, flattened for a shader — see MlpWeights.
fun export(): FlowMatchExport Everything a shader needs to reproduce velocity — the network plus the three things around it: how wide the position is, how t is encoded, and the frame the training happened in.
fun velocity(at: Vec3, t: Double): Vec3 The velocity the field predicts at at and time t, in input coordinates.
fun sample(count: Int, steps: Int = 32, integrator: FlowIntegrator = FlowIntegrator.MIDPOINT, seed: Long? = null): List<Vec3> Draws count points from the source and follows the field to t = 1.
fun transport(points: List<Vec3>, from: Double = 0.0, to: Double = 1.0, steps: Int = 32, integrator: FlowIntegrator = FlowIntegrator.MIDPOINT): List<Vec3> Carries points along the field from time from to time to.
fun paths(count: Int, steps: Int = 32, integrator: FlowIntegrator = FlowIntegrator.MIDPOINT, seed: Long? = null): List<FLine> The trajectories of count fresh samples, one FLine each, from t = 0 to t = 1.
fun pathsFrom(points: List<Vec3>, from: Double = 0.0, to: Double = 1.0, steps: Int = 32, integrator: FlowIntegrator = FlowIntegrator.MIDPOINT): List<FLine> The trajectories of points specifically, from time from to time to — see paths.

corpora.kt

Synthetic drawings to teach a learned pen: Hershey plotter fonts and four drawing generators, and a writer that turns a list of drawings into a corpus python/fieldpen can train on.

Every generator draws in the order a hand (or a plotter) would, which is the one thing a pen needs from its training data and the one thing a scanned drawing cannot supply. PenCorpus resamples each drawing exactly the way Pen.feed reads a line, so a pen trained on a corpus sees the same spacing when it is later shown a line. Tests: PenCorporaTest.

src/jvmMain/kotlin/afield/ai/learning/pen/corpora.kt — read the source

Used in: Teaching a pen, A pen that sees

Top-level functions

Declaration Summary
fun curliness(line: FLine, scale: Double = 8.0, step: Double = 0.35, shortest: Int = 6): Double Mean absolute turn per unit length along line's marks, a rough measure of how curly it is, taken after resampling the way a pen reads a line (PenCorpus.strokes). Measured on the raw points instead, it mostly counts the small jitter between close points, and choosing by it picks the shakiest lines rather than the curliest. Marks of fewer than shortest resampled points are left out, so a line of dots does not count as curly either.
fun PenModel.probe(drawings: List<FLine>, labels: List<List<Int>>, scale: Double = 8.0, step: Double = 0.35): List<Pair<Int, Double>> Which memory cells track a label: show model each of drawings (a fresh pen per drawing), and correlate every cell's output with the label of the subpath being drawn, labels[d](workspace.md#stdlib-collections-kt--s) for subpath s of drawing d. Returns every cell with its correlation, strongest (positive or negative) first.
fun following(line: FLine, within: Double = 8.0, recent: Int = 8, scale: Double = 8.0, step: Double = 0.35): Double How much of line runs alongside ink it drew earlier: the share of its points (resampled the way a pen reads a line) with an earlier point, more than recent points back, within within drawing units. Hatching scores high; a single wandering line scores near zero.
fun joins(line: FLine, within: Double = 4.0, recent: Int = 8, scale: Double = 8.0, step: Double = 0.35): Double How many of line's strokes end on other ink: the share of its subpaths whose last point is within within drawing units of a point of any earlier subpath, or of this one more than recent points back. A web of cracks scores high; strokes that stop in mid-air score low.

HersheyFont

class HersheyFont private constructor(val name: String, private val glyphs: List<Glyph>)

One of the Hershey fonts: single-stroke vector lettering from 1967, drawn as a pen plotter draws it, stroke by stroke.

Declaration Summary
fun strokes(c: Char): List<List<IntArray>>? The strokes of c in font units (y down, about 32 units from ascender to descender), or null.
fun text(text: String, size: Double = 24.0, at: Vec3 = Vec3(0f, 0f, 0f)): FLine text as one FLine, a subpath per stroke, starting at at. size is the height of a capital in drawing units.

PenDrawings

object PenDrawings

Generators of ordered drawings for training a pen. Each takes a seed and returns one drawing at a size that suits the default scale of 8 drawing units per model unit.

Declaration Summary
fun word(random: Random, length: Int = random.nextInt(2, 8)): String A made-up word of length letters, alternating consonants and vowels often enough to look like one.
fun hershey(seed: Long, font: String = "scripts", words: Int = 5, size: Double = 22.0): FLine A line of made-up words in a Hershey font: words words, capitals size units tall.
fun harmonograph(seed: Long, radius: Double = 70.0, turns: Double = 14.0): FLine A harmonograph: two damped pendulums per axis, summed, drawn as one long decaying stroke. The frequencies sit near small whole-number ratios, which is what makes the figure close up.
fun plant(seed: Long, generations: Int = 4, height: Double = 140.0): FLine A branching plant from an L-system, drawn by a turtle in one unbroken stroke: each branch is drawn out to its tip and back down to its fork, the way a hand draws a twig.
fun truchet(seed: Long, columns: Int = 9, rows: Int = 5, cell: Double = 18.0): FLine Truchet tiles: a grid of squares, each holding two quarter-circle arcs one way or the other, whose arcs join into long winding curves. Each curve is one stroke, traced end to end.
fun boids(seed: Long, count: Int = 10, steps: Int = 160, size: Double = 160.0): FLine Flocking trails: a dozen boids that steer towards each other, align and keep apart, wandering a box. Each boid's path is one stroke, drawn one boid after another.
fun michaux(seed: Long, columns: Int = 2, lines: Int = 14, length: Double = 200.0, gap: Double = 5.0): FLine After Henri Michaux's mescaline drawings: columns of fine, trembling lines, each laid down beside the last at a small gap and following its undulation, a little changed, so the field accumulates. A column is one unbroken stroke going down, turning, and coming back up beside itself, the way hatching is drawn; now and then a pass leans into its neighbour and touches it.
fun cracks(seed: Long, size: Double = 110.0, count: Int = 26): FLine A web of cracks: a wobbling outline, then crack after crack, each starting on the ink near the last one and wandering until it meets a line, where it stops. Every crack ends in a join: the thing a sensing pen can learn and a blind one cannot.
fun echoes(seed: Long, guides: Int = 3, passes: Int = 7, gap: Double = 5.0): FLine Echo hatching: a few guide strokes (a stroke of a Hershey letter, a smooth curve, or a closed blob, each at a random angle), each followed by passes laid alongside it at a small gap, back and forth in one unbroken stroke with a tremor, outwards from the guide. A pass is cut short where it would run into other ink, so hatching stops at the lines around it.
fun twoHands(seed: Long, words: Int = 6, hands: MutableList<Int>? = null): FLine A line in two hands: words that are either chains of small loops (hand 0) or sharp zigzags (hand 1), the hand changing every word or two. hands gets the hand of each word, one entry per subpath of the result, which is what a probe needs to find the cell that tracks it.

PenCorpus

object PenCorpus

A corpus of drawings for python/fieldpen: one JSON file per drawing, split into train/ and valid/ under a folder.

Declaration Summary
fun write(folder: String, drawings: List<FLine>, scale: Double = 8.0, step: Double = 0.35, validEvery: Int = 10): Int Write drawings under folder, every validEveryth one into valid/. Returns the number of points written.
fun strokes(line: FLine, scale: Double = 8.0, step: Double = 0.35): List<List<Vec3>> line's subpaths resampled every step model units: what the pen is shown.

PenCorpora

object PenCorpora

Writes the corpora Field's bundled synthetic pens were trained on: run-jvm-main afield.ai.learning.pen.PenCorpora --arg <folder> makes one sub-folder per corpus, which tools/train_pen_corpora.py then trains and exports.

Declaration Summary
fun all(): Map<String, () -> List<FLine>> Every corpus by name: the drawings to write.
fun rounder(candidates: Int = 2400, keep: Int = 300): List<FLine> The handwriting pen's own output, kept only where it is curliest: 2400 short unprimed scribbles of 120 points, the top eighth by curliness. Fine-tuning on it is a pen learning a taste from its own work.
fun main(args: Array<String>)

guide.kt

Guided sampling for a learned pen: the pen proposes several next points and a PenGuide picks one. The pen keeps its hand (its tremor, its rhythm, its habits) while the guide decides where it goes. FollowGuide keeps a pen a set distance from the ink beside it, which is hatching. Tests: PenHatchTest.

src/jvmMain/kotlin/afield/ai/learning/pen/guide.kt — read the source

Used in: A pen that sees

PenGuide

interface PenGuide

Chooses among a pen's proposals for its next point; set one on PenLine.guide.

Declaration Summary
val candidates: Int How many proposals the pen makes each point.
fun score(from: Vec3, to: Vec3, lift: Boolean, penUp: Boolean): Double How bad a proposal is, lower is better: a step from from to to in drawing units, after which the pen lifts if lift. penUp says whether this step is a move rather than a mark.
fun drawn(at: Vec3, lift: Boolean) The pen drew the chosen point; a guide that tracks ink records it here.

FollowGuide

class FollowGuide(val gap: Double = 4.0, override val candidates: Int = 12, val recent: Int = 10, val reach: Double = 30.0, val liftCost: Double = 2.0, val longest: Double = 6.0) : PenGuide

Keeps a pen gap drawing units from the ink beside it, without crossing any: hatching.

Declaration Summary
val gap: Double
override val candidates: Int
val recent: Int
val reach: Double
val liftCost: Double
val longest: Double
fun see(lines: List<FLine>, spacing: Double = 2.0) Ink the pen should keep its distance from, visible at once: the drawing being hatched.
override fun drawn(at: Vec3, lift: Boolean)
override fun score(from: Vec3, to: Vec3, lift: Boolean, penUp: Boolean): Double

hatch.kt

Hatching around a drawing with a sensing pen: every stroke of the drawing gets passes of the pen's own trembling line laid beside it, which keep clear of the other strokes and of the hatching already done.

A pen cannot remember which strokes it has not hatched yet, so Hatcher chooses: it hands the pen one stroke at a time as a finished primer, with everything else on the page as surroundings, and moves on when the pen has drawn enough or wandered off. Tests: PenHatchTest.

src/jvmMain/kotlin/afield/ai/learning/pen/hatch.kt — read the source

Top-level functions

Declaration Summary
fun hatch(lines: List<FLine>, model: StrokeModel = PenModel.bundled("echoes"), passes: Int = 6, seed: Long = 0, controls: PenControls = PenControls(temperature = 1.0, noise = 1.0), gap: Double? = 4.0): List<FLine> Hatch around lines with a sensing pen and return the hatching, one FLine per stroke: Hatcher run to the end.

Hatcher

class Hatcher(lines: List<FLine>, val model: StrokeModel = PenModel.bundled("echoes"), val passes: Int = 6, val scale: Double = 8.0, var controls: PenControls = PenControls(temperature = 1.0, noise = 1.0), val seed: Long = 0, val step: Double = 0.35, val wander: Double = 40.0, val gap: Double? = 4.0)

Hatches lines stroke by stroke with model, grow points at a time, so a box can watch it.

Declaration Summary
val model: StrokeModel
val passes: Int
val scale: Double
var controls: PenControls
val seed: Long
val step: Double
val wander: Double
val gap: Double?
val hatching: MutableList<FLine> The hatching so far, one FLine per stroke started.
val done: Boolean True once every stroke has been hatched.
val current: FLine? The stroke being hatched now, or null when done.
fun grow(steps: Int = 8): Boolean Draw up to steps more points; returns false once everything is hatched.
fun finish(): List<FLine> Hatch everything; returns hatching.

pen.kt

A learned pen: a recurrent network that has watched people write and draws the next point.

PenModel is a one-layer (or deeper) LSTM whose output is a mixture of 2D Gaussians for the next pen displacement plus a probability that the pen lifts. PenModel.distill is the handwriting model from Carter, Ha, Johnson and Olah, Experiments in Handwriting with a Neural Network (Distill, 2016), bundled with Field. PenModel.load reads a model retrained or fine-tuned with the optional Python package python/fieldpen and exported with fieldpen.export_for_field.

The everyday call is continueWith: hand it an FLine and get back the next few hundred points the pen would have drawn. Pen is the state underneath, for when you want to feed, step, fork or edit the pen yourself. Tests: PenModelTest.

src/jvmMain/kotlin/afield/ai/learning/pen/pen.kt — read the source

Used in: A learned pen, Teaching a pen, A pen that sees

Top-level functions

Declaration Summary
fun FLine.continueWith(model: StrokeModel = PenModel.distill, steps: Int = 300, scale: Double = 8.0, controls: PenControls = PenControls(), seed: Long = 0, step: Double = 0.35, liftAtEnd: Boolean = false): FLine Continue this line with a learned pen: the next steps points it would draw, as a new FLine that starts where this one ends.
fun FLine.futuresWith(model: StrokeModel = PenModel.distill, count: Int = 16, steps: Int = 60, scale: Double = 8.0, controls: PenControls = PenControls(), seed: Long = 1, step: Double = 0.35): List<FLine> Several continuations of this line at once: count different futures of steps points, each a fresh FLine from this line's end. Futures share the pen's reading of the line and differ only in the random choices after it.

StrokeModel

interface StrokeModel

Anything that can continue a stroke one displacement at a time.

Declaration Summary
fun pen(seed: Long = 0): StrokePen A fresh pen that has seen nothing, sampling with a generator seeded by seed.

StrokePen

interface StrokePen

The running state of a StrokeModel: everything it has drawn or been shown so far.

Declaration Summary
fun feed(dx: Double, dy: Double, lift: Boolean) Show the pen a displacement in model units; lift means the pen comes up after this point.
fun next(controls: PenControls = PenControls()): PenStep Sample the next displacement and feed it back, as though the pen had drawn it.
fun fork(seed: Long): StrokePen An independent copy: the same memory, its own random generator seeded by seed.

PenStep

data class PenStep(val dx: Double, val dy: Double, val lift: Boolean)

One step of a pen, in model units: move by (dx, dy), then lift if lift.

PenControls

data class PenControls(val temperature: Double = 0.65, val noise: Double = 0.65, val liftBias: Double = 0.0, val driftX: Double = 0.0, val driftY: Double = 0.0)

How a pen samples its next step.

Declaration Summary
val temperature: Double
val noise: Double
val liftBias: Double
val driftX: Double
val driftY: Double

PenDistribution

class PenDistribution(val weights: DoubleArray, val meanX: DoubleArray, val meanY: DoubleArray, val sigmaX: DoubleArray, val sigmaY: DoubleArray, val correlation: DoubleArray, val liftProbability: Double)

A pen's prediction for its next step: a mixture of 2D Gaussians and a lift probability.

Declaration Summary
val weights: DoubleArray
val meanX: DoubleArray
val meanY: DoubleArray
val sigmaX: DoubleArray
val sigmaY: DoubleArray
val correlation: DoubleArray
val liftProbability: Double
val size: Int The number of mixture components.
fun toDrawing(at: Vec3, scale: Double) The same distribution, moved to at and scaled by scale: drawing coordinates.

PenMoment

class PenMoment(val from: Vec3, val to: Vec3, val mark: Boolean, val lifted: Boolean, val liftProbability: Double, val cells: FloatArray?, val hidden: FloatArray?)

One point a PenLine drew: from from to to in drawing units, as a mark or (when mark is false) a move with the pen up. liftProbability is the chance the pen gave of lifting after this point, read before it was drawn. cells and hidden are the pen's memory just after the point, kept only when the PenLine was made with recordMemory = true.

Declaration Summary
val from: Vec3
val to: Vec3
val mark: Boolean
val lifted: Boolean
val liftProbability: Double
val cells: FloatArray?
val hidden: FloatArray?

PenModel

class PenModel internal constructor(val hidden: Int, val mixtures: Int, val layers: Int, private val gates: List<FloatArray>, private val gateBias: List<FloatArray>, private val headWeight: FloatArray, private val headBias: FloatArray, val inputs: Int = 3, val sectors: Int = 0, val senseRadius: Double = 0.0, val senseRecent: Int = 0) : StrokeModel

The weights of an LSTM pen model, shared by every Pen made from it.

Declaration Summary
val hidden: Int
val mixtures: Int
val layers: Int
val inputs: Int 3 for a pen that reads only its own displacements, 3 + sectors + 2 for one that also sees the ink around it (PenSense), in sectors directions out to senseRadius model units, ignoring its last senseRecent points.
val sectors: Int
val senseRadius: Double
val senseRecent: Int
override fun pen(seed: Long): Pen
fun pen(primer: FLine, scale: Double = 8.0, step: Double = 0.35, seed: Long = 0): Pen A pen that has already been shown primer (see Pen.feed for scale and step).

Pen

class Pen internal constructor(val model: PenModel, seed: Long) : StrokePen

The state of one PenModel: its memory, where it is, whether its pen is up, and its random generator.

Declaration Summary
var sense: PenSense? The ink a sensing pen sees; null for a pen that does not sense.
var x0 Pen position in model units, the sum of every displacement fed so far.
var y0 The y of x0.
var penUp Whether the pen is off the paper, so the next step is a move rather than a mark.
var count Steps fed or drawn, not counting the start-of-sequence step.
override fun feed(dx: Double, dy: Double, lift: Boolean)
fun addInk(points: List<Pair<Double, Double>>) Let a sensing pen see ink it did not draw: points in model units, in the pen's own coordinates (its first point is the origin). Does nothing for a pen that does not sense.
fun feed(line: FLine, scale: Double = 8.0, step: Double = 0.35, liftAtEnd: Boolean = false) Show the pen line, so that what it draws next continues it.
override fun next(controls: PenControls): PenStep
fun propose(controls: PenControls = PenControls()): PenStep A sample of the next step, as next would draw it, without drawing it: the pen is unchanged apart from its random generator. Calling this several times and then feeding one is how a PenGuide chooses among proposals.
override fun fork(seed: Long): Pen
fun distribution(controls: PenControls = PenControls()): PenDistribution The pen's prediction for its next step, in model units relative to where it is: one weight, mean, spread and correlation per mixture component, with controls' temperature applied to the weights and its noise to the spreads, as next would sample them.
val liftProbability: Double The probability that the pen lifts after its next step, before any PenControls.liftBias.
fun cells(layer: Int = model.layers - 1): FloatArray A copy of layer layer's cell memory (the LSTM's c), one value per cell.
fun hidden(layer: Int = model.layers - 1): FloatArray A copy of layer layer's hidden output (the LSTM's h).
fun setCell(cell: Int, value: Double, layer: Int = model.layers - 1) Overwrite one memory cell, the Distill article's fourth experiment. The change shows up from the step after next: the head has already read the current output.
fun rawOutput(): FloatArray The head's raw output for the next step (see PenModel for its layout).

PenLine

class PenLine(primer: FLine? = null, val model: StrokeModel = PenModel.distill, val scale: Double = 8.0, var controls: PenControls = PenControls(), seed: Long = 0, step: Double = 0.35, start: Vec3 = Vec3(0f, 0f, 0f), val recordMemory: Boolean = false, surroundings: List<FLine> = emptyList(), liftAtEnd: Boolean = false)

A line that a pen keeps drawing: the primer you gave it, then everything the pen added.

Declaration Summary
val model: StrokeModel
val scale: Double
var controls: PenControls
val recordMemory: Boolean Also keep each step's memory (cells and hidden) in history, for colouring by a cell.
var pen: StrokePen The pen underneath, for Pen.setCell or forking by hand.
val line: FLine The primer followed by what has been drawn so far.
val drawn: FLine Only the pen's own contribution, without the primer.
val history: MutableList<PenMoment> One PenMoment per point the pen has drawn, oldest first.
val tip: Vec3 Where the pen is now, in drawing units.
val penUp: Boolean Whether the pen is off the paper, so its next point starts a new stroke.
fun grow(steps: Int = 1): FLine Draw steps more points onto line (and drawn); returns line.
var guide: PenGuide? Steers the pen without taking its hand: each point, the pen proposes PenGuide.candidates next steps (by forking itself) and the guide's choice is the one drawn. null, the default, lets the pen draw as it would.
fun distribution(): PenDistribution? What the pen expects next, in drawing units around tip: its mixture of Gaussians, with controls' temperature and noise applied. null if the pen is not a PenModel's.
fun futures(count: Int = 16, steps: Int = 40, seed: Long = 1): List<FLine> Ask what the pen might do next without letting it do it: count independent continuations of steps points each, from here. line and the pen are unchanged.
fun sense(lines: List<FLine>, step: Double = 0.35) Let a sensing pen see lines from now on, as ink already on the page: other strokes to join, follow or stop at. Resampled at the pen's own spacing. Does nothing for a pen that does not sense.
fun lift() Make the pen lift now, so the next point starts a new stroke.

penviz.kt

Pictures of what a learned pen is thinking: its next-step distribution as ellipses, a stroke coloured by one memory cell or drawn thick where the pen is sure to stay down, and the density of hundreds of possible futures as an image.

Everything here reads a PenLine: its PenLine.history for what it has drawn and PenLine.distribution for what it expects next. Tests: PenVizTest.

src/jvmMain/kotlin/afield/ai/learning/pen/penviz.kt — read the source

Used in: A learned pen, Teaching a pen, A pen that sees

Top-level functions

Declaration Summary
fun PenDistribution.ellipses(sigmas: Double = 2.0, minWeight: Double = 0.01, color: Vec4 = Vec4(0.55f, 0.85f, 1f, 1f)): List<FLine> One ellipse per mixture component, at sigmas standard deviations, with opacity following the component's weight; components lighter than minWeight are left out. Read it as "where the next point will land": a tight cluster is a pen that knows, a spread of faint rings one that is choosing.
fun PenLine.coloredBy(bands: Int = 16, low: Vec4 = Vec4(0.2f, 0.45f, 1f, 1f), high: Vec4 = Vec4(1f, 0.55f, 0.2f, 1f), range: ClosedFloatingPointRange<Double>? = null, thickness: Double = 1.5, value: (PenMoment) -> Double): List<FLine> The pen's marks, split into bands FLines by value, each coloured between low and high. range is the span of values mapped onto the colours; by default, the values' own.
fun PenLine.coloredByCell(cell: Int, bands: Int = 16, thickness: Double = 1.5, range: ClosedFloatingPointRange<Double>? = -1.0..1.0): List<FLine> The pen's marks coloured by memory cell cell's output (the LSTM's h): blue where it is low, orange where it is high. range is the span of outputs the colours cover, by default all of it, -1 to 1; null spreads them over the values this cell actually took, for a cell that never moves far. Needs a PenLine made with recordMemory = true.
fun PenLine.widthByLift(bands: Int = 12, thinnest: Double = 0.3, thickest: Double = 4.0, color: Vec4 = Vec4(0.9f, 0.92f, 1f, 1f), smoothing: Int = 4): List<FLine> The pen's marks drawn thick where it was sure the pen would stay on the paper and thin where it expected to lift: the lift probability read as pressure. It was never trained on pressure, so this is the model's confidence, not a hand's. Widths follow the logarithm of the probability, averaged over smoothing points either side and spread over this line's own range of it.
fun PenLine.liveliestCells(count: Int = 12): List<Int> The count memory cells whose output changed most while drawing, most variable first: the place to start looking for a cell that means something. Needs recordMemory = true.
fun PenLine.futuresDensity(area: DoubleArray? = null, count: Int = 400, steps: Int = 60, width: Int = 1040, height: Int = 520, seed: Long = 1, ink: Vec4 = Vec4(1f, 0.85f, 0.3f, 1f)): ByteArray Where the pen might go: count futures of steps points from here, accumulated into an image of width by height pixels over area (drawing coordinates, x, y, w, h), on a log scale so a single stray future still shows. line and extra are drawn on top in ink. Returns PNG bytes for display(...).
fun framing(lines: List<FLine>, aspect: Double): DoubleArray A rectangle (x, y, w, h) around lines's points, padded by a tenth, widened or heightened to aspect (width over height) so nothing is stretched. Ignores the farthest 1% of points, so one wild future does not shrink everything else.
fun PenLine.tail(points: Int = 20): FLine The last points points the pen drew, as an FLine ending at PenLine.tip: the context for a close-up.
fun PenLine.closeUp(points: Int = 8): PenCloseUp This moment, as a PenCloseUp over the last points points.
fun sideBySide(groups: List<List<FLine>>, gap: Double = 12.0, y: Double = 0.0): List<FLine> groups laid out left to right, each moved so it starts gap after the previous one ends and sits with its top at y: small multiples that never overlap, whatever their sizes.
fun stacked(groups: List<List<FLine>>, gap: Double = 12.0, x: Double = 0.0): List<FLine> groups stacked top to bottom, each starting gap below the previous one's lowest point.
fun PenLine.seeing(color: Vec4 = Vec4(1f, 0.6f, 0.3f, 1f)): List<FLine> What a sensing pen sees from where it is now: a faint circle as far as it can see, and a ray in each direction that has ink in reach, out to the nearest ink there, brighter the closer it is. Empty for a pen that does not sense.

Top-level values

Declaration Summary
val PenDistribution.uncertainty: Double How unsure a distribution is, in squared drawing units: the spread of where the next point could land, counting both each component's own spread and how far apart the components sit. Use it to find the moments worth a close-up.

DensityImage

class DensityImage(val width: Int, val height: Int, val area: DoubleArray)

Accumulates FLines into a grid of counts over a region of drawing coordinates and tone-maps them into an image: futuresDensity's canvas, usable on its own for any pile of lines.

Declaration Summary
fun add(line: FLine, weight: Float = 1f) Add line's marks, one unit of ink per pixel of length, anti-aliased.
fun toneMapped(): IntArray Counts as ARGB pixels: black through blue to white, logarithmically.
fun ink(line: FLine, color: Vec4, pixels: IntArray) Draw line solidly over pixels in color.

PenCloseUp

class PenCloseUp(val tail: FLine, val penUp: Boolean, val distribution: PenDistribution)

One moment of a PenLine: its last few points, whether its pen was up, and what it expected next.

Declaration Summary
fun lines(color: Vec4 = Vec4(0.85f, 0.9f, 1f, 1f)): List<FLine> The tail in color and the distribution's ellipses.

sense.kt

What a sensing pen sees of the ink around it: the Kotlin half of python/fieldpen/sense.py, which has the canonical definition. PenModelTest holds the two together.

At each point the pen sees every point drawn before it except its own last recent, plus any ink added from outside with addInk. Around the pen, sectors equal wedges of direction (sector k covers atan2(dy, dx) in [-pi + k * 2pi / sectors, -pi + (k + 1) * 2pi / sectors)) each report 1 - d / radius for their nearest visible point within radius, or 0. Two more values follow, the way to the middle of all the visible ink, tanh((cx - x) / CENTRE_SCALE) and the same for y: what is close, then where the mass of the drawing is. Units are model units throughout.

src/jvmMain/kotlin/afield/ai/learning/pen/sense.kt — read the source

PenSense

class PenSense(val sectors: Int, val radius: Double, val recent: Int)

The ink a pen has drawn or been shown, kept in a grid of cells radius wide so that a look around the pen touches only the nine cells near it.

Declaration Summary
fun add(x: Double, y: Double) Record the point the pen has just reached, and make the points recent back visible.
fun addInk(x: Double, y: Double) Add ink the pen did not draw, visible at once: lines around it it should sense.
fun features(x: Double, y: Double, out: FloatArray, offset: Int) Write what the pen sees from (x, y) into out from offset, size values.
fun copy(): PenSense An independent copy, for a forked pen.
fun points(): List<Pair<Double, Double>> Every point the pen has drawn or been shown, in model units: for drawing what it senses.

Ai4Installer.kt

Installs Field's AI assistant: the websocket handlers behind the box and document chat, and the code tools the model calls.

installAi4 registers the ai-assist, ai-chat-* and related message handlers, picks an LlmBridge for the configured provider, and keeps one conversation thread per chat key. Ai4CodeToolProviders implements the tools that read, edit, compile and evaluate box code, shared by the chat and the MCP backdoor. The top-level helpers (uniqueBoxOrRefuse, setupSnapshot, clipToolText, primitiveToolValue) turn boxes and runtime values into tool results.

src/jvmMain/kotlin/afield/ai/models/Ai4Installer.kt — read the source

Top-level functions

Declaration Summary
fun fileRefFor(docName: String, uid: String, property: String): String
fun boxLabel(target: Box): String
fun uniqueBoxOrRefuse(name: String, matches: List<Box>): Box? The single box in matches, or null if there were none — refusing rather than choosing when name matched more than one.
fun diagnosticToCompileDiagnostic(diagnostic: ScriptDiagnostic): CompileDiagnostic
fun setupSnapshot(target: Box): SetupSnapshot
fun setupChanges(before: SetupSnapshot, after: SetupSnapshot): List<SetupPropertyChange>
fun clipToolText(value: String, requestedMaxChars: Int): String
fun safeString(value: Any?): String
fun runtimeTypeName(value: Any?): String
fun primitiveKind(value: Any?): String
fun primitiveToolValue(value: Any?, maxChars: Int = 6000): Any?
fun installAi4()

FileRef

data class FileRef(val document: String, val uid: String, val property: String)

AmbiguousBoxNameException

class AmbiguousBoxNameException(val name: String, val matchCount: Int) : IllegalArgumentException("$matchCount boxes are named '$name'; reference one by its uid instead.")

Thrown when a box name an agent supplied matches more than one box.

SetupSnapshot

data class SetupSnapshot(val preamble: String, val preambleCache: String, val intro: String, val introCache: String, val attic: String, val atticCache: String)

Declaration Summary
val preamble: String
val preambleCache: String
val intro: String
val introCache: String
val attic: String
val atticCache: String

Ai4CodeToolProviders

class Ai4CodeToolProviders(private val docName: String, private val refResolver: (String, String) -> FileRef, private val boxByIdOrNull: (String) -> Box?, private val stagedOrCurrentSource: (Box, String, String?) -> String?)

Box-scoped Kotlin code tools (CompileKotlin, EvaluateKotlin), extracted from the ai-assist chat handler so the OpenAI harness and the MCP backdoor share one implementation. Context is supplied via four closures; everything else resolves to the promoted top-level helpers above. refResolver replaces the chat handler's exec-coupled requestedRefOrCurrent — the backdoor passes an explicit-target resolver with no "current box" fallback.

Declaration Summary
val evaluateKotlinExpression: (EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult
val compileKotlin: (CompileKotlinRequest) -> CompileKotlinResult

Ai4ModelDefaults.kt

src/jvmMain/kotlin/afield/ai/models/Ai4ModelDefaults.kt — read the source

AnthropicBridge.kt

The Anthropic (Claude) implementation of LlmBridge, running Field's agent loop over the Messages API.

It uses the same ToolRegistry and tool definitions as OpenAiIdeCodexBridge. The Messages API keeps no server-side conversation, so AnthropicBridge holds each thread's history locally and marks the system prompt and tool list for prompt caching.

src/jvmMain/kotlin/afield/ai/models/AnthropicBridge.kt — read the source

AnthropicBridge

class AnthropicBridge(private var client: AnthropicClient = anthropicClientFromFieldConfig(), private val model: String = Ai4ModelDefaults.flagshipModel(), private val log: LogSink = StdoutLog, private val baseUrl: String = configuredAnthropicBaseUrl() ?: ANTHROPIC_DEFAULT_BASE_URL) : LlmBridge

Anthropic (Claude) implementation of LlmBridge, mirroring OpenAiIdeCodexBridge's agent loop over the Messages API.

Declaration Summary
override val modelIdentity: AiChatModelIdentity
fun <T : Any> registerTool(toolClass: Class<T>, handler: (ToolExecutionContext, T) -> Any) As OpenAiIdeCodexBridge.registerTool: one more tool for this bridge only (tests use it for a probe).
override fun startThread(fileKey: String, fileText: String, instructions: String): LlmThreadState
override fun send(state: LlmThreadState, userPrompt: String, fileText: String?, ctx: ToolExecutionContext, vectorStoreIds: List<String>, maxOutputTokens: Long, maxToolRounds: Int, imageDataUrls: List<String>, onTextDelta: ((String) -> Unit)?, onProgress: ((LlmProgressUpdate) -> Unit)?, turnContext: TurnContext?): LlmSendResult

AnthropicCredentials.kt

Credential + client construction for the Anthropic (Claude) provider.

Field's enterprise setup can't mint a raw ANTHROPIC_API_KEY, so the default path here authenticates the way the official clients do: a short-lived OAuth bearer token minted from an ant auth login profile via ant auth print-credentials --access-token, sent as Authorization: Bearer plus the anthropic-beta: oauth-2025-04-20 header. A real API key (ANTHROPIC_API_KEY) or a pre-supplied bearer token (ANTHROPIC_AUTH_TOKEN) are honored first when present, so nothing here is ant-only.

This mirrors openAiClientFromFieldConfig / activeAiProviderRuntimeConfig for the OpenAI/OpenRouter providers; the client it returns exposes the same client.messages() surface the AnthropicBridge drives.

src/jvmMain/kotlin/afield/ai/models/AnthropicCredentials.kt — read the source

Top-level functions

Declaration Summary
fun configuredAnthropicProfile(): String? The ant profile to bind to, if any: env → ~/.afield/anthropic-profile.txt → active/default.
fun configuredAnthropicBaseUrl(): String? Base URL override: env → ai-models.propertiesanthropicBaseUrl`` → null (SDK default, api.anthropic.com).
fun invalidateAnthropicOAuthToken() Force a fresh token on the next resolve — call this after a 401 from the API.
fun resolveAnthropicAuth(forceRefresh: Boolean = false): AnthropicAuth Resolve auth for a request. Precedence: explicit API key → explicit bearer token → ant OAuth profile. Only the OAuth/bearer paths set AnthropicAuth.isOAuth.
fun anthropicClientFromFieldConfig(forceRefresh: Boolean = false): AnthropicClient
fun anthropicStatus(): AnthropicStatus Non-throwing status for the /welcome screen. Reports whether we can currently obtain a credential; the token mint is cached so repeat calls are cheap.

Top-level values

Declaration Summary
const val ANTHROPIC_API_KEY_ENV
const val ANTHROPIC_AUTH_TOKEN_ENV
const val ANTHROPIC_BASE_URL_ENV
const val ANTHROPIC_DEFAULT_BASE_URL Where the SDK sends requests when configuredAnthropicBaseUrl is null.
const val ANTHROPIC_PROFILE_ENV
const val AFIELD_ANT_BIN_ENV

MissingAnthropicCredentialException

class MissingAnthropicCredentialException(message: String) : IllegalStateException(message)

AnthropicAuth

data class AnthropicAuth(val token: String, val source: String, val isOAuth: Boolean)

Resolved Anthropic auth for one request. isOAuth selects the header shape: bearer token + oauth beta header vs. x-api-key.

Declaration Summary
val token: String
val source: String
val isOAuth: Boolean

AnthropicStatus

data class AnthropicStatus(val available: Boolean, val source: String, val detail: String, val baseUrl: String, val profile: String)

Declaration Summary
val available: Boolean
val source: String
val detail: String
val baseUrl: String
val profile: String

LlmBridge.kt

The interface the IDE agent loop talks to, with one implementation per provider.

Both the OpenAI/OpenRouter engine (OpenAiIdeCodexBridge, via OpenAiLlmBridge) and the Anthropic engine (AnthropicBridge) implement this, so IdeChatHelpers and the installer can drive whichever provider is selected on /welcome without caring which one is behind the interface.

The state/result types are the ones OpenAiIdeCodexBridge already defines — they carry no OpenAI-SDK types in their signatures, so they double as the neutral currency here. The typealiases below give them provider-neutral names without moving the declarations their callers already reference.

src/jvmMain/kotlin/afield/ai/models/LlmBridge.kt — read the source

Top-level functions

Declaration Summary
fun configuredAiReasoningEffort(): LlmReasoningEffort A provider-scoped local preference; explicit per-call choices still take precedence.
fun createLlmBridge(model: String = Ai4ModelDefaults.flagshipModel(), log: LogSink = StdoutLog, reasoningEffort: LlmReasoningEffort = configuredAiReasoningEffort()): LlmBridge Provider-aware factory: builds the agent engine matching the /welcome provider setting. Shared by the IDE installer and the docs-search bridge so provider selection lives in exactly one place.
fun createIdeBridge(log: LogSink = StdoutLog): LlmBridge The bridge every AI4 chat uses: the configured provider at the flagship model. The box panel and the document chat build theirs here, and so does a live AgentTask, so a task runs on exactly the engine a person's chat would.

LlmThreadState

typealias LlmThreadState = OpenAiIdeCodexBridge.ThreadState

LlmSendResult

typealias LlmSendResult = OpenAiIdeCodexBridge.SendResult

LlmProgressUpdate

typealias LlmProgressUpdate = OpenAiIdeCodexBridge.ProgressUpdate

LlmToolTranscriptEntry

typealias LlmToolTranscriptEntry = OpenAiIdeCodexBridge.ToolTranscriptEntry

LlmReasoningEffort

enum class LlmReasoningEffort

Constants: LOW, MEDIUM, HIGH.

LlmBridge

interface LlmBridge

Declaration Summary
val modelIdentity: AiChatModelIdentity? Configured identity while a request is pending; response metadata arrives through progress/results.
fun startThread(fileKey: String, fileText: String, instructions: String = ""): LlmThreadState
fun send(state: LlmThreadState, userPrompt: String, fileText: String? = null, ctx: ToolExecutionContext = ToolExecutionContext(), vectorStoreIds: List<String> = emptyList(), maxOutputTokens: Long = 20480, maxToolRounds: Int = 24, imageDataUrls: List<String> = emptyList(), onTextDelta: ((String) -> Unit)? = null, onProgress: ((LlmProgressUpdate) -> Unit)? = null, turnContext: TurnContext? = null): LlmSendResult

OpenAiLlmBridge

class OpenAiLlmBridge(private val inner: OpenAiIdeCodexBridge) : LlmBridge

Adapter that presents the existing (untouched) OpenAiIdeCodexBridge as an LlmBridge. Overrides carry no default values (Kotlin forbids that on an override); the defaults live on the interface and reach every call site typed as LlmBridge. A blank startThread instruction defers to the inner bridge's own default so existing behaviour is preserved exactly.

Declaration Summary
val delegate: OpenAiIdeCodexBridge
override val modelIdentity: AiChatModelIdentity
override fun startThread(fileKey: String, fileText: String, instructions: String): LlmThreadState
override fun send(state: LlmThreadState, userPrompt: String, fileText: String?, ctx: ToolExecutionContext, vectorStoreIds: List<String>, maxOutputTokens: Long, maxToolRounds: Int, imageDataUrls: List<String>, onTextDelta: ((String) -> Unit)?, onProgress: ((LlmProgressUpdate) -> Unit)?, turnContext: TurnContext?): LlmSendResult

LocalAiConnection.kt

The connection check behind the local-model panel on Field's welcome page.

Field asks the endpoint for its OpenAI-style model list at /models, from the server JVM, so no code is sent and no inference runs. The answer says whether the endpoint replied, which model ids it lists, and whether the one chosen in the panel is among them; a non-200 reply, or a body that is not a model list, fails with that as the reason rather than as a missing model. A saved key is reused only for the endpoint it was saved against, and a cloud credential is never offered to a local one.

src/jvmMain/kotlin/afield/ai/models/LocalAiConnection.kt — read the source

LocalAiContextRecovery.kt

Recovery from a local model that cannot fit the conversation: when a self-hosted, OpenAI-compatible provider rejects a request for exceeding its context window, Field shortens the history and retries rather than failing the turn.

isLocalContextRecoveryFailure recognises that case from the exception chain, including the empty answer some servers return in place of an error, and returns false for a cancelled turn or a response that was already partly delivered. shortenLocalHistory then drops older exchanges until the request is about half its original size, measured in characters, while keeping the seeded start of the conversation, the current prompt, and any tool call whose output is still pending; it adds a developer-role note saying that older exchanges were omitted and that completed tool work has not been replayed. Two rounds are allowed, and the failure after them is LocalContextRecoveryExhausted.

src/jvmMain/kotlin/afield/ai/models/LocalAiContextRecovery.kt — read the source

LocalAiFeedback.kt

src/jvmMain/kotlin/afield/ai/models/LocalAiFeedback.kt — read the source

OpenAiCredentials.kt

Where Field's AI features find their provider, API key, endpoint and model names.

Each setting is read from an environment variable first and then from a file under ~/.afield/: the provider (AFIELD_AI_PROVIDER, else ai-provider.txt, defaulting to openai), the OpenAI key (OPENAI_PLATFORM_KEY or OPENAI_API_KEY, else openai-platform-key.txt), the OpenRouter key and the per-provider model settings. The /welcome page writes the files through saveAiSettings and saveOpenAiPlatformKey, and aiProviderConfigRevision changes on each saveAiSettings so cached clients know to rebuild. activeAiProviderRuntimeConfig resolves all of it into the one AiProviderRuntimeConfig a bridge is built from.

src/jvmMain/kotlin/afield/ai/models/OpenAiCredentials.kt — read the source

Top-level functions

Declaration Summary
fun aiProviderConfigRevision(): Long Changes whenever /welcome saves provider, credential, endpoint, or model settings.
fun openAiCredentialFile(): File
fun openRouterCredentialFile(): File
fun readOpenAiCredential(): Pair<String, String>?
fun readOpenRouterCredential(): Pair<String, String>?
fun configuredAiProvider(): String
fun configuredAiModelSettings(provider: String = configuredAiProvider()): AiModelSettings
fun configuredLocalAiContextTokens(): Int? The local inference server's context window in tokens: AFIELD_LOCAL_AI_CONTEXT_TOKENS, else local.contextTokens in ai-models.properties, else null, in which case the bridge learns it from the server's first refusal of an oversized request.
fun configuredAiBaseUrl(provider: String = configuredAiProvider()): String
fun openAiCredentialStatus(): OpenAiCredentialStatus
fun aiProviderStatus(): AiProviderStatus
fun saveOpenAiPlatformKey(key: String): OpenAiCredentialStatus
fun saveAiSettings(request: AiSettingsSaveRequest): AiProviderStatus
fun activeAiProviderRuntimeConfig(): AiProviderRuntimeConfig
fun openAiClientFromFieldConfig(): OpenAIClient

Top-level values

Declaration Summary
const val OPENAI_PLATFORM_KEY_ENV Where Field's AI features find their provider, API key, endpoint and model names.
const val OPENAI_API_KEY_ENV
const val OPENROUTER_API_KEY_ENV
const val AFIELD_AI_PROVIDER_ENV
const val AFIELD_AI_BASE_URL_ENV
const val OPENROUTER_BASE_URL_ENV
const val LOCAL_AI_BASE_URL_ENV
const val LOCAL_AI_API_KEY_ENV
const val LOCAL_AI_DEFAULT_BASE_URL

MissingOpenAiCredentialException

class MissingOpenAiCredentialException(message: String) : IllegalStateException(message)

OpenAiCredentialStatus

data class OpenAiCredentialStatus(val configured: Boolean, val source: String, val configPath: String)

Declaration Summary
val configured: Boolean
val source: String
val configPath: String

AiModelSettings

data class AiModelSettings(val flagshipModel: String = "", val fastModel: String = "", val fallbackModel: String = "", val reasoningEffort: String = "")

Declaration Summary
val flagshipModel: String
val fastModel: String
val fallbackModel: String
val reasoningEffort: String

AiProviderStatus

data class AiProviderStatus(val provider: String, val hasOpenAiPlatformKey: Boolean, val hasOpenRouterApiKey: Boolean, val source: String, val configPath: String, val openRouterConfigPath: String, val baseUrl: String, val modelSettings: AiModelSettings)

Declaration Summary
val provider: String
val hasOpenAiPlatformKey: Boolean
val hasOpenRouterApiKey: Boolean
val source: String
val configPath: String
val openRouterConfigPath: String
val baseUrl: String
val modelSettings: AiModelSettings

AiSettingsSaveRequest

data class AiSettingsSaveRequest(val provider: String, val openAiPlatformKey: String?, val openRouterApiKey: String?, val openRouterBaseUrl: String?, val flagshipModel: String?, val fastModel: String?, val fallbackModel: String?, val localBaseUrl: String? = null, val localApiKey: String? = null)

Declaration Summary
val provider: String
val openAiPlatformKey: String?
val openRouterApiKey: String?
val openRouterBaseUrl: String?
val flagshipModel: String?
val fastModel: String?
val fallbackModel: String?
val localBaseUrl: String?
val localApiKey: String?

AiProviderRuntimeConfig

data class AiProviderRuntimeConfig(val provider: String, val apiKey: String, val source: String, val baseUrl: String, val modelSettings: AiModelSettings)

Declaration Summary
val provider: String
val apiKey: String
val source: String
val baseUrl: String
val modelSettings: AiModelSettings
val isOpenRouter: Boolean
val usesStatelessResponses: Boolean

OpenAiIdeCodexBridge.kt

The OpenAI implementation of LlmBridge: Field's agent loop over the Responses API.

Each turn sends the conversation and the shared tool definitions, runs the function calls the model returns through the ToolRegistry, and feeds the outputs back until the model answers in text. Against OpenAI the history lives in a server-side conversation; against OpenRouter, which has none, the bridge keeps it locally per thread. Rate-limit waits, suspended turns and the request ledger are handled here too.

src/jvmMain/kotlin/afield/ai/models/OpenAiIdeCodexBridge.kt — read the source

OpenAiIdeCodexBridge

class OpenAiIdeCodexBridge(private val runtimeConfig: AiProviderRuntimeConfig = activeAiProviderRuntimeConfig(), private val client: OpenAIClient = openAiClientFromFieldConfig(), model: String = Ai4ModelDefaults.flagshipModel(), private val log: LogSink = StdoutLog, private val reasoningEffort: ReasoningEffort = ReasoningEffort.MEDIUM)

The OpenAI implementation of LlmBridge: Field's agent loop over the Responses API.

Declaration Summary
val modelName: String
val modelIdentity: AiChatModelIdentity
fun <T : Any> registerTool(toolClass: Class<T>, handler: (ToolExecutionContext, T) -> Any)
fun startThread(fileKey: String, fileText: String, instructions: String = defaultThreadInstructions): ThreadState
fun send(state: ThreadState, userPrompt: String, fileText: String? = null, ctx: ToolExecutionContext = ToolExecutionContext(), vectorStoreIds: List<String> = emptyList(), maxOutputTokens: Long = 20480, maxToolRounds: Int = 24, imageDataUrls: List<String> = emptyList(), onTextDelta: ((String) -> Unit)? = null, onProgress: ((ProgressUpdate) -> Unit)? = null, turnContext: TurnContext? = null): SendResult Send a prompt, chaining with previous_response_id when available.
fun threadStateToJson(state: ThreadState): String
fun threadStateFromJson(json: String): ThreadState
data class SendResult(val threadState: ThreadState, val text: String, val rawResponseId: String, val commentary: List<String> = emptyList(), val conversationAdditions: List<String> = emptyList(), val toolTranscript: List<ToolTranscriptEntry> = emptyList(), val modelIdentity: AiChatModelIdentity? = null, val rounds: Int = -1, val hitToolRoundCap: Boolean = false, val suspension: TurnSuspension? = null, val inputTokens: Long? = null, val outputTokens: Long? = null, val cacheReadTokens: Long? = null, val cacheWriteTokens: Long? = null, val contextReport: ContextReport? = null)
data class ToolTranscriptEntry(val name: String, val callId: String, val argumentsJson: String, val resultSummary: String, val resultJson: String, val elapsedMs: Long = -1, val parentCallId: String = "")
data class ProgressUpdate(val title: String, val body: String = "", val modelIdentity: AiChatModelIdentity? = null, val phase: String? = null)
data class ThreadState(val localThreadId: String, val conversationId: String, val fileKey: String, val initialInstructions: String = "", val initialFileText: String = "", var previousResponseId: String? = null, var lastFileText: String? = initialFileText, val loadedTools: MutableSet<String> = linkedSetOf())
data class CappedTurnContinuation(val pendingCalls: List<PendingToolCall> = emptyList(), val finalText: String = "") How a capped turn's answer reaches the conversation: as plain assistant text after "not run" outputs for its pending calls, sent with the next turn, rather than as the forced final response itself. That response was made under "summarize ... without calling tools", and what the server keeps of it does not stay request-scoped, even with the stop sent as instructions. On house-machine-11 (2026-10-05) the forced final request also compacted the conversation (the next turn's input was 55k tokens, down from 100k); the next two turns, chained onto it, were offered every tool with tool_choice auto and answered "tool execution is still disabled here" without a call, though the first of them carried the reset note. 2026-10-04 had been the same, without the note.
data class PendingToolCall(val callId: String, val name: String)

ScriptedLlmBridge.kt

An LlmBridge that replays a fixed script of tool calls instead of asking a model.

Tests and the no-credentials mode of agent tasks use it to drive the real tool dispatch; ScriptedLlmBridge documents the script format.

src/jvmMain/kotlin/afield/ai/models/ScriptedLlmBridge.kt — read the source

ScriptedLlmBridge

class ScriptedLlmBridge(private val script: List<Step>, private val label: String = "scripted") : LlmBridge

An LlmBridge with no model behind it: it replays a script of tool calls and a final answer through the real ToolRegistry, exactly as a provider bridge would, and records what it was given.

Declaration Summary
sealed class Step
data class Received(val instructions: String, val prompt: String, val fileText: String?, val maxToolRounds: Int, val availableTools: List<String>, val turnContext: TurnContext? = null, val resumedRound: SuspendedRound? = null) What one send was handed, for assertions.
val received
val interjections The user messages handed over between rounds (AiTurnInbox), as the model would have read them.
override val modelIdentity: AiChatModelIdentity
override fun startThread(fileKey: String, fileText: String, instructions: String): LlmThreadState
override fun send(state: LlmThreadState, userPrompt: String, fileText: String?, ctx: ToolExecutionContext, vectorStoreIds: List<String>, maxOutputTokens: Long, maxToolRounds: Int, imageDataUrls: List<String>, onTextDelta: ((String) -> Unit)?, onProgress: ((LlmProgressUpdate) -> Unit)?, turnContext: TurnContext?): LlmSendResult

StatelessHistory.kt

Keeping a stateless provider's conversation (local models, OpenRouter) inside its context window.

A stateless provider is sent its whole conversation on every request, so the conversation is ours to size, and an item count does not size it: every turn's user message carries the full instructions, and each of those copies stays in history. Three things keep it inside:

  • settle, once per turn boundary: the finished turn's prompt is swapped for its durable text (the request without the instructions, which the newest message always carries), and older tool outputs and images are cut down. Done once, so the history stays a stable prefix for the server's prompt cache.
  • dropOldestTurns, when an estimate says the next request will not fit: whole turns go from the front, always at a user message, so no function call loses its output.
  • contextOverflow reads the window out of the server's own refusal, in llama.cpp's wording ("request (65833 tokens) exceeds the available context size (65536 tokens)") or vLLM's, which calibrates the estimate; the bridge then fits the request and retries.

src/jvmMain/kotlin/afield/ai/models/StatelessHistory.kt — read the source

VoiceTranscriber.kt

Local speech-to-text for AI4 push-to-talk.

The browser records while the push-to-talk key is held and sends the clip as ai-transcribe; this hands it to python/fieldvoice/whisper_worker.py, a Python process started on first use and kept alive so the model loads once (large-v3-turbo on a GPU, small.en on a CPU). The worker is an optional dependency (uv sync --inexact --group voice); without it the reply says how to install it.

The interpreter is FIELD_VOICE_PYTHON if set, otherwise the one Field's Python boxes resolved (the same venv the voice group is installed into), otherwise the checkout's .venv (Scripts\python.exe on Windows).

One clip is transcribed at a time, on a dedicated thread: a second press queues behind the first.

src/jvmMain/kotlin/afield/ai/models/VoiceTranscriber.kt — read the source

VoiceTranscriber

object VoiceTranscriber

The push-to-talk transcriber: owns the Whisper worker process and the thread clips queue on.

Declaration Summary
fun install() Registers the ai-transcribe and ai-transcribe-warm handlers the browser's recorder calls.

MarkdownChunker.kt

Markdown cut into the pieces semantic search indexes and an embedder can read whole.

Four things go wrong when a page is cut on its # lines and indexed raw, and each has an answer here. A # line inside a code fence is a comment rather than a heading, so headings count only outside fences. The generated reference pages carry their <span class=…> markup into the indexed text, so markup outside code is reduced to the text it wraps. A section longer than the window the embedder reads (around 512 tokens, which DEFAULT_MAX_CHARS approximates) loses its end, so a long section is split on paragraph boundaries. And a section alone says nothing about where it sits ("Kotlin side", of what?), so every chunk carries a breadcrumb of page title, parent headings and its own.

src/jvmMain/kotlin/afield/ai/retrieval/MarkdownChunker.kt — read the source

RelatedContextDiscovery.kt

Suggests the guides and example files most related to a source path, for AI tools that hand an agent a symbol or file.

RelatedContextDiscovery.forPath scores the Markdown files and the .kt/.kts files under an examples/ directory beneath src, dev_docs, public and docs in the checkout against the path's directory and camel-case name tokens, and returns up to six of each, best first. The file list is walked once and each path's answer is cached. An afield:// path gets the connected compute-and-render example.

src/jvmMain/kotlin/afield/ai/retrieval/RelatedContextDiscovery.kt — read the source

RelatedContextHints

data class RelatedContextHints(val guides: List<String> = emptyList(), val examples: List<String> = emptyList())

Up to six related guide paths and six example paths, best match first.

Declaration Summary
val guides: List<String>
val examples: List<String>

RelatedContextDiscovery

object RelatedContextDiscovery

Process-wide related-context lookup rooted at the current working directory.

Declaration Summary
fun forPath(rawPath: String?): RelatedContextHints

RetrievalEval.kt

How well documentation search answers the questions the repository already asks of it.

Queries and answers come from two sources the authors wrote for readers rather than for a benchmark: the text of every cross-link between authored docs pages ([drawing with FLines](fline_examples.md) is a query whose answer is fline_examples.md; the standard anchor-text method), and the why text of each docs: entry in the agent transcripts (dev_docs/agents/transcripts/<name>.yaml), each of which says what a page explains. An anchor is left out when its text is generic (here, this page, api) or points at three or more different pages; the page a link sits on is not counted against it, since it contains the anchor text by construction.

src/jvmMain/kotlin/afield/ai/retrieval/RetrievalEval.kt — read the source

Top-level functions

Declaration Summary
fun main(args: Array<String>) Score documentation search on RetrievalEval.cases. Run from a checkout with an isolated index: -Dafield.semanticSearch.cacheRoot=<dir with config.json + models> and -Dafield.semanticSearch.indexPath=<dir>; -Dafield.semanticSearch.corpora=documentation keeps the build to minutes. Arguments: rebuild to (re)build first; then fusion modes to compare (default: hybrid lexical dense). Writes per-case ranks to build/retrieval-eval/<mode>.tsv.

RetrievalEval

object RetrievalEval

Query sets built from the repository's own prose, and the hit@1, hit@5 and MRR over them.

Declaration Summary
data class Case(val set: String, val query: String, val pages: Set<String>, val excludePage: String = "")
fun anchorCases(docsRoot: File): List<Case> Cross-links between authored pages (not the generated reference, not assets).
fun whyCases(transcriptsDir: File): List<Case> docs: [{page, why}] in the agent transcripts: what a page explains, in its author's words.
data class Lesson(val title: String, val files: List<String>, val summary: String)
fun graphicsLessons(index: File): List<Lesson> The graphics curriculum's lessons from its generated index: the lesson table and the reverse API index.
fun tutorialCases(checkout: Path): List<Case> Tutorial search, from the transcripts' docs links: the title a docs page's own author gave it is a query, and the tutorials that link to that page answer it. The summaries indexed for tutorials leave these links out.
fun cases(checkout: Path): List<Case>
data class Score(val set: String, val n: Int, val hit1: Int, val hit5: Int, val mrr: Double)
fun rank(case: Case, results: List<String>): Int? Rank of the first result on an answering page, counting pages once and skipping Case.excludePage. search returns result paths relative to the checkout (public/docs/...), best first.
fun score(cases: List<Case>, search: (String) -> List<String>): Pair<List<Score>, List<Pair<Case, Int?>>>

SemanticRetrievalService.kt

Hybrid semantic search over Field's own source, the open workspace and the documentation, backed by a local Lucene index and an ONNX embedding model.

Each indexed item is a SemanticCapsule carrying an intent text, an implementation text and a lexical text. SemanticRetrievalService.query runs BM25 and symbol matches alongside vector nearest-neighbour searches on the intent and implementation embeddings, fuses the ranks, and filters by corpus (field-internals, workspace, documentation), path, kind, document, box and property.

Models and cache

OnnxSemanticEmbedder implements SemanticEmbedder with a Hugging Face tokenizer and ONNX Runtime. The model presets download from Hugging Face through SemanticRetrievalService.downloadPreset; models, settings and the index live under ~/.afield/semantic-search unless afield.semanticSearch.cacheRoot (or AFIELD_SEMANTIC_SEARCH_CACHE_ROOT) says otherwise.

Rebuilding

SemanticRetrievalService.rebuild and SemanticRetrievalService.startRebuild rebuild the index; SemanticRetrievalService.startIdleRebuildWatcher rebuilds when workspace documents change.

src/jvmMain/kotlin/afield/ai/retrieval/SemanticRetrievalService.kt — read the source

SemanticRetrievalStatus

data class SemanticRetrievalStatus(val enabled: Boolean, val indexPath: String, val indexExists: Boolean, val indexedDocuments: Int = 0, val indexedCapsules: Int = 0, val capsuleBackend: String = "source-index-capsules", val embedderBackend: String = "disabled", val embeddingDimension: Int = 0, val lastBuildEpochMs: Long = 0, val workspaceRoot: String = "", val workspaceDocuments: Int = 0, val workspaceSidecarFiles: Int = 0, val corpusFingerprint: String = "", val currentCorpusFingerprint: String = "", val indexStale: Boolean = false, val staleDocuments: Int = -1, val message: String = "")

Declaration Summary
val enabled: Boolean
val indexPath: String
val indexExists: Boolean
val indexedDocuments: Int
val indexedCapsules: Int
val capsuleBackend: String
val embedderBackend: String
val embeddingDimension: Int
val lastBuildEpochMs: Long
val workspaceRoot: String
val workspaceDocuments: Int
val workspaceSidecarFiles: Int
val corpusFingerprint: String
val currentCorpusFingerprint: String
val indexStale: Boolean
val staleDocuments: Int
val message: String

SemanticRetrievalConfig

data class SemanticRetrievalConfig(val enabled: Boolean = true, val presetId: String = "granite-97m-r2-quantized", val modelPath: String = "", val tokenizerPath: String = "", val dimension: Int = 384, val maxTokens: Int = 512, val queryPrefix: String = "", val documentPrefix: String = "")

Declaration Summary
val enabled: Boolean
val presetId: String
val modelPath: String
val tokenizerPath: String
val dimension: Int
val maxTokens: Int
val queryPrefix: String
val documentPrefix: String

SemanticRetrievalModelPreset

data class SemanticRetrievalModelPreset(val id: String, val label: String, val modelUrl: String, val tokenizerUrl: String, val modelFileName: String, val tokenizerFileName: String, val dimension: Int, val maxTokens: Int, val queryPrefix: String = "", val documentPrefix: String = "", val note: String = "")

Declaration Summary
val id: String
val label: String
val modelUrl: String
val tokenizerUrl: String
val modelFileName: String
val tokenizerFileName: String
val dimension: Int
val maxTokens: Int
val queryPrefix: String
val documentPrefix: String
val note: String

SemanticRetrievalFileState

data class SemanticRetrievalFileState(val path: String = "", val exists: Boolean = false, val sizeBytes: Long = 0, val lastModifiedEpochMs: Long = 0)

Declaration Summary
val path: String
val exists: Boolean
val sizeBytes: Long
val lastModifiedEpochMs: Long

SemanticRetrievalSettingsStatus

data class SemanticRetrievalSettingsStatus(val config: SemanticRetrievalConfig, val status: SemanticRetrievalStatus, val model: SemanticRetrievalModelInfo, val presets: List<SemanticRetrievalModelPreset>, val modelFile: SemanticRetrievalFileState, val tokenizerFile: SemanticRetrievalFileState, val indexSizeBytes: Long, val modelCacheSizeBytes: Long, val configPath: String, val cacheRoot: String, val downloadRunning: Boolean = false, val rebuild: SemanticRetrievalBuildStatus = SemanticRetrievalBuildStatus(), val message: String = "")

Declaration Summary
val config: SemanticRetrievalConfig
val status: SemanticRetrievalStatus
val model: SemanticRetrievalModelInfo
val presets: List<SemanticRetrievalModelPreset>
val modelFile: SemanticRetrievalFileState
val tokenizerFile: SemanticRetrievalFileState
val indexSizeBytes: Long
val modelCacheSizeBytes: Long
val configPath: String
val cacheRoot: String
val downloadRunning: Boolean
val rebuild: SemanticRetrievalBuildStatus
val message: String

SemanticRetrievalBuildStatus

data class SemanticRetrievalBuildStatus(val running: Boolean = false, val status: String = "idle", val startedAtEpochMs: Long = 0, val finishedAtEpochMs: Long = 0, val message: String = "", val step: String = "", val indexedCapsules: Int = 0)

Declaration Summary
val running: Boolean
val status: String
val startedAtEpochMs: Long
val finishedAtEpochMs: Long
val message: String
val step: String
val indexedCapsules: Int

SemanticRetrievalBuildResult

data class SemanticRetrievalBuildResult(val success: Boolean, val message: String, val status: SemanticRetrievalStatus, val rebuild: SemanticRetrievalBuildStatus = SemanticRetrievalBuildStatus())

Declaration Summary
val success: Boolean
val message: String
val status: SemanticRetrievalStatus
val rebuild: SemanticRetrievalBuildStatus

SemanticRetrievalQueryRequest

data class SemanticRetrievalQueryRequest(val query: String, val limit: Int = 10, val corpus: String = "", val corpora: List<String> = emptyList(), val pathPrefix: String = "", val kinds: List<String> = emptyList(), val documentName: String = "", val boxId: String = "", val property: String = "", val rebuildIfEmpty: Boolean = true, val includeCapsule: Boolean = false, val includeSource: Boolean = false)

Declaration Summary
val query: String
val limit: Int
val corpus: String
val corpora: List<String>
val pathPrefix: String
val kinds: List<String>
val documentName: String
val boxId: String
val property: String
val rebuildIfEmpty: Boolean
val includeCapsule: Boolean
val includeSource: Boolean

SemanticRetrievalQueryResult

data class SemanticRetrievalQueryResult(val success: Boolean, val message: String, val status: SemanticRetrievalStatus, val results: List<SemanticRetrievalHit> = emptyList())

Declaration Summary
val success: Boolean
val message: String
val status: SemanticRetrievalStatus
val results: List<SemanticRetrievalHit>

SemanticRetrievalHit

data class SemanticRetrievalHit(val id: String, val scale: String, val corpus: String, val kind: String, val symbol: String, val title: String, val path: String, val line: Int, val endLine: Int = 0, val parentId: String = "", val documentName: String = "", val boxName: String = "", val boxId: String = "", val property: String = "", val language: String = "", val score: Double, val vectorRank: Int = 0, val bm25Rank: Int = 0, val symbolRank: Int = 0, val vectorScore: Float = 0f, val bm25Score: Float = 0f, val symbolScore: Float = 0f, val capsule: String = "", val sourcePreview: String = "")

Declaration Summary
val id: String
val scale: String
val corpus: String
val kind: String
val symbol: String
val title: String
val path: String
val line: Int
val endLine: Int
val parentId: String
val documentName: String
val boxName: String
val boxId: String
val property: String
val language: String
val score: Double
val vectorRank: Int
val bm25Rank: Int
val symbolRank: Int
val vectorScore: Float
val bm25Score: Float
val symbolScore: Float
val capsule: String
val sourcePreview: String

SemanticRetrievalModelInfo

data class SemanticRetrievalModelInfo(val backend: String, val dimension: Int, val modelPath: String = "", val tokenizerPath: String = "", val queryPrefix: String = "", val documentPrefix: String = "", val message: String = "")

Declaration Summary
val backend: String
val dimension: Int
val modelPath: String
val tokenizerPath: String
val queryPrefix: String
val documentPrefix: String
val message: String

SemanticRetrievalDownloadRequest

data class SemanticRetrievalDownloadRequest(val presetId: String = "granite-97m-r2-quantized")

Declaration Summary
val presetId: String

SemanticRetrievalConfigSaveRequest

data class SemanticRetrievalConfigSaveRequest(val enabled: Boolean? = null, val presetId: String? = null, val modelPath: String? = null, val tokenizerPath: String? = null, val dimension: Int? = null, val maxTokens: Int? = null, val queryPrefix: String? = null, val documentPrefix: String? = null)

Declaration Summary
val enabled: Boolean?
val presetId: String?
val modelPath: String?
val tokenizerPath: String?
val dimension: Int?
val maxTokens: Int?
val queryPrefix: String?
val documentPrefix: String?

SemanticRetrievalDeleteCacheRequest

data class SemanticRetrievalDeleteCacheRequest(val target: String = "index")

Declaration Summary
val target: String

SemanticCapsule

data class SemanticCapsule(val id: String, val scale: String, val corpus: String, val kind: String, val symbol: String, val title: String, val path: String, val line: Int, val endLine: Int = 0, val parentId: String = "", val documentName: String = "", val boxName: String = "", val boxId: String = "", val property: String = "", val language: String = "", val signature: String = "", val documentation: String = "", val metadata: String = "", val source: String = "")

Declaration Summary
val id: String
val scale: String
val corpus: String
val kind: String
val symbol: String
val title: String
val path: String
val line: Int
val endLine: Int
val parentId: String
val documentName: String
val boxName: String
val boxId: String
val property: String
val language: String
val signature: String
val documentation: String
val metadata: String
val source: String
fun intentText(): String
fun implementationText(): String
fun lexicalText(): String

SemanticEmbedder

interface SemanticEmbedder : AutoCloseable

Declaration Summary
val dimension: Int
val backend: String
fun encodeQueries(texts: List<String>): List<FloatArray>
fun encodeDocuments(texts: List<String>): List<FloatArray>

SemanticRetrievalService

object SemanticRetrievalService

Declaration Summary
val availableCorpora: List<String>
val presets: List<SemanticRetrievalModelPreset>
fun modelInfo(): SemanticRetrievalModelInfo
fun status(): SemanticRetrievalStatus
fun settingsStatus(): SemanticRetrievalSettingsStatus
fun settingsStatusSnapshot(): SemanticRetrievalSettingsStatus Non-blocking counterpart to settingsStatus for the welcome screen. Returns the last computed status immediately (or a cheap placeholder before the first compute finishes) and refreshes in the background when stale. Keeps the one-time corpus-fingerprint rescan and the Lucene/directory-size walks off the caller's thread so they can't stall the shared workspace-search dispatcher — and, through it, a document the user opens right after.
fun saveConfig(request: SemanticRetrievalConfigSaveRequest): SemanticRetrievalSettingsStatus
fun downloadPreset(request: SemanticRetrievalDownloadRequest): SemanticRetrievalSettingsStatus
fun deleteCache(request: SemanticRetrievalDeleteCacheRequest): SemanticRetrievalSettingsStatus
fun rebuild(): SemanticRetrievalBuildResult
fun startRebuild(): SemanticRetrievalBuildResult
fun query(request: SemanticRetrievalQueryRequest): SemanticRetrievalQueryResult
fun startIdleRebuildWatcher() Starts a daemon that rebuilds a stale index once the workspace has been quiet for IDLE_QUIET_MS. It only touches an index this workspace built (the meta's workspaceRoot), since the index is shared machine-wide and an automation server has a workspace of its own. AFIELD_SEMANTIC_IDLE_REBUILD=0 turns it off.

OnnxSemanticEmbedder

class OnnxSemanticEmbedder private constructor(private val config: Config, private val env: OrtEnvironment, private val session: OrtSession, private val tokenizer: HuggingFaceTokenizer, private val actualProvider: String) : SemanticEmbedder

Declaration Summary
override val dimension: Int
override val backend: String
override fun encodeQueries(texts: List<String>): List<FloatArray>
override fun encodeDocuments(texts: List<String>): List<FloatArray>
override fun close()
data class Config(val modelPath: Path?, val tokenizerPath: Path?, val dimension: Int, val maxTokens: Int, val queryPrefix: String, val documentPrefix: String, val provider: String, val fixedTokenLength: Int, val fixedBatchSize: Int)

AgentExtensions.kt

agentExtension { } lets a box extend its document's AI agent, beyond the single tool aiTool { } gives it. An extension can register tools, context the agent should know before every turn, a look at — or a veto over — every tool call, and slash commands in the chat. Pi (earendil-works/pi) has extensions with this shape; what Field adds is that they are live-coded. Every part is an ownership in the afield.runtime.lifecycle.LifecycleLedger, owned by the run that registered it, so re-running the box replaces its whole extension at once, a run that stops registering a part drops it, and deleting the box retires it.

agentExtension("studio") {
    context("tempo") { "The piece is at ${bpm} bpm; keep edits in time." }
    beforeTool({ it == "DeleteBoxRequest" }) { refuse("Ask before deleting boxes in this document.") }
    afterTool { call, result -> if (!result.ok) println("${call.name} failed: ${result.summary}") }
    command("tempo", "show the tempo") { "**${bpm} bpm**" }
    tool("studio.bpm") { describe("The tempo."); action { bpm } }
}

What the hooks may do

They run on the agent's thread, with a deadline (HOOK_DEADLINE_MS); a hook that throws or overruns is treated as "proceed" and reported, because a broken observer must not stop the agent. A beforeTool hook sees the call's name, arguments, origin (the model, a tool program, the backdoor) and declared effects, and answers proceed() or refuse(message) — the model then gets the message as the tool's result. An afterTool hook sees a one-line summary and whether the call succeeded; it cannot change the result. Context arrives in the instructions as document-authored data, framed as such, at most CONTEXT_CHARS_EACH characters each and CONTEXT_CHARS_TOTAL in all.

Extensions are per document: they apply to AI conversations about that document only.

src/jvmMain/kotlin/afield/ai/tools/AgentExtensions.kt — read the source

Top-level functions

Declaration Summary
fun Box.agentExtension(id: String = "default", build: AgentExtensionBuilder.() -> Unit) Registers this box's agent extension id, replacing whatever the box's previous run registered under it.

Top-level values

Declaration Summary
const val HOOK_DEADLINE_MS
const val CONTEXT_CHARS_EACH
const val CONTEXT_CHARS_TOTAL

AgentExtensionBuilder

class AgentExtensionBuilder internal constructor(private val box: Box, private val id: String)

Declaration Summary
fun tool(name: String, build: BoxToolBuilder.() -> Unit): BoxTool A box-authored tool, exactly as aiTool makes one.
fun context(name: String, changesOften: Boolean = true, body: (ContextTurnView) -> String?) Text the agent should have before each turn — computed when the turn starts, so it can describe the document as it is. changesOften = false lets the bridges cache it with the stable instructions. Return null or blank to say nothing this turn.
fun beforeTool(match: (String) -> Boolean = { true }, mandatory: Boolean = false, timeoutMs: Long = HOOK_DEADLINE_MS, body: ToolCallView.() -> ToolDecision) Look at a tool call before it runs, and let it ToolCallView.proceed or ToolCallView.refuse it.
fun afterTool(match: (String) -> Boolean = { true }, body: (ToolCallView, ToolResultView) -> Unit) Look at a tool call after it has run. Observe only.
fun command(name: String, help: String = "", body: (CommandScope) -> String) A chat command, /name args; the returned markdown is shown in the panel, not saved.
fun branchState(name: String, save: (java.io.File) -> Unit, restore: (java.io.File?) -> Unit, onConflict: BranchConflict = BranchConflict.KEEP, resumeOnOpen: Boolean = false) State of this box's that belongs to the conversation rather than the document — a canvas the agent paints on, a simulation it is steering. Field calls save (into a file it gives you) before and after every chat turn in this document, keeps each snapshot with that turn in the chat's session tree, and calls restore when the conversation moves: rewinding to a message restores the state from before that message, switching branch the state at that branch's end. restore gets null where the conversation has no snapshot (before the extension existed): start fresh, or keep what you have. The file is deleted when restore returns, so read it there.

ContextTurnView

data class ContextTurnView(val mode: String, val request: String, val focusBox: String?)

What a context hook is told about the turn about to start.

ToolCallView

class ToolCallView internal constructor(val name: String, val argumentsJson: String, val origin: String, val effects: Set<String>)

Declaration Summary
val name: String
val argumentsJson: String
val origin: String MODEL, PROGRAM or BACKDOOR.
val effects: Set<String> The effects the tool declares, by name (MUTATE_DOCUMENT, ...); UNDECLARED when it said nothing.
fun proceed(): ToolDecision
fun refuse(message: String): ToolDecision

ToolResultView

data class ToolResultView(val ok: Boolean, val summary: String, val elapsedMs: Long)

CommandScope

data class CommandScope(val args: String, val chatBox: Box?)

ToolDecision

sealed interface ToolDecision

Declaration Summary
data object Proceed : ToolDecision
data class Refuse(val message: String) : ToolDecision

AgentExtensionRegistry

object AgentExtensionRegistry

Declaration Summary
const val KIND
sealed class Hook(val box: Box, val extension: String, val name: String)
class ContextHook(box: Box, extension: String, name: String, val changesOften: Boolean, val body: (ContextTurnView) -> String?)
class BeforeToolHook(box: Box, extension: String, name: String, val match: (String) -> Boolean, val body: ToolCallView.() -> ToolDecision, val mandatory: Boolean = false, val timeoutMs: Long = HOOK_DEADLINE_MS) : Hook(box, extension, name)
class AfterToolHook(box: Box, extension: String, name: String, val match: (String) -> Boolean, val body: (ToolCallView, ToolResultView) -> Unit)
class CommandHook(box: Box, extension: String, name: String, val help: String, val body: (CommandScope) -> String)
class BranchStateHook(box: Box, extension: String, name: String, val save: (java.io.File) -> Unit, val restore: (java.io.File?) -> Unit, val onConflict: BranchConflict = BranchConflict.KEEP, val resumeOnOpen: Boolean = false) : Hook(box, extension, name)
fun hooksFor(documentKey: String?): List<Hook>
fun runCommand(chatBox: Box, name: String, args: String): String? Run /name args for the chat in chatBox's document; null when no extension defines it.
fun snapshotBranchStates(chatBox: Box): Map<String, String> Snapshot every branch state in chatBox's document: hook key to the content hash of what its save wrote, the bytes kept in AiStateBlobs. Runs the hooks on the Runloop, where box code runs; a hook that fails is reported and left out.
data class BranchRestore(val notes: List<String>, val effective: Map<String, String>, val restored: List<String>) What restoreBranchStates did: a line per state it did not simply restore, and the state each hook now has.
fun restoreBranchStates(chatBox: Box, states: Map<String, String>, expected: Map<String, String> = emptyMap()): BranchRestore Restore every branch state in chatBox's document to states (missing keys get null), checking each against expected, the state the conversation last left it in (AiSessionTree.lastKnownStates). The state as it is now is saved first, so a conflict never loses it.
fun commandHelp(chatBox: Box): List<String> The commands extensions add in chatBox's document, as /help lines.

AiStateBlobs

object AiStateBlobs

Branch-state snapshots (AgentExtensionBuilder.branchState), stored once per content: <document>/ai4_blobs/state/ for a document that writes to disk, a per-process temporary folder otherwise.

Declaration Summary
fun put(file: java.io.File, box: Box): String Keep file's bytes, gzipped, under their content hash, and return the hash. A paint canvas's state is tens of megabytes and its ground texture does not compress much (49 MB to 28 MB measured), so each turn that changes it costs that much disk. The first put in a document each session also sweeps the states no conversation refers to (AiStatePruning.sweepOrphans).
fun get(sha: String, box: Box): java.io.File? The state stored under sha, unpacked into a temporary file the caller deletes. Null when no such state was stored; throws when it is there but cannot be read (a damaged file).

BranchConflict

enum class BranchConflict

What AgentExtensionBuilder.branchState does when the state changed outside the conversation since it last saw it.

Constants: KEEP, RESTORE.

AgentToolExtensions.kt

The registration point through which code outside the AI core, including a document's own boxes, adds tools to the AI4 agents.

AgentToolExtensions.register takes a request class, an availability check and a handler; registering the same tool name again in the same scope replaces the handler. A result type that implements SummarisesForToolLog supplies its own one-line summary for the tool log.

src/jvmMain/kotlin/afield/ai/tools/AgentToolExtensions.kt — read the source

AgentToolExtensions

object AgentToolExtensions

How code outside core adds tools to the AI4 agents.

Declaration Summary
class Registration<T : Any> internal constructor(val toolClass: Class<T>, val available: (ToolExecutionContext) -> Boolean, val handler: (ToolExecutionContext, T) -> Any, val scope: Any? = null, val meta: ToolMeta? = null) One registered tool: its request class, its availability gate, its handler and its scope.
fun registeredNames(): Set<String> The tool names currently registered here (request class simple names).
fun <T : Any> register(toolClass: Class<T>, available: (ToolExecutionContext) -> Boolean = { true }, scope: Any? = null, meta: ToolMeta? = null, handler: (ToolExecutionContext, T) -> Any) Add a tool, replacing any previous registration of the same name, and push it to every agent that is already running.
fun unregister(toolClass: Class<*>) Drop a tool, in every scope. Mostly for tests, and for a live-coded registration being retired.
fun unregister(name: String, scope: Any? = ALL_SCOPES) Drop a tool by the name the model sees, for a box retiring a registration whose request class it no longer has in scope — which, after an edit that renamed or deleted the class, is normal. With scope, only that scope's registration goes; the others keep answering.
fun registeredToolNames(): List<String> Every registered tool's name, once each, for diagnostics and for a status readout.
fun isAvailable(ctx: ToolExecutionContext, name: String): Boolean True if name is registered here and its gate passes under ctx.
fun availableToolNames(ctx: ToolExecutionContext): List<String> The names of every extension tool available under ctx, sorted.
fun execute(ctx: ToolExecutionContext, name: String, argsJson: String): Any Runs an extension tool by name with JSON arguments, exactly as a model's tool call would.
fun describe(ctx: ToolExecutionContext): List<String> MCP-style descriptors for the extension tools available under ctx — {name, description, inputSchema}.

SummarisesForToolLog

interface SummarisesForToolLog

A tool result that can describe itself in one line for the tool I/O log.

Declaration Summary
fun toolLogSummary(): String One line for the tool I/O log — see the interface docs for what belongs in it.

AgentToolRegistrations.kt

src/jvmMain/kotlin/afield/ai/tools/AgentToolRegistrations.kt — read the source

Ai4BackdoorTools.kt

The AI4 tool surface as an external agent sees it: the same registry the chat panel uses, wired to one attached document, with every target named outright.

Building a context for a session assembles what a tool call will need: compiling and evaluating Kotlin against a box, reading, writing and replaying operation macros, semantic retrieval over the indexed corpora, text and symbol search across the workspace, the runtime providers for shader reload and render-window capture, whatever tools the document's own boxes declare, and the agent memory kept per document. The backdoor and the MCP bridge both call it, and so do the tests that pin a tool's answer.

Targeting is the one place it differs from the chat panel. A chat tool may mean "the box whose chat is open"; here a box is named by uid, by a name unique in the document, or by an afield:// reference, and there is no current-box fallback. A name matching two boxes is refused with the count rather than resolved to either, because replaying a macro over the wrong box is the one failure here that edits a document. A name matching nothing still falls through to the literal uid, which is how a document root reaches its box and how a typo earns the provider's own message. Passing a focus box names the box whose chat is to be played, which is how a backdoor call reproduces what the panel would have answered.

Workspace text search is rooted at the attached document, the workspace and this checkout's src, never at the process working directory, and refuses to run on Field's Runloop because it reads disk.

src/jvmMain/kotlin/afield/ai/tools/Ai4BackdoorTools.kt — read the source

Ai4RenderCapture.kt

src/jvmMain/kotlin/afield/ai/tools/Ai4RenderCapture.kt — read the source

Ai4RuntimeToolProviders.kt

The five AI4 tools that need the running Field rather than the saved document: reload a box's shaders, capture the render window, list a box's Kotlin context variables, query the code-use index, and search workspace symbols.

The in-Field chat panel and the backdoor registry share all five and differ only in what they hand the constructor. The caller supplies how a box reference resolves and how to look a box up by uid, which is what lets the panel mean "the box whose chat is open" where the backdoor insists on an explicit target. The panel also supplies a staged-source hook, so a shader reload compiles the text the editor is showing rather than the text last written to the box; the backdoor leaves that argument out and compiles what the box holds.

Shader reload is the largest of the five, because one box can carry several shader programs at once and a request may not say which to rebuild. An explicit mode wins outright. Failing that the property being reloaded implies one: vertex and fragment mean graphics, compute means compute, task and mesh mean mesh, and the ray-tracing stages mean ray. Failing that too, the box is asked which programs it actually has and each is rebuilt, which is what auto and an absent mode both come to.

src/jvmMain/kotlin/afield/ai/tools/Ai4RuntimeToolProviders.kt — read the source

AiToolUseImprovementMonitor.kt

The runner behind the AI tool-use improvement task, which summarises the AI4 tool input/output logs and starts the Gradle task that proposes fixes, optionally in a generated worktree.

Logs are read from ~/.afield/logs/ai4_tool_io (or AFIELD_AI4_TOOL_IO_LOG_PATH); the report, dry-run patch and transcript are written under this checkout's build/.

src/jvmMain/kotlin/afield/ai/tools/AiToolUseImprovementMonitor.kt — read the source

AiToolUseImprovementMonitor

object AiToolUseImprovementMonitor

Declaration Summary
fun status(): AiToolUseImprovementStatus
fun view(): AiToolUseImprovementView
fun start(dryRun: Boolean = true, proposalCount: Int = 1): AiToolUseImprovementStatus
fun clearReviewed(recentOnly: Boolean = true): AiToolUseImprovementStatus

AskUserTools.kt

Asking the person a question mid-task: AskUserQuestionRequest ends the turn with a TurnSuspension carrying the question. The box chat ends its transcript with a question row (AiChatBlock.questionRow), which the panel shows with a button per suggested answer; the person's next message, a button's or typed, is the answer, and the next turn opens with this call's output ahead of it, as any suspended turn does.

src/jvmMain/kotlin/afield/ai/tools/AskUserTools.kt — read the source

AskUserQuestionRequest

data class AskUserQuestionRequest(@JsonPropertyDescription("The one question, in a sentence or two, with whatever the person needs to answer it. Markdown is allowed.") val question: String, @JsonPropertyDescription("Up to 6 short suggested answers (a few words each), your recommendation first; empty to ask for a free-text answer.") val choices: List<String> = emptyList())

Asking the person a question mid-task: AskUserQuestionRequest ends the turn with a TurnSuspension carrying the question. The box chat ends its transcript with a question row (AiChatBlock.questionRow), which the panel shows with a button per suggested answer; the person's next message, a button's or typed, is the answer, and the next turn opens with this call's output ahead of it, as any suspended turn does.

Declaration Summary
val question: String
val choices: List<String>

AskUserQuestionResult

data class AskUserQuestionResult(val success: Boolean, val message: String, val suspended: Boolean = false, @get:JsonIgnore override val suspension: TurnSuspension? = null) : SummarisesForToolLog, SuspendsTurn

AskUserQuestionRequest's result. A set suspension ends the turn; the model reads only the rest.

Declaration Summary
override fun toolLogSummary()

AttachmentExcerpt.kt

Reading a long attachment one window at a time, under the guards every attachment shares.

The slice is taken after ordinary resolution, so an attachment already in the turn's context and one the document's own resolver fetches are treated alike. The window is bounded (2 to 65536 characters) and has to start inside the content. The content's SHA-256 comes back as sourceHash, and a later read that passes it back is refused when the source has changed since, with a message asking for a fresh read from the start rather than a splice of two versions. A find string moves the window to the first match at or after the offset, with a little room before it, and an empty window with searchMatched false says there was none. An offset that would split a surrogate pair is refused, and a computed start or end that would is moved back one, so a window never ends mid-character. nextOffset is null on the last one.

src/jvmMain/kotlin/afield/ai/tools/AttachmentExcerpt.kt — read the source

BoxAiTools.kt

A box contributes tools to its document's AI agents with aiTool(name) { }, an idempotent Kotlin DSL that serves both the in-app chat and external MCP agents. A tool is a suspend block that runs with the box in scope, can span many frames, and has its arguments/description/return value marshaled automatically.

val a = aiTool("rectsl.run") {
    describe("Compile & run RectSL in this document's window, capture PNGs")
    val source by param<String>("RectSL program text")
    val times  by param<List<Double>>("capture times (s)", default = listOf(0.0, 1.0))
    action {                       // suspend; box in scope; may call frame()/delay()
        val d = afield.lang.rectsl.RectAnimationOracle.compile(source())
        if (!d.ok) return@action toolError(d.diagnostics)
        "compiled ${source().length} chars, would capture at ${times()}"
    }
}
// Test it yourself, same marshaling the agent uses — positional & type-coerced:
start { println(a("wall 1 1 ; rect 0 0 1 1 fill", listOf(0.0, 0.5))) }

The returned handle is directly suspend-invocable from box code — positional (a(src, times), coerced by declared param order) or named (a.call("source" to src)) — so you can exercise a tool without going through an agent. See BoxTool.

Registration is a side effect of running box code: re-running a box replaces its tools by name (idempotent); a re-run that no longer registers one, or retiring the box, drops it — see DocumentToolRegistry for the lifecycle.

src/jvmMain/kotlin/afield/ai/tools/BoxAiTools.kt — read the source

Top-level functions

Declaration Summary
fun Box.aiTool(name: String, build: BoxToolBuilder.() -> Unit): BoxTool Registers (or replaces) the tool name owned by this box and returns its BoxTool handle.

BoxToolBuilder

class BoxToolBuilder internal constructor(val box: Box, val name: String)

Declaration Summary
val box: Box
val name: String
fun describe(text: String) Human-readable description shown to the agent.
fun namespace(name: String) The catalogue namespace; defaults to the part of the name before its first . (rectsl.run → rectsl), else doc.
fun exposure(exposure: ToolExposure) How the model meets this tool (ToolExposure): DIRECT (the default) sends its schema with every request; DEFERRED lists it by name and sends the schema only once the model asks; PROGRAM leaves it to tool programs.
fun effects(vararg effects: ToolEffect) What calling it does (ToolEffect); effects() with nothing says "none". Undeclared by default. A tool declaring MUTATE_DOCUMENT is refused on a read-only document without its action running.
inline fun <reified T> param(description: String = "", default: Any? = NoDefault): PendingParam<T> Declare a parameter. Use with by so the argument name is taken from the property: val source by param<String>("RectSL program text"). The JSON type is derived from T (reusing the same emitter as the static tool schemas). Supply default = … to make the argument optional.
fun action(body: suspend BoxToolScope.() -> Any?) The tool body. Runs on the runloop with the box in scope; may call frame()/delay().
fun test(name: String, arguments: Map<String, Any?> = emptyMap(), verify: (Any) -> Unit) Attach an explicit, executable example to this tool. Tests run only when requested. Supply ordinary arguments; verify must throw when the returned result is wrong. A test may call a mutating tool: run those tests in a disposable project.
fun testFailure(name: String, arguments: Map<String, Any?> = emptyMap(), messageContains: String) Attach an example that must reject its arguments with a specific diagnostic. messageContains must occur in the thrown exception message or structured tool error. An unexpected success or unrelated error fails the test; cancellation always propagates. Like test, this invokes the real tool and should use a disposable project for mutations.

PendingParam

class PendingParam<T> @PublishedApi internal constructor(private val builder: BoxToolBuilder, private val kType: KType, private val description: String, private val default: Any?)

Intermediate returned by BoxToolBuilder.param; provideDelegate captures the property name.

Declaration Summary
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): BoxToolArg<T>

BoxToolArg

class BoxToolArg<T> internal constructor(internal val name: String, internal val kType: KType, val description: String, internal val hasDefault: Boolean, internal val default: T?)

An argument accessor. Inside action { } call it like a function — source() — to read the current invocation's (coerced) value. Reads a coroutine/thread-local set for the duration of the call, so it is safe across frame() suspensions.

Declaration Summary
val description: String
operator fun getValue(thisRef: Any?, property: KProperty<*>): BoxToolArg<T>
operator fun invoke(): T

BoxToolScope

class BoxToolScope internal constructor(val box: Box, val ctx: ToolExecutionContext?)

Receiver of a tool's action { } body.

Declaration Summary
val box: Box
val ctx: ToolExecutionContext? The chat/MCP execution context when driven by an agent; null for direct box-code calls.
val thisBox: Box
suspend fun frame() Advance one runloop frame (yields the continuation to resume next frame).
suspend fun delay(ms: Long) Wait ms real milliseconds, then settle onto the next frame.
fun toolError(vararg messages: String): BoxToolError Return a structured error to the agent (also usable as a normal return value).
fun toolError(messages: List<String>): BoxToolError
fun images(dataUrls: List<String>, note: String = ""): BoxToolImages Return one or more images to the conversation. Return the result from action { }; the chat loop attaches the images as visible content and lets the model inspect them. Each entry must be a data:image/...;base64,... URL (see afield.media.image.util.PngEncoder or a captured JPEG). The base64 payload is kept out of the tool's JSON output (only note and count are serialized) so it does not bloat the transcript.
fun image(dataUrl: String, note: String = ""): BoxToolImages

BoxToolError

data class BoxToolError(val messages: List<String>, val error: Boolean = true)

A structured tool error, serialized back to the agent.

BoxToolImages

data class BoxToolImages(val note: String = "", val count: Int = 0, @get:JsonIgnore val imageDataUrls: List<String> = emptyList()) : CarriesToolImages

Image(s) produced by a tool, injected into the conversation as visible content by the chat loop (mirrors the shader-reload visual-check path). The base64 URLs are JsonIgnored so the tool's JSON function-output stays small; only note and count are serialized.

Declaration Summary
val note: String
val count: Int
override val toolImages: List<ToolImage> Carried by both bridges and the MCP backdoor through the shared CarriesToolImages path.

BoxTool

class BoxTool internal constructor(val name: String, val description: String, val boxId: String, internal val box: Box, internal val params: List<BoxToolArg<*>>, internal val tests: List<BoxToolTest> = emptyList(), internal val body: suspend BoxToolScope.() -> Any?, val meta: ToolMeta = ToolMeta(namespace = name.substringBefore('.', "doc").ifBlank { "doc" }), val sourceRevision: String = "")

Declaration Summary
val name: String
val description: String
val boxId: String
val meta: ToolMeta
val sourceRevision: String A hash of the registering box's code when it registered this, for capability checkpoints.
val source: String Exact provider source at registration; a later source edit cannot relabel old evidence.
val sourceHash: String
val diagnostics: ToolDiagnostics Local diagnostic records, shared by chat, MCP and direct Kotlin calls.
val parametersSchema: JsonObject by lazy JSON-Schema for the parameters (kotlinx form), reusing the static-tool emitter.
val wireName: String
fun toFunctionTool(): FunctionTool Build the SDK descriptor, using a portable alias when the authored name needs one.
suspend fun call(argsJson: String, ctx: ToolExecutionContext? = null): Any Core execution: bind+coerce argsJson, then run the body. suspend.
suspend operator fun invoke(vararg positional: Any?): Any Terse positional call from box code: a(src, listOf(0.0)). Values are coerced by declared parameter order and arity-checked. suspend — invoke from a coroutine (e.g. inside start { } / frames { }).
suspend fun call(vararg named: Pair<String, Any?>): Any Named call from box code: a.call("source" to src). suspend.
suspend fun test(): List<ToolTestOutcome> Run the attached examples through the same binding/execution path as AI4.

DocumentToolRegistry

object DocumentToolRegistry

Document-scoped registry of box-authored tools. A tool registered by any box in a document is visible to every agent anchored in that document — the in-app AI4 chat and external MCP users alike. Keyed by document path; tools remember their owning box id so clearBox can drop them on box disposal.

Declaration Summary
const val AI_TOOL_KIND The afield.runtime.lifecycle.LifecycleLedger kind of a box-authored tool registration.
fun register(documentKey: String, tool: BoxTool) Register (upsert by name) — re-running a box replaces its prior tool of the same name.
fun toolsFor(documentKey: String): List<BoxTool> All tools for a document, sorted by name (stable for schemas/tests).
fun get(documentKey: String, name: String): BoxTool?
fun clearBox(documentKey: String, boxId: String) Drop every tool owned by boxId (call on box dispose/stop).
fun clearBox(box: Box) Drop tools owned by box across whatever document it belongs to.
fun remove(documentKey: String, name: String)
fun clearDocument(documentKey: String)

DocsSearchBridge.kt

The search engine behind the documentation panel, which retrieves from Field's local index and answers through the selected language model.

DocsSearchBridge holds the conversation state and runs each query; retrieval stays local, and only the synthesis step goes to the provider chosen on /welcome.

src/jvmMain/kotlin/afield/ai/tools/DocsSearchBridge.kt — read the source

DocsSearchBridge

class DocsSearchBridge(private val log: LogSink = StdoutLog, model: String = Ai4ModelDefaults.envOrDefault("AFIELD_DOCS_SEARCH_MODEL", Ai4ModelDefaults.flagshipModel()), private val bridge: LlmBridge = createLlmBridge(model, log))

Docs-search engine for the documentation iframe.

Declaration Summary
data class SearchState(val thread: LlmThreadState) Opaque to the installer; carries the synthesis thread whose history the bridge maintains.
data class SearchResult(val success: Boolean, val text: String, val newState: SearchState)
fun startConversation(): SearchState
fun search(state: SearchState, query: String, onTextDelta: ((String) -> Unit)? = null): SearchResult

DocsSearchInstaller.kt

The server side of the documentation search box: the docs_search_input Field message handler.

The prefix of Message.uid before _ is the browser session key. One DocsSearchBridge.SearchState is retained per key for the lifetime of this installer; there is no eviction and no reset message, though every state is dropped when the AI provider settings change. Work runs on the process-wide IO scope and replies are marshalled through inMain. Phase 1 is provisional HTML and phase 20 is terminal HTML. The incoming phase is not consulted.

src/jvmMain/kotlin/afield/ai/tools/DocsSearchInstaller.kt — read the source

Top-level functions

Declaration Summary
fun installDocsSearch() Installs the docs_search_input handler, which answers a query with provisional then final HTML.

DocumentGuideTools.kt

The AI4 tools for a document's Markdown guides: the durable notes and handoffs kept in the document's own folder, its docs/ or its guides/.

An agent can list, read, search, create, patch and append them. Writes are revision-checked: a patch or append names the revision from its last read and fails on a conflict instead of overwriting another writer, and create never overwrites. Mutations are refused on a read-only document.

src/jvmMain/kotlin/afield/ai/tools/DocumentGuideTools.kt — read the source

DocumentGuideListRequest

data class DocumentGuideListRequest(val unused: Int = 0)

List the document's guides, with their titles and links, and no contents.

DocumentGuideReadRequest

data class DocumentGuideReadRequest(val path: String, val startLine: Int = 1, val lineCount: Int = 200)

Read a window of one guide's lines. The revision it returns covers the whole file, not the window, so it is still the right revision to quote back on a write.

DocumentGuideSearchRequest

data class DocumentGuideSearchRequest(val query: String, val path: String = "", val limit: Int = 20)

Search the guides for literal text, case-insensitively. Each hit carries the heading it falls under and a link to that section, so an answer can cite where it came from.

DocumentGuideCreateRequest

data class DocumentGuideCreateRequest(val path: String, val contents: String)

Write a new guide. A path that already exists is a conflict, never an overwrite.

DocumentGuidePatchRequest

data class DocumentGuidePatchRequest(val path: String, val revision: String, val oldText: String, val newText: String)

Replace one exact passage of a guide. oldText has to occur exactly once, and an empty newText deletes the passage rather than leaving a gap.

DocumentGuideAppendRequest

data class DocumentGuideAppendRequest(val path: String, val revision: String, val text: String)

Add text to the end of a guide, verbatim: any blank line or heading it needs has to be in the text.

GuideToolEntry

data class GuideToolEntry(val path: String, val title: String, val link: String)

One guide in a listing: its path in the document, its title, and a field://guide/ link to open it.

GuideSearchHit

data class GuideSearchHit(val path: String, val line: Int, val text: String, val heading: String, val link: String)

One matching line, with the heading above it and a link that opens the guide at that section.

DocumentGuideToolResult

data class DocumentGuideToolResult(val success: Boolean = true, val message: String, val document: String, val readOnly: Boolean, val path: String = "", val link: String = "", val revision: String = "", val contents: String = "", val startLine: Int = 0, val endLine: Int = 0, val totalLines: Int = 0, val truncated: Boolean = false, val entries: List<GuideToolEntry> = emptyList(), val hits: List<GuideSearchHit> = emptyList(), val conflict: Boolean = false) : SummarisesForToolLog

What every guide tool answers with; which fields are filled depends on which tool answered.

Declaration Summary
val success: Boolean False when the operation failed or conflicted, with the reason in message.
val message: String One sentence about what happened, and what this result writes to the tool log.
val document: String The document folder the guides live in.
val readOnly: Boolean Whether this document refuses writes, answered on every call rather than only on a refused one.
val path: String The guide the call was about, where it was about one.
val link: String A field://guide/ link to that guide.
val revision: String The revision of the whole file, which a patch or append has to quote back.
val contents: String The lines read, to 32000 characters.
val startLine: Int The first line contents holds, counting from one.
val endLine: Int The last line it holds.
val totalLines: Int How many lines the whole guide has, of which contents is a window.
val truncated: Boolean True when the guide runs past endLine, or when contents was cut at 32000 characters.
val entries: List<GuideToolEntry> Every guide in the document, for a listing.
val hits: List<GuideSearchHit> The matching lines, for a search, up to the limit it was given.
val conflict: Boolean True when a write was refused because the guide had changed, or already existed.
override fun toolLogSummary(): String The message, which is already one line.

DocumentGuideTools

class DocumentGuideTools internal constructor(private val root: File, internal val sources: DocumentSourceTools? = null, internal val states: StateTools? = null, private val isReadOnly: () -> Boolean)

Shared by every chat provider and the MCP/backdoor registry. No ambient session lookup.

Declaration Summary
fun list(): DocumentGuideToolResult Every guide in the document, with titles and links and no contents.
fun read(req: DocumentGuideReadRequest): DocumentGuideToolResult A window of one guide's lines, at most 500 of them and at most 32000 characters, with the revision of the whole file.
fun search(req: DocumentGuideSearchRequest): DocumentGuideToolResult Lines containing a literal query, case-insensitively, across every guide or inside one. Each hit carries the heading above it and a link to that section.
fun create(req: DocumentGuideCreateRequest) Write a guide that does not exist yet; an existing path comes back as a conflict.
fun patch(req: DocumentGuidePatchRequest) Replace one passage that occurs exactly once, in a guide still at the revision given.
fun append(req: DocumentGuideAppendRequest) Add text to the end of a guide still at the revision given.

DocumentSourceTools.kt

The AI4 tools that read and patch the live source of a box, navigate the editor to it, and export a snapshot of part of the graph.

DocumentSourceReadRequest reports the live, saved and last executed revisions of one property; DocumentSourcePatchRequest replaces it only when the revision still matches; DocumentBoxNavigateRequest opens the editor, box or output channel; and DocumentSourceExportRequest writes a snapshot with provenance warnings. None of them launches a box.

src/jvmMain/kotlin/afield/ai/tools/DocumentSourceTools.kt — read the source

Used in: AI4's built-in tools

DocumentSourceReadRequest

data class DocumentSourceReadRequest(val uid: String, val property: String = "code")

Read one box property as it stands in memory, with the revisions of what is on disk and of what last ran beside it. Those three disagreeing is the ordinary state of a box being edited.

DocumentSourcePatchRequest

data class DocumentSourcePatchRequest(val uid: String, val property: String, val revision: String, val contents: String)

Replace one box property, if its live revision is still the one the last read reported. A property that changed in between comes back with DocumentSourceResult.conflict set and nothing written.

DocumentBoxNavigateRequest

data class DocumentBoxNavigateRequest(val uid: String, val property: String = "code", val line: Int? = null, val surface: String = "editor", val channel: String = "")

Bring a box, its source or one of its output channels into view in the person's own window. It waits five seconds for a frontend to confirm, and reports that the surface was opened rather than what is in it.

Declaration Summary
val uid: String The box to go to, looked up in this document only and never as a global UID.
val property: String Which text property the editor should open, when the surface is the editor.
val line: Int? A line for the editor to reveal, passed through to the frontend.
val surface: String editor, box or output; anything else is refused before a frontend is asked.
val channel: String Which output channel to reveal. Only for output, and only blank when the box has exactly one.

DocumentSourceExportRequest

data class DocumentSourceExportRequest(val uids: List<String>, val directory: String, val guides: List<String> = emptyList())

Write the live source and topology of some boxes, and any guides named with them, into a new directory as an installable snapshot. The directory has to be new, and has to sit outside the document being exported.

DocumentSourceResult

data class DocumentSourceResult(val success: Boolean = true, val uid: String, val property: String, val liveRevision: String, val savedRevision: String?, val executed: ExecutionReport?, val attempted: ExecutionReport?, val contents: String, val savedLocation: String?, val conflict: Boolean = false, val message: String = "Live source inspected; no save or execution performed.")

What a read or a patch answers with: one property's text, and where else a copy of it exists.

Declaration Summary
val success: Boolean False only when a patch was refused; a read that cannot find its box throws instead.
val uid: String The box the call resolved to, which for the document's root is the root's own id.
val property: String The property that was read or patched.
val liveRevision: String A hash of the text now in memory, and the revision a patch has to quote back.
val savedRevision: String? The same hash of what is on disk, or null when the property has never been saved.
val executed: ExecutionReport? The last run of this property that reached setup complete, or null if none has.
val attempted: ExecutionReport? The last run of it attempted, whatever became of it.
val contents: String The live text itself, whole.
val savedLocation: String? The sidecar file holding the saved copy, the document's metadata.edn when it is inline there, or null when nothing is saved.
val conflict: Boolean True when a patch was refused because the live revision had moved on.
val message: String One sentence about what happened, and a reminder that nothing was launched or reloaded.

GlslIncludeLibraryIndex.kt

GlslIncludeLibraryIndex scans directories of GLSL include files, Lygia by default plus any roots named in AFIELD_GLSL_INCLUDE_DIRS or repositories in AFIELD_GLSL_INCLUDE_REPOS, and records each file's include path, content hash and declared functions and macros in glsl_include_index.json under the AI filestore, so an agent can find which include defines a function. AFIELD_GLSL_INCLUDE_INDEX=off disables it and AFIELD_GLSL_INCLUDE_DEFAULTS=off drops the Lygia default.

src/jvmMain/kotlin/afield/ai/tools/GlslIncludeLibraryIndex.kt — read the source

Top-level functions

Declaration Summary
fun syncGlslIncludeLibraryIndex()

GlslIncludeSymbolRecord

data class GlslIncludeSymbolRecord(val name: String, val kind: String)

One function or macro declared in an include file.

Declaration Summary
val name: String
val kind: String

GlslIncludeFileRecord

data class GlslIncludeFileRecord(val includePath: String, val localPath: String, val contentHash: String, val bytes: Long, val symbols: List<GlslIncludeSymbolRecord> = emptyList())

Declaration Summary
val includePath: String
val localPath: String
val contentHash: String
val bytes: Long
val symbols: List<GlslIncludeSymbolRecord>

GlslIncludeLibraryRecord

data class GlslIncludeLibraryRecord(val name: String, val includePrefix: String, val root: String, val source: String, val gitUrl: String? = null, val gitRef: String? = null, val revision: String? = null, val updatedAtEpochMs: Long, val files: List<GlslIncludeFileRecord> = emptyList(), val vectorStoreId: String? = null, val bundleVersion: String? = null)

Declaration Summary
val name: String
val includePrefix: String
val root: String
val source: String
val gitUrl: String?
val gitRef: String?
val revision: String?
val updatedAtEpochMs: Long
val files: List<GlslIncludeFileRecord>
val vectorStoreId: String?
val bundleVersion: String?

GlslIncludeIndexDb

data class GlslIncludeIndexDb(val version: Int = 1, val updatedAtEpochMs: Long = System.currentTimeMillis(), val libraries: List<GlslIncludeLibraryRecord> = emptyList())

Declaration Summary
val version: Int
val updatedAtEpochMs: Long
val libraries: List<GlslIncludeLibraryRecord>

GlslIncludeLibraryIndex

object GlslIncludeLibraryIndex

Indexes local or managed directories of GLSL include files for AI grounding.

Declaration Summary
var latestIndex: GlslIncludeIndexDb?
fun loadIndex(): GlslIncludeIndexDb?
fun startBackgroundSync()
fun contextAttachmentFor(prompt: String, limit: Int = 24): ContextAttachment?
fun manifestFor(prompt: String, limit: Int = 24): String
fun resolveAttachment(raw: String): AttachmentResponse?

OutputIframeAiTools.kt

src/jvmMain/kotlin/afield/ai/tools/OutputIframeAiTools.kt — read the source

Top-level functions

Declaration Summary
fun Box.exposeOutputIframeTools(channel: String, name: String = channel): List<BoxTool> Opt this box's named output iframe into AI4's tool catalogue for this document.

PresentationTools.kt

The AI4 tools for a document's slides: read and revise an editable slide file, and inspect or drive a presentation that box code has already built.

Those are two different decks. PresentationReadRequest and PresentationWriteSlideRequest work on a .slides.json file in the document's folder, one Markdown slide at a time, and a write into an existing file carries the revision from the read it was based on: a file that changed in between fails the write instead of overwriting the other writer. PresentationInspectRequest and PresentationControlRequest work on the live slide show of a box that built a presentation in Kotlin — inspect reports its steps, current cue and checkpoint, control navigates it. Control can run a step's own callbacks, so what it reports is the state of the JVM rather than proof that an audience saw anything.

src/jvmMain/kotlin/afield/ai/tools/PresentationTools.kt — read the source

PresentationReadRequest

data class PresentationReadRequest(val path: String = "files/talk.slides.json", val slideId: String = "", val offset: Int = 0, val limit: Int = 50)

Read a slide file's outline, and one slide's Markdown when slideId names one.

PresentationWriteSlideRequest

data class PresentationWriteSlideRequest(val revision: String, val slide: PresentationSlide, val path: String = "files/talk.slides.json", val afterId: String = "")

Write one slide. A slide whose id the file already has is replaced where it stands; a new id is inserted after afterId, or appended when that is empty.

Declaration Summary
val path: String Which slide file in the document to write; the same default as the read.
val afterId: String The slide to insert a new slide after. Empty appends it, and it is refused for a slide that exists.

PresentationInspectRequest

data class PresentationInspectRequest(val unused: Int = 0)

List the presentations this document's boxes have built, with their host box UIDs. A box whose slide show is a guide rather than a presentation is left out.

PresentationControlRequest

data class PresentationControlRequest(val hostUid: String, val command: String)

Navigate a built presentation by its host box UID: next, previous, refresh, present, stop, reset-checkpoint, run-cue, or a zero-based slide number. A box that holds a guide rather than a presentation is refused, as is a slide number past the end.

PresentationOutlineEntry

data class PresentationOutlineEntry(val id: String, val title: String, val section: List<String>, val cues: List<String>)

One slide in an outline: its id, its title, the section path it sits under, and its reveal cues.

PresentationEditResult

data class PresentationEditResult(val success: Boolean = true, val message: String, val revision: String = "", val outline: List<PresentationOutlineEntry> = emptyList(), val slide: PresentationSlide? = null, val totalSlides: Int = 0, val nextOffset: Int? = null)

What a read or a write answers with: the outline either way, and the whole slide when one was named.

Declaration Summary
val outline: List<PresentationOutlineEntry> A page of the deck's outline, one entry per slide, in the order the file keeps them.
val slide: PresentationSlide? The whole slide, when the call named one: the slide a read asked for, or the one a write just saved.
val totalSlides: Int How many slides the whole file holds, of which outline is a page.
val nextOffset: Int? The offset of the next page of outline, or null once it reaches the last slide.

PresentationTools

class PresentationTools internal constructor(root: File, private val readOnly: () -> Boolean, private val session: SessionInformation? = null)

One document's authoring surface; shared by chat providers and the backdoor registry.

Declaration Summary
fun read(req: PresentationReadRequest): PresentationEditResult Read a page of one slide file's outline, with the revision a later write will have to match.
fun write(req: PresentationWriteSlideRequest): PresentationEditResult Save one slide into the file, replacing a slide of the same id or inserting a new one. An empty revision starts a fresh deck; any other revision has to be the one the file still carries.
fun inspect(): Any The presentation state of every box in the session that has built one, read on the Runloop thread.
fun control(req: PresentationControlRequest): Any Send one navigation command to a built presentation and answer with the state it reached. Both this and inspect have to be called off the Runloop, since they step onto it to reach the slide show.

RepoCommandTools.kt

Bounded commands for a headless AI4 job

The repo command tool lets a job that already has RepoFileTools run a named command in its checkout, so it can reproduce a failure and confirm a fix (the landing queue's fixer needs both). It is not a shell. A job lists the names it allows (RepoCommandScope.allowed); each name is an argv template (RepoCommandTemplates) whose free slots take only what that slot's check admits (a Gradle task name, a tools/test_*.py path the job may read, a git revision). The process:

  • is started from an argv list (no shell, no interpolation), with the job's root as its cwd and stdin closed;
  • gets the Field JVM's environment minus every GIT_* variable, VIRTUAL_ENV (uv would warn and ignore it) and the verification-lock owner marker, plus AFIELD_VERIFICATION_LOCK=fail, GIT_TERMINAL_PROMPT=0 and GIT_PAGER=cat;
  • is refused outright while another live process holds the root's .field-backdoor/verification.lock (unless that process is an ancestor of this JVM, i.e. the job's own runner);
  • is stopped with its whole process tree (TERM, then KILL after a grace) at its wall-clock cap. A Gradle daemon in that tree is left alone: daemons are shared by every checkout, and killing the client cancels its build;
  • has stdout and stderr merged and capped: the first HEAD_BYTES and the last bytes up to the cap are kept, the middle is dropped and counted.

One command runs at a time per job.

src/jvmMain/kotlin/afield/ai/tools/RepoCommandTools.kt — read the source

RepoCommandScope

data class RepoCommandScope(val allowed: Set<String>, val maxTimeoutSec: Int = 900, val maxOutputBytes: Int = 48 * 1024)

What one job may run, and how long and how loud each run may be.

Declaration Summary
val allowed: Set<String>
val maxTimeoutSec: Int
val maxOutputBytes: Int

RepoRunCommandRequest

data class RepoRunCommandRequest(val command: String, val args: List<String> = emptyList(), @param:JsonPropertyDescription("Wall-clock limit in seconds; 0 = the command's default. Capped by the job.") val timeoutSec: Int = 0)

Declaration Summary
val command: String
val args: List<String>
val timeoutSec: Int

RepoCommandResult

data class RepoCommandResult(val success: Boolean, val message: String, val command: String = "", val exitCode: Int? = null, val timedOut: Boolean = false, val elapsedMs: Long = 0, val output: String = "", val outputBytes: Long = 0, val truncated: Boolean = false)

Declaration Summary
val success: Boolean
val message: String
val command: String
val exitCode: Int?
val timedOut: Boolean
val elapsedMs: Long
val output: String
val outputBytes: Long
val truncated: Boolean

RepoCommandTemplate

class RepoCommandTemplate(val name: String, val summary: String, val defaultTimeoutSec: Int, val build: (args: List<String>, files: RepoFileTools) -> List<String>)

One allowed command: a fixed argv prefix, a check that turns the model's args into the rest, and its default timeout.

Declaration Summary
val name: String
val summary: String
val defaultTimeoutSec: Int
val build: (args: List<String>, files: RepoFileTools) -> List<String>

RepoCommandTemplates

object RepoCommandTemplates

The command names a job can allow. FIXER_SET is what the landing queue's fixer gets.

Declaration Summary
val ALL: Map<String, RepoCommandTemplate>
val FIXER_SET: List<String> Reproduce and confirm, nothing that changes git state: what field_agent.headless_argv("field", edits=True) allows.

RepoCommandTools

class RepoCommandTools(val files: RepoFileTools, val scope: RepoCommandScope)

The command tool over one job's RepoFileTools.

Declaration Summary
val runs: MutableList<String> Every run, in order, for the job's report: name args -> exit (or timeout).
fun describeAllowed(): String The lines the job's prompt shows: one per allowed command.
fun run(req: RepoRunCommandRequest): RepoCommandResult

RepoFileTools.kt

The repo tools read, list, search, patch and create files in one checkout, for a job that hands AI4 work a coding agent would otherwise do (a docs-notes revision, a landing question). A job supplies a RepoScope: the root, the globs it may read and the globs it may write. Every path is relative to the root with / separators; an absolute path, a .. segment, a symbolic link anywhere on the way, .git, or a path no glob admits is refused with the reason. The tools are offered only when a job wires a scope (ToolExecutionContext.repoFiles); a chat never sees them.

src/jvmMain/kotlin/afield/ai/tools/RepoFileTools.kt — read the source

Used in: AI4's built-in tools

RepoScope

data class RepoScope(val root: Path, val readable: List<String>, val writable: List<String> = emptyList())

What one job may touch. Globs are java.nio globs over the root-relative path: ** crosses directories.

RepoReadFileRequest

data class RepoReadFileRequest(val path: String, val startLine: Int = 1, @param:JsonPropertyDescription("Last line to return; 0 = up to 2000 lines from startLine.") val endLine: Int = 0)

Declaration Summary
val path: String
val startLine: Int
val endLine: Int

RepoListFilesRequest

data class RepoListFilesRequest(val glob: String = "**")

RepoSearchRequest

data class RepoSearchRequest(val query: String, val glob: String = "**", val regex: Boolean = false, val ignoreCase: Boolean = false, @param:JsonPropertyDescription("1..300 matching lines.") val maxMatches: Int = 100)

Declaration Summary
val query: String
val glob: String
val regex: Boolean
val ignoreCase: Boolean
val maxMatches: Int

RepoPatchFileRequest

data class RepoPatchFileRequest(val path: String, val oldText: String, val newText: String, val replaceAll: Boolean = false)

RepoWriteFileRequest

data class RepoWriteFileRequest(val path: String, val content: String, val overwrite: Boolean = false)

RepoFileResult

data class RepoFileResult(val success: Boolean, val message: String, val path: String = "", val text: String = "", val totalLines: Int = 0, val truncated: Boolean = false)

Declaration Summary
val success: Boolean
val message: String
val path: String
val text: String
val totalLines: Int
val truncated: Boolean

RepoMatch

data class RepoMatch(val path: String, val line: Int, val text: String)

RepoListResult

data class RepoListResult(val success: Boolean, val message: String, val files: List<String> = emptyList(), val complete: Boolean = true)

RepoSearchResult

data class RepoSearchResult(val success: Boolean, val message: String, val matches: List<RepoMatch> = emptyList(), val complete: Boolean = true, val filesSearched: Int = 0)

RepoPathRefused

class RepoPathRefused(message: String) : IllegalArgumentException(message)

A path the scope refuses; the message is what the model sees.

RepoFileTools

class RepoFileTools(val scope: RepoScope)

The repo tools over one RepoScope. Every call re-checks its path: nothing is cached between calls.

Declaration Summary
val written: MutableList<String> Paths this job wrote, in order, for the job's report.
fun resolve(raw: String, write: Boolean): Path raw as a confined absolute path, or RepoPathRefused.
fun read(req: RepoReadFileRequest): RepoFileResult
fun list(req: RepoListFilesRequest): RepoListResult
fun search(req: RepoSearchRequest): RepoSearchResult
fun patch(req: RepoPatchFileRequest): RepoFileResult
fun write(req: RepoWriteFileRequest): RepoFileResult

StateTools.kt

The AI4 tools that list, read, show and fork the saved histories of a document, over the same providers the States browser shows a person.

StateListRequest, StateReadRequest, StateShowRequest and StateForkRequest are the tools, and StateToolResult is their answer. None of them restores a state or runs code; forking is offered only by a history that reports canFork.

src/jvmMain/kotlin/afield/ai/tools/StateTools.kt — read the source

Used in: AI4's built-in tools

StateListRequest

data class StateListRequest(val history: String = "", val query: String = "", val offset: Int = 0, val limit: Int = 40)

List the histories, or the saved states inside one of them. With history empty the answer's StateToolResult.histories names every history this document has; with it set, StateToolResult.nodes is a page of that history's states.

StateReadRequest

data class StateReadRequest(val history: String, val state: String, val startChar: Int = 0, val maxChars: Int = 6000, val revision: String = "")

Read one saved state's text, a page at a time. The whole text is hashed into StateToolResult.revision; passing that revision back on a later page makes a provider whose data changed mid-read fail rather than stitch two versions together.

StateShowRequest

data class StateShowRequest(val history: String, val state: String = "")

Reveal a history, and optionally one of its states, in the person's own state browser. This is the one tool here that needs a connected frontend: with none, it answers unsuccessfully within twenty seconds.

StateForkRequest

data class StateForkRequest(val history: String, val state: String, val name: String)

Branch from a saved state under a new name, leaving the active state where it is. Only a history whose provider supplies a fork can do this, which is what StateHistoryInfo reports as canFork.

StateHistoryInfo

data class StateHistoryInfo(val id: String, val name: String, val canFork: Boolean)

One history in a listing: its exact id, the label the browser shows, and whether it can fork.

StateToolResult

data class StateToolResult(val success: Boolean = true, val message: String, val history: String = "", val state: String = "", val title: String = "", val histories: List<StateHistoryInfo> = emptyList(), val nodes: List<StateTreeNode> = emptyList(), val current: String? = null, val note: String = "", val canFork: Boolean = false, val total: Int = 0, val nextOffset: Int? = null, val contents: String = "", val revision: String = "", val startChar: Int = 0, val totalChars: Int = 0, val nextChar: Int? = null) : SummarisesForToolLog

What all four state tools answer with. Most fields are empty for most calls: a listing fills histories or nodes, a read fills contents and the paging fields, and a fork fills state with the new child's ID.

Declaration Summary
val success: Boolean False when the operation failed, with the reason in message.
val message: String One sentence about what happened, and the summary this result writes to the tool log.
val history: String The history the call resolved to, as its exact ID rather than the name that was asked for.
val state: String The saved state the call resolved to, or the new child's ID after a fork.
val title: String That state's title, to 180 characters, or the branch name a fork was given.
val histories: List<StateHistoryInfo> Every history in the document, for a listing that named none.
val nodes: List<StateTreeNode> One page of a history's saved states, each with its parents, titles cut to 180 characters.
val current: String? The ID of the state the history currently sits on, when its provider reports one.
val note: String The provider's own note about the history, to 1000 characters.
val canFork: Boolean Whether this history can fork, repeated here so a read answers the question too.
val total: Int How many histories or states matched before paging.
val nextOffset: Int? The offset of the next page of histories or nodes, or null at the end.
val contents: String One page of the saved state's text.
val revision: String A hash of the whole saved text, to pass back on the next page.
val startChar: Int The character index contents starts at.
val totalChars: Int The length of the whole saved text, of which contents is a window.
val nextChar: Int? Where the next page of contents starts, or null at the end of the text.
override fun toolLogSummary() The message, which is what the tool log keeps rather than any state text.

StateTreeBrowser.kt

The States browser of AI4: how a box exposes a saved history with stateTree, and the read-only view of it shared by the browser and by chat references.

A provider supplies a snapshot, a reader for one saved state and, optionally, a fork callback. StateTreeBrowser pages and bounds the text it shows, and never restores a state.

src/jvmMain/kotlin/afield/ai/tools/StateTreeBrowser.kt — read the source

Top-level functions

Declaration Summary
fun Box.stateTree(name: String, label: String, read: () -> StateTreeSnapshot, inspect: (String) -> String, fork: ((String, String) -> String)?) Expose an existing history to AI4's States browser. Callbacks only read saved data; they must not restore state or run admission checks. read returns metadata; inspect reads one exact saved ID. Optional fork creates a named saved child and returns its ID without activation or restoration. Leave it null for histories that cannot safely retain branches. Re-running replaces this named provider and the run's lifecycle owns its registration.
fun Box.stateTree(name: String, label: String, read: () -> StateTreeSnapshot, inspect: (String) -> String) Read-only provider overload; keeps the inspector usable as a trailing lambda.
fun Box.stateTree(name: String, label: String, study: SourceStudy<*>) Browse a SourceStudy without invoking its compiler or candidate checks.

StateTreeBrowser

object StateTreeBrowser

Read-only projection shared by the browser and its chat references. No navigation or restoration.

Declaration Summary
fun read(boxes: Collection<Box>, request: StateTreeRequest): StateTreeReply

ToolMeta.kt

Tool metadata answers three questions beyond the schema: which namespace a tool belongs to, how the model discovers it (ToolExposure), and what calling it does (ToolEffect). Both of the last two follow Pi (earendil-works/pi). Exposure decides whether a tool's schema is sent with every request or only on demand. Field uses effects for three things rather than for confirmation dialogs: a central refusal of document mutations on a read-only document, eval checks (effects-absent:FILESYSTEM_WRITE), and the catalogue line that tells a model which calls are cheap looks and which change things.

Metadata comes from AgentToolMeta on the request class, from aiTool { namespace(); exposure(); effects() } for a box-authored tool, or from AgentToolExtensions.register(meta = ...). A class with none gets ToolMeta.DEFAULT: namespace core, DIRECT, ToolEffect.UNDECLARED — undeclared rather than "no effects", so an eval asserting an effect is absent fails on a tool that never said.

src/jvmMain/kotlin/afield/ai/tools/ToolMeta.kt — read the source

ToolExposure

enum class ToolExposure

How a model discovers a tool: schema up front, by program, on request, or never.

Constants: DIRECT, PROGRAM, DEFERRED, HIDDEN.

ToolEffect

enum class ToolEffect

Never shown to a model, nor over MCP; callable only by Field itself and the backdoor.

Constants: READ_DOCUMENT, MUTATE_DOCUMENT, EXECUTE_CODE, CONTROL_RUNTIME, CAPTURE_IMAGE, GPU, FILESYSTEM_READ, FILESYSTEM_WRITE, NETWORK, AGENT_STATE, UNDECLARED.

AgentToolMeta

annotation class AgentToolMeta(val namespace: String, val exposure: ToolExposure = ToolExposure.DIRECT, val effects: Array<ToolEffect> = [ToolEffect.UNDECLARED])

The tool never said. Distinct from an empty set, which means "no effects".

Declaration Summary
val namespace: String
val exposure: ToolExposure
val effects: Array<ToolEffect>

ToolMeta

data class ToolMeta(val namespace: String = "core", val exposure: ToolExposure = ToolExposure.DIRECT, val effects: Set<ToolEffect> = setOf(ToolEffect.UNDECLARED))

Declaration Summary
val namespace: String
val exposure: ToolExposure
val effects: Set<ToolEffect>
val mutates: Boolean
val readOnly: Boolean Only looks: every effect is a read or a picture. What MCP calls readOnlyHint.

CallOrigin

enum class CallOrigin

Who is calling: the model directly, a tool program, or Field's backdoor / MCP face.

Constants: MODEL, PROGRAM, BACKDOOR.

ToolFace

enum class ToolFace

Which descriptor list ToolRegistry.describe builds: what a model is shown, or what MCP clients are.

Constants: MODEL, MCP, PROGRAM.

DescribeToolsRequest

data class DescribeToolsRequest(@com.fasterxml.jackson.annotation.JsonPropertyDescription("A namespace from the tool catalogue, such as `macros`; blank to use `names` instead.") val namespace: String = "", @com.fasterxml.jackson.annotation.JsonPropertyDescription("Tool names from the catalogue, such as MacroRunRequest.") val names: List<String> = emptyList())

Declaration Summary
val namespace: String
val names: List<String>

DescribeToolsResult

data class DescribeToolsResult(val success: Boolean, val message: String, val tools: List<DescribedTool>) : SummarisesForToolLog

Declaration Summary
val success: Boolean
val message: String
val tools: List<DescribedTool>
override fun toolLogSummary(): String

DescribedTool

data class DescribedTool(val name: String, val namespace: String, val description: String, val effects: List<String>, val inputSchema: String)

ToolRunloopDispatch.kt

The check that turns a deadlocking aiTool call into an error message.

A tool body runs on the Runloop's main thread, so a caller that is already blocking that thread inside runBlocking can never let the body run: the call would wait for a thread that is waiting for it. Giving the runBlocking a Dispatchers.IO context moves the body but not the blocked caller, so that case is checked as well, by looking for runBlocking on the owning thread's stack. Both messages name the two ways of calling a tool that do work: directly from AI4, or inside start { ... } in box code.

src/jvmMain/kotlin/afield/ai/tools/ToolRunloopDispatch.kt — read the source

Tools.kt

The request and result classes of the tools Field's AI agent can call, and the registry that dispatches a call by name to its handler.

Each tool is a Kotlin data class: its simple name is the tool name, @JsonClassDescription and @JsonPropertyDescription are what the model reads, and AgentToolMeta records the tool's group and effects. The file covers attachments and definitions, Kotlin compile and evaluate, shader reload, render-window capture, macros, code-use queries, and the agent's scratchpad and checklist (AgentMemoryState). A result that carries pictures implements CarriesToolImages, so the images travel as images rather than as base64 text inside the JSON output.

ToolSchema emits the JSON Schema for a request class from Kotlin reflection alone, skipping @JsonIgnore and placeholder unused fields; openAiTool builds the OpenAI function tool and keeps nullable arguments optional. AgentToolSchemaTest checks that OpenAI accepts the argument schema of every registered tool.

src/jvmMain/kotlin/afield/ai/tools/Tools.kt — read the source

Used in: AI4's built-in tools

Top-level functions

Declaration Summary
fun virtualFilesToAttachments(files: List<afield.document.workspace.VirtualTextFile>): List<ContextAttachment>
fun validateNoMarkers(code: String)

Commentary

data class Commentary(val text: String)

Commentary tool: model uses this for non-code notes/questions/alternatives. You collect these separately and keep code output clean.

ContextAttachment

data class ContextAttachment(val fileName: String, val language: String, val code: String, val description: String = "")

Represents a file/language/code triple that can be attached to a request. Attachments are retrieved via tools rather than stuffed into the prompt body.

Declaration Summary
val fileName: String
val language: String
val code: String
val description: String

ListAttachmentsRequest

data class ListAttachmentsRequest(val unused: Int = 0)

Tool: list available attachment filenames (+ languages) for this request.

ListAttachmentsResult

data class ListAttachmentsResult(val attachments: List<AttachmentInfo>)

AttachmentInfo

data class AttachmentInfo(val file: String, val language: String, val description: String = "")

AttachmentRequest

data class AttachmentRequest(val file: String, val offset: Int = 0, val maxChars: Int = 6000, val expectedSourceHash: String = "", val find: String = "")

Tool: read a bounded excerpt of an attachment or resolvable source.

Declaration Summary
val file: String
val offset: Int
val maxChars: Int
val expectedSourceHash: String
val find: String

AttachmentResponse

data class AttachmentResponse(val file: String, val language: String, val content: String, val relatedGuides: List<String> = emptyList(), val relatedExamples: List<String> = emptyList(), val offset: Int = 0, val totalChars: Int = content.length, val sourceHash: String = "", val nextOffset: Int? = null, val isExcerpt: Boolean = false, val searchMatched: Boolean? = null, val matchOffset: Int? = null, val startLine: Int? = null, val matchLine: Int? = null, val nearMatches: List<String> = emptyList())

Declaration Summary
val file: String
val language: String
val content: String
val relatedGuides: List<String>
val relatedExamples: List<String>
val offset: Int
val totalChars: Int
val sourceHash: String
val nextOffset: Int?
val isExcerpt: Boolean
val searchMatched: Boolean?
val matchOffset: Int?
val startLine: Int? 1-based line of offset and of matchOffset: offsets are not line numbers, so cite these.
val matchLine: Int?
val nearMatches: List<String> When find missed: up to eight line: text rows containing its longest identifier.

ListDefinitionsRequest

data class ListDefinitionsRequest(val unused: Int = 0)

Tool: list all definitions visible from the current Kotlin file (incl shaders).

ListDefinitionsResult

data class ListDefinitionsResult(val definitions: List<DefinitionInfo>)

DefinitionInfo

data class DefinitionInfo(val name: String, val type: String, val fileRef: String, val language: String)

ListContentsRequest

data class ListContentsRequest(val unused: Int = 0)

Tool: get full contents of text properties for the current box/file context.

ListContentsResult

data class ListContentsResult(val fileRef: String? = null, val content: String? = null, val language: String? = null, val contents: List<ListContentEntry> = emptyList(), val preambleHints: List<PreamblePropertyHint> = emptyList(), val message: String = "")

Declaration Summary
val fileRef: String?
val content: String?
val language: String?
val contents: List<ListContentEntry>
val preambleHints: List<PreamblePropertyHint>
val message: String Set when there is something to say about the listing itself, such as an empty box.

PreamblePropertyHint

data class PreamblePropertyHint(val property: String, val receiver: String, val delegate: String, val type: String = "", val boxUid: String = "", val boxName: String = "", val relation: String = "current", val graphRelevant: Boolean = false)

Declaration Summary
val property: String
val receiver: String
val delegate: String
val type: String
val boxUid: String
val boxName: String
val relation: String
val graphRelevant: Boolean

ListContentEntry

data class ListContentEntry(val fileRef: String, val property: String, val language: String, val content: String, val boxUid: String = "", val boxName: String = "", val relation: String = "current")

Declaration Summary
val fileRef: String
val property: String
val language: String
val content: String
val boxUid: String
val boxName: String
val relation: String

ListKotlinContextVariablesRequest

data class ListKotlinContextVariablesRequest(val target: String = "", val maxValueChars: Int = 300)

Declaration Summary
val target: String
val maxValueChars: Int

KotlinContextVariableInfo

data class KotlinContextVariableInfo(val name: String, val declaration: String, val declaredType: String, val runtimeType: String, val mutable: Boolean, val valuePreview: String, val primitiveValue: Any? = null, val primitiveKind: String = "")

Declaration Summary
val name: String
val declaration: String
val declaredType: String
val runtimeType: String
val mutable: Boolean
val valuePreview: String
val primitiveValue: Any?
val primitiveKind: String

ListKotlinContextVariablesResult

data class ListKotlinContextVariablesResult(val success: Boolean, val target: String, val boxUid: String, val boxName: String, val generation: Int, val message: String, val variables: List<KotlinContextVariableInfo> = emptyList())

Declaration Summary
val success: Boolean
val target: String
val boxUid: String
val boxName: String
val generation: Int
val message: String
val variables: List<KotlinContextVariableInfo>

EvaluateKotlinExpressionRequest

data class EvaluateKotlinExpressionRequest(val target: String = "", val expression: String, val renderHtml: Boolean = true, val maxChars: Int = 12000)

Declaration Summary
val target: String
val expression: String
val renderHtml: Boolean
val maxChars: Int

EvaluateKotlinExpressionResult

data class EvaluateKotlinExpressionResult(val success: Boolean, val target: String, val boxUid: String, val boxName: String, val expression: String, val valueKind: String, val type: String = "", val runtimeType: String = "", val primitiveValue: Any? = null, val primitiveKind: String = "", val stringValue: String = "", val html: String = "", val diagnostics: List<CompileDiagnostic> = emptyList(), val message: String = "")

Declaration Summary
val success: Boolean
val target: String
val boxUid: String
val boxName: String
val expression: String
val valueKind: String
val type: String
val runtimeType: String
val primitiveValue: Any?
val primitiveKind: String
val stringValue: String
val html: String
val diagnostics: List<CompileDiagnostic>
val message: String

EditFileRequest

data class EditFileRequest(val file: String, val newContent: String)

Tool: edit another file by name (your integration decides how to apply this).

EditFileResponse

data class EditFileResponse(val success: Boolean, val message: String? = null)

CompileKotlinRequest

data class CompileKotlinRequest(@JsonPropertyDescription("Which box: its name or uid, or its file ref afield://<document>/<box uid>/code exactly as ListContents reports it. Empty = the box this chat belongs to, where there is one.") val file: String = "", @param:JsonPropertyDescription("Optional replacement source to compile instead of the box's code. Omit it, or leave it empty, to compile the box's saved code.") val source: String? = null, val runSetupBlocks: Boolean = true)

Agentic verification tools ---------------------------------------------

Declaration Summary
val file: String
val source: String?
val runSetupBlocks: Boolean

CompileDiagnostic

data class CompileDiagnostic(val severity: String, val message: String, val line: Int = -1, val column: Int = -1, val endLine: Int = -1, val endColumn: Int = -1, val code: String? = null)

Declaration Summary
val severity: String
val message: String
val line: Int
val column: Int
val endLine: Int
val endColumn: Int
val code: String?

SetupPropertyChange

data class SetupPropertyChange(val property: String, val changed: Boolean, val cacheChanged: Boolean, val beforeChars: Int, val afterChars: Int)

Declaration Summary
val property: String
val changed: Boolean
val cacheChanged: Boolean
val beforeChars: Int
val afterChars: Int

CompileKotlinResult

data class CompileKotlinResult(val success: Boolean, val file: String, val message: String, val diagnostics: List<CompileDiagnostic> = emptyList(), val setupChanges: List<SetupPropertyChange> = emptyList())

Declaration Summary
val success: Boolean
val file: String
val message: String
val diagnostics: List<CompileDiagnostic>
val setupChanges: List<SetupPropertyChange>

ReloadShadersRequest

data class ReloadShadersRequest(val target: String = "", val mode: String = "auto", val visualCheck: Boolean = true)

Declaration Summary
val target: String
val mode: String
val visualCheck: Boolean

ShaderReloadError

data class ShaderReloadError(val property: String, val line: Int = -1, val message: String)

Declaration Summary
val property: String
val line: Int
val message: String

ReloadShadersResult

data class ReloadShadersResult(val success: Boolean, val target: String, val mode: String, val message: String, val reloaded: List<String> = emptyList(), val errors: List<ShaderReloadError> = emptyList(), val visualCheckAttached: Boolean = false, val visualCheckMessage: String = "", @get:JsonIgnore val visualDataUrl: String = "") : CarriesToolImages

Declaration Summary
val success: Boolean
val target: String
val mode: String
val message: String
val reloaded: List<String>
val errors: List<ShaderReloadError>
val visualCheckAttached: Boolean
val visualCheckMessage: String
val visualDataUrl: String
override val toolImages: List<ToolImage> Derived from visualDataUrl rather than passed separately, so "was a picture attached" has one source of truth.

CaptureRenderWindowRequest

data class CaptureRenderWindowRequest(@JsonPropertyDescription("Which window, when more than one is visible: a 1-based index, or a substring of the window " + "title. Empty picks the first capturable window visible from the target box.") val target: String = "", @JsonPropertyDescription("Box uid or afield:// ref to look for windows from. Empty uses the box this chat belongs to.") val uid: String = "", @JsonPropertyDescription("Milliseconds to wait before capturing, to let an animation or a recompile settle. " + "Clamped to 0..5000.") val settleMs: Int = 0, @JsonPropertyDescription("What you are looking for in this capture. Travels with the image as its caption.") val note: String = "")

Looking at the render window ----------------------------------------------

Declaration Summary
val target: String
val uid: String
val settleMs: Int
val note: String

CaptureRenderWindowResult

data class CaptureRenderWindowResult(val success: Boolean, val message: String, val title: String = "", val width: Int = 0, val height: Int = 0, val settledMs: Int = 0, @get:JsonIgnore val dataUrl: String = "") : CarriesToolImages, SummarisesForToolLog

Declaration Summary
val success: Boolean
val message: String
val title: String
val width: Int
val height: Int
val settledMs: Int
val dataUrl: String
override val toolImages: List<ToolImage>
override fun toolLogSummary(): String One line for the tool I/O log, and specifically including images=.

MacroListRequest

data class MacroListRequest(val unused: Int = 0)

Macro transcript tools ---------------------------------------------------

MacroListResult

data class MacroListResult(val success: Boolean, val message: String, val macros: List<String> = emptyList())

Declaration Summary
val success: Boolean
val message: String
val macros: List<String>

MacroReadRequest

data class MacroReadRequest(val name: String)

MacroReadResult

data class MacroReadResult(val success: Boolean, val message: String, val name: String, val content: String = "", val operationCount: Int = 0)

Declaration Summary
val success: Boolean
val message: String
val name: String
val content: String
val operationCount: Int

MacroWriteRequest

data class MacroWriteRequest(val name: String, val content: String)

Declaration Summary
val name: String
val content: String

MacroWriteResult

data class MacroWriteResult(val success: Boolean, val message: String, val name: String, val operationCount: Int = 0)

Declaration Summary
val success: Boolean
val message: String
val name: String
val operationCount: Int

MacroRunRequest

data class MacroRunRequest(val name: String, val direction: String = "do", val targetBoxes: List<String> = emptyList(), val strict: Boolean = false)

Declaration Summary
val name: String
val direction: String
val targetBoxes: List<String>
val strict: Boolean

MacroRunResult

data class MacroRunResult(val success: Boolean, val message: String, val name: String, val direction: String, val targetBoxes: List<String> = emptyList(), val applied: Int = 0, val skipped: Int = 0, val errors: List<String> = emptyList(), val output: List<afield.app.protocol.MacroOutputLine> = emptyList(), val setupVerified: Boolean = true)

Declaration Summary
val success: Boolean
val message: String
val name: String
val direction: String
val targetBoxes: List<String>
val applied: Int
val skipped: Int
val errors: List<String>
val output: List<afield.app.protocol.MacroOutputLine>
val setupVerified: Boolean

CodeUseQueryRequest

data class CodeUseQueryRequest(val kind: String = "failures", val limit: Int = 20, val document: String = "", val box: String = "", val sourcePath: String = "", val line: Int = 0, val code: String = "", val sourceName: String = "AgentSnippet.kt", val window: String = "all", val earlyWindowMs: Long = 600000, val callersPerSymbol: Int = 5, val sourceRadiusLines: Int = 24)

Code-use telemetry tools -------------------------------------------------

Declaration Summary
val kind: String
val limit: Int
val document: String
val box: String
val sourcePath: String
val line: Int
val code: String
val sourceName: String
val window: String
val earlyWindowMs: Long
val callersPerSymbol: Int
val sourceRadiusLines: Int

CodeUseQueryResult

data class CodeUseQueryResult(val success: Boolean, val kind: String, val message: String, val summary: CodeUseSummary? = null, val target: CodeUseSourceTarget? = null, val failures: List<CodeUseFailureRow> = emptyList(), val snippets: List<CodeUseSnippetRow> = emptyList(), val launches: List<CodeUseLaunchRow> = emptyList(), val symbols: List<CodeUseSymbolRow> = emptyList(), val unresolved: List<CodeUseUnresolvedRow> = emptyList(), val callers: List<CodeUseSnippetUseRow> = emptyList(), val references: List<CodeUseSourceReferenceRow> = emptyList(), val hotSymbols: List<CodeUseHotSourceSymbolRow> = emptyList(), val earlySymbols: List<CodeUseEarlySourceSymbolRow> = emptyList(), val relatedSources: List<CodeUseRelatedSourceRow> = emptyList(), val semanticAnalysis: CodeUseSemanticAnalyzeResult? = null)

Declaration Summary
val success: Boolean
val kind: String
val message: String
val summary: CodeUseSummary?
val target: CodeUseSourceTarget?
val failures: List<CodeUseFailureRow>
val snippets: List<CodeUseSnippetRow>
val launches: List<CodeUseLaunchRow>
val symbols: List<CodeUseSymbolRow>
val unresolved: List<CodeUseUnresolvedRow>
val callers: List<CodeUseSnippetUseRow>
val references: List<CodeUseSourceReferenceRow>
val hotSymbols: List<CodeUseHotSourceSymbolRow>
val earlySymbols: List<CodeUseEarlySourceSymbolRow>
val relatedSources: List<CodeUseRelatedSourceRow>
val semanticAnalysis: CodeUseSemanticAnalyzeResult?

CodeUseCallersForSourceLineRequest

data class CodeUseCallersForSourceLineRequest(val sourcePath: String, val line: Int, val limit: Int = 20)

Declaration Summary
val sourcePath: String
val line: Int
val limit: Int

CodeUseReferencesForCodeRequest

data class CodeUseReferencesForCodeRequest(val code: String, val limit: Int = 5)

Declaration Summary
val code: String
val limit: Int

CodeUseHotSymbolsRequest

data class CodeUseHotSymbolsRequest(val window: String = "all", val limit: Int = 20)

Declaration Summary
val window: String
val limit: Int

CodeUseEarlyDocumentSymbolsRequest

data class CodeUseEarlyDocumentSymbolsRequest(val document: String = "", val earlyWindowMs: Long = 600000, val limit: Int = 20)

Declaration Summary
val document: String
val earlyWindowMs: Long
val limit: Int

CodeUseRelatedSourceForCodeRequest

data class CodeUseRelatedSourceForCodeRequest(val code: String, val limit: Int = 5, val callersPerSymbol: Int = 5, val sourceRadiusLines: Int = 24)

Declaration Summary
val code: String
val limit: Int
val callersPerSymbol: Int
val sourceRadiusLines: Int

CodeUseSemanticAnalyzeCodeRequest

data class CodeUseSemanticAnalyzeCodeRequest(val code: String, val sourceName: String = "AgentSnippet.kt", val limit: Int = 5, val callersPerSymbol: Int = 5, val sourceRadiusLines: Int = 24)

Declaration Summary
val code: String
val sourceName: String
val limit: Int
val callersPerSymbol: Int
val sourceRadiusLines: Int

SemanticRetrievalSearchRequest

data class SemanticRetrievalSearchRequest(val query: String, val limit: Int = 10, val corpus: String = "", val corpora: List<String> = emptyList(), val pathPrefix: String = "", val kinds: List<String> = emptyList(), val documentName: String = "", val boxId: String = "", val property: String = "", val includeCapsule: Boolean = false, val includeSource: Boolean = false)

Semantic retrieval tools ------------------------------------------------

Declaration Summary
val query: String
val limit: Int
val corpus: String
val corpora: List<String>
val pathPrefix: String
val kinds: List<String>
val documentName: String
val boxId: String
val property: String
val includeCapsule: Boolean
val includeSource: Boolean

SemanticRetrievalStatusRequest

data class SemanticRetrievalStatusRequest(val unused: Int = 0)

SemanticRetrievalRebuildRequest

data class SemanticRetrievalRebuildRequest(val unused: Int = 0)

SemanticRetrievalToolResult

data class SemanticRetrievalToolResult(val success: Boolean, val message: String, val status: SemanticRetrievalStatus, val model: SemanticRetrievalModelInfo? = null, val availableCorpora: List<String> = emptyList(), val results: List<SemanticRetrievalHit> = emptyList())

Declaration Summary
val success: Boolean
val message: String
val status: SemanticRetrievalStatus
val model: SemanticRetrievalModelInfo?
val availableCorpora: List<String>
val results: List<SemanticRetrievalHit>

ScratchpadReadRequest

data class ScratchpadReadRequest(val unused: Int = 0)

Long-running task tools --------------------------------------------------

ScratchpadUpdateRequest

data class ScratchpadUpdateRequest(val operation: String = "append", val text: String = "")

Declaration Summary
val operation: String
val text: String

ScratchpadResult

data class ScratchpadResult(val success: Boolean, val message: String, val text: String = "", val charCount: Int = 0)

Declaration Summary
val success: Boolean
val message: String
val text: String
val charCount: Int

ChecklistReadRequest

data class ChecklistReadRequest(val unused: Int = 0)

ChecklistUpdateRequest

data class ChecklistUpdateRequest(val operation: String, val itemId: String = "", val text: String = "", val status: String = "pending", val items: List<ChecklistItemInput> = emptyList())

Declaration Summary
val operation: String
val itemId: String
val text: String
val status: String
val items: List<ChecklistItemInput>

ChecklistItemInput

data class ChecklistItemInput(val text: String, val status: String = "pending", val id: String = "")

Declaration Summary
val text: String
val status: String
val id: String

ChecklistItem

data class ChecklistItem(val id: String, val text: String, val status: String = "pending")

Declaration Summary
val id: String
val text: String
val status: String

ChecklistResult

data class ChecklistResult(val success: Boolean, val message: String, val items: List<ChecklistItem> = emptyList())

Declaration Summary
val success: Boolean
val message: String
val items: List<ChecklistItem>

AgentMemoryState

class AgentMemoryState

Declaration Summary
fun beginTurn() Called when an agent turn starts.
fun checklistSummary(): String? "2 of 6 done, 1 in progress", plus "; not updated this turn" when it is stale; null when empty.
data class Snapshot(val scratchpad: String, val checklistJson: String) The scratchpad and checklist as they are, for the session tree: memory is branch state, so a rewound conversation gets back the memory of the turn it was rewound to (AiSessionTree).
fun snapshot(): Snapshot
fun restore(snapshot: Snapshot)
fun readScratchpad(): ScratchpadResult
fun updateScratchpad(request: ScratchpadUpdateRequest): ScratchpadResult
fun readChecklist(): ChecklistResult
fun updateChecklist(request: ChecklistUpdateRequest): ChecklistResult
fun checklistMarkdown(): String

ToolImage

data class ToolImage(val dataUrl: String, val caption: String = "")

One image for the model to look at, as a data:image/…;base64,… URL.

Declaration Summary
val dataUrl: String
val caption: String

CarriesToolImages

interface CarriesToolImages

A tool result that has something for the model to see, not just to read.

Declaration Summary
val toolImages: List<ToolImage> The pictures, excluded from this result's JSON on purpose — see the interface docs.

WorkspaceSymbolSearch

data class WorkspaceSymbolSearch(val query: String, val limit: Int = 25, val includeRelatedContext: Boolean = false)

Example tool (optional) -------------------------------------------------

WorkspaceSymbolSearchResult

data class WorkspaceSymbolSearchResult(val matches: List<WorkspaceSymbolMatch>, val unindexedMatches: List<String> = emptyList(), val message: String = "")

Declaration Summary
val matches: List<WorkspaceSymbolMatch>
val unindexedMatches: List<String> Source files saved since the index was built whose text contains the query; their symbols may be missing above.
val message: String

WorkspaceSymbolMatch

data class WorkspaceSymbolMatch(val symbol: String, val kind: String, val locationHint: String, val filePath: String? = null, val fileId: String? = null, val line: Int? = null, val definitionLine: String? = null, val definitionBody: String? = null, val relatedGuides: List<String> = emptyList(), val relatedExamples: List<String> = emptyList(), val kdoc: String? = null)

Declaration Summary
val symbol: String
val kind: String
val locationHint: String
val filePath: String?
val fileId: String?
val line: Int?
val definitionLine: String?
val definitionBody: String?
val relatedGuides: List<String>
val relatedExamples: List<String>
val kdoc: String?

GetUtcTime

data class GetUtcTime(val format: String = "yyyy-MM-dd HH:mm:ss")

CLI example tools --------------------------------------------------------

Remember

data class Remember(val key: String, val value: String)

Recall

data class Recall(val key: String)

ToolExecutionContext

data class ToolExecutionContext(val attachments: List<ContextAttachment> = emptyList(), val resolveAttachment: ((String) -> AttachmentResponse?)? = null, val listDefinitions: (() -> ListDefinitionsResult)? = null, val listFileContents: (() -> ListContentsResult)? = null, val listKotlinContextVariables: ((ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult)? = null, val evaluateKotlinExpression: ((EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult)? = null, val editOtherFile: ((EditFileRequest) -> EditFileResponse)? = null, val compileKotlin: ((CompileKotlinRequest) -> CompileKotlinResult)? = null, val reloadShaders: ((ReloadShadersRequest) -> ReloadShadersResult)? = null, val captureRenderWindow: ((CaptureRenderWindowRequest) -> CaptureRenderWindowResult)? = null, val listMacros: ((MacroListRequest) -> MacroListResult)? = null, val readMacro: ((MacroReadRequest) -> MacroReadResult)? = null, val writeMacro: ((MacroWriteRequest) -> MacroWriteResult)? = null, val runMacro: ((MacroRunRequest) -> MacroRunResult)? = null, val codeUseQuery: ((CodeUseQueryRequest) -> CodeUseQueryResult)? = null, val semanticRetrievalSearch: ((SemanticRetrievalSearchRequest) -> SemanticRetrievalToolResult)? = null, val semanticRetrievalStatus: (() -> SemanticRetrievalToolResult)? = null, val semanticRetrievalRebuild: (() -> SemanticRetrievalToolResult)? = null, val agentMemory: AgentMemoryState? = null, val documentGuides: DocumentGuideTools? = null, val workspaceSymbolSearch: ((WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult)? = null, val workspaceSearch: ((WorkspaceSearchRequest) -> WorkspaceSearchResult)? = null, val repoFiles: RepoFileTools? = null, val repoCommands: RepoCommandTools? = null, val focus: ToolFocus? = null, val dynamicTools: List<BoxTool> = emptyList(), val fieldGraph: afield.ai.agent.FieldGraphTools? = null, val withheld: Set<String> = emptySet(), val progress: ((String) -> Unit)? = null, val promoted: MutableSet<String>? = null, val reportProgress: ((LlmProgressUpdate) -> Unit)? = null, val inbox: AiTurnInbox? = null, val suspendable: Boolean = false)

Declaration Summary
val attachments: List<ContextAttachment>
val resolveAttachment: ((String) -> AttachmentResponse?)?
val listDefinitions: (() -> ListDefinitionsResult)?
val listFileContents: (() -> ListContentsResult)?
val listKotlinContextVariables: ((ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult)?
val evaluateKotlinExpression: ((EvaluateKotlinExpressionRequest) -> EvaluateKotlinExpressionResult)?
val editOtherFile: ((EditFileRequest) -> EditFileResponse)?
val compileKotlin: ((CompileKotlinRequest) -> CompileKotlinResult)?
val reloadShaders: ((ReloadShadersRequest) -> ReloadShadersResult)?
val captureRenderWindow: ((CaptureRenderWindowRequest) -> CaptureRenderWindowResult)?
val listMacros: ((MacroListRequest) -> MacroListResult)?
val readMacro: ((MacroReadRequest) -> MacroReadResult)?
val writeMacro: ((MacroWriteRequest) -> MacroWriteResult)?
val runMacro: ((MacroRunRequest) -> MacroRunResult)?
val codeUseQuery: ((CodeUseQueryRequest) -> CodeUseQueryResult)?
val semanticRetrievalSearch: ((SemanticRetrievalSearchRequest) -> SemanticRetrievalToolResult)?
val semanticRetrievalStatus: (() -> SemanticRetrievalToolResult)?
val semanticRetrievalRebuild: (() -> SemanticRetrievalToolResult)?
val agentMemory: AgentMemoryState?
val documentGuides: DocumentGuideTools?
val workspaceSymbolSearch: ((WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult)?
val workspaceSearch: ((WorkspaceSearchRequest) -> WorkspaceSearchResult)?
val repoFiles: RepoFileTools? The repo tools over one checkout (RepoFileTools); only a headless job with a repo scope wires it.
val repoCommands: RepoCommandTools? The repo command tool (RepoCommandTools) over repoFiles' root; only a job that allows commands wires it.
val focus: ToolFocus? What this conversation is about, when it is about one box: see ToolFocus.
val dynamicTools: List<BoxTool> Document-authored tools contributed by boxes via the aiTool DSL. These are resolved per-request (the anchoring document's DocumentToolRegistry snapshot) and layered on top of the static built-ins by ToolRegistry: they surface on both the OpenAI chat path (ToolRegistry.addAllTo/ToolRegistry.execute) and the provider-neutral MCP face (ToolRegistry.describe/ToolRegistry.executeByName). A dynamic tool whose name collides with a built-in is ignored (built-ins win).
val fieldGraph: afield.ai.agent.FieldGraphTools? Native box graph + execution tools (NewBox, RunBox, ReadBoxOutput, ...) for one document. null hides them: only contexts that mean to let the agent edit and run the document wire it.
val withheld: Set<String> Tool names this conversation must not see or call, whatever their gates say. The registry enforces it everywhere (schemas, MCP descriptors, dispatch), so an A/B arm that withholds a tool cannot leak it through a second path. An eval's programs=off arm withholds RunToolProgramRequest.
val progress: ((String) -> Unit)? Progress lines from inside a long tool — a tool program reports each nested call through it, so the panel shows program: RunBox Cell03 (4/12) rather than one silent spinner. The bridges wire it to their onProgress; null where nobody is watching.
val promoted: MutableSet<String>? Deferred tools this conversation has loaded with DescribeToolsRequest — the bridges pass their thread's own set, so a load lasts for the conversation (and, with branching, for its branch). Null where there is no conversation to remember it in.
val reportProgress: ((LlmProgressUpdate) -> Unit)? Structured progress from native tools, scoped to this conversation.
val inbox: AiTurnInbox? Messages the person sends while this turn runs. The bridges drain it after each tool round and send what it held as a user message alongside the tool results; null where nobody can write in.
val suspendable: Boolean Whether a tool may end this turn to wait (SuspendsTurn). Only a conversation that something will resume says so: the box chat's general turn, which saves the suspension with the chat. Elsewhere (the editor kinds, the document chat, a headless task, MCP) the waiting tools are not offered.
fun documentReadOnly(): Boolean Whether the document this conversation acts on refuses edits: the session's flag, or Field's global one.

ToolFocus

data class ToolFocus(val document: String, val box: afield.document.graph.Box, val property: String)

The document, box and property a conversation is focused on — the box whose AI4 chat is open.

Declaration Summary
val document: String The document's path, as SessionInformation.filename.
val box: afield.document.graph.Box The focused box.
val property: String The property the chat was opened on, such as code or stackSL.

TutorialCodeTools.kt

Finding tutorial code: a worked example of box code to start from.

Field ships about 260 tutorial documents: the materialized agent transcripts under public/docs/assets/agent_transcripts/<name>/ and the graphics curriculum in public/docs/assets/canonical_graphics/. Semantic search indexes them box by box as its tutorials corpus, the way it indexes a workspace; this tool is a query on that corpus, grouped by document (by lesson for the graphics curriculum, whose boxes are its lessons), with each document's description and docs links from its transcript and the code files to read. A match names files rather than quoting them: read one with AttachmentRequest to see the box code itself.

src/jvmMain/kotlin/afield/ai/tools/TutorialCodeTools.kt — read the source

TutorialCodeSearchRequest

data class TutorialCodeSearchRequest(@param:JsonPropertyDescription("What you want to make or learn, in plain words.") val query: String, @param:JsonPropertyDescription("Tutorials to return, 1..12.") val limit: Int = 6)

Ask for tutorials by what they do. The answer is a TutorialCodeSearchResult.

Declaration Summary
val query: String What to make or learn, in plain words, matched against the tutorials corpus rather than by keyword.
val limit: Int How many tutorials to return; anything outside 1..12 is clamped into it.

TutorialCodeMatch

data class TutorialCodeMatch(val name: String, val description: String, val boxes: List<String>, val docs: List<String>, val codeFiles: List<String>)

One tutorial that matched, with the files a reader would open next.

Declaration Summary
val name: String The tutorial document's name, or canonical_graphics and the lesson box's name for one lesson.
val description: String What the document's transcript says it shows, to 400 characters, empty when it says nothing.
val boxes: List<String> Names of the boxes inside it that matched, up to six.
val docs: List<String> The docs pages it illustrates, each as page: why from the transcript, up to four.
val codeFiles: List<String> Paths of its box code and shader files, up to eight, for AttachmentRequest to read.

TutorialCodeSearchResult

data class TutorialCodeSearchResult(val success: Boolean, val message: String, val matches: List<TutorialCodeMatch>)

The tool's answer: success is false both for a failed search and for a search that matched nothing.

WorkspaceSearch.kt

WorkspaceSearchRequest is the agent tool that finds literal text in exactly one WorkspaceSearchScope: the current document's files, its history, the whole workspace, or Field's own Kotlin source. It reads one bounded file at a time under byte, match and time limits, and the WorkspaceSearchResult says whether it is complete and, if not, why it stopped and what it skipped. There is no regex or shell syntax, and unsaved editor state is not searched.

src/jvmMain/kotlin/afield/ai/tools/WorkspaceSearch.kt — read the source

WorkspaceSearchScope

enum class WorkspaceSearchScope

The one tree a search covers.

Constants: DOCUMENT_FILES, DOCUMENT_HISTORY, WORKSPACE_FILES, FIELD_KOTLIN_SOURCE, FIELD_DOCS.

WorkspaceSearchScopeDeserializer

class WorkspaceSearchScopeDeserializer : JsonDeserializer<WorkspaceSearchScope>()

Jackson otherwise accepts enum ordinals, which could silently widen a search.

Declaration Summary
override fun deserialize(parser: JsonParser, context: DeserializationContext): WorkspaceSearchScope

WorkspaceSearchRequest

data class WorkspaceSearchRequest(@param:JsonPropertyDescription("Required: DOCUMENT_FILES, DOCUMENT_HISTORY, WORKSPACE_FILES, FIELD_KOTLIN_SOURCE, or FIELD_DOCS (Field's Markdown documentation, public/docs). Never combines roots. Workspace/document files exclude backups.") val scope: WorkspaceSearchScope, @param:JsonPropertyDescription("Literal single-line text, 1..256 UTF-16 units. Whitespace is preserved.") val query: String, @param:JsonPropertyDescription("Optional relative file/directory path under the scope root, using / separators. FIELD_KOTLIN_SOURCE's root is src/, so jvmMain/kotlin/afield/geometry; FIELD_DOCS's root is public/docs/. No absolute paths, .., or symlinks.") val pathPrefix: String = "", @param:JsonPropertyDescription("Optional file extensions, e.g. [kt, glsl] (a leading dot is ignored). Empty searches all eligible text files. FIELD_KOTLIN_SOURCE searches only kt; FIELD_DOCS searches only md.") val extensions: List<String> = emptyList(), val ignoreCase: Boolean = false, @param:JsonPropertyDescription("Global matching-line limit, 1..500. Each line is returned once; reaching the cap reports partial results.") val maxMatches: Int = 80)

Declaration Summary
val scope: WorkspaceSearchScope
val query: String
val pathPrefix: String
val extensions: List<String>
val ignoreCase: Boolean
val maxMatches: Int

WorkspaceSearchStop

enum class WorkspaceSearchStop

Constants: MATCH_LIMIT, RESPONSE_LIMIT, ENTRY_LIMIT, DEPTH_LIMIT, INPUT_LIMIT, DEADLINE, CANCELLED.

WorkspaceSearchMatch

data class WorkspaceSearchMatch(val path: String, val line: Int, val column: Int, val excerpt: String, val excerptStartColumn: Int, val excerptClipped: Boolean)

Declaration Summary
val path: String
val line: Int
val column: Int
val excerpt: String
val excerptStartColumn: Int
val excerptClipped: Boolean

WorkspaceSearchSkips

data class WorkspaceSearchSkips(val oversized: Int = 0, val nonText: Int = 0, val unreadable: Int = 0, val changed: Int = 0)

Declaration Summary
val oversized: Int
val nonText: Int
val unreadable: Int
val changed: Int

WorkspaceSearchResult

data class WorkspaceSearchResult(val success: Boolean, val scope: WorkspaceSearchScope, val root: String?, val matches: List<WorkspaceSearchMatch>, val complete: Boolean, val stopReason: WorkspaceSearchStop?, val filesSearched: Int, val bytesRead: Long, val skipped: WorkspaceSearchSkips, val message: String)

Declaration Summary
val success: Boolean
val scope: WorkspaceSearchScope
val root: String?
val matches: List<WorkspaceSearchMatch>
val complete: Boolean
val stopReason: WorkspaceSearchStop?
val filesSearched: Int
val bytesRead: Long
val skipped: WorkspaceSearchSkips
val message: String

WorkspaceSymbolSearchSupport.kt

src/jvmMain/kotlin/afield/ai/tools/WorkspaceSymbolSearchSupport.kt — read the source

Top-level functions

Declaration Summary
fun searchWorkspaceSymbols(index: KotlinSourceIndexDb?, rawQuery: String, limit: Int, includeRelatedContext: Boolean = false): WorkspaceSymbolSearchResult

ToolEvidence.kt

Complete tool results that are too large for a transcript, kept whole outside the model's context.

A program's nested calls are recorded in the turn's transcript and the tool I/O log, but a result of tens of kilobytes would make both unreadable, and cutting it at a fixed length leaves invalid JSON that hides exactly the part of a failure the next agent needs. So a nested result over ToolProgramRunner.MAX_NESTED_RESULT_CHARS is stored here whole, content-addressed, and the transcript carries a small, valid stub with its id, SHA-256 and size; ReadToolEvidenceRequest reads it back.

store is replaceable: a host with its own evidence store (a study's build records, say) can put these there instead. The default keeps them gzipped beside the tool I/O log, ~/.afield/logs/ai4_tool_io/evidence/ (AFIELD_AI4_TOOL_EVIDENCE_PATH or -Dafield.ai4.toolEvidence.path to move it). Nothing deletes them yet.

src/jvmMain/kotlin/afield/ai/tools/program/ToolEvidence.kt — read the source

ToolEvidence

object ToolEvidence

The evidence store for oversized nested tool results: store puts and gets them by content id.

Declaration Summary
data class Ref(val id: String, val sha256: String, val bytes: Int) Where a result was put: id is what ReadToolEvidenceRequest takes; sha256 and bytes are of the UTF-8 text.
interface Store
class FileStore(private val dir: File) : Store gzip files named by content hash in dir.
const val ID_PREFIX
var store: Store
fun sha256(bytes: ByteArray): String

ReadToolEvidenceRequest

data class ReadToolEvidenceRequest(@JsonPropertyDescription("The evidence id, ai4ev:…") val id: String, @JsonPropertyDescription("Character offset to start at; default 0.") val offset: Int = 0, @JsonPropertyDescription("Most characters to return; default 16000, at most 64000.") val limit: Int = 16_000)

Declaration Summary
val id: String
val offset: Int
val limit: Int

ReadToolEvidenceResult

data class ReadToolEvidenceResult(val success: Boolean, val id: String, val bytes: Int = 0, val sha256: String = "", val offset: Int = 0, val text: String = "", val more: Boolean = false, val message: String = "")

Declaration Summary
val success: Boolean
val id: String
val bytes: Int
val sha256: String
val offset: Int
val text: String
val more: Boolean
val message: String

ToolProgramRunner.kt

The sandbox a tool program runs in: one RunToolProgramRequest's JavaScript, in a fresh GraalJS context that can reach the conversation's tools and nothing else. RunToolProgramRequest's own file overview says what a program is for; this file is how it is kept to its tools.

The boundary

Only strings cross it. The guest gets six host functions (__call, __out, __progress, __show, __describe, __settle) and one JSON string of tool names; a prelude builds the friendly tools.X(...), tools.X.raw(...), check(...), progress(...), text(...) and show(...) surface from those in JS. The context runs under SandboxPolicy.CONSTRAINED with no IO, no threads, no processes, no native access, no polyglot access, no environment and no host class lookup, and carries a statement limit, so a program cannot reach a Java object even by accident.

Ending a program

A program ends by settling its promise (stage ok; or tool-failed for an uncaught failing call, check-failed for an uncaught check, runtime for any other error), by a syntax error, by running out of statements, calls or time, or because its turn was cancelled. Time and cancellation are a watchdog's job: it closes the context from its own thread, which Graal honours the next time the program thread is in guest code, so a tool call already running is allowed to finish. A cancellation is rethrown as a CancellationException so the turn ends rather than reporting a tool error, matching every bridge's isAiTurnCancellation handling.

Threads

The program runs on the calling thread, which is the agent loop's: an IO thread for a panel turn, a daemon for an agent task, the backdoor's scope for MCP. Never the Runloop — the graph tools a program calls hop onto the Runloop and wait for it, which would deadlock there, so run refuses.

src/jvmMain/kotlin/afield/ai/tools/program/ToolProgramRunner.kt — read the source

ToolProgramTools.kt

RunToolProgramRequest is one tool whose argument is a short JavaScript program, so a model that would have spent twelve rounds calling twelve tools spends one. The program can call every other tool the conversation has, as a function — tools.RunBox({box: "Cell03"}) — and only what it returns, or prints with text(...), goes back to the model, plus a one-line record per call. The intermediate results stay in the program. The idea is Pi's "codemode" (earendil-works/pi); the mapping onto Field is in dev_docs/ai/AI4_PI_LESSONS.md.

The saving is in what a round costs. A model working through tools one call per round pays for every round twice: a request carrying the whole conversation so far, and a result that lands in that conversation for good. For "run each of these twelve boxes and tell me which ones fail" that is twelve rounds and twelve results the model reads in full, when what it needed was three names.

What a program can reach

Only the tools, and only through one string-in, string-out bridge: the runner builds a GraalJS context with no host access, no IO, no threads, no processes, no environment and a statement limit, so there is no Java.type, no require, no fetch and no timers. A tool call from a program goes through ToolRegistry.executeByName — the same dispatcher a model's own call goes through, with the same gates — so a program can do nothing the conversation could not do one call at a time, and every edit it makes is recorded exactly as the direct call would record it.

A program is not a transaction. One that fails halfway leaves its earlier edits in place; they are recorded and undoable like any other agent edit, but nothing rolls them back.

Why JavaScript and not the macro YAML

What makes this pay is the middle of the program — filter, loop, branch on a result — and the OperationTranscript YAML has no data flow at all: it is the durable, reversible record of edits, and the edits a program makes still land there. Kotlin would be the house language, but every program would pay a K2 compile, and box code can reach the whole JVM. JS is what models write fluently as glue, and GraalJS is already on the classpath.

src/jvmMain/kotlin/afield/ai/tools/program/ToolProgramTools.kt — read the source

Top-level values

Declaration Summary
const val RUN_TOOL_PROGRAM

RunToolProgramRequest

data class RunToolProgramRequest(@JsonPropertyDescription("The body of an async JavaScript function (ES2024). `return` a value (it is JSON-serialised); " + "text(...) appends a line to the output.") val code: String, @JsonPropertyDescription("One line saying what the program does; shown in the panel and the tool log.") val purpose: String = "", @JsonPropertyDescription("Seconds before the program is stopped; default 60, at most 300. A tool call already running finishes first.") val timeoutSeconds: Double = 60.0, @JsonPropertyDescription("The most tool calls the program may make; default 40, at most 200.") val maxCalls: Int = 40)

The RunToolProgram tool: a JavaScript program that calls the conversation's other tools and returns one result.

Declaration Summary
val code: String
val purpose: String
val timeoutSeconds: Double
val maxCalls: Int

ProgramToolCall

data class ProgramToolCall(val n: Int, val tool: String, val args: String, val ok: Boolean, val elapsedMs: Long, val summary: String, val error: String? = null, val evidence: String? = null)

One tool call a program made, as the model sees it: enough to tell what happened, never the result itself.

Declaration Summary
val n: Int
val tool: String
val args: String
val ok: Boolean
val elapsedMs: Long
val summary: String
val error: String?
val evidence: String? Set when the result was too large to keep in the transcript: read it whole with ReadToolEvidenceRequest.

NestedToolCall

data class NestedToolCall(val n: Int, val tool: String, val argumentsJson: String, val resultJson: String, val summary: String, val ok: Boolean, val elapsedMs: Long, val resultBytes: Int = 0, val resultSha256: String = "", val evidence: String? = null)

One nested call in full, for transcripts and logs rather than the model: the bridges expand these into child afield.ai.models.LlmToolTranscriptEntrys with ids <programCallId>/<n>, so the tool I/O log, the eval's tool-call records and AgentLessons' misuse count all see what the program did.

Declaration Summary
val n: Int
val tool: String
val argumentsJson: String
val resultJson: String
val summary: String
val ok: Boolean
val elapsedMs: Long
val resultBytes: Int Size and SHA-256 of the complete result, whether or not resultJson holds all of it.
val resultSha256: String
val evidence: String? Where the complete result is when resultJson is only a stub (ToolEvidence).

CarriesNestedToolCalls

interface CarriesNestedToolCalls

A tool result made of other tool calls. Implemented by RunToolProgramResult; read by the bridges.

Declaration Summary
val nestedCalls: List<NestedToolCall>

RunToolProgramResult

data class RunToolProgramResult(val success: Boolean, val stage: String, val message: String, val output: String, val returned: JsonNode?, val calls: List<ProgramToolCall>, val totalCalls: Int, val failedCalls: Int, val elapsedMs: Long, val truncated: Boolean, val imagesShown: Int, @get:JsonIgnore override val toolImages: List<ToolImage> = emptyList(), @get:JsonIgnore override val nestedCalls: List<NestedToolCall> = emptyList(), val nestedClipped: Int = 0) : CarriesToolImages, SummarisesForToolLog, CarriesNestedToolCalls

Declaration Summary
val success: Boolean
val stage: String ok, tool-failed, check-failed, runtime, syntax, timeout, statement-limit, call-cap or unsettled.
val message: String
val output: String What the program passed to text(...), one line per call, at most 16000 characters.
val returned: JsonNode? What the program returned, or {"truncated": N, "preview": ...} when it was too large.
val calls: List<ProgramToolCall>
val totalCalls: Int
val failedCalls: Int
val elapsedMs: Long
val truncated: Boolean True when output, the returned value, the call list or a nested result was cut to fit.
val imagesShown: Int
val nestedClipped: Int Nested results kept whole in ToolEvidence because they were too large for the transcript.
override fun toolLogSummary(): String

AiToolUseImprovementCli.kt

A command-line task in which a model reads Field's recent AI tool I/O logs and edits the AI4 tool descriptions and prompts that caused the trouble it finds.

main runs it; Gradle's improveAiToolUse and improveAiToolUseDryRun tasks launch it. It summarises the newest logs under ~/.afield/logs/ai4_tool_io/ (--log-dir, or AFIELD_AI4_TOOL_IO_LOG_PATH), attaches a manifest of the source tree, and gives the model a small tool set over it, including ReplaceInFileRequest for exact snippet edits. It writes a Markdown report and a transcript of the run. With --dry-run the edits go to an in-memory overlay and come out as a patch file instead of touching the source. Logs of this task's own runs are left out of the review (isToolUseImprovementLog).

src/jvmMain/kotlin/afield/ai/tools/tooluse/AiToolUseImprovementCli.kt — read the source

Top-level functions

Declaration Summary
fun isToolUseImprovementLog(path: Path): Boolean Whether a tool I/O log is one of this task's own runs — its session_start carries the file key cli://ai-tool-use-improvement. A review leaves those out, so that a window of recent logs holds the agent work being reviewed rather than earlier passes of the reviewer. main also turns this run's own logging off, so this filter applies to logs already on disk.
fun main(args: Array<String>)

ReplaceInFileRequest

data class ReplaceInFileRequest(val file: String, val oldText: String, val newText: String, val expectedOccurrences: Int = 1)

Declaration Summary
val file: String
val oldText: String
val newText: String
val expectedOccurrences: Int

ReplaceInFileResponse

data class ReplaceInFileResponse(val success: Boolean, val message: String, val file: String, val occurrences: Int = 0, val newContentChars: Int = 0)

Declaration Summary
val success: Boolean
val message: String
val file: String
val occurrences: Int
val newContentChars: Int

stippletransport.kt

Runs a learned stipple transport on the CPU: a small SiLU network, trained by the optional PyTorch tool in optional/transport-flow, that moves points from a uniform source rectangle toward an image's density.

loadStippleTransport reads the exported JSON model. StippleTransport.transport and StippleTransport.flowMap integrate points through the learned velocity field (Euler or midpoint steps), StippleTransport.pathsFrom returns each point's path as an FLine, and StippleTransport.sourceParticles draws the source points. A temporal model also takes a movie time, so you can draw source points once and re-transport the same list for each frame. StippleTransportTest covers loading and integration.

src/jvmMain/kotlin/afield/geometry/spatial/stippletransport.kt — read the source

Top-level functions

Declaration Summary
fun loadStippleTransport(file: File): StippleTransport Loads the optional PyTorch stipple trainer's portable field.transport-flow.v1 model.
fun loadStippleTransport(path: String): StippleTransport Convenience spelling of loadStippleTransport for a filename.

StippleTransport

class StippleTransport private constructor(private val data: StippleTransportData)

A reusable density-conditioned coordinate warp, independent of its training stipples.

Declaration Summary
val temporal: Boolean Whether the network has a fourth input for movie time.
val frameTimes: List<Double> Discrete movie times used for training, copied so callers cannot alter the model.
val parameterCount: Int Number of learned scalar weights and biases.
val sourceHalfExtent: List<Double> Half-width and half-height of the uniform canonical source rectangle.
fun velocity(at: Vec3, transportTime: Double, movieTime: Double = 0.0): Vec3 Predicts (vx,vy,0) at any point, at two independent times.
fun transport(points: List<Vec3>, transportTime: Double = 1.0, movieTime: Double = 0.0, steps: Int = 32, fromTransportTime: Double = 0.0, integrator: FlowIntegrator = FlowIntegrator.MIDPOINT): List<Vec3> Integrates arbitrary particles, curve vertices or lattice samples. No training-point lookup. fromTransportTime can exceed transportTime for inversion; both can overshoot 0..1.
fun flowMap(at: Vec3, transportTime: Double = 1.0, movieTime: Double = 0.0, steps: Int = 32): Vec3 Single-point flow map, with the same semantics as transport.
fun pathsFrom(points: List<Vec3>, transportTime: Double = 1.0, movieTime: Double = 0.0, steps: Int = 32, fromTransportTime: Double = 0.0, integrator: FlowIntegrator = FlowIntegrator.MIDPOINT): List<FLine> Traces supplied geometry in transport time, one FLine per input point.
fun sourceParticles(count: Int, seed: Long = 1L, grid: Boolean = false): List<Vec3> Draw once and retain the returned list for persistent movie identity.
fun unsafeBlendWeights(other: StippleTransport, amount: Double): StippleTransport Deliberately unsafe weight interpolation; matching metadata does not align hidden neurons. Prefer conditioning one shared model on movie time. Sequential fine-tuning gives checkpoints shared ancestry but still supplies no guarantee. amount is not clamped.

neuralparticles.kt

Taking a small trained network to the GPU: MlpWeights is a flat, layer-major snapshot of a multilayer perceptron's weights, and MlpWeights.glsl emits a GLSL function that evaluates the same network out of a storage buffer.

What that buys is a particle whose update rule was trained rather than written. flowMatch (see flowmatching.kt) already fits a velocity field v(x, t) in a couple of seconds of pure Kotlin; a compute shader stepping a buffer of particles can evaluate that field per particle per frame, provided the two sides agree about what the network is. This file is that agreement, and afield.examples.four.NeuralFieldParitySmoke measures that it holds on real hardware.

Nothing here is particle-specific: a net that takes a position and returns a velocity, a net that takes a neighbour offset and returns an interaction term, and a net that takes anything and returns anything are all the same object. The particle system is what consumes it.

Two comparisons, not one

There are two distinct errors between "the network Smile trained" and "the network the GPU runs", they have very different sizes, and conflating them makes the unavoidable one look like a bug.

  • Precision. Smile evaluates in Double; a shader evaluates in float. Agreement is therefore limited to fp32 by construction — a few parts in a million, relative, and rather more for an output that is a cancelling sum. MlpWeights.evaluate is the fp32 reference in Kotlin, so this error can be measured on its own with no GPU present at all.
  • Transcription. The order of the layers, the orientation of each weight matrix, the slope of the leaky ReLU, whether the output layer has an activation. Every one of these is either exactly right or grossly wrong, and a grossly wrong one still produces smooth, plausible, entirely fictional output.

So the GPU is compared against MlpWeights.evaluate, fp32 against fp32, where the only remaining freedom is the order the hardware chooses to accumulate a dot product in; and evaluate is compared against Smile separately. A single comparison against Double would have to be loose enough to absorb the precision gap, which is a tolerance wide enough to hide a transposed matrix in a small network.

The weight layout

MlpWeights.flat runs layer by layer from the first hidden layer to the output layer. Each layer contributes its weight matrix, row-major with one row per output neuron, then its bias vector. So for a layer with n outputs over p inputs, the weight from input i to output o is at weightOffset(layer) + o * p + i, and its bias is at biasOffset(layer) + o. The input layer contributes nothing — it holds no weights.

This is the layout a shader wants to read, and it is also the layout Smile happens to store. The two are not relied on to coincide: VectorMLP.exportWeights in flowmatching.kt reads element by element through DenseMatrix.get(row, column) rather than copying memory, so a change of storage order upstream cannot silently transpose the network.

src/jvmMain/kotlin/afield/simulation/particles/neuralparticles.kt — read the source

Used in: Neural particle systems

Top-level functions

Declaration Summary
fun MlpWeights.blendWith(other: MlpWeights, t: Float): MlpWeights Two networks of the same shape, averaged weight by weight — (1-t)·this + t·other.
fun FlowMatchExport.blendWith(other: FlowMatchExport, t: Float): FlowMatchExport MlpWeights.blendWith for a whole field, carrying the training frame and source with it.
fun MlpWeights.glsl(name: String = "neuralField"): String GLSL that evaluates this network out of a storage buffer, as a buffer_reference type and a function named name.
fun FlowMatchExport.glsl(name: String = "neuralField"): String GLSL for the whole field: the network from MlpWeights.glsl, the time encoding, and a neuralVelocity that converts in and out of the training frame.

MlpWeights

class MlpWeights(val inputs: Int, val outputs: Int, val hidden: IntArray, val activation: MlpActivation, val flat: FloatArray)

A flat, layer-major snapshot of a trained multilayer perceptron: the layer widths, the hidden activation, and one FloatArray holding every weight and bias.

Declaration Summary
val inputs: Int Width of the input vector.
val outputs: Int Width of the output vector.
val hidden: IntArray Hidden layer widths, in order.
val activation: MlpActivation The activation applied after every hidden layer. The output layer is always linear.
val flat: FloatArray Every weight and bias, laid out as described above.
val layerWidths: IntArray The width of each weight-bearing layer's output: the hidden widths, then outputs.
val layerInputs: IntArray The width of each weight-bearing layer's input: inputs, then the hidden widths.
val layerCount: Int How many weight-bearing layers there are — one more than the number of hidden layers.
fun weightOffset(layer: Int): Int Index into flat of the first weight of layer, which is layer's row 0, column 0.
fun biasOffset(layer: Int): Int Index into flat of the first bias of layer.
fun evaluate(x: FloatArray, into: FloatArray = FloatArray(outputs)): FloatArray The network's output for x, in Float, accumulated in exactly the order glsl accumulates it — bias first, then one multiply-add per input in index order.
val parameterCount: Int How many weights and biases this network is, which is flat.size.
override fun toString(): String

MlpActivation

enum class MlpActivation(val smileName: String)

The hidden activations this file can evaluate on both sides of the CPU/GPU line.

Constants: LEAKY, RELU, TANH, SIGMOID, LINEAR.

Declaration Summary
val smileName: String The name smile.base.mlp.Layer.builder knows this activation by.
fun apply(v: Float): Float This activation applied to one value, in Float.

FlowMatchExport

class FlowMatchExport(val network: MlpWeights, val dimension: Int, val timeFeatures: Int, val center: FloatArray, val scale: Float, val source: FloatArray? = null)

A trained velocity field packaged for a shader: the network, plus the three things around it that decide what its numbers mean.

Declaration Summary
val network: MlpWeights The trained network. Its inputs are dimension positions followed by the time encoding.
val dimension: Int 2 or 3 — how many position components the network takes.
val timeFeatures: Int How many sin/cos octaves of t follow raw t in the input vector.
val center: FloatArray The centre of the frame training happened in, in input coordinates.
val scale: Float The isotropic scale of that frame. One number, not one per axis.
val source: FloatArray? The cloud the field was trained to start from, interleaved by dimension and already in the normalised frame — or null if it was trained from a standard Gaussian.
val sourceCount: Int How many points are in source, or 0 if the field starts from a Gaussian.
override fun toString(): String

MlpAgreement

data class MlpAgreement(val worstRelative: Double, val worstAbsolute: Double, val worstAt: FloatArray, val comparisons: Int)

The largest relative disagreement between two evaluations of the same network, and the input it happened at — the statistic both halves of the parity test report.

Declaration Summary
val worstRelative: Double The largest relative disagreement seen.
val worstAbsolute: Double The largest absolute disagreement seen, in output units.
val worstAt: FloatArray The input at which worstRelative occurred.
val comparisons: Int How many outputs were compared in total.
override fun toString(): String

neuralparticles_system.kt

A particle system driven by a trained velocity field: ShaderNeuralParticles integrates a buffer of particles along the field in a compute pass and draws the same allocation as points, with no copy between the two.

neuralparticles.kt settles what a network is on both sides of the CPU/GPU line and emits the GLSL that evaluates one; this file is what consumes that GLSL. The chain from a target shape to a moving cloud is flowMatch, then FlowMatchField.export(), then ShaderNeuralParticles and installInto. afield.examples.four.NeuralParticlesSmoke is the GPU smoke over that whole path, and the neural_particles agent transcript is that chain as three boxes.

Two of the design decisions reach the caller, so they are stated here rather than only on the declarations that implement them:

  • Positions are stored in the training frame, not in the coordinates the field was authored in, because integrating in input coordinates would convert into the frame and back out again on every step to reach the same answer. So NeuralParticleSettings's viewScale and viewCenter are in normalised units, where a scale of about 2.6 frames a cloud of roughly unit variance.
  • A particle is travelling a journey, not wandering a force field. A flow is a map: each particle goes from one specific source point to wherever the field takes it by t = 1, then starts again. That is why it carries a start and a t, and why reset has to seed from the field's own source cloud whenever the field has one.

Which settings are live follows from where each one is read. particleCount sizes the particle buffer, the compute dispatch and the point mesh at construction, so it cannot be raised afterwards, and name is written into the module and buffer names there. phaseSpread, sourceJitter and randomSeed are read by reset, which fills the buffer, so changing one takes a reset call. Everything else is read through a push-constant lambda every frame and takes effect on the next one, including stepsPerCycle and dwellCycles.

src/jvmMain/kotlin/afield/simulation/particles/neuralparticles_system.kt — read the source

Used in: Neural particle systems

NeuralParticleSettings

data class NeuralParticleSettings(var name: String = "neuralParticles", var particleCount: Int = 200_000, var stepsPerCycle: Int = 150, var dwellCycles: Float = 3f, var phaseSpread: Float = 1f, var pointSize: Float = 1.5f, var sourceColor: Vec4 = vec(0.15, 0.45, 1.0, 1.0), var targetColor: Vec4 = vec(1.0, 0.62, 0.22, 1.0), var intensity: Float = 0.5f, var arrivalBias: Float = 0.8f, var viewScale: Float = 2.6f, var viewCenter: Vector2f = Vector2f(0f, 0f), var flipY: Boolean = true, var sourceJitter: Float = 0.02f, var randomSeed: Long = 1L)

The knobs ShaderNeuralParticles takes.

Declaration Summary
var name: String Names the modules and buffers, so two systems in one renderer do not collide.
var particleCount: Int How many particles. A few hundred thousand is comfortable; see the class KDoc for cost.
var stepsPerCycle: Int Frames a particle takes to travel from t = 0 to t = 1.
var dwellCycles: Float How long a particle rests on the target once it arrives, as a multiple of the travel time.
var phaseSpread: Float How far apart in the cycle the particles start, as a fraction of a whole cycle.
var pointSize: Float Point size in pixels.
var sourceColor: Vec4 Colour a particle leaves the source with.
var targetColor: Vec4 Colour it arrives at the target with.
var intensity: Float Overall brightness. With additive blending this is the exposure control.
var arrivalBias: Float How strongly a particle's brightness is weighted toward the end of its journey, 0..1.
var viewScale: Float Half-height of the visible region, in the field's normalised units.
var viewCenter: Vector2f Centre of the visible region, in the field's normalised units.
var flipY: Boolean Whether to negate y on the way to clip space. On by default.
var sourceJitter: Float Gaussian spread, in the field's normalised units, added to each particle's source point.
var randomSeed: Long Seed for the source draw, so a scene reloads identically.

ShaderNeuralParticles

class ShaderNeuralParticles(val export: FlowMatchExport, val renderer: Renderer = fromContext(), configure: NeuralParticleSettings.() -> Unit = {})

A particle system whose velocity field is a trained network rather than a written rule.

Declaration Summary
val export: FlowMatchExport The trained field, from FlowMatchField.export().
val renderer: Renderer The renderer both passes are built against; the box's own by default.
val settings The settings configure produced, kept live.
val particleBuffer: FrameBufferGroup The particle state. Read by both passes, owned by neither.
val weightBuffer: FrameBufferGroup The network's weights, swappable between frames without recompiling anything.
val computeModule: ComputeModulePointer The integrator.
val drawModule: ModulePointer The draw.
val computeSource: String The compute shader source, as installed — editable.
val vertexSource: String The vertex shader source, as installed — editable.
val fragmentSource: String The fragment shader source, as installed — editable.
fun reset(seed: Long = settings.randomSeed) Redraws every particle's source point and restarts the cycle.
fun updateWeights(network: MlpWeights) Replaces the weights in place, with no recompilation — the network may be retrained live.
fun installInto(renderTarget: RenderTarget = renderer.renderList) Adds both passes to renderTarget's renderer: the integrator, then the draw.
fun removeFrom(renderTarget: RenderTarget = renderer.renderList) Takes both passes back out again.