-
Notifications
You must be signed in to change notification settings - Fork 0
Types and Errors
The programmatic surface: what a host reads when it wants structured results instead of a
printed Display summary. The result enums a host matches on (Outcome, AgentStatus,
SkipReason, CheckStatus, Error) are #[non_exhaustive], so give them a wildcard arm.
The data types a backend builds or reads by field (Capabilities, AgentResult, the
components IR) are not, so you can construct and exhaustively match those.
What a lifecycle op actually did.
| variant | meaning | Display |
|---|---|---|
NoOp |
already converged, nothing changed | no changes needed |
Installed |
a fresh install | installed |
Updated { from: Option<String>, to: String } |
version bump; from is None when the prior version could not be read |
updated (X -> Y) or updated (to Y)
|
Repaired |
a broken or partial state was reconciled back to healthy | repaired |
Adopted |
self_heal found a healthy install with no marker and wrote one | adopted existing install |
Removed |
uninstalled | removed |
Cleared |
self_heal found a cleanly-uninstalled plugin and cleared the stale marker | cleared stale marker |
Every *_report lifecycle method (install_report, install_into_report, update_report,
uninstall_report, self_heal_report) returns Result<AgentReport>. The plain methods
(install, update, ...) collapse the same report to one Outcome and surface any per-agent
failure as Err(Error::Backend).
pub struct AgentReport {
pub results: Vec<AgentResult>,
}
pub struct AgentResult {
pub agent: &'static str, // the backend id, "claude", "codex", ...
pub status: AgentStatus,
}
pub enum AgentStatus {
Converged(Outcome), // ran; this is its own outcome, not the merged one
Skipped(SkipReason), // never got to write anything
Failed(String), // failed; rendered for the user, fan-out continued past it
}
pub enum SkipReason {
NotDetected, // detect() returned false
ScopeUnsupported, // no config surface at the requested scope
SourceUnsupported, // a GitHub source has no local tree for a config-merge backend
}results holds one entry per agent reconcile/remove was asked to run; an agent excluded by an
explicit install_into filter gets no entry at all, distinct from being skipped. Read the vector
directly to build a per-agent UI, or use the two convenience methods:
-
AgentReport::merged() -> Outcome: the first real change across the agents (a change outranks a no-op),Outcome::NoOpwhen nothing changed. This is what the plain lifecycle methods return. -
AgentReport::is_healthy() -> bool: true when no agent's status isFailed. Skips don't count as unhealthy.
AgentReport and AgentStatus both have a Display impl (one agent: status line per result,
using Outcome's or SkipReason's own wording), a ready setup summary when a host just wants
to print it.
What one AgentBackend can host. A backend reports these as fixed, compile-time-known facts about
the tool it targets, not runtime detection.
| field | type | meaning |
|---|---|---|
plugins |
bool |
native plugin install (copies the whole tree); implies every other surface except instructions
|
mcp |
bool |
can host MCP servers |
hooks |
bool |
can host hook bindings |
commands |
bool |
can host slash-command definitions |
agents |
bool |
can host agent (subagent) definitions |
skills |
bool |
can host skill directories |
instructions |
bool |
can receive host-authored always-loaded guidance through its own native context channel (a dedicated file plus any registration); false for a plugin-native backend, which delivers guidance through its own channel instead |
statusline |
bool |
manages the tool's status-line slot from PluginHost::statusline. Not implied by plugins: the slot lives in the tool's own settings file, outside any plugin tree, so a plugin-native backend still writes it (both do: claude, copilot-cli; the other three true today are qwen-code, antigravity-cli, droid) |
scopes |
&'static [&'static str] |
which of "user"/"project" this backend can target |
setup reads this to report a partial fit ("this agent hosts MCP servers, not hooks") instead of
dropping features silently; it is the runtime truth behind the per-tool coverage table on
Harness comparison.
doctor()'s Display is covered on the Doctor page; this is the shape for a host that
wants to walk the checks itself instead of printing them.
pub struct DoctorReport { /* checks: Vec<DoctorCheck>, private */ }
pub struct DoctorCheck {
pub name: &'static str,
pub status: CheckStatus,
}
pub enum CheckStatus {
Ok(String),
Warn(String),
Fail { problem: String, fix: String },
}checks is private so a report is always built whole; read it back with .checks() -> &[DoctorCheck].
.is_healthy() -> bool is true when no check is Fail (a Warn is tolerated).
Building a report is public too, for an external AgentBackend's own report():
DoctorReport::from_checks(checks: Vec<DoctorCheck>) wraps a check list as-is;
DoctorReport::from_error(err: Error) collapses an upfront failure (say, an unreadable config)
into a single Fail check named "doctor" with fix: "see the error above".
Plugin::components(&source) -> Result<PluginComponents> parses a plugin tree once into a
harness-agnostic shape every non-CC backend renders from, instead of each one walking the tree by
hand. Errors on Source::GitHub: a config-merge backend has no local tree to parse from a github
ref.
pub struct PluginComponents {
pub mcp_servers: Vec<McpServer>,
pub hooks: Vec<HookBinding>,
pub commands: Vec<MarkdownDoc>,
pub agents: Vec<MarkdownDoc>,
pub skills: Vec<SkillDir>,
}| type | fields | notes |
|---|---|---|
McpServer |
name: String, kind: McpKind, command: String, args: Vec<String>, env: BTreeMap<String, String>
|
.is_portable() is false when command/args carry ${CLAUDE_PLUGIN_ROOT}
|
McpKind |
Stdio, Http { url: String }, Sse { url: String }
|
|
HookBinding |
event: String, matcher: Option<String>, command: String
|
.is_portable() mirrors McpServer's, checked against command
|
MarkdownDoc |
name: String (file stem), rel: String (the file's path in the tree), frontmatter: BTreeMap<String, Value>, body: String, raw: Vec<u8> (verbatim file bytes) |
backs both commands and agents
|
SkillDir |
name: String, files: Vec<(String, Vec<u8>)> (each file's path under the skill dir, and its bytes) |
What gets parsed from where, and the frontmatter parser's limits, are on Plugin tree.
Every fallible call in the crate returns Result<T, Error> (type Result<T> = std::result::Result<T, Error>).
| variant | meaning |
|---|---|
ClaudeNotFound |
claude is not on PATH |
ClaudeTooOld { found, floor } |
claude is below the required version floor |
CopilotNotFound |
copilot is not on PATH |
CopilotTooOld { found } |
copilot is below 1.0.71, the plugin-management floor |
EmptyConfigDirOverride { var } |
CLAUDE_CONFIG_DIR or COPILOT_HOME is set to the empty string; both CLIs read that as the current directory, so no write target is correct. Raised before any CLI call, so the install fails with the tool's registry untouched. Only reachable for a host that declares a status line, the one surface whose path agentgear resolves itself |
Cli { bin, args, code, stderr } |
a CLI invocation exited nonzero |
Json { what, source } |
a JSON parse failed |
Io { context, source } |
a filesystem operation failed |
Tree(String) |
the embedded or path plugin tree is malformed (an authoring bug in the host, e.g. missing plugin.json or author) |
Config { path, detail } |
a harness config file exists but does not parse; the read-modify-write refuses to clobber it rather than risk destroying the user's config |
Backend { agent, detail } |
one agent's failure, rendered as a string |
Verify(String) |
a mutating CLI call exited 0 but list --json does not reflect the expected end state |
Lock { path, source } |
failed to acquire the shared flock
|
Backend is the one variant an out-of-crate AgentBackend should return for its own failures,
since every other variant carries in-crate semantics (CLI orchestration, tree parsing, config
merging) that don't fit a tool this crate never shipped a backend for. It does double duty
internally too: the plain lifecycle methods collapse an AgentReport to one Result<Outcome>, so any
agent's Failed(String), built-in or external, becomes Error::Backend there too. The *_report
methods skip that collapse entirely; a per-agent failure stays inside AgentStatus::Failed and the
call still returns Ok.