@chamvi/chat

中文文档
0.8.0发布于 2026/11/09Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 1866

Chat SDK channels for chamvi agents.

原始使用文档
文档目录11 节
  1. Installation
  2. Usage
  3. Webhooks
  4. Platform packages
  5. Subscriptions and replies
  6. Memory
  7. Trust
  8. Limits
  9. With @chamvi/app
  10. API
  11. License

@chamvi/chat

@chamvi/chat

Chat channels for chamvi agents.
Chat SDK adapters start runs and get replies through one channel,
and platform packages plug into it through a public seam.

npm version CI License


Table of Contents

Installation

npm install @chamvi/chat @chamvi/core @chamvi/http chat --save

ESM only, Node.js >=26.0.0. @chamvi/core, @chamvi/http (the webhook body reader) and chat (the Chat SDK, 4.44 or later in the 4.x line) are peer dependencies; smob is the only runtime dependency. @chamvi/app is an optional peer of @chamvi/chat/module. The package imports no platform SDK: WhatsApp Web lives in @chamvi/chat-whatsapp.

EntryHolds
@chamvi/chatchatChannel(), createChatIngressAdapter(), chat.on()/chat.to(), the platform seam IChatPlatform, the limits and error codes
@chamvi/chat/modulechatModule(), which contributes a prebuilt channel to a @chamvi/app app

Usage

Any Chat SDK Adapter works through the channel. Wrap it with createChatIngressAdapter() before registering it with the Chat, so chamvi's intake, not the SDK's handlers and pre-handler deduplication, accepts its messages; the channel owns that Chat alone. Register the channel on the engine, subscribe an agent with channel.on() and reply from a tool with ctx.channels.findOneOrFail('chat').emit():

import type { LanguageModel } from 'ai';
import { z } from 'zod';
import { Chat } from 'chat';
import type { Adapter, StateAdapter } from 'chat';
import {
    Engine,
    createMemoryStore,
    defineModelAgent,
    defineTool,
} from '@chamvi/core';
import type { IClock, IEngine } from '@chamvi/core';
import { chatChannel, createChatIngressAdapter } from '@chamvi/chat';

export function createChatEngine(
    model: LanguageModel,
    // adapter: any Chat SDK adapter whose platform posts webhooks; state: a Chat SDK state adapter
    options: {
        adapter: Adapter;
        state: StateAdapter;
        clock?: IClock
    },
): IEngine {
    const { CHAT_USERS, APP_URL } = process.env;

    if (!CHAT_USERS || !APP_URL) {
        throw new Error('Set CHAT_USERS (platform user ids, comma separated) and APP_URL.');
    }

    const allowed = new Set(CHAT_USERS.split(','));

    // chamvi's intake, not the Chat SDK's handlers, accepts the adapter's messages
    const ingress = createChatIngressAdapter(options.adapter);
    const chat = new Chat({
        userName: 'chamvi',
        adapters: { [ingress.name]: ingress },
        state: options.state,
        logger: 'warn',
        concurrency: 'concurrent',
    });

    const channel = chatChannel({
        chat,
        adapters: [ingress],
        // the platform posts to POST /channels/chat/<adapter> (the http app's channel mount)
        webhooks: [ingress.name],
        // only these users start runs; everybody else is ignored
        allowMessage: ({ message }) => allowed.has(message.author.userId),
        resolveInvoker: ({ message }) => ({
            id: message.author.userId,
            kind: 'chat',
            permissions: [],
        }),
        // a link to the approval in the chamvi UI: ids only, answered there
        approvalUrl: ({ runId, approvalId }) => `${APP_URL}/ui/approvals?run=${encodeURIComponent(runId)}&approval=${encodeURIComponent(approvalId)}`,
    });

    const reply = defineTool({
        description: 'Reply in the chat the message came from.',
        inputSchema: z.object({ text: z.string() }),
        execute: async ({ text }, { context }) => {
            // the source chat: no address needed
            await context.channels.findOneOrFail('chat').emit({ body: text });

            return 'sent';
        },
    });

    const assistant = defineModelAgent({
        name: 'assistant',
        model,
        instructions: 'You are the team\'s assistant. Answer with the reply tool.',
        // one memory per direct chat and one per group
        conversation: true,
        // direct messages and mentions in a group
        channels: [channel.on()],
        tools: { reply },
    });

    return new Engine({
        // memory store: conversations included; use @chamvi/store-db in production
        store: createMemoryStore({ clock: options.clock }),
        agents: [assistant],
        channels: [channel],
        clock: options.clock,
        conversations: { authorize: ({ agent }) => agent === 'assistant' },
    });
}

Nothing initializes when the channel is built: the channel's start (the engine's) binds the adapters, initializes the Chat, then calls connect. allowMessage is required for every adapter (who may start runs); resolveInvoker names the runs' invoker (none by default); connect/disconnect hook a socket-style adapter into the channel's start and stop. A message starts runs only when allowMessage allows it; a refused message is dropped silently: no run, no reply, no download. Generic webhook adapters are covered by tests with fake adapters only: chamvi has verified no platform but WhatsApp Web.

Webhooks

A webhook adapter's platform posts to POST /channels/<channel>/<adapter> (the http app's channel mount; only the adapters listed in webhooks, anything else 404). The body is read up to CHAT_BODY_LIMIT_BYTES (1 MiB, 413 above it) and handed to the adapter's own handleWebhook, which verifies the request. The answer waits until every message the adapter handed over is stored as runs (at most CHAT_WEBHOOK_TIMEOUT_MS, 30 s), and is a 503 when one could not be for a reason a redelivery may fix (a store failure, the channel not started or stopping, the timeout), so the platform redelivers. A message only refused (media, no text, invalid ids, a denied conversation, a throwing filter) is reported once and gets the adapter's own answer, since a redelivery would be refused again. GET challenge flows are not served. Only messages the adapter hands over within the request's async context are tracked: an adapter that passes them to a consumer started outside it (a queue, a pre-request listener) is not waited for, and its own 2xx stands. The mount sits under the http app's general rate limit unless channelRateLimits names the channel.

Platform packages

A platform package (such as @chamvi/chat-whatsapp) builds its Chat, its wrapped adapter and a chatChannel() like any app, and passes what only the transport knows as platform (IChatPlatform, every hook optional and called on the platform object):

HookRunsAnswers
online()per send, after prepare, only while the channel is startedfalse refuses the send (chat_request, nothing posted); default true
prepare({ adapter, threadId, text, key })per send, after the address decoded and the message rendered, before onlinea throw refuses the target (its error as is); else the post (signal) => Promise the channel calls once, bounded by timeoutMs and the send's signal (a failure a chat_request); its signal aborts at whichever comes first, so the platform can cancel its request; key is the message key, so a transport can derive a stable message id. Default the adapter's postMessage(threadId, { raw: text })
conversationTarget(incoming, payload)per agent with conversation, after allowMessage, the text checks and resolveInvokerthe conversation identity; undefined refuses that agent (chat_intake_failed), a throw fails the message. Default from the payload

The channel checks the platform at construction (InvalidAgentDefinitionError for one that is no object, a hook that is no function, or a plain object with a key that is no hook). Its socket lifecycle stays with connect/disconnect, its message policy with allowMessage/resolveInvoker.

Subscriptions and replies

channel.on({ adapter?, events?, id?, filter?, concurrencyMaxRuns? }) subscribes an agent; events are direct (a direct chat), mention (the bot mentioned in a group) and message (any other group message), default ['direct', 'mention']. chat.on() does the same for the channel named chat. A run's payload is a small ChatPayload (adapter, event, threadId, channelId, message: { id, text, author, sentAt? }), never the platform's raw object; its address is the chat, so a tool's emit() without an address replies there. channel.to({ adapter, threadId }) builds the address of another chat.

A message is accepted once per agent (the idempotency key hashes channel, adapter, thread, sender and message id), so a redelivery starts no second run; a transient store failure is retried twice (1 s, 2 s), then reported as chat_intake_failed.

Approvals (ctx.channels.findOneOrFail('chat').approve()) send a text with the title, the body (cut to fit), the options and the approvalUrl link (Approve or deny: <link>, with options Choose and approve or deny: <link>); the owner answers in the chamvi UI. The link carries ids only; there are no native buttons, and a reply in the chat does not answer an approval.

Memory

An agent with conversation: true gets one memory per chat through core's conversation memory: the channel binds each message through the engine's trusted conversation intake, never through a payload field. It needs a store with conversations (createMemoryStore() or @chamvi/store-db) and the engine's conversations: { authorize } (createApp({ conversations })). The identity is the channel, the account alias, the chat and an audience: direct:<sender> for a direct chat, group:<channel id> for a group, so a group shares one memory among its members. Accepted message text and replies emitted to the source chat are remembered; history: 0 is the only setting (no platform history is fetched). A platform package may name its own identity (IChatPlatform.conversationTarget).

Trust

Every run a chat channel starts is tainted (untrusted: true by default; false only for a fully trusted private deployment), so tools classified guardWhenTainted ask for approval (or are denied) in these runs. A reply to the source chat is exempt from the outbound gate; a send to another chat goes through it. A configured defaultAddress is exempt too: it is an ungated sink for tainted runs, so none is set by default.

Limits

  • Text only: a message with attachments or without text is refused (reported, no run). No media download.
  • No history (history: 0), no message splitting: a message above maxTextChars (default CHAT_TEXT_MAX_CHARS, 4096 UTF-16 code units) is refused; an approval cuts only its prose.
  • ChannelMessage.slot is ignored: every message is a new chat message.
  • Delivery is at least once: a send is bounded by timeoutMs (default CHAT_TIMEOUT_MS, 30 s); a send that times out may still be delivered and a retry may send it again. A send while the channel is stopped or draining fails.

With @chamvi/app

import { createApp } from '@chamvi/app';
import { chatChannel } from '@chamvi/chat';
import { chatModule } from '@chamvi/chat/module';

const channel = chatChannel({ ... });

export default createApp({
    agents: [assistant],
    modules: [chatModule({ channel })],
    conversations: { authorize: ({ agent }) => agent === 'assistant' },
});

chatModule() contributes the channel to AppToken.CHANNELS (module name chamvi:channel:<name>); the engine starts and stops it, and the http module mounts its in when it serves webhooks. It adds no env variable and constructs nothing, so chamvi start --check neither initializes the Chat nor connects.

API

ExportEntry
chatChannel(options), createChatIngressAdapter(adapter), chat, chatAddress(), createChatSubscription()@chamvi/chat
Types ChatChannelOptions, ChatChannel, ChatPayload, ChatEvent, ChatIncoming, ChatIngress, ChatApprovalRef, IChatPlatform, ChatPlatformSend, ChatPlatformPost@chamvi/chat
CHAT_CHANNEL_NAME 'chat', CHAT_TEXT_MAX_CHARS 4096, CHAT_TIMEOUT_MS 30 000, CHAT_BODY_LIMIT_BYTES 1 MiB, CHAT_WEBHOOK_TIMEOUT_MS 30 000, CHAT_STOP_TIMEOUT_MS 30 000, CHAT_EVENTS, DEFAULT_CHAT_EVENTS, CHAT_ADDRESS_PREFIX@chamvi/chat
Error codes chat_request, chat_intake_failed, chat_stop_timeout (CHAT_*_ERROR_CODE)@chamvi/chat
chatModule(options), ChatModule@chamvi/chat/module

The full option reference is the chat reference.

License

Made with 💚

Published under Apache License 2.0.

版本与发布记录

@chamvi/chat 共发布 10 个版本,首次发布于 2026/11/02;过去 12 个月发布了 10 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.0latest2026/11/09今天
0.7.32026/11/064 天前
0.7.22026/11/054 天前
0.7.12026/11/055 天前
0.7.02026/11/045 天前
0.6.12026/11/045 天前

关于 @chamvi/chat 的常见问题

@chamvi/chat 是什么?

Chat SDK channels for chamvi agents.

如何安装 @chamvi/chat?

在项目目录运行 npm install @chamvi/chat,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:chat(^4.44.0)、@chamvi/core(^0.8.0)、@chamvi/http(^0.8.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@chamvi/chat 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chamvi/chat 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chamvi/chat 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=26.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@chamvi/chat 可以商用吗?

@chamvi/chat 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@chamvi/chat 的下载量有多少?

根据 npm 官方下载统计,@chamvi/chat 在 2026/10/10 – 2026/11/08 这 30 天内被下载 1,866 次,日均约 62 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chamvi/chat 还在维护吗?

最新版本 0.8.0 发布于 2026/11/09(今天);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2026/11/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。