Replace the hardcoded Approve/Ignore card with a multi-step flow: - Single agent: "Connect to [name]" / "Connect new agent" / "Reject" - Multiple agents: "Choose existing agent" (follow-up list) / "Connect new agent" / "Reject" - "Connect new agent" prompts for a free-text name via DM, creates immediately on reply - Add setMessageInterceptor router hook for capturing free-text replies - Add resolveChannelName optional method to ChannelAdapter interface Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
6.8 KiB
TypeScript
180 lines
6.8 KiB
TypeScript
/**
|
|
* v2 Channel Adapter interface.
|
|
*
|
|
* Channel adapters bridge NanoClaw with messaging platforms (Discord, Slack, etc.).
|
|
* Two patterns: native adapters (implement directly) or Chat SDK bridge (wrap a Chat SDK adapter).
|
|
*/
|
|
|
|
/** Passed to the adapter at setup time. */
|
|
export interface ChannelSetup {
|
|
/** Called when an inbound message arrives from the platform. */
|
|
onInbound(platformId: string, threadId: string | null, message: InboundMessage): void | Promise<void>;
|
|
|
|
/**
|
|
* Called by admin-transport adapters (CLI) that want to route a message to
|
|
* an arbitrary channel/platform and optionally redirect replies elsewhere.
|
|
* Regular chat adapters should use `onInbound`; `onInboundEvent` skips the
|
|
* adapter-channel-type injection so the caller can target any wired mg.
|
|
*/
|
|
onInboundEvent(event: InboundEvent): void | Promise<void>;
|
|
|
|
/** Called when the adapter discovers metadata about a conversation. */
|
|
onMetadata(platformId: string, name?: string, isGroup?: boolean): void;
|
|
|
|
/** Called when a user clicks a button/action in a card (e.g., ask_user_question response). */
|
|
onAction(questionId: string, selectedOption: string, userId: string): void;
|
|
}
|
|
|
|
/** Delivery address used for reply-to overrides and (normally) the inbound's own origin. */
|
|
export interface DeliveryAddress {
|
|
channelType: string;
|
|
platformId: string;
|
|
threadId: string | null;
|
|
}
|
|
|
|
/**
|
|
* Full inbound event handed to the router.
|
|
*
|
|
* `channelType` + `platformId` + `threadId` identify which messaging group /
|
|
* session receives the message. `replyTo`, when set, overrides where the
|
|
* agent's reply is delivered — used by the CLI admin transport when the
|
|
* operator wants a message routed to one channel but replies echoed back to
|
|
* their terminal. Agents cannot set `replyTo`; it is a router-layer concept
|
|
* set only by external adapters carrying operator intent.
|
|
*/
|
|
export interface InboundEvent {
|
|
channelType: string;
|
|
platformId: string;
|
|
threadId: string | null;
|
|
message: {
|
|
id: string;
|
|
kind: 'chat' | 'chat-sdk';
|
|
content: string; // JSON blob
|
|
timestamp: string;
|
|
/**
|
|
* Platform-confirmed bot-mention signal forwarded from the adapter.
|
|
* See InboundMessage.isMention for the full explanation.
|
|
*/
|
|
isMention?: boolean;
|
|
/** True when the source is a group/channel thread, false for DMs. */
|
|
isGroup?: boolean;
|
|
};
|
|
replyTo?: DeliveryAddress;
|
|
}
|
|
|
|
/** Inbound message from adapter to host. */
|
|
export interface InboundMessage {
|
|
id: string;
|
|
kind: 'chat' | 'chat-sdk';
|
|
content: unknown; // JS object — host will JSON.stringify before writing to session DB
|
|
timestamp: string;
|
|
/**
|
|
* Platform-confirmed signal that this message is a mention of the bot.
|
|
*
|
|
* Set by adapters that know the platform's own mention semantics — e.g.
|
|
* the Chat SDK bridge sets it true from `onNewMention` / `onDirectMessage`
|
|
* and forwards `message.isMention` from `onSubscribedMessage`. Use this
|
|
* in the router instead of agent-name regex matching, which breaks on
|
|
* platforms where the mention text is the bot's platform username (e.g.
|
|
* Telegram's `@nanoclaw_v2_refactr_1_bot`) rather than the agent_group
|
|
* display name (e.g. `@Andy`).
|
|
*
|
|
* Adapters that don't set it (native / legacy) leave it undefined — the
|
|
* router falls back to text-match against agent_group_name.
|
|
*/
|
|
isMention?: boolean;
|
|
/** True when the source is a group/channel thread, false for DMs. */
|
|
isGroup?: boolean;
|
|
}
|
|
|
|
/** A file attachment to deliver alongside a message. */
|
|
export interface OutboundFile {
|
|
filename: string;
|
|
data: Buffer;
|
|
}
|
|
|
|
/** Outbound message from host to adapter. */
|
|
export interface OutboundMessage {
|
|
kind: string;
|
|
content: unknown; // parsed JSON from messages_out
|
|
files?: OutboundFile[]; // file attachments from the session outbox
|
|
}
|
|
|
|
/** Discovered conversation info (from syncConversations). */
|
|
export interface ConversationInfo {
|
|
platformId: string;
|
|
name: string;
|
|
isGroup: boolean;
|
|
}
|
|
|
|
/** The v2 channel adapter contract. */
|
|
export interface ChannelAdapter {
|
|
name: string;
|
|
channelType: string;
|
|
|
|
/**
|
|
* Whether this adapter models conversations as threads.
|
|
*
|
|
* true — adapter's platform uses threads as the primary conversation unit
|
|
* (Discord, Slack, Linear, GitHub). One thread = one session; the
|
|
* agent replies into the originating thread.
|
|
* false — adapter's platform treats the channel itself as the conversation
|
|
* (Telegram, WhatsApp, iMessage). Thread ids are stripped at the
|
|
* router; agent replies go to the channel.
|
|
*/
|
|
supportsThreads: boolean;
|
|
|
|
// Lifecycle
|
|
setup(config: ChannelSetup): Promise<void>;
|
|
teardown(): Promise<void>;
|
|
isConnected(): boolean;
|
|
|
|
// Outbound delivery — returns the platform message ID if available
|
|
deliver(platformId: string, threadId: string | null, message: OutboundMessage): Promise<string | undefined>;
|
|
|
|
// Optional
|
|
setTyping?(platformId: string, threadId: string | null): Promise<void>;
|
|
syncConversations?(): Promise<ConversationInfo[]>;
|
|
resolveChannelName?(platformId: string): Promise<string | null>;
|
|
|
|
/**
|
|
* Subscribe the bot to a thread so follow-up messages route via the
|
|
* platform's "subscribed message" path (onSubscribedMessage in Chat SDK).
|
|
* Called by the router when a mention-sticky wiring first engages in a
|
|
* thread. Idempotent: calling twice on the same thread is a no-op.
|
|
*
|
|
* Platforms without a subscription concept can omit this; the router
|
|
* treats absence as a no-op.
|
|
*/
|
|
subscribe?(platformId: string, threadId: string): Promise<void>;
|
|
|
|
/**
|
|
* Open (or fetch) a DM with this user, returning the platform_id of the
|
|
* resulting DM channel. Called by the host on demand to initiate cold
|
|
* DMs — approvals, pairing handshakes, host-initiated notifications — to
|
|
* users who may never have messaged the bot themselves.
|
|
*
|
|
* Omit this method on channels where the user handle IS already the DM
|
|
* chat id (Telegram, WhatsApp, iMessage, email, Matrix). Callers will
|
|
* fall through to using the handle directly.
|
|
*
|
|
* For channels that distinguish user id from DM channel id (Discord,
|
|
* Slack, Teams, Webex, gChat): implement by delegating to Chat SDK's
|
|
* chat.openDM, which hits the platform's idempotent open-DM endpoint.
|
|
* Returning the same platform_id on repeated calls is expected.
|
|
*/
|
|
openDM?(userHandle: string): Promise<string>;
|
|
}
|
|
|
|
/** Factory function that creates a channel adapter (returns null if credentials missing). */
|
|
export type ChannelAdapterFactory = () => ChannelAdapter | Promise<ChannelAdapter> | null;
|
|
|
|
/** Registration entry for a channel adapter. */
|
|
export interface ChannelRegistration {
|
|
factory: ChannelAdapterFactory;
|
|
containerConfig?: {
|
|
mounts?: Array<{ hostPath: string; containerPath: string; readonly: boolean }>;
|
|
env?: Record<string, string>;
|
|
};
|
|
}
|