Skip to content
Crystal CodeDocs
ENEnglish

Engine contract

CrystalCode.Engine runs the conversation and reports what happens. A front end hands it a SessionFrontEnd, calls it, and observes events. It never reads engine state directly and the engine never draws, reads keys, or writes to the console.

A front end supplies (SessionFrontEnd):

  • Observer (ISessionObserver) receives every SessionEvent.
  • Approvals (IApprovalPrompt) asks the operator to approve a tool call and is told when a call auto-passes or is under review.
  • Questions (IUserPrompt) answers the built-in question tool.
  • Sessions (ISessionChooser) picks a saved session for /resume, including one workspace or every workspace.
  • Trust (IWorkspaceTrustPrompt) asks whether to trust a directory. A front end that cannot ask returns false.

A front end calls CodingSession: Create, StartAsync, SubmitAsync (slash commands, follow-ups while a turn runs, interrupts, and turn starts; returns true when the operator asked to quit), CompleteTurnAsync once TurnTask finishes, FinishTurnAsync to collect a turn before exit, TryInterrupt, TogglePlan, SetVerbose, NotifyDraftChanged, Enqueue, PasteClipboardImageAsync, and Close. TurnActive, TurnTask, PlanMode, and SessionId report state. The usual turn is: SubmitAsync, await TurnTask, then CompleteTurnAsync.

Events are immutable sealed records, one type per file under Events:

  • Lifecycle and chrome: SessionStarted, ChromeChanged, PreferencesChanged, SlashCommandsChanged, PromptHistoryLoaded, ImageHistoryInvalidated, ActivityChanged.
  • Turn: UserMessageSent, TurnStarted, StreamReceived (carries Crystal’s ChatStreamEvent), ToolCallsIssued, ToolResultsReceived, ModelRoundClosed, RetryScheduled, UsageChanged, TurnFinished, QueueChanged.
  • Conversation: HistoryReplayed, ConversationCleared, TodosChanged.
  • Text and reports: NoteWritten, ErrorWritten, HelpRequested, StatusReported, ToolsListed.

The engine publishes from whichever thread produced the change, including the turn thread while a model round streams. Observers must be thread-safe and return quickly; a front end with a UI thread marshals to it. Events carry engine values, not captions: the engine reports SessionActivity and the front end chooses the wording and layout.

The engine owns the usage numbers. It keeps the context usage and the session cumulative totals, including the baseline that in-turn cumulative counts are added to, and reports them through UsageChanged and TurnFinished. A front end renders them and does not compute totals.

Some engine calls take time (a turn collecting, /compact, a model switch). The front end awaits them without freezing its surface. The terminal host does this by pumping the frame while the call runs.

Image markers cross the boundary as text. UserMessageSent and replayed transcripts carry [Image #N] markers, and a marker is trusted only when it starts with ImageMarkerText.Prefix (U+2063). A composer that attaches images prefixes exactly the attached spans. Display cannot reference the engine, so it holds its own copy of the prefix; ImageMarkerContractTests keeps the two equal.

Adding a front end means referencing CrystalCode.Engine, implementing the five contracts above, and projecting events onto its own surface. The terminal host in CrystalCode is the reference implementation. The headless test in CrystalCode.Engine.Tests is the smallest correct driver. A later Avalonia desktop front end uses C# markup and does not add XAML or AXAML files.

crystal run is the headless front end in this executable, not a second assembly. It reads one task from an argument or from stdin, applies process-only overrides, starts one user turn, and exits. The interactive crystal and crystal space commands apply the same session overrides: approval, the approval model and its thinking gear, Plan or Work, thinking, the selected model’s context budget, service tier, speed, prompt set, prompt attachments, skills, external tools, plugins, and turn quotas. Overrides are not written to config.json. That includes the approval-model switch (--approval-model, --approval-provider, --approval-model-id) and gear (--approval-thinking). A later interactive preference command writes the field the operator changed, and can also write a --provider or --model passed at launch. It does not write the other launch overrides. The task text, --format, --show-thinking, --space, and --workspace-trust stay on crystal run. Slash commands are rejected on crystal run so they cannot change saved settings. Secrets are not command flags.

The run supplies RunLog or RunJsonLog, UnattendedApprovalPrompt, UnattendedUserPrompt, UnattendedSessionChooser, and UnattendedTrustPrompt. The trust prompt is a fail-closed fallback. The directory check itself runs before the session is created and follows --workspace-trust, not workspaceTrust. The approval prompt denies every call that would have asked the operator. The question prompt dismisses. Session choice is declined. Review and Audit still call the reviewing model: allow executes the tool, and deny returns the reviewer’s reason. Plan does not offer write, edit, or bash. The approval policy also rejects those side effects when a catalog still contains them.

--format default writes a readable trace. Each model round prints thinking text only when --show-thinking is set, then the assistant reply, then each tool beside its own result. With --show-thinking, the reply is labeled [Assistant]. read, list, glob, grep, and other successful tools keep a short head excerpt and an omitted-line count. edit and write keep their result. bash keeps the command, the exit status, and a short tail, with a longer tail when the command fails. Other failures keep the result text. Labels use square brackets, exit status uses parentheses, and omitted-line markers use angle brackets. --format json writes one JSON object per line, with type, a UTC timestamp, and sessionID, and keeps the full tool output. Event types are step_start, text, reasoning, tool_use, error, note, retry, step_finish, stopped, and session. Thinking text is omitted unless --show-thinking is set. The process then prints the saved session id. Exit 0 means the turn completed and no operator prompt was denied or dismissed. Exit 1 is an invalid command, configuration, credential, workspace, or prompt set. Exit 2 is a failed model request. Exit 3 is a model-call, tool-call, or duration budget, or context overflow. Exit 4 is a finish that was not a failure, budget stop, or interrupt, after an operator denial or a dismissed question. Exit 5 is an interrupt. A failure, budget stop, or interrupt outranks a denial.