Skip to content

ranuts/conversation — از گزارش رویداد تا گره‌های قابل رسم

یک گزارش رویدادِ فقط-افزودنی را به گره‌هایی می‌نگارد که نمای گفت‌وگو رسمشان می‌کند.

js
import { createConversationEngine } from 'ranuts/conversation';

نقطه ورودی خودش را دارد و بی‌نیاز از DOM است: خودِ این نگاشت را می‌شود آزمود و روی سرور هم رسم کرد. مصرف‌کننده آن در DOM، <r-conversation> است.

چرا بر پایه نوع رویداد شاخه نمی‌زنیم

راه معمول رسم یک گفت‌وگو، نمایی است که بر پایه نوع رویداد شاخه می‌زند و درختی از کامپوننت‌ها را دستکاری می‌کند. این کار ترتیب و هویت و آشتی‌دادن به‌روزرسانی‌های جزئی را درون نما می‌گذارد؛ پس هر نوع تازه‌ای از محتوا (یک فراخوانی ابزار، یک درخواست تأیید، یک خط وضعیت) باید دستی از میان آن رد شود و نما به‌ازای هر نوع یک شاخه دیگر می‌گیرد.

اینجا هر نوع یک ماشین حالت است که مستقل ثبت می‌شود. یک تعریف می‌گوید کدام رویدادها از آنِ اوست، آن‌ها را در وضعیت خودش تا می‌کند، و هرگز نمی‌فهمد که بقیه وجود دارند. افزودن یک نوع یعنی افزودن یک تعریف، نه ویرایش یک رِندِرِر.

یک تعریف

ts
const message = {
  kind: 'message',
  // کدام رویدادها مال من‌اند و به کدام گره تعلق دارند.
  match: (event) =>
    event.type === 'message/start'
      ? { id: event.id, role: 'start' }
      : event.type === 'message/delta'
        ? { id: event.id, role: 'update' }
        : null,
  // آن‌ها را در وضعیت خودم تا کن.
  start: (event, reader) => ({ text: '', after: reader.previous('message')?.id }),
  update: (state, event) => ({ ...state, text: state.text + event.text }),
  // مشترکان هر چند وقت یک بار نتیجه را ببینند.
  publication: (event) => (event.type === 'message/delta' ? 'animation-frame' : 'immediate'),
};

const engine = createConversationEngine({ definitions: [message, toolCall] });
engine.subscribe((nodes) => render(nodes));
engine.push(event);

definitions روی وضعیت unknown اعلام شده، پس تعریف‌هایی با نوع‌های وضعیت متفاوت بدون هیچ تبدیلی در محل فراخوانی کنار هم ثبت می‌شوند، در حالی که هرکدام آنجا که نوشته شده کاملاً تایپ‌دار می‌ماند.

معناشناسی

  • هر تعریف همه رویدادها را می‌بیند. موتور سر نخستین ادعا نمی‌ایستد، پس یک رویداد از گزارش می‌تواند دو گره را بگرداند.
  • ترتیب در start قفل می‌شود. گره‌ای که همچنان به‌روز می‌شود همان‌جا که باز شده می‌ماند، پس پیامی که استریم می‌شود با هر دلتا به انتهای فهرست نمی‌پرد.
  • updateی برای idی که گره بازی ندارد دور ریخته می‌شود. وقتی رویداد آغاز از پنجره صفحه‌بندی‌شده بیرون افتاده باشد، همین درست است؛ ساختن گره تنها از یک به‌روزرسانی جزئی، چیزی را رسم می‌کند که هرگز وجود نداشته.
  • start تکراری، گره را در جای خودش دوباره باز می‌کند. تعریف حکم کرده که این گرهی تازه است، پس وضعیت پیشین به‌جای ادغام دور ریخته می‌شود و جایگاه حفظ می‌ماند.
  • reader.previous(kind) تنها به عقب نگاه می‌کند. تعریفی که بتواند گره‌های آغازشده پس از خودش را ببیند، بسته به اینکه کِی اجرا شده پاسخ متفاوتی می‌دهد و پخش دوباره همان گزارش، همان نما را بازنمی‌سازد.

آهنگ انتشار

publication تعیین می‌کند مشترکان هر چند وقت یک بار به‌روزرسانی‌ها را ببینند، و تنها تنظیمی است که برای کارایی لازم است کوکش کنید:

آهنگبرای چه
animation-frameدلتاهای توکن‌به‌توکن: هر دلتا میان دو رسم در یک اعلان یکی می‌شود
immediateواقعیت‌های گسسته: نتیجه یک ابزار، یک تأیید؛ منتظر یک فریم ماندن فقط تأخیر می‌افزاید
noneوضعیتی که انتشار بعدی به‌هرحال با خود می‌آورد؛ ثبت می‌شود بی‌آنکه نما را بیدار کند

آهنگ فقط تندتر می‌شود و هرگز شل نمی‌شود. انتشاری با immediate در حالی که فریمی در انتظار است، همین حالا شلیک می‌کند و آن فریم را لغو می‌کند، به‌جای اینکه دو بار خبر بدهد. نگذاشتن publication یعنی immediate.

گزینه scheduler جای زمان‌بندی فریمی را می‌گیرد، و آهنگ با همان بی‌نیاز از رسم آزموده می‌شود. پیش‌فرض در مرورگر requestAnimationFrame است و جای دیگر یک میکروتسک.

گره‌ها

ts
interface ConversationNode<State> {
  key: string; // `kind:id`، در تمام عمر گره پایدار
  kind: string;
  id: string;
  seq: number; // شماره ترتیبِ رویداد آغاز — کلید مرتب‌سازی
  state: State;
}

nodes() تا رویداد پذیرفته‌شده بعدی همان آرایه را برمی‌گرداند و هر گره منجمد است، پس یک نما می‌تواند گرهی را از میان یک انتشار با خود نگه دارد بی‌آنکه زیر دستش عوض شود.

همچنین ببینید

  • ranuts/stream: رویدادها را می‌سازد
  • <r-conversation>: گره‌ها را رسم می‌کند
  • createBottomFollower در ranuts/utils: نما را به پایین سنجاق نگه می‌دارد

منتشرشده تحت مجوز MIT.