Building a plugin
A plugin is a .NET class library that implements one interface. From there it can contribute skills, channels, providers, middleware, settings, CLI commands, and UI — all through typed registries, never by patching the core.
1 · The project
Create a class library targeting the same framework and reference AgentParley.Abstractions — that's the only contract you compile against.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- the public contract; mark it private so you don't ship a copy of the host's assembly -->
<ProjectReference Include="../AgentParley.Abstractions/AgentParley.Abstractions.csproj"
Private="false" ExcludeAssets="runtime" />
</ItemGroup>
</Project>2 · The entry point
Implement IAgentParleyPlugin. It has a manifest and four lifecycle methods:
public interface IAgentParleyPlugin
{
PluginManifest Manifest { get; }
void Configure(IPluginContext context); // pre-build: register
Task Start(IPluginRuntime runtime, CancellationToken ct); // post-build: resolve & wire
Task Stop(CancellationToken ct); // shutdown
Task Uninstall(IPluginRuntime runtime, CancellationToken ct); // operator removed the plugin
}Lifecycle phases
| Phase | When | Use it for |
|---|---|---|
Configure | Pre-build, container assembling | Register services, providers, channels, hooks, settings, CLI. Receives IPluginContext. |
Start | Post-build, container ready | Resolve services and add things that need them — e.g. skills that depend on providers. Receives IPluginRuntime. |
Stop | Host shutdown | Dispose async resources (reverse order). |
Uninstall | Operator removes the plugin | Delete plugin-owned data (rows, files). |
IPluginContext (Configure) exposes the registries; IPluginRuntime (Start) exposes the built IServiceProvider. Register in Configure, resolve in Start.3 · The manifest
public PluginManifest Manifest => new()
{
Id = "mysearch", // stable id; used in load order & config
Version = "1.0.0",
Author = "My Company",
DependsOn = ["base"], // hard topological constraint (base is always present)
ContractVersion = "0.1", // optional: abstractions version you built against
};The host guards compatibility two ways: it checks the AgentParley.Abstractions version your DLL was compiled against, and (if set) your declared ContractVersion — a major mismatch is refused, a minor mismatch warns.
4 · A complete skeleton
using Microsoft.Extensions.DependencyInjection;
using AgentParley.Abstractions.Plugins;
using AgentParley.Abstractions.Providers;
namespace MyCompany.Search;
public sealed class MyPlugin : IAgentParleyPlugin
{
public PluginManifest Manifest => new()
{
Id = "mysearch", Version = "1.0.0", Author = "My Company",
DependsOn = ["base"], ContractVersion = "0.1",
};
public void Configure(IPluginContext ctx)
{
// contribute a selectable web-search backend
ctx.Services.AddSelectableProvider<IWebSearchProvider, MySearchProvider>(
"websearch", "Web search", "mysearch", "My Search",
"Custom search via My API (requires an API key).");
}
public Task Start(IPluginRuntime runtime, CancellationToken ct) => Task.CompletedTask;
public Task Stop(CancellationToken ct) => Task.CompletedTask;
public Task Uninstall(IPluginRuntime runtime, CancellationToken ct) => Task.CompletedTask;
}5 · What you can contribute
Everything hangs off the registries on IPluginContext. The guides:
- Providers — swap or add a files/secrets/memory/search/fetch/compaction backend (
ctx.Services). - Skills — new capabilities the model can call (
ctx.Skills/runtime.Skills). - Middleware & hooks — intercept the lifecycle and model calls (
ctx.Hooks,ctx.Models). - Channels — connect agents to new platforms (
ctx.Channels).
Plugins can also declare operator settings(ctx.Settings), persist private state (ctx.Store / runtime.Store), add CLI commands (ctx.Cli), contribute console UI (ctx.Ui), and register model providers and shell targets.
6 · Settings in the console
Plugins declare the operator config they need (API keys, endpoints, toggles) by registering a ConfigSchema in Configure via ctx.Settings. The console renders a form for it under Settings → {Category}, and you read the values back at runtime.
ConfigFieldsproperty and the host auto-collects them — no separate registration. See Providers → Declaring settings. The rest of this section is for non-provider settings.using AgentParley.Abstractions.Config;
public void Configure(IPluginContext ctx)
{
ctx.Settings.Register(new ConfigSchema(
Scope: "myplugin", // namespace for these values
Title: "My Plugin", // form heading
Category: "Integrations", // groups scopes in the console
Fields:
[
new("apiKey", "API key", "From my.service.com", ConfigFieldType.Secret, Required: true),
new("workspace", "Workspace", "Default workspace id", ConfigFieldType.Text),
new("verbose", "Verbose logging", null, ConfigFieldType.Bool, Default: "false"),
new("region", "Region", null, ConfigFieldType.Enum, Options: ["us", "eu"]),
]));
}Field types
| Type | Stored | Renders as |
|---|---|---|
Text | parley.yaml | Text input |
Secret | Encrypted vault (never returned in bulk) | Masked input + set/not-set badge |
Bool | parley.yaml | Toggle |
Int | parley.yaml | Number input |
Enum | parley.yaml | Dropdown from Options |
Reading values at runtime
Inject ISettings and read by scope + key. Plain values come back synchronously; secret fields resolve from the vault.
using AgentParley.Abstractions.Config;
public sealed class MyClient(ISettings settings)
{
public async Task Call(CancellationToken ct = default)
{
var workspace = settings.Get("myplugin", "workspace"); // plain value, or null
var apiKey = await settings.ResolveSecret("myplugin", "apiKey", ct); // from the vault
if (string.IsNullOrEmpty(apiKey))
throw new InvalidOperationException(
"My Plugin: API key not set — add it under Settings → Integrations");
// ... use apiKey / workspace
}
}ISettings also offers SetValue / SetSecret to write, HasSecret for the console's set/not-set badge, and SecretName for the vault key a field maps to.
ResolveSecret must be operator config, never agent input — keep it that way so a prompt-injected agent can't reach a secret.7 · Build & ship
dotnet publish -c Release -o out cp -r out/* ~/parley/plugins/mysearch/ # folder layout keeps deps with the plugin # restart 'parley serve'
See Installing plugins for the layout rules and load order.