Plugin contract
Reference CrystalCode.Plugins. Reference CrystalCode.Tools only when
a tool needs ToolHostContext. Do not reference CrystalCode.Engine,
CrystalCode, CrystalCode.Display, or CrystalCode.Providers.
Contribute returns tools, client factories, classifiers, commands,
hooks, raw hooks, prompt placeholders, and status fields. A null entry is omitted with a note. One bad plugin does not
stop the others.
Attach runs after Contribute, once plugins, external tools, and skills
are loaded, and again when those catalogs reload. It runs before the
session-start hook. The default method does nothing. Reading the snapshot
inside Contribute is too early: the host has not finished the other
catalogs.
A tool implements IPluginTool: a model-facing Name, Catalogs,
an ITool, and an optional IMultimodalTool with the same name.
Catalogs is PluginToolCatalogs.Plan, Work, or PlanAndWork.
A tool must name at least one catalog. The same choice applies to its
multimodal tool.
Names match ^[A-Za-z][A-Za-z0-9_-]*$ (1–64 characters) and must not be
a built-in name. The definition name must match Name. Later duplicates
are omitted with a note.
Catalog order is built-in plugin tools, disk plugin tools, external
tools, then skill when enabled. A disk tool name is reserved against
external tools.
The plugin constructs the tool. Host facts arrive per call when the tool
implements IHostTool or IHostMultimodalTool. The tool does not
receive the question prompt.
Protocol factories
Section titled “Protocol factories”IPluginClientFactory.CanCreate returns true only for protocols that
factory owns. A factory that returns true for deepseek, openai,
responses, anthropic, gemini, or ollama is omitted. Built-in
adapters stay first. A custom protocol is a token of 1–64 characters:
a letter, then letters, digits, _, or -. Put that token in
providers.json as protocol.
PluginClientRequest carries the endpoint, model sampling, and API key
for that call. Do not log the key. It also carries declared audioInput
and videoInput, plus ServiceTier and Speed when those slots are set.
A factory that cannot write a non-null slot must throw
NotSupportedException from Create or CreateMultimodal. The host
surfaces that when it builds the client, including the first connection
and a later /tier or /speed. Those flags do not make Create or
CreateMultimodal accept audio or video. CreateMultimodal is used only when
the selected model accepts image input and no built-in factory owns the
protocol.
Classifiers
Section titled “Classifiers”IPluginClassifier runs only after the built-in switch misses. It
assigns risk and authority for an unknown tool. It does not reclassify
read, bash, or the other built-in names.
Slash commands
Section titled “Slash commands”IPluginCommand adds a /name verb. Built-in verbs and their aliases
win. Names use the same pattern as tool names. IPluginOutput.Write
and Fail become session notes.
Placeholders
Section titled “Placeholders”IPluginPlaceholder adds a {{name}} token. The name is 1–64 characters:
a letter, then letters, digits, or _. Matching is case-insensitive. The
host rejects a name it already owns. Two plugins cannot share a name; the
first in load order wins and the later one is omitted with a note.
The host calls Resolve each time it binds a template, including
attachments, Review, compaction, and image description. The context carries the mode,
workspace, session id, approval mode, provider, model, the current
catalog snapshot, and the live session and review model. Strings the host
does not have yet are empty. The
returned text is inserted as-is. Tokens inside that text are not expanded
again. A throw, or a missing return, leaves the original token and records
an English note.
Before the first Attach, the snapshot on that context is empty.
Status fields
Section titled “Status fields”IPluginStatusField names one custom status-line field. Add it with
PluginContribution.WithStatusFields. The original constructor stays
unchanged. The name starts with a letter, then letters, digits, or single
hyphens, at most 32 characters. Admission is case-insensitive and the host
stores the lowercase form. A built-in status field, on, off, reset,
an illegal token, or a duplicate is omitted with a note. The first loaded
plugin wins.
The host does not place the field on the adaptive status bar, the progress
row, /status, or /stats. The operator adds the lowercase name to
statusLine or /statusline. A saved name is kept when the plugin is not
loaded; the custom line omits it and /statusline marks it unavailable.
After data directories, the host calls AttachStatus with
IPluginStatusUpdates when at least one of this plugin’s fields was
accepted. That call happens once for the loaded instance. Reloading tools
or changing tool approval does not call it again. A later plugin reload
retires the previous sink first.
Set replaces that field’s text. Clear, or blank text, removes it. The
plugin pushes. The host does not poll and does not run a status timer. A
plugin may call Set from any thread it already has, including a timer it
owns. The host keeps one line: control characters and escapes are dropped,
newlines and tabs become spaces, and text longer than 64 characters is cut.
The description shown by /statusline is at most 80 characters. The same
text is not published again. A name this plugin did not register is ignored,
and that name is noted once. A sink from an older catalog ignores later
calls. If AttachStatus throws, that sink is closed and any text it already
published is removed. The plugin does not choose color, markup, or an extra row.
Catalog snapshot
Section titled “Catalog snapshot”IPluginEnvironment is read-only. It does not enable, disable, invoke, or
reorder anything, and it does not declare a load dependency.
| List | Contains |
|---|---|
Plugins |
Every discovered plugin directory, including this plugin. Directory, source (home or project), display name, enabled, effective, loaded, and skip reason. No assembly path, type name, or manifest path. |
ToolSets |
Every discovered external tool set, with the same directory fields. |
ExternalTools |
Tools that loaded: name, set, source, and whether the tool is in Plan, Work, or both. |
Skills |
Skills the skill tool can load right now: name and description. One entry per name. Empty when Skills is off. No file body and no path. |
enabled is null when the manifest could not say. Display name is empty
until that plugin loads. Built-in tools are not listed.
Model facts
Section titled “Model facts”AttachSession runs with Attach, after Contribute, and again when
that plugin instance is loaded again. The object stays live. /model,
/approval model, /approval thinking, /thinking, /tier, and
/speed show up on the next read. The host does
not call AttachSession again for those changes.
Session carries the provider, protocol, model name, context window,
maximum tokens, temperature, top-p, image input, declared audio input,
declared video input, the current thinking gear, and the selected
service tier and speed. Audio and video
flags copy the model entry. They do not make a turn, a tool result, or
CreateMultimodal accept audio or video.
default and off are host sentinels for thinking. Other thinking values are effort
tokens the model lists. Tokens are stored in lowercase and forwarded
unchanged. max and maximum are different tokens.
ServiceTier and Speed are null when that slot is unset. A listed
default is a service-tier token, not the thinking sentinel. The plugin
sees the token, not the JSON field name.
Review.Independent is true only when review uses its own model. That
model’s thinking gear is its stored thinkingEffort, default when
unset. Its service tier and speed are the tokens saved for that model.
While the switch is off, review uses
the session model, Model is null, and a saved provider or model name is
omitted. The facts include no API key, endpoint, organization, or project.
PluginPlaceholderContext.Models is the same live view. It is empty until
the session publishes it.
Model clients
Section titled “Model clients”A model call spends the operator’s credentials. It does not enter the
transcript, the approval path, or the turn budget. The entry type receives
clients only when it implements IPluginModelClient. The host names that
plugin when it loads, for example Plugin 'Acme' can call the session and review models. Built-in plugins do not receive these clients.
AttachClients runs after AttachSession. The clients are new instances
on the same provider and model. They are kept apart from the instances
inside a turn or a review.
| Member | Meaning |
|---|---|
Session |
Text client for the session model. |
Images |
Image client for the session model, or null when that model does not accept images. |
IndependentReview |
True only when review uses its own model. Reading it does not create a client. |
Review |
Text client for the review model while IndependentReview is true. Null while review uses the session model. |
When IndependentReview is true and the review client cannot be created,
reading Review fails. The host leaves the session client in place.
The first read creates a client. Later reads return that instance until the host drops it. The host drops the session and image clients when it rebuilds the session client, and drops the review client when it rebuilds the review client or the switch turns off. Read the property again after that.
IPluginClientFactory remains the other direction. It builds a client for
a protocol the built-in adapters do not own.
Data directories
Section titled “Data directories”A plugin that keeps its own runtime state implements
CrystalCode.Plugins.Data.IPluginDataDirectory. The host calls
AttachDataDirectories(globalDataDirectory, projectDataDirectory) after
Attach, and again when the plugin instance is loaded again. The interface
is opt-in, like IPluginModelClient.
The global directory is {home}/data/plugins/<directory> and is shared
across workspaces. The project directory is
<workspace>/.crystal/data/plugins/<directory> and belongs to the current
workspace. The host creates both before that call; a directory that
cannot be created is not reported, so create it when it is missing. They are
separate from the plugins/ install directory and are not a secret store:
do not write credentials there, and the project directory is inside the
workspace.
A plugin tool that implements IHostTool or IHostMultimodalTool also
receives the same two directories on ToolHostContext.