@yaebal/conversation
write multi-step dialogs as a straight line — await cv.wait() resolves with the next
update for that chat. A coroutine, not a replay engine.
install
pnpm add @yaebal/conversationusage
import { conversation, createConversation } from "@yaebal/conversation";
const greet = createConversation("greet", async (cv, ctx) => {
await ctx.send("what's your name?");
const a = await cv.wait(); // next update for this chat
await a.send(`age, ${a.text}?`);
const b = await cv.wait();
await b.send(`${a.text} is ${b.text}`);
});
bot.install(conversation([greet]));
bot.command("greet", (ctx) => ctx.conversation.enter("greet"));how it works
Unlike grammY's replay-based conversations, this is a coroutine: the builder runs
once, detached, and cv.wait() parks until the next update arrives. While a conversation
is active it owns the chat's updates — they're routed to wait() instead of reaching other handlers. No replay means no duplicated side effects.
api
| member | what |
|---|---|
createConversation(name, builder) | define a dialog; builder(cv, ctx) |
conversation(defs, options?) | plugin adding ctx.conversation |
cv.wait() | resolve with the next update's context |
cv.ctx | the most recent context |
ctx.conversation.enter(name) | start a conversation for this chat |
ctx.conversation.active() | whether one is running |
ctx.conversation.leave() | abandon the active one |
State is in-memory (lost on restart), like prompt and scenes. For a single follow-up use
prompt; for a
branching wizard use scenes; for a straight-line script use this.Works with or without @yaebal/runner. No deadlock in the
sequential loop, because the builder is detached and updates are routed to it — not awaited inside
handleUpdate.