Table of Contents

Echo Plugin

Last updated:

The Echo plugin is the repository's smallest complete plugin package. Use it to see how a manifest, an IAgentPlugin entry point, a reflected toolset, and provider contributions fit together.

Caution

Plugins run as trusted code inside the Oscova process. The Echo plugin is a test fixture, not a security boundary or a production integration.

Find the sample

The sample lives in tests/Oscova.Agents.Plugins.Echo/:

File Purpose
Oscova.Agents.Plugins.Echo.csproj Targets .NET 10, references Oscova.Agents, and declares an OscovaPlugin NuGet package.
oscova-plugin.json Identifies the plugin assembly and entry type.
EchoAgentPlugin.cs Contains the entry point, toolset, model-provider double, and speech-synthesis-provider double.
README.md Describes the package on NuGet.

The project keeps the host's framework assembly out of the package and places the manifest at the package root:

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <EnableDynamicLoading>true</EnableDynamicLoading>
  <PackageId>Oscova.Agents.Plugins.Echo</PackageId>
  <Version>0.2.0</Version>
  <PackageType>OscovaPlugin</PackageType>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Humanizer.Core" Version="2.14.1" />
  <ProjectReference Include="..\..\src\Oscova.Agents\Oscova.Agents.csproj"
                    Private="false"
                    PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
  <None Include="oscova-plugin.json" Pack="true" PackagePath="\">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
  </None>
</ItemGroup>

That project shape produces oscova-plugin.json at the package root, Oscova.Agents.Plugins.Echo.dll under lib/net10.0/, and an ordinary dependency on Humanizer.Core. Adjust the project-reference path when adapting the sample outside this repository.

Read the manifest

The checked-in manifest is:

{
  "id": "oscova.agents.plugins.echo",
  "name": "Oscova Echo Plugin",
  "version": "0.2.0",
  "assembly": "Oscova.Agents.Plugins.Echo.dll",
  "entryType": "Oscova.Agents.Plugins.Echo.EchoAgentPlugin"
}

The assembly path is relative to the manifest directory. The entryType value is the namespace-qualified name of the public class that implements IAgentPlugin.

Register the contributions

The entry point uses the public AgentPluginBuilder methods and chains their returned builder:

using Oscova.Agents.Plugins;

namespace Oscova.Agents.Plugins.Echo;

public sealed class EchoAgentPlugin : IAgentPlugin
{
    public void Configure(AgentPluginBuilder builder)
    {
        builder.AddToolset(_ => new EchoToolset())
            .AddModelProvider(_ => new EchoModelProvider())
            .AddSpeechSynthesisProvider(_ => new EchoSpeechSynthesisProvider());
    }
}

Configure records registrations. The toolset factory runs later with an AgentPluginContext for a user scope. Provider factories run during startup with one shared context for this plugin. The Echo factories ignore their contexts. EchoToolset.Pluralize separately proves that a plugin-private package dependency loads.

The sample model provider and speech synthesis provider are catalog test doubles:

  • EchoModelProvider reports type echo, endpoint https://echo.invalid/, model echo-1, and no embedding support. Creating a client throws NotSupportedException.
  • EchoSpeechSynthesisProvider reports type echo-tts, reports one voice named echo, and returns an empty 24 kHz, mono, 16-bit payload.

Do not use either provider as an implementation template for a real external service. They exist to prove registration and catalog integration without a network call.

Reflect the toolset

The toolset implements ICrewToolset. DisplayName supplies the tool's base name, while Description supplies the model-visible description:

using System.ComponentModel;
using Humanizer;
using Oscova.Agents;
using Oscova.Agents.Credentials;

namespace Oscova.Agents.Plugins.Echo;

[CredentialSlot(
    Key = "test-echo",
    Label = "Test Echo",
    Description = "Credential used by the plugin loader test assembly.")]
public sealed class EchoToolset : ICrewToolset
{
    [DisplayName("test_echo")]
    [Description("Returns the supplied text unchanged.")]
    public static string Echo(string text)
    {
        return text;
    }

    [DisplayName("test_pluralize")]
    [Description("Returns the plural form of an English word.")]
    public static string Pluralize(string text)
    {
        return text.Pluralize();
    }
}

Neither method resolves or uses the declared test-echo credential. The attribute is fixture metadata only. test_pluralize calls Humanizer.Core, which is restored into the installed plugin's package-scoped layout.

Oscova builds the tool's qualified wire name from three values:

Source Normalized segment
Plugin ID oscova.agents.plugins.echo oscova_agents_plugins_echo
Toolset class EchoToolset echo
Display name test_echo test_echo

The resulting name is:

oscova_agents_plugins_echo__echo__test_echo

Use ToolNaming.BuildToolsetQualifiedName("oscova.agents.plugins.echo", "echo", "test_echo") in code instead of assembling this string yourself.

The pluralization tool follows the same rule and ends in __test_pluralize.

Know what the focused tests prove

AgentPluginLoaderTests.LoadDirectory_LoadsTestPluginAndReflectsToolset copies the compiled fixture into a temporary plugin root and proves that:

  • the manifest values above survive loading.
  • EchoToolset is captured as an ICrewToolset contribution.
  • EchoModelProvider and EchoSpeechSynthesisProvider are captured, while no speech recognition provider is registered.
  • the toolset factory creates an ICrewToolset.
  • reflection publishes test_echo under the qualified name above.

NuGetPluginInstallServiceTests.InstallAsync_DependencyGraph_MaterializesCurrentRidAndLoadsOffline builds a local folder feed and proves that installation restores managed, native, and resource assets for the current RID. It deletes the feed before loading the plugin and invokes Pluralize("person"), which returns people.

Separate host integration tests prove later boundaries:

  • Startup_WithEnabledProviderPlugin_MergesProviderContributions proves that an enabled Echo plugin contributes the echo model defaults, the echo credential slot from EchoModelProvider, and the echo-tts speech provider.
  • PluginTool_EchoToolset_VisibleAndCalledInRun is a skip-gated live-model test that proves the activated tool reaches an agent run and echoes the supplied token.

The loader implements diagnostics for a missing entry type, a type that does not implement IAgentPlugin, and an entry type without a public parameterless constructor. The current focused loader suite does not directly exercise those three branches.

Troubleshooting

The manifest loads but the plugin does not

Check that the built DLL is beside the manifest, the version in the project and manifest agree, and entryType still names Oscova.Agents.Plugins.Echo.EchoAgentPlugin.

The tool cannot be found

Use the complete qualified name, not test_echo by itself. Confirm the plugin was enabled before startup and the agent allows oscova_agents_plugins_echo__echo__test_echo.

The Echo model provider throws

That is expected. It is a catalog double and deliberately cannot create a chat or embedding client.

A package install rejects an adapted sample

Keep oscova-plugin.json at the package root, the declared assembly under lib/net10.0/, and the package type set to OscovaPlugin. See the manifest article for the complete installer contract.