4.9 KiB
0048 — Plugins invoke a plugin-selected utility agent
- Status: accepted
- Date: 2026-07-21
- Area: backend, frontend, protocol
- Related: 0043 — Plugin host data API, 0002 — Host utility agentctl for sessionless ACP flows (the inference tier this reuses), 0018 — Runtime settings overrides
Context
A plugin that wants to do an LLM step (summarize a conversation, classify an issue) has no sanctioned way to run a completion: it would need to ship and manage its own provider API key. Kandev already runs LLM completions on the operator's configured agents; a plugin should be able to borrow one.
Kandev also already has the right primitive. ADR 0002's host-utility tier
(internal/agent/hostutility.Manager.ExecutePrompt) runs a one-shot,
non-interactive, sessionless completion against a warm agentctl instance —
exactly what title generation, commit messages, and the Slack assistant use.
There is no need for a new agent loop, and the interactive agent runtime
(internal/agent/runtime, internal/office) is the wrong tool: it is
streaming, stateful, and requires an executor + workspace + worktree.
Decision
Add a capability-gated Host.InvokeUtilityAgent, backed by each plugin's
configuration and the existing host-utility tier.
-
Plugin config:
utility_agent. Plugins declaringagent_invokedeclare aconfig_schemafield namedutility_agent, withtype: stringandformat: utility-agent. Settings > Plugins renders it as a picker of the existing built-in and custom utility agents. The picker displays the name but stores the selected utility agent's stable ID, scoped to the plugin. The plugin does not select an execution profile directly; after ADR-2026-08-08-utility-agent-profile-execution, the selected utility agent resolves its own effective profile. -
New capability
agent_invoke. A booleanCapabilities.AgentInvoke, enforced exactly likestate/secrets:Host.InvokeUtilityAgentreturns gRPCPermissionDenied(capability 'agent_invoke' not declared) when the manifest doesn't declare it. -
New RPC + SDK method.
InvokeUtilityAgent(InvokeUtilityAgentRequest{prompt}) returns (InvokeUtilityAgentResponse{text})onservice Host; SDKHost.InvokeUtilityAgent(ctx, prompt string) (string, error). The request message is the forward-compatible extension point (a futuresystem_promptormax_tokensis an added proto field, no SDK signature change). -
Reuse the host-utility tier (ADR 0002). The kandev-side handler: gate
agent_invoke→ read the calling plugin'sutility_agentconfig → resolve its ID through the established utility-agent configuration → resolve that utility agent's effective profile → call the narrow profile-aware utility runner and return the text.utilityRunneris a thinpluginsHostUtilityAdapteroverhostutility.Managerwired inbackendapp, sointernal/pluginsnever imports the agent runtime (the same cycle-avoidance as the Slack assistant's adapter and ADR 0043's data sources). No task, session, workspace, or worktree is involved. -
Typed "not configured" failure. If no utility agent is selected — or the selected utility agent has since been deleted or disabled — the handler returns gRPC
FailedPrecondition(no utility agent configured/configured utility agent "<id>" not found), never a silent empty completion. A stale selection is treated as "unconfigured", not an internal error.
Consequences
- A plugin declares
capabilities.agent_invoke: trueand callshost.InvokeUtilityAgent(ctx, prompt)— no API key, no provider wiring. This is what unblocks the "My Daily Standup" plugin's summarization step. - The operator stays in control: nothing runs until each plugin selects a configured utility agent, including the effective profile's cost, availability, launcher, and permission characteristics.
- We reused the sessionless inference path instead of building an agent loop; the only net-new machinery is a plugin config picker and one gated RPC handler.
- The
InvokeUtilityAgentRequest/Responseproto is a public contract, extended additively.
Alternatives considered
- Let plugins bring their own API key. Rejected: every plugin re-implements key management and secret storage, and the operator loses cost control. The whole point is to delegate to kandev's already-configured agent.
- Run the completion through
internal/agent/runtime/office. Rejected: both are streaming, stateful, and require an executor + workspace; neither offers a synchronous prompt→text call. ADR 0002's host-utility tier already does exactly this, sessionlessly. - Reuse
default_utility_agent_id. Rejected: that default serves Kandev's internal utility calls and cannot express a plugin-specific selection.