93 lines
4.9 KiB
Markdown
93 lines
4.9 KiB
Markdown
# 0048 — Plugins invoke a plugin-selected utility agent
|
|
|
|
- Status: accepted
|
|
- Date: 2026-07-21
|
|
- Area: backend, frontend, protocol
|
|
- Related: [0043 — Plugin host data API](0043-plugin-host-data-api.md),
|
|
[0002 — Host utility agentctl for sessionless ACP flows](0002-host-utility-agentctl-for-sessionless-flows.md)
|
|
(the inference tier this reuses),
|
|
[0018 — Runtime settings overrides](0018-runtime-settings-overrides.md)
|
|
|
|
## 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.
|
|
|
|
1. **Plugin config: `utility_agent`.** Plugins declaring `agent_invoke` declare
|
|
a `config_schema` field named `utility_agent`, with `type: string` and
|
|
`format: 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.
|
|
|
|
2. **New capability `agent_invoke`.** A boolean `Capabilities.AgentInvoke`,
|
|
enforced exactly like `state`/`secrets`: `Host.InvokeUtilityAgent` returns
|
|
gRPC `PermissionDenied` (`capability 'agent_invoke' not declared`) when the
|
|
manifest doesn't declare it.
|
|
|
|
3. **New RPC + SDK method.**
|
|
`InvokeUtilityAgent(InvokeUtilityAgentRequest{prompt}) returns
|
|
(InvokeUtilityAgentResponse{text})` on `service Host`; SDK
|
|
`Host.InvokeUtilityAgent(ctx, prompt string) (string, error)`. The request
|
|
message is the forward-compatible extension point (a future `system_prompt`
|
|
or `max_tokens` is an added proto field, no SDK signature change).
|
|
|
|
4. **Reuse the host-utility tier (ADR 0002).** The kandev-side handler:
|
|
gate `agent_invoke` → read the calling plugin's `utility_agent` config →
|
|
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. `utilityRunner` is a thin
|
|
`pluginsHostUtilityAdapter` over `hostutility.Manager` wired in `backendapp`,
|
|
so `internal/plugins` never 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.
|
|
|
|
5. **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: true` and calls
|
|
`host.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`/`Response` proto 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.
|