Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
5.5 KiB
0048 — Plugins invoke a selected direct profile or utility agent
- Status: superseded by explicit plugin utility selection
- 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: direct profile first. Plugins declaring
agent_invokecan declare anagent_profilefield withtype: stringandformat: agent-profile. Settings > Plugins renders enabled, global, non-CLI profiles whose agent supports sessionless inference and stores the selected stable ID. A missing, deleted, disabled, CLI-passthrough, workspace-scoped, or non-inference profile is aFailedPrecondition. A non-empty declaredagent_profileis the direct execution selection and takes precedence overutility_agent; when it is unset, the legacy selection remains available if the active manifest declares that selector, or if a verified manifest transition recordedLegacyUtilityAgentFallbackfor a retained value. Arbitrary undeclared submittedutility_agentvalues are ignored. Existing plugins may continue declaringutility_agentwithformat: utility-agent; that legacy selector resolves the utility agent's effective profile as before. -
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→ use a non-empty declared direct profile, otherwise resolve the legacy utility agent → validate direct-profile eligibility → 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 configuration failures. An absent selection with no usable legacy selector, or a deleted/ineligible non-empty direct profile, returns gRPC
FailedPrecondition(no agent profile configured/configured agent profile "<id>" not found). Legacy utility-agent failures retain their existing messages. Capability denial is evaluated before either lookup; runner failures remain operational errors rather than configuration failures.
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 direct profile or 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.