Skip to Content
ExtendingChannels

Channels

Channels connect Pandora to external messaging platforms. They receive messages from the platform, pass them through Pandora’s AI pipeline, and send responses back. Channels can use realtime mode (persistent connections like WebSockets), webhook mode (HTTP callbacks), or both.

Channel Definition

A channel plugin exports a factory function that receives env vars and config, and returns a Channel object:

import type { ChannelFactory } from '@pandorakit/sdk/channels' import { createDiscordAdapter } from './adapter' export const factory: ChannelFactory = (env, config) => { const token = env.DISCORD_BOT_TOKEN if (!token) return null return createDiscordAdapter(token, config.guildId as string, config.channelId as string) }

Return null if required env vars are missing.

The Channel object has an id, name, and either realtime, webhook, or both:

import type { Channel, ChannelRealtime } from '@pandorakit/sdk/channels' export function createDiscordAdapter(token: string, guildId: string, channelId: string): Channel { let client: Client | null = null const realtime: ChannelRealtime = { async start(runtime) { client = new Client({ intents: [/* ... */] }) client.on('messageCreate', async (message) => { if (message.author.bot) return const threadId = await runtime.resolveThread('discord', message.channelId) try { const result = await runtime.generate({ threadId, channelId: 'discord', externalId: message.channelId, parts: [{ type: 'text', text: message.content }], }) if (result.text) await message.reply(result.text) } catch (err) { await message.reply(err instanceof Error ? err.message : 'Something went wrong.') } }) await client.login(token) }, async stop() { if (client) { await client.destroy() client = null } }, } return { id: 'discord', name: 'Discord', realtime } }

Manifest Fields

Channel plugins use "sandbox": "host" because they need native dependencies:

{ "provides": { "channels": { "entry": "./src/index.ts", "sandbox": "host" } } }
FieldDescription
entryRelative path to the entry module
sandboxTypically "host" for channels
permissionsCapabilities granted in compartment mode (rare for channels)

Gateway API

The ChannelGateway object passed to your adapter provides:

MethodDescription
generate(opts)Non-streaming: send message, get full response
stream(opts)Streaming: send message, get live text stream
approveToolCall(opts)Approve a pending tool call and resume
declineToolCall(opts)Decline a pending tool call and resume
resolveThread(channelId, externalId)Map a platform conversation to a Pandora thread
newThread(channelId, externalId)Start a fresh thread (synchronous)
envEnvironment variables
loggerStructured logger (log, warn, error)

Use generate() when the platform expects a single reply. Use stream() when you can edit messages in real-time.

File Attachments

Include files as file parts alongside text. The data field accepts Uint8Array, ArrayBuffer, or a data: URL string:

const parts = [ { type: 'text', text: message.content }, ] for (const attachment of message.attachments) { const response = await fetch(attachment.url) const buffer = new Uint8Array(await response.arrayBuffer()) parts.push({ type: 'file', data: buffer, mimeType: attachment.contentType, filename: attachment.name }) } const result = await runtime.generate({ threadId, parts })

Tool Approval

When a tool has Require Approval enabled, generate() returns with pendingToolApproval set. Present the user with a way to approve or deny, then call approveToolCall or declineToolCall to resume:

async function handleResult(ctx: PlatformContext, runtime: ChannelGateway, result: GenerateResult) { if (result.pendingToolApproval) { const { toolCallId, toolName, args } = result.pendingToolApproval const approved = await askUser(ctx, toolName, args) const resumed = approved ? await runtime.approveToolCall({ runId: result.runId!, toolCallId }) : await runtime.declineToolCall({ runId: result.runId!, toolCallId }) return handleResult(ctx, runtime, resumed) } if (result.text) await ctx.reply(result.text) }

Error Handling

Never let gateway errors go unhandled. If an error isn’t caught, the user’s message silently disappears. Always wrap gateway calls in try/catch and reply with the error.

try { const result = await runtime.generate({ threadId, parts }) // ... } catch (err) { await ctx.reply(err instanceof Error ? err.message : 'Something went wrong.') }

Webhook Mode

If your platform pushes events via HTTP, implement ChannelWebhook instead of or in addition to ChannelRealtime:

import type { ChannelWebhook } from '@pandorakit/sdk/channels' const webhook: ChannelWebhook = { async verify(request, env) { const signature = request.headers.get('x-signature') return signature === env.MY_WEBHOOK_SECRET }, async handle(request, runtime) { const body = await request.json() const threadId = await runtime.resolveThread('my-channel', body.channelId) const result = await runtime.generate({ threadId, parts: [{ type: 'text', text: body.text }], }) return new Response(JSON.stringify({ text: result.text }), { headers: { 'Content-Type': 'application/json' }, }) }, }

The webhook path is available from the GET /api/plugins endpoint under provides.channels.webhookPath. The verify() method runs before handle() — return false to reject with 401.

Reference

All types are exported from @pandorakit/sdk/channels:

import type { Channel, ChannelFactory, ChannelGateway, ChannelRealtime, ChannelWebhook, GenerateResult, StreamResult, MessagePart, PendingToolApproval, } from '@pandorakit/sdk/channels'

ChannelFactory

type ChannelFactory = (env: Record<string, string | undefined>, config: PluginConfig) => Channel | null

Channel

FieldTypeRequiredDescription
idstringYesChannel identifier
namestringYesDisplay name
webhookChannelWebhookNoWebhook mode handler
realtimeChannelRealtimeNoRealtime mode handler
notify(message: { subject: string; body: string }) => Promise<void>NoSend a one-way notification

ChannelWebhook

interface ChannelWebhook { verify(request: Request, env: Record<string, string | undefined>): Promise<boolean> handle(request: Request, runtime: ChannelGateway): Promise<Response> }

ChannelRealtime

interface ChannelRealtime { start(runtime: ChannelGateway): Promise<void> stop(): Promise<void> }

GenerateResult

FieldTypeDescription
textstringGenerated text response
sourcesSource[]Referenced sources
toolCallsToolCall[]Tool calls made during generation
toolResultsToolResult[]Results from tool executions
filesFileData[]Generated files
usageUsageToken usage statistics
runIdstring?Pass to approveToolCall/declineToolCall when approval is pending
pendingToolApprovalPendingToolApproval?Present when a tool requires user approval

StreamResult

FieldTypeDescription
textStreamReadableStream<string>Live text stream
textPromise<string>Full text when complete
sourcesPromise<Source[]>Sources when complete
toolCallsPromise<ToolCall[]>Tool calls when complete
toolResultsPromise<ToolResult[]>Tool results when complete
filesPromise<FileData[]>Files when complete
usagePromise<Usage>Usage when complete

For ConfigFieldDescriptor and EnvVarDescriptor types, see the manifest reference.

Last updated on