Crystal consumption
The interactive turn uses IStreamingChatClient and ToolExecutor. When the
selected model declares imageInput and its adapter supports it, the session
uses IStreamingMultimodalChatClient and a host executor that preserves the
same approval policy. The session owns those clients and may replace them on
/model. It does
not use Crystal.Agents.Agent for the live UI: that Agent completes model
turns without token streaming.
The live session does not yet use Crystal.Harness.AgentHarness. Parent/child
Agent composition is deferred product work; when that topology is implemented,
the host consumes AgentHarness for named invocation ancestry and shared
budgets while retaining ownership of prompts, tools, approval, persistence, and
the terminal projection.
Approval is a ToolInvocationPolicy supplied to ToolExecutor. Rejection
returns Harness-authored ToolOutput. Tool exceptions become model-visible
only through a Harness ToolExceptionMapper.
Image input boundary
Section titled “Image input boundary”CrystalCode.Engine owns image attachment, MIME signature validation,
session persistence, transcript markers, and projection onto Crystal’s typed
multimodal contracts. A front end owns only how it presents and edits the
markers in its composer. [Image #N] is presentation and persistence metadata;
providers receive typed ImageContent, never a marker in place of its bytes.
Pasted markers are atomic composer spans with a distinct color: cursor movement
and deletion cannot leave a partial attached marker. User-entered text with the
same spelling remains plain text. On submission, only attached spans receive an
invisible provenance prefix; only prefixed markers in transcript items produce
model images. The prefix is removed for display and text-only model requests.
Session JSON records whether markers have this prefix. Older sessions without
that flag tag references to their saved images on load, preserving their prior
semantics. Deleted unsent images are discarded immediately while idle and at
submission during an active turn. Workspace and clipboard images
are checked against the 20 MiB limit and validated by MIME signature before
they enter the session; the media store validates again before writing. Saved
image bytes live in owner-only, content-addressed files under
~/.crystal/media/<sha256>. Session JSON stores the hash and MIME type, not
Base64 bytes. The media file is written before the session references it, and
both writes use a temporary file followed by an atomic replacement. Sessions
persist only images referenced by the archive or the model context. A missing, truncated, or
modified media file is skipped on resume while the text and other attachments
remain available; the operator receives a note, and its reference is retained
on later saves so restoring the file can repair the session. Inline data is
a supported session representation; local saves move its image bytes to the
media store.
JSON session export retains marker provenance and inlines available image bytes
in data so its contents do
not depend on Home media files. If a referenced media file is unavailable, its
metadata remains in the export but bytes cannot be included. The store does
not automatically delete unreferenced media files.
Inline image bytes and absolute image URIs are supported. Images returned by
an in-process plugin, a dotnet operator tool’s native IMultimodalTool path,
or an exec tool with "output": "content" are assigned the same markers and
become input on the following model round. A text-only turn rejects exec
image results instead of dropping them.
Clipboard paste reserves its image number before waiting for the OS reader.
Tool images reserve numbers through the same session lock, which also protects
image lookup and mutation. A failed paste leaves a gap rather than reusing its
number. Input batches dispatch keys in order, so Ctrl+V finishes attaching
before a following Enter submits the prompt.
Dotnet multimodal tools are omitted from active catalogs when the selected
model or provider does not support image input. Declared audioInput or
videoInput does not keep them.
describe_image is a text tool on Plan and Work when config.json
imageModel names a provider and model whose catalog entry has
imageInput. It sends that file to a side multimodal client with the
image-description prompt and returns the reply as text. The side
request has no tools, is not written into the session transcript, and uses
imageModel.thinkingEffort through the same gear mapping as the host.
view_image is registered only for an image-capable turn. It returns the
file as ImageContent so the current model sees it on the next round.
Both follow the read path fence. A missing image-model credential fails
the tool call and does not block session start.
The Responses, OpenAI-compatible Chat Completions, DeepSeek Chat Completions,
Anthropic Messages, Gemini GenerateContent, and Ollama Chat adapters accept
text and image input and emit only text, reasoning, and tool-call events.
The native Gemini adapter sends tool-result images as inlineData parts on
functionResponse. That is the Gemini 3 multimodal function-response field;
an older model may reject the request. The native Ollama adapter sends them
on the tool message images array. Transcripts keep markers either way.
Chat Completions and Anthropic images
are restricted to user and tool messages, matching their wire contracts. Clipboard
image input is read through Windows PowerShell on Windows, the macOS system
osascript command on macOS, and wl-paste or xclip on Linux; it does not
capture the screen. Windows reads clipboard PNG bytes before falling back to
bitmap conversion, and passes Base64 text from PowerShell to the host. macOS
tries PNG, JPEG, and GIF clipboard representations in that order. Linux checks
advertised clipboard types and tries PNG, JPEG, GIF, and WebP in that order.
Reader output and temporary files are bounded by the image size limit. A
missing reader, an empty image clipboard, unsupported image bytes, an oversized
image, and a reader failure have separate operator errors. Reader commands are
optional at runtime, and their absence
does not prevent the application from starting. A model must
opt in with imageInput: true; unsupported combinations are rejected before
sending a request. audioInput and videoInput are stored declarations.
They are published on plugin model facts and on PluginClientRequest, and
they do not change adapter capabilities, tool registration, attachment,
persistence, or the outbound request. Selected service tiers and speeds are
also published on both contracts; a custom protocol factory must apply a
non-null slot or reject it when the client is created. Built-in adapters
still accept only text and image input. Accepting audio or video, and
non-text model output, remain TODO. MCP
media transport is TODO.
CrystalCode does not capture screenshots, control browsers, phones, virtual machines, interpret coordinates, or define frame/device protocols. External plugins own those behaviors and may return ordinary typed images through the generic tool boundary.