Plugins / Building a plugin

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.

MyPlugin.csprojxml
<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:

csharp
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

PhaseWhenUse it for
ConfigurePre-build, container assemblingRegister services, providers, channels, hooks, settings, CLI. Receives IPluginContext.
StartPost-build, container readyResolve services and add things that need them — e.g. skills that depend on providers. Receives IPluginRuntime.
StopHost shutdownDispose async resources (reverse order).
UninstallOperator removes the pluginDelete plugin-owned data (rows, files).
The two surfaces are deliberate. IPluginContext (Configure) exposes the registries; IPluginRuntime (Start) exposes the built IServiceProvider. Register in Configure, resolve in Start.

3 · The manifest

csharp
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

MyPlugin.cscsharp
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.

Building a provider? It exposes its fields through its own ConfigFieldsproperty and the host auto-collects them — no separate registration. See Providers → Declaring settings. The rest of this section is for non-provider settings.
csharp
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

TypeStoredRenders as
Textparley.yamlText input
SecretEncrypted vault (never returned in bulk)Masked input + set/not-set badge
Boolparley.yamlToggle
Intparley.yamlNumber input
Enumparley.yamlDropdown from Options

Reading values at runtime

Inject ISettings and read by scope + key. Plain values come back synchronously; secret fields resolve from the vault.

csharp
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.

Secret values live in the vault and are never returned in bulk or placed in a model's context. The scope/key you pass 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

bash
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.