Developer guide / Channels

Channels

A channel connects agents to a messaging platform. You register a factory (the channel kind); the operator creates one or more instances of it. Telegram ships as a plugin and is the reference implementation.

The interfaces

A channel instance implements IHumanChannel:

csharp
public interface IHumanChannel : IAsyncDisposable
{
    string InstanceId { get; }
    ChannelCapabilities Capabilities { get; }              // Typing / Attachments / Markdown / Voice
    Task Start(CancellationToken ct = default);

    // returns true only if the provider actually accepted it (so callers stop reporting "sent")
    Task<bool> Send(string conversationId, OutboundMessage msg, CancellationToken ct = default);
    Task SetPresence(string conversationId, PresenceState state, CancellationToken ct = default);
    IAsyncEnumerable<InboundHumanMessage> Receive(CancellationToken ct = default);
}

A factory creates instances for a provider kind and declares the operator config it needs:

csharp
public interface IHumanChannelFactory
{
    string ProviderId { get; }                       // "telegram", "slack", ...
    IHumanChannel Create(ChannelInstanceConfig cfg);
    string? Title => null;                            // console display name
    IReadOnlyList<ConfigField> ConfigFields => [];    // fields collected when creating an instance
}

Registering the factory

Channels register pre-build (Configure), but a real channel usually needs services that only exist post-build (e.g. the vault to resolve its bot token). The pattern: register the factory in Configure, bind it to the service provider in Start.

TelegramPlugin.cscsharp
public sealed class TelegramPlugin : IAgentParleyPlugin
{
    private readonly TelegramChannelFactory factory = new();

    public PluginManifest Manifest => new()
    {
        Id = "telegram", Version = "1.0.0", Author = "AgentParley",
        DependsOn = ["base"], ContractVersion = "0.1",
    };

    public void Configure(IPluginContext context) => context.Channels.Register(factory);

    public Task Start(IPluginRuntime runtime, CancellationToken ct)
    {
        factory.Bind(runtime.Services);   // now it can resolve ISecretsProvider when creating instances
        return Task.CompletedTask;
    }

    public Task Stop(CancellationToken ct) => Task.CompletedTask;
    public Task Uninstall(IPluginRuntime runtime, CancellationToken ct) => Task.CompletedTask;
}

Instances & routing

An operator configures instances in parley.yaml (or the console). Credentials are vault references, never plaintext:

~/parley/parley.yamlyaml
channels:
  - instanceId: tg
    providerId: telegram
    secretRefs: { token: telegram-token }   # resolved from the vault

Inbound messages carry an InstanceId, ConversationId, and Sender. The router binds (instance, conversation) to one session, so a person stays in a single continuous thread across turns.

Channel access is default-deny. A sender must be on the agent's authorized-senders allowlist for that instance before any message reaches the agent. Wire that up on the agent, not the channel.

Setting up Telegram

  • Create a bot with @BotFather and copy its token.
  • parley secret set telegram-token (paste it — stored in the vault).
  • Add the channel instance (above), publish the plugin into ~/parley/plugins/, restart.
  • On the agent, list the tg instance and add your numeric Telegram user id to its authorized senders.