cc33b2a813
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
101 lines
5.5 KiB
Markdown
101 lines
5.5 KiB
Markdown
# 0048 — Plugins invoke a selected direct profile or utility agent
|
|
|
|
- Status: superseded by [explicit plugin utility selection](2026-09-14-explicit-plugin-utility-selection.md)
|
|
- 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: direct profile first.** Plugins declaring `agent_invoke`
|
|
can declare an `agent_profile` field with `type: string` and
|
|
`format: 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 a
|
|
`FailedPrecondition`. A non-empty declared `agent_profile` is the direct
|
|
execution selection and takes precedence over `utility_agent`; when it is
|
|
unset, the legacy selection remains available if the active manifest
|
|
declares that selector, or if a verified manifest transition recorded
|
|
`LegacyUtilityAgentFallback` for a retained value. Arbitrary undeclared
|
|
submitted `utility_agent` values are ignored.
|
|
Existing plugins may continue declaring `utility_agent` with
|
|
`format: utility-agent`; that legacy selector resolves the utility agent's
|
|
effective profile as before.
|
|
|
|
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` → 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. `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 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: 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 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`/`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.
|