تصور کنید یک برنامهنویس در تیمی کوچک است که از یک عامل کدنویس برای مدیریت پروژهای حجیم استفاده میکند؛ در این حالت، هرگونه قطع اتصال یا تأخیر در پاسخ، نباید به معنای از دست رفتن تمام پیشرفتهای کدنویسی باشد. اگر هنوز از مدلهای چتبات ساده برای اتوماسیون کدنویسی استفاده میکنید، باید بدانید که تفاوت میان یک «دموی جذاب» و یک «محصول حرفهای»، در نحوه مدیریت وضعیت (State) نهفته است.
یک عامل کدنویس را در نظر بگیرید که با یک جلسه را به عنوان یک API چت با حافظه طولانی میبیند: یک پرامپت میفرستد، پاسخی دریافت میکند و هر دو را به یک متن تاریخچه (Transcript) اضافه میکند. در حالی که این رویکرد برای یک دمو کافی است، اما برای یک محصول حرفهای کفایت نمیکند. چارچوب داخلی OpenCode ثابت میکند که برای بقا در پیچیدگیهای دنیای واقعی، محیطهای اجرای عامل (Agent Runtimes) باید ارسال پرامپت را از مرحله اجرا جدا کنند. تلقی کردن یک جلسه به عنوان یک فراخوانی API چت با حافظه گسترده، سادهترین راه برای درک نادرست از نحوه عملکرد واقعی یک عامل کدنویسی حرفهای است.
به گزارش مستندات فنی OpenCode، رویکرد رایج توسعهدهندگان بر پایه افزودن دستورات و پاسخها به یک تاریخچه (Transcript) است. این روش در مواجهه با پیچیدگیهای دنیای واقعی شکست میخورد؛ بهویژه وقتی عامل باید فایلها را ویرایش کند، مجوزهای دسترسی بگیرد یا وظایفی را در پسزمینه اجرا کند. در این حالت، اگر درخواست HTTP که کار را شروع میکند، همان درخواستی باشد که منتظر پاسخ میماند، سیستم بهشدت شکننده میشود. یک Timeout ساده میتواند عامل را در پسزمینه رها کند، بدون اینکه کاربر راهی برای ردیابی پیشرفت آن داشته باشد. در یک محیط اجرای عامل کدنویسی، یک وظیفه کند اغلب به معنای انجام کاری مفید است، نه یک درخواست خراب. این چالشها دقیقاً همان موانعی هستند که در تحلیل مسیرهای شکست استقرار تجاری عاملهای هوش مصنوعی به آنها اشاره کردیم.
OpenCode برای حل این مشکل، مفهوم «جلسه» (Session) را به عنوان یک کانتینر بادوام پیادهسازی کرده است. در این مدل، جلسه دیگر صرفاً «کار کردن مدل» نیست، بلکه یک هویت پایدار است که مالک یک دایرکتوری پروژه، یک مدل خاص، زمینههای دسترسی و مرز تاریخچه پیامهاست. این تفکیک اجازه میدهد یک جلسه حتی پیش از تولید اولین توکن (Token) — مثل برشهای کوچکی از یک کیک طولانی که مدل تکهتکه میخورد — وجود داشته باشد و وضعیت خود را حفظ کند.
همانطور که در تحلیلهای قبلی ما دربارهی امنیت مدلهای بازمتن اشاره کردیم، جداسازی لایهی اجرا از لایهی رابط، کلید دستیابی به پایداری در سیستمهای عاملمحور است.
زمینه: قرارداد کلاینت Fleet
برای درک این سازوکار، میتوان به کلاینت opencode-fleet نگاه کرد. این کلاینت تعمداً کوچک طراحی شده و ابزارهای MCP مانند fleet_create_session ،fleet_send_message ،fleet_get_session_status ،fleet_get_session_messages ،fleet_interrupt_session و fleet_reset_session را ارائه میدهد.
در پشت این ابزارها، دو کلاس اصلی قرار دارند:
SessionManager: یک نقشه در حافظه (In-memory map) از نام نود به شناسه جلسه فعال را نگه میدارد. این کلاس در اولین ارسال پیام، جلسه را بهصورت تنبل (Lazy) ایجاد میکند، برای پرامپتهای بعدی از آن مجدداً استفاده میکند و اگر سرور خطای 404 برگرداند، آن را دوباره میسازد.OpenCodeNode: APIهای HTTP راه دور را پوشش میدهد و مالک یک مشترک SSE پایدار است که به مسیر/eventگوش میدهد.
جریان معمول به این صورت است: fleet_send_message $ \rightarrow $ SessionManager.send $ \rightarrow $ دریافت یا ایجاد جلسه $ \rightarrow $ POST /session/:id/prompt_async $ \rightarrow $ انتظار برای وضعیت idle جلسه از طریق SSE $ \rightarrow $ GET /session/:id/message $ \rightarrow $ استخراج متن دستیار یا خلاصه پیشرفت ابزار.
این جریان شامل چندین انتخاب طراحی حیاتی است. اول، کلاینت به جای ایجاد یک جلسه تازه برای هر پرامپت، یک جلسه طولانیمدت را به هر نود راه دور متصل میکند تا زمینه کاری (Working Context) حفظ شود. دوم، ارسال پرامپت بهصورت ناهمگام (Asynchronous) است؛ متد OpenCodeNode.sendPromptAsync(...) بلافاصله پس از پذیرش کار توسط سرور بازمیگردد. این امر به کلاینت اجازه میدهد تفاوت میان «سروری که هرگز کار را نپذیرفته» و «عاملی که صرفاً هنوز در حال اجرا است» را تشخیص دهد. این رویکرد در کاهش تأخیرها مشابه تجربیات حذف محیطهای ابری در اپلیکیشنهای دسکتاپ است که بهرهوری اجرای عامل را افزایش میدهد.
زمینه: تکامل API
پلتفرم OpenCode دو مسیر API را دنبال میکند که نشاندهنده انتقال از یک پروتکل کلاینت کاربردی به یک مدل زمان اجرای داخلی پاکتر است:
- API سازگار با دسکتاپ: از مسیرهایی مانند
/session،/session/:id/prompt_async،/session/:id/message،/session/statusو/eventاستفاده میکند. اینها در گروه مسیرهای قدیمی تعریف شدهاند که در آنPOST /sessionبهsession.createمتصل میشود. - API نسخه V2/Core: معماری را بهطور صریحتر از طریق
/api/session،/api/session/:id/prompt،/api/session/active،/api/session/:id/eventو یک خط لوله بادوامSessionInputوSessionEventنمایش میدهد.
هر دو API اهمیت دارند زیرا تغییر به سمت مدلی را نشان میدهند که در آن هویت جلسه از اجرای پرامپت جدا شده است. یک جلسه میتواند طولانیتر از هر پرامپت واحد باشد و مالک دایرکتوری، پروژه، عامل، مدل، عنوان، مجوزها، پیامها، بخشها و وضعیت زمان اجرا باشد.
زمینه: مراجع پیادهسازی هسته
برای کسانی که قصد بررسی سورسکد را دارند، معماری در چندین بسته کلیدی توزیع شده است:
- کلاینت Fleet: فایلهای
opencode-fleet/src/tools.ts،opencode-fleet/src/session.tsوopencode-fleet/src/node.ts. - هندلرهای سرور: مسیرهای
packages/opencode/src/server/routes/instance/httpapi/groups/session.tsوhandlers/session.ts. - منطق جلسه: فایلهای
packages/opencode/src/session/session.ts،prompt.ts،run-state.ts،status.tsوprocessor.ts. - زمان اجرای هسته (Core Runtime): فایلهای
packages/core/src/session.ts،session/input.ts،session/run-coordinator.ts،session/runner/llm.ts،event.tsوsession/projector.ts.
خط لوله پذیرش (The Admission Pipeline)
یکی از حیاتیترین تغییرات در معماری OpenCode، مفهوم «پذیرش پرامپت» (Prompt Admission) است. وقتی کلاینت پیامی را از طریق نقطه انتهایی /session/:id/prompt_async ارسال میکند، سرور بلافاصله مدل زبانی بزرگ (LLM) را فراخوانی نمیکند.
در مسیر سازگار با دسکتاپ، هندلر متد promptSvc.prompt(...) را در محدوده سرور فورک (Fork) کرده و بلافاصله پاسخ 204 No Content برمیگرداند. این بدان معناست که پاسخ HTTP نشاندهنده پذیرش مسئولیت کار توسط سرور است، نه اینکه دستیار کار را به پایان رسانده باشد. در داخل SessionPrompt.prompt(...) ،سیستم یک پیام کاربر ایجاد میکند، بخشهای آن را ذخیره میکند، جلسه را بهروز میکند و پیش از فراخوانی حلقه، بازنویسیهای مجوز ابزار برای هر پرامپت را اعمال میکند.
API نسخه V2/core این موضوع را از طریق SessionV2.Service.prompt(...) که متد SessionInput.admit(...) را فراخوانی میکند، صریحتر میکند. این عمل session.next.prompt.admitted را به عنوان یک رویداد بادوام منتشر میکند. تنها پس از این پذیرش است که سرویس متد execution.wake(sessionID) را فراخوانی میکند.
این جداسازی چندین ویژگی فراهم میکند:
- هویت: پرامپت پیش از اجرای مدل، دارای یک هویت است.
- حذف تکرار: سیستم میتواند شناسههای پیام تکراری را رد کند.
- بادوام بودن: سیستم ثبت میکند که یک پرامپت وارد جلسه شده است، حتی اگر اجرا دیرتر شروع شود یا کلاینت قطع شود.
- کنترل: زمان اجرا میتواند دستورات را در صف قرار دهد، آنها را هدایت کند یا تصمیم بگیرد که فوراً آنها را از سر نگیرد.
- بازیابی: سیستم میتواند تاریخچه ورودیهای بادوام را مجدداً در یک جریان پیامهای تصویر شده (Projected) پخش کند.
هماهنگکننده هر جلسه (The Per-Session Coordinator)
برای جلوگیری از «کارخانه شرایط مسابقه» (Race Condition Factory) که در عاملهای چند-گام رایج است، OpenCode از یک هماهنگکننده برای سریالسازی اجرا استفاده میکند. در مسیر سازگار با دسکتاپ، SessionRunState یک Runner برای هر جلسه با وضعیتهایی مانند Idle ،Running ،Shell و ShellThenRun نگه میدارد. متد ensureRunning(...) اگر سیستم در حالت Idle باشد، کار را شروع میکند؛ اگر یک اجرا فعال باشد، به جای شروع دومی، منتظر همان اجرا میماند. اگر کار در محیط Shell فعال باشد، میتواند یک اجرا را در صف قرار دهد تا پس از اتمام Shell اجرا شود.
در V2/core، کلاس SessionRunCoordinator نقشهای از شناسه جلسه به ورودی فعال را نگه میدارد. متد wake(sessionID) اگر سیستم Idle باشد، یک Fiber تخلیه (Drain Fiber) را شروع میکند. اگر یک Fiber در حال اجرا باشد، مقدار pendingWake = true را تنظیم میکند. وقتی Fiber فعال به ثبات برسد، هماهنگکننده در صورت ثبت شدن یک wake، جانشین آن را شروع میکند. متد interrupt(sessionID) ورودی را به عنوان «در حال توقف» علامتگذاری میکند، wakeهای در انتظار را پاک میکند و Fiber مالک را متوقف میکند.
این سازوکار تضمین میکند که این اصل برقرار باشد: یک جلسه $ \rightarrow $ حداکثر یک حلقه تخلیه فعال. بدون این سیستم، دو پرامپت میتوانستند همزمان یک زمینه را بخوانند، مدل را فراخوانی کنند و ابزارها را روی فایلسیستم اجرا کنند. در یک عامل کدنویسی، این موضوع خطرناک است، زیرا پرامپت دوم ممکن است فرض کند فایلها تغییر نکردهاند در حالی که پرامپت اول در حال ویرایش آنهاست. در این صورت مجوزهای ابزار و وضعیتها مبهم شده و رابط کاربری نمیتواند بهطور صادقانه فعالیت جلسه را گزارش کند.
اجرای حلقه تخلیه (The Drain Loop Execution)
به جای یک تابع ساده مانند completeChat ،OpenCode از یک «حلقه تخلیه» (Drain Loop) استفاده میکند. اجراکننده بهطور مکرر شرایطی را بررسی میکند که نیاز به کار بیشتر داشته باشند. در مسیر سازگار با دسکتاپ، SessionPrompt.runLoop(...) موارد زیر را مدیریت میکند:
- بارگذاری تاریخچه فشرده شده و تعیین عامل و مدل فعلی.
- جمعآوری دستورالعملهای سیستم و تبدیل پیامهای ذخیره شده به پیامهای ارائهدهنده (Provider).
- فراخوانی
SessionProcessor.process(...)برای مصرف جریان ارائهدهنده. - بهروزرسانی بخشهای پیام به صورت متن، استدلال، فراخوانی ابزار، نتایج ابزار، خطاها و وضعیتهای پایان.
در V2/core، متد SessionRunner.run(...) شکل مشابهی دارد. این متد از runTurnAttempt(...) برای ارتقای ورودیهای در انتظار به زمینه فعال، ساخت یک LLM.request(...) و استریم کردن رویدادهای ارائهدهنده استفاده میکند.
این حلقه بر اساس شرایط ادامه خاصی ادامه مییابد:
- مدل ابزارهایی را درخواست کرده و نتایج باید بازگردانده شوند.
- هدایتهای (Steering) جدید در حالی که یک نوبت فعال است، رسیده باشد.
- ورودیهای در صف منتظر باشند.
- پیش از فراخوانی بعدی ارائهدهنده، نیاز به فشردهسازی (Compaction) باشد.
- ارائهدهنده پیش از ایجاد خروجی بادوام دستیار، شکست خورده باشد.
- کاربر مجوز را رد کرده باشد و حلقه مجبور به توقف شود.
- جلسه متوقف (Interrupt) شده باشد.
وضعیت مقتدر و تصویرسازی رویدادها
OpenCode این ایده را که وضعیت عامل را با استخراج چند خط آخر تاریخچه «حدس» بزنیم، رد میکند. در عوض، وضعیت یک حقیقت مقتدر است که توسط لایه اجرا ارائه میشود.
در زمان اجرای سازگار با دسکتاپ، SessionStatus یک نقشه محلی از جلسات غیر-idle نگه میدارد. فراخوانی set(sessionID, { type: "busy" }) یک رویداد session.status منتشر میکند. وقتی set(sessionID, { type: "idle" }) فراخوانی میشود، هم رویداد session.status و هم رویداد قدیمی session.idle منتشر شده و سپس جلسه از نقشه حذف میشود. نبود وضعیت به معنای Idle بودن است. سرور این را از طریق GET /session/status و جریان /event ارائه میدهد.
API نسخه V2/core از GET /api/session/active برای بازگرداندن مجموعهای از تخلیههای پیشزمینه (Foreground Drains) که در حال حاضر توسط پردازش مالک هستند، استفاده میکند. اگر جلسهای در آنجا ظاهر شود، در حال اجراست؛ در غیر این صورت غیرفعال است.
کلاینتها برای بهروزرسانیها Polling نمیکنند، بلکه به یک جریان Server-Sent Events (SSE) مشترک متصل میشوند. نقطه انتهایی /event در نسخه دسکتاپ، یک شنونده را در EventV2Bridge ثبت میکند، بر اساس دایرکتوری نمونه و فضای کاری فیلتر میکند و یک رویداد server.connected را همراه با ضربان قلب (Heartbeats) ارسال میکند. سپس کلاینت اینها را به یک وضعیت محلی تصویر میکند. این لایهبندی سه سطح متمایز از حقیقت ایجاد میکند:
- رویدادهای بادوام (Durable Events): توالی تغییرناپذیر از آنچه اتفاق افتاده است (مثلاً
session.next.step.started،session.next.tool.success،session.next.text.delta). - پیامهای تصویر شده (Projected Messages): یک نمای مناسب برای پرسوجو در UI، جایی که
SessionProjectorرویدادها را به ردیفهای پیام تبدیل میکند. - ذخیرهساز کلاینت (Client Store): یک حافظه محلی (مثلاً یک Solid store در اپلیکیشن دسکتاپ) برای بهروزرسانیهای خوشبینانه و دلتاهای استریم شده.
تصویرسازیهای ساختاریافته پیام
OpenCode با گفتگو به عنوان متن ساده برخورد نمیکند. پیامها به ردیفها و بخشها تقسیم میشوند.
- پیامهای کاربر: میتواند شامل متن، فایلها، عاملها و زیر-وظایف باشد.
- پیامهای دستیار: میتواند شامل متن، استدلال، بخشهای ابزار، نشانگرهای گام، اسنپشاتها، پچها، تلاشهای مجدد، بخشهای فشردهسازی و خطاها باشد.
- بخشهای ابزار: شامل شناسه فراخوانی، نام ابزار، ورودی، وضعیت، خروجی، متادیتا، پیوستها و زمانبندی است.
این ساختار اجازه میدهد کلاینت opencode-fleet در زمان Timeout خروجیهای جزئی مفیدی را برگرداند. اگر دستیار هنوز متنی تولید نکرده اما فراخوانیهای ابزار در حال اجرا هستند، کلاینت میتواند به جای برگرداندن یک رشته خالی، فعالیت ابزارها را خلاصه کند. نسخه V2/core این قابلیت را با استفاده از createLLMEventPublisher(...) تقویت میکند تا رویدادهای ارائهدهنده را به رویدادهای خاص جلسه مانند session.next.tool.failed یا session.next.step.ended تبدیل کند.
کنترل چرخه عمر: توقف در مقابل بازنشانی (Interrupt vs. Reset)
عاملهای طولانیمدت به کنترلهای دقیقی نیاز دارند که فراتر از یک دکمه ساده «لغو» باشد. OpenCode بین چهار عملیات متمایز تفاوت قائل میشود:
- Timeout: فراخواننده منتظر ماندن را متوقف کرده است (مثلاً
SessionManager.sendخطایTimeoutErrorرا میگیرد)، اما عامل احتمالاً هنوز در حال کار است. کلاینت مقدارtimedOut: trueرا علامتگذاری کرده و توصیه میکند وضعیت را بررسی یا پیامها را بازرسی کند. - Interrupt: درخواستی به زمان اجرا برای متوقف کردن اجرای فعال.
SessionRunState.cancel(...)یاSessionRunCoordinator.interrupt(...)فیبرهای فعال را متوقف کرده و ابزارها را به ثبات میرساند. - Reset: تصمیم کلاینت برای رها کردن اتصال جلسه فعلی.
fleet_reset_sessionوضعیت را بررسی میکند و از بازنشانی یک جلسه مشغول (Busy) خودداری میکند تا دسترسی به کارهای در جریان از دست نرود. - Delete: تصمیم در سطح ذخیرهسازی برای حذف رکورد جلسه.
تکامل معماری
جالب است که کدبیس OpenCode در حال حاضر دو API را حفظ کرده است: یک نسخه قدیمی سازگار با دسکتاپ و یک API جدیدتر V2/core. درخت مسیرها در packages/opencode/src/server/routes/instance/httpapi/server.ts هر دو را نصب میکند.
این موضوع یک اصل کلیدی برای سازندگان عامل را نشان میدهد: پوسته سازگاری (Compatibility Shell) را نازک نگه دارید. API قدیمی وجود دارد چون کلاینتها به آن وابسته هستند، اما معماری V2/core مدل داخلی را صریحتر میکند. پذیرش پرامپت یک رویداد بادوام است، ورودیهای در انتظار در SessionInputTable زندگی میکنند و اجرا از طریق SessionExecution و SessionRunCoordinator هماهنگ میشود.
با انتقال منطق هسته به یک زمان اجرای پایدار، OpenCode میتواند از انواع مختلف کلاینتها پشتیبانی کند بدون اینکه شکل API، محدودیتهای معماری عامل را دیکته کند. اگر کد سازگاری مالک مدل زمان اجرا باشد، هر شکل قدیمی از نقطه انتهایی به یک محدودیت دائمی تبدیل میشود. اما اگر زمان اجرا مالک مدل باشد، هندلرهای سازگاری میتوانند دادهها را ترجمه کنند.
این تغییر در تفکر — از «چت کردن با یک مدل» به «مدیریت یک کانتینر اجرا» — همان چیزی است که اجازه میدهد یک عامل از یک چتبات ساده به یک مهندس نرمافزار قابل اعتماد تبدیل شود. فراخوانی مدل صرفاً یک جزء در داخل زمان اجرای جلسه است، نه خودِ زمان اجرا.
اگر در حال ساخت یک عامل تولیدی (Production) هستید، اولین قدم این است که دست از تلقی کردن تاریخچه گفتگو به عنوان منبع حقیقت (Source of Truth) بردارید. ابتدا یک لاگ رویداد ساختاریافته بسازید و با متن چت به عنوان یک تصویر (Projection) از آن لاگ برخورد کنید. این رویکرد مشابه راهکاری است که TeamBrain برای تبدیل حافظه عاملها به فایلهای Git به کار گرفت تا مشکل فراموشی حافظه در پروژههای بزرگ را حل کند.
گام بعدی شما
- اگر در حال توسعه عاملهای AI هستید، معماری خود را از حالت «درخواست-پاسخ» به حالت «رویداد-وضعیت» تغییر دهید.
- برای مدیریت کارهای طولانیمدت، یک لایه پذیرش (Admission) ایجاد کنید تا دستورات پیش از اجرا در دیتابیس ثبت شوند.
- از یک Coordinator برای سریالسازی دسترسی به فایلسیستم استفاده کنید تا از Race Condition جلوگیری شود.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو