تفترض معظم أطر عمل وكلاء الذكاء الاصطناعي أن الوكيل ينهي عمله ضمن طلب واحد. يفكّر النموذج، ويستدعي بضع أدوات، ويُرجع إجابة، ثم تنتهي العملية. ينهار هذا الافتراض في اللحظة التي تطلب فيها من الوكيل عملاً حقيقياً — البحث في موضوع عبر خمسين مصدراً، أو إعادة هيكلة قاعدة شيفرة، أو مراقبة تدفّق بيانات لست ساعات. في مكان ما في المنتصف تُطرد الحاوية، أو ينقطع الاتصال، أو تُدخل المنصّة نسختك في سبات، فيختفي كل ما تعلّمه الوكيل.
Project Think، الذي أُعلن عنه خلال أسبوع الوكلاء (Agents Week) لدى Cloudflare في أغسطس 2026، هو جواب Cloudflare عن هذه المشكلة. إنه مجموعة من البدائيات المخصّصة للوكلاء طويلي الأمد — التنفيذ المعمّر، والوكلاء الفرعيون، وتنفيذ الشيفرة في بيئة معزولة، والجلسات الدائمة — إضافة إلى صنف أساس ذي رأي واضح اسمه Think يربط كل ذلك معاً.
يبني هذا الدرس وكيل Think حقيقياً انطلاقاً من مجلّد فارغ: مساعد بحث معمّر يمتلك ذاكرة دائمة وأدوات مخصّصة وعملاً خلفياً بنقاط استئناف ووكلاء فرعيين مفوَّضين.
ملاحظة حول الإصدار التجريبي: Project Think في طور المعاينة اعتباراً من أغسطس 2026. تصف Cloudflare سطح الـAPI بأنه مستقر لكنه ما يزال يتطوّر — توقّع تغييرات قبل الإصدار المستقر. تعامل معه على أنه صالح للإنتاج في الأدوات الداخلية والنماذج الأولية، وثبّت إصدارات اعتمادياتك.
المتطلبات المسبقة
قبل البدء، تأكّد من توفّر:
- Node.js 20+ مع npm أو pnpm
- حساب Cloudflare مع تفعيل Workers (الخطة المجانية تكفي لمتابعة الدرس)
- TypeScript 5.5+ وإلمام بـ async/await والأصناف والأنواع العامة (generics)
- معرفة أساسية بـ Cloudflare Workers وDurable Objects — لست بحاجة إلى خبرة عميقة، لكن إدراك أن الـDurable Object هو نسخة وحيدة قابلة للعنونة وذات حالة سيساعدك
- نحو 45 دقيقة
ما الذي ستبنيه
في نهاية هذا الدرس سيكون لديك وكيل منشور:
- يبثّ ردود المحادثة عبر WebSockets دون أي توصيل يدوي
- يتذكّر حقائق عن المستخدم عبر إعادات التشغيل والسبات
- يعرض أدوات مخصّصة إلى جانب نظام ملفات workspace مدمج
- ينفّذ مهمة بحث من عشر خطوات تستأنف من نقطة استئناف إذا طُردت النسخة
- يفوّض العمل إلى وكلاء فرعيين معزولين يعملون بالتوازي
- يستيقظ وفق جدول زمني لإنتاج إحاطة يومية
لماذا Think بدلاً من SDK الوكلاء الحالي
أطلقت Cloudflare بالفعل SDK للوكلاء، وهو لن يختفي — بل إن Think مبني فوقه. يهمّ التمييز بينهما عند اختيار أيّهما تستخدم.
يتولّى صنف AIChatAgent الأصلي التوجيه واستدعاء الأدوات الأساسي. أما النموذج ومخزن الرسائل وحلقة البثّ ومعالجة الأخطاء فتوصّلها بنفسك — نحو خمسة عشر سطراً من الشيفرة النمطية قبل أن يفعل وكيلك أي شيء مفيد.
يقلب Think هذه المعادلة. فهو يقدّم هيكلاً ذا رأي واضح يملك دورة حياة المحادثة كاملة — البثّ، والحفظ الدائم، والإلغاء، والتدفّقات القابلة للاستئناف، ومعالجة الأخطاء، ونظام ملفات workspace — ويطلب منك تجاوز ما يختلف فقط. وكيل Think الأدنى يقع في ثلاثة أسطر. وفوق ذلك يضيف أربعة أمور لا يملك AIChatAgent لها مقابلاً:
| القدرة | ما تمنحه |
|---|---|
| الـFibers المعمّرة | عمل طويل الأمد يضع نقاط استئناف ويستأنف بعد العطل |
| الوكلاء الفرعيون | وكلاء أبناء مشتركو الموقع عبر Durable Object Facets، لكلٍّ قاعدة SQLite معزولة |
| كتل السياق | ذاكرة دائمة قابلة للكتابة من النموذج تنجو من السبات |
| سلّم التنفيذ | خمسة مستويات حوسبة تصاعدية، من نظام ملفات افتراضي حتى بيئة معزولة كاملة |
البدائيات قابلة للاستخدام منفردة أيضاً. فحزم مثل @cloudflare/codemode و@cloudflare/shell و@cloudflare/worker-bundler تعمل دون صنف الأساس Think إن أردت القطع دون الآراء.
الخطوة 1: تهيئة المشروع
أنشئ المشروع وثبّت الاعتماديات.
mkdir research-agent && cd research-agent
npm init -y
npm install @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider react react-dom
npm install -D wrangler @cloudflare/vite-plugin @cloudflare/workers-types @vitejs/plugin-react @tailwindcss/vite tailwindcss typescript viteننتقل الآن إلى إعداد الـWorker. أنشئ ملف wrangler.jsonc:
{
"name": "research-agent",
"compatibility_date": "2026-01-28",
"compatibility_flags": ["nodejs_compat"],
"ai": { "binding": "AI" },
"assets": {
"not_found_handling": "single-page-application",
"run_worker_first": ["/agents/*"]
},
"durable_objects": {
"bindings": [{ "class_name": "ResearchAgent", "name": "ResearchAgent" }]
},
"migrations": [{ "new_sqlite_classes": ["ResearchAgent"], "tag": "v1" }],
"main": "src/server.ts"
}ثلاثة أسطر هنا أهمّ من بقيتها:
"ai": { "binding": "AI" }يتيح استدلال Workers AI للوكيل عبرthis.env.AI.- ربط
durable_objectsهو ما يجعل الوكيل قابلاً للعنونة وذا حالة. كل محادثة تحصل على نسختها الخاصة. "new_sqlite_classes"في الترحيل إلزامي. يخزّن Think الرسائل والجلسات والذاكرة ونقاط استئناف الـfibers في قاعدة SQLite التابعة للـDurable Object. تسجيل الصنف كـDurable Object عادي دون SQLite سيفشل أثناء التشغيل.
تضمن قاعدة run_worker_first: ["/agents/*"] أن تصل ترقيات WebSocket الخاصة بالوكلاء إلى الـWorker بدلاً من تقديمها كموارد ثابتة.
بعد ذلك، ملف vite.config.ts:
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), cloudflare(), tailwindcss()],
});وملف tsconfig.json، الذي يوسّع ببساطة الإعداد الذي تشحنه الحزمة:
{
"extends": "agents/tsconfig"
}الخطوة 2: أول وكيل Think لك
أنشئ src/server.ts. هذا هو الخادم بأكمله.
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";
export class ResearchAgent extends Think<Env> {
getModel() {
return createWorkersAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
);
}
getSystemPrompt() {
return "أنت مساعد بحث لديك نظام ملفات workspace. احفظ النتائج في ملفات أثناء عملك.";
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;هذا وكيل كامل. فقد منحك صنف الأساس Think بالفعل بروتوكول محادثة عبر WebSocket، وحفظاً دائماً للرسائل في SQLite، وبثّاً قابلاً للاستئناف، وأدوات ملفات workspace، ودعماً للإلغاء، ومعالجةً للأخطاء — ولا شيء من ذلك يظهر في شيفرتك.
getModel() هي التجاوز الوحيد المطلوب فعلاً. تُرجع أي نموذج متوافق مع واجهة النماذج في Vercel AI SDK، ما يعني أن Workers AI وOpenAI وAnthropic أو أي شيء يمرّ عبر AI Gateway من Cloudflare يعمل هنا. أما getSystemPrompt() فاختيارية؛ تجاهلها وستحصل على قيمة افتراضية معقولة.
تفحص routeAgentRequest الطلب الوارد، وتربطه بنسخة الـDurable Object الصحيحة، وتسلّم الاتصال. وإن لم يطابق المسار أي مسار وكيل تُرجع null، ولهذا يوجد الـResponse الاحتياطي.
الخطوة 3: عميل React
أنشئ src/client.tsx:
import { createRoot } from "react-dom/client";
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ResearchAgent" });
const { messages, sendMessage, status } = useAgentChat({ agent });
return (
<div>
<h1>وكيل البحث</h1>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem(
"input",
) as HTMLInputElement;
if (!input.value.trim()) return;
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="input" placeholder="اطلب مني بحثاً..." />
<button type="submit">إرسال</button>
</form>
<p>الحالة: {status}</p>
</div>
);
}
const root = document.getElementById("root");
if (root) {
createRoot(root).render(<Chat />);
}لاحظ أن messages مصفوفة من كائنات الرسائل التي يعيش محتواها في مصفوفة parts لا في سلسلة نصية مسطّحة. هذه هي بنية الرسائل في AI SDK v5 — فرسالة مساعد واحدة قد تحتوي على أجزاء نصية وأجزاء استدعاء أدوات وأجزاء تفكير. الاكتفاء بعرض part.type === "text" يبقي هذا المثال قصيراً؛ أما الواجهة الحقيقية فتتفرّع على كل نوع جزء.
أضف index.html في جذر المشروع:
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>وكيل البحث</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>شغّله:
npx vite devأرسل رسالة. تصل الردود رمزاً بعد رمز، والنموذج يملك أصلاً أدوات ملفات — اطلب منه أن يكتب شيئاً في ملف ثم يقرأه، وسيفعل.
ولأن Think يستخدم بروتوكول WebSocket نفسه الذي يستخدمه @cloudflare/ai-chat، فإن أي واجهة محادثة قائمة مبنية على ذلك البروتوكول تندمج دون أي تعديل.
الخطوة 4: ذاكرة دائمة عبر كتل السياق
المحادثة المتدفّقة هي الحدّ الأدنى. أما الذاكرة التي تنجو من السبات فهي حيث يبدأ Think بالتميّز.
يدخل الـDurable Object في سبات عند الخمول — تُمحى الحالة الموجودة في الذاكرة وتُعاد بناء النسخة عند الطلب التالي. وكل ما خزّنته على نسخة الصنف يختفي. تحلّ كتل السياق هذه المشكلة بالإقامة في SQLite وإعادة الحقن في موجّه النظام في كل دور.
تجاوز configureSession() في صنف وكيلك:
import type { Session } from "agents/experimental/memory/session";
configureSession(session: Session) {
return session
.withContext("soul", {
provider: {
get: async () =>
"أنت مساعد بحث. تذكّر مجال المستخدم ومصادره المفضّلة وأسلوب كتابته.",
},
})
.withContext("memory", {
description: "حقائق مهمة عن المستخدم واهتماماته البحثية.",
maxTokens: 2000,
})
.withCachedPrompt();
}يعمل هنا نوعان مختلفان من الكتل.
كتلة soul هي للقراءة فقط من منظور النموذج. تعمل دالة provider.get() الخاصة بها في كل دور، ما يتيح لك سحب القيمة من قاعدة بيانات أو من راية ميزة أو من إعداد خاص بكل مستأجر — إنها موجّه نظام ديناميكي لا ثابت.
أما كتلة memory فهي قابلة للكتابة. تصريحها بـdescription وميزانية maxTokens يدفع Think إلى تسليم النموذج أداة set_context. فحين يذكر المستخدم أنه يعمل في التقنية المالية ويفضّل المصادر الأوّلية، يمكن للنموذج استدعاء تلك الأداة، فتُحفظ الحقيقة في SQLite. وفي الأسبوع التالي، بعد أن تكون النسخة قد دخلت السبات مئة مرة، تبقى الحقيقة في موجّه النظام.
تُعلِّم withCachedPrompt() الموجّه المُجمَّع لأغراض التخزين المؤقت لدى المزوّد. وبما أن كتل السياق تقع في مقدّمة الموجّه الثابتة بينما تتراكم رسائل المحادثة في نهايته، فهذه بالضبط البنية التي صُمّم لها التخزين المؤقت للموجّهات — توقّع خفضاً ملموساً في التكلفة على المحادثات الطويلة.
ميزانية maxTokens مهمة. فالذاكرة التي تنمو بلا حدّ تزاحم المحادثة نفسها في النهاية. يفرض Think السقف ويطلب من النموذج الدمج والتلخيص حين تمتلئ الكتلة.
الخطوة 5: أدوات مخصّصة
أدوات نظام ملفات الـworkspace تأتي مجاناً. أما القدرات الخاصة بمجالك فمتروكة لك. تجاوز getTools():
import { tool } from "ai";
import { z } from "zod";
import type { ToolSet } from "ai";
getTools(): ToolSet {
return {
searchPapers: tool({
description: "يبحث في الأوراق الأكاديمية بالكلمات المفتاحية ويُرجع العناوين والمؤلفين والملخّصات.",
inputSchema: z.object({
query: z.string().describe("كلمات البحث المفتاحية"),
limit: z.number().min(1).max(25).default(10),
}),
execute: async ({ query, limit }) => {
const res = await fetch(
`https://api.crossref.org/works?query=${encodeURIComponent(query)}&rows=${limit}`,
);
const data = await res.json();
return data;
},
}),
};
}تعريفات tool() قياسية من Vercel AI SDK مع مخطّطات Zod — لا شيء خاصّ بـThink في هذه البنية.
ما يضيفه Think هو الدمج. تصل الأدوات من سبعة مصادر مستقلة: نظام ملفات الـworkspace، وما تُرجعه دالتك getTools()، والامتدادات في وقت التشغيل، وأدوات الجلسة، والمهارات، وخوادم MCP المتصلة، والأدوات من جهة العميل التي يسجّلها المتصفّح. يدمج Think كل ذلك في مجموعة أدوات واحدة قبل كل دور. ولن تجمّع تلك القائمة يدوياً أبداً.
نتيجتان عمليتان. أولاً، تضارب الأسماء أمر واقعي — ضع بادئة مميّزة لأدواتك المخصّصة إن كنت تصل خوادم MCP أيضاً. ثانياً، يتضخّم عدد الأدوات بسرعة، وكل تعريف أداة يكلّف رموزاً في الموجّه في كل دور. وإن وجدت نفسك تتجاوز الثلاثين أداة، فتلك إشارة للنظر في Code Mode في الخطوة 8.
الخطوة 6: التنفيذ المعمّر عبر الـFibers
هذه هي البدائية التي تجعل تبنّي Think مجدياً.
تأمّل مهمة بحث تنفّذ عشرة استدعاءات LLM بالتتابع، كلٌّ منها يستغرق عشرين ثانية. هذا يزيد على ثلاث دقائق من الزمن الفعلي. وضمن هذه النافذة قد يُطرد الـDurable Object لأسباب عدّة — نشر جديد، أو عملية صيانة للمنصّة، أو ضغط على الذاكرة. ومع دالة async عادية، يضيع كل شيء عند الطرد ولا يحصل المستخدم على شيء.
الـfiber هو استدعاء دالة معمّر. يسجّله Think في SQLite قبل بدء التنفيذ، فيبقى سجلّ «هذا العمل ينبغي أن يجري» حيّاً بعد فناء العملية التي كانت تنفّذه.
async startResearch(topic: string) {
void this.runFiber("research", async (ctx) => {
const findings = [];
for (let i = 0; i < 10; i++) {
const result = await this.callLLM(`خطوة البحث ${i}: ${topic}`);
findings.push(result);
// نقطة استئناف: إن حدث طرد، نستأنف من هنا
ctx.stash({ findings, step: i, topic });
this.broadcast({ type: "progress", step: i });
}
return { findings };
});
}
async onFiberRecovered(ctx) {
if (ctx.name === "research" && ctx.snapshot) {
const { topic } = ctx.snapshot;
await this.startResearch(topic);
}
}لنستعرض ما تفعله كل قطعة.
تسجّل runFiber(name, fn) الـfiber تحت اسم معيّن وتبدأه. البادئة void مقصودة — فأنت لا تنتظر النتيجة. يعود المستدعي فوراً، ويستمرّ الـfiber في الخلفية. وتُبقي الحزمة الوكيلَ حيّاً طوال مدة الـfiber تلقائياً؛ فلا يوجد keepalive يدوي ولا waitUntil تضبطه.
تكتب ctx.stash(snapshot) نقطة استئناف. مرّر إليها كل ما تحتاجه للاستئناف — النتائج المتراكمة، والفهرس الحالي، والمدخل الأصلي. ضع نقطة استئناف بعد كل خطوة مكلفة، لا عند كل سطر: فكل استدعاء عملية كتابة في SQLite، ووضع نقاط استئناف في حلقة تدور ألف مرة في الثانية سيلتهم زمن استجابتك.
تدفع this.broadcast(message) تحديثاً إلى كل عملاء WebSocket المتصلين. هكذا يتابع المستخدم التقدّم لحظياً بدل التحديق في مؤشّر تحميل ثلاث دقائق.
onFiberRecovered(ctx) هي خطّاف الاستئناف. بعد عطل أو طرد، يعثر Think على الـfibers غير المكتملة في SQLite ويستدعي هذا الخطّاف مع آخر stash. وأنت من يقرّر معنى «الاستئناف». المثال أعلاه يعيد تشغيل المهمة كاملة، وهو أبسط سلوك صحيح لكنه يهدر العمل المنجز. النسخة الأفضل تقرأ ctx.snapshot.step وتستأنف من الفهرس التالي:
async onFiberRecovered(ctx) {
if (ctx.name !== "research" || !ctx.snapshot) return;
const { topic, findings, step } = ctx.snapshot;
void this.runFiber("research", async (fiberCtx) => {
const collected = [...findings];
for (let i = step + 1; i < 10; i++) {
const result = await this.callLLM(`خطوة البحث ${i}: ${topic}`);
collected.push(result);
fiberCtx.stash({ findings: collected, step: i, topic });
this.broadcast({ type: "progress", step: i, resumed: true });
}
return { findings: collected };
});
}تنبثق من ذلك قاعدة تصميمية واحدة: يجب أن يكون جسم الـfiber عديم التأثير الجانبي المكرّر ابتداءً من آخر نقطة استئناف. فإن كانت خطوة ما تخصم من بطاقة ائتمان أو ترسل بريداً إلكترونياً، فقد يتكرّر ذلك الأثر الجانبي عند الاستئناف. ضع العمليات غير القابلة للتكرار الآمن مباشرة بعد نقطة استئناف، واحمها بمفتاح idempotency.
الخطوة 7: الوكلاء الفرعيون
وكيل واحد يحمل مئة أداة وموجّه نظام يغطّي ستة مجالات يؤدّي أسوأ من عدّة وكلاء مركّزين. يجعل Think التفويض رخيصاً عبر Durable Object Facets — وهي Durable Objects أبناء مشتركة الموقع مع الأب، لكلٍّ قاعدة SQLite معزولة خاصة به.
import { Agent } from "agents";
export class SearchAgent extends Agent {
async search(query: string) {
/* منطق بحث مركّز، بأدواته وموجّهه الخاصين */
}
}
export class CritiqueAgent extends Agent {
async analyze(text: string) {
/* منطق نقد مركّز */
}
}
export class Orchestrator extends Agent {
async handleTask(task: string) {
const searcher = await this.subAgent(SearchAgent, "search");
const critic = await this.subAgent(CritiqueAgent, "critique");
const [research, review] = await Promise.all([
searcher.search(task),
critic.analyze(task),
]);
return this.synthesize(research, review);
}
}الوسيط الثاني في subAgent() هو اسم ثابت لا معرّف عشوائي. فاستدعاء subAgent(SearchAgent, "search") مرتين يُرجع النسخة نفسها بحالتها المتراكمة نفسها — الوكلاء الفرعيون قابلون للعنونة ودائمون، لا عمّال يُستهلكون ويُرمون.
ولأن الـfacets مشتركة الموقع مع الأب، فإن استدعاءات RPC تكافئ عملياً استدعاءات دوال محلية. لا توجد قفزة شبكية بين Orchestrator وSearchAgent، وهذا ما يجعل Promise.all أعلاه متوازياً فعلاً بدل أن يكون رحلتين متتاليتين عبر مركز بيانات.
يحتفظ كل وكيل فرعي بقاعدة SQLite منفصلة تماماً. فلا يستطيع وكيل النقد قراءة سجلّ محادثة وكيل البحث ما لم تمرّره صراحةً. هذا العزل ميزة: فهو ما يبقي نافذة سياق كل وكيل مركّزة.
تذكّر تسجيل كل صنف وكيل فرعي في ترحيلات wrangler.jsonc، وإلا فشل الإنشاء أثناء التشغيل:
"migrations": [
{
"new_sqlite_classes": ["Orchestrator", "SearchAgent", "CritiqueAgent"],
"tag": "v1"
}
]الخطوة 8: سلّم التنفيذ وCode Mode
ينظّم Think الحوسبة في خمسة مستويات تصاعدية. والمبدأ التصميمي الذي تعلنه Cloudflare هو أن يكون الوكيل مفيداً عند المستوى 0 وحده، وأن يكون كل مستوى إضافةً محضة.
| المستوى | البيئة | مدعوم بـ | القدرة |
|---|---|---|---|
| 0 | Workspace | @cloudflare/shell | نظام ملفات افتراضي معمّر على SQLite وR2 — قراءة وكتابة وتحرير وبحث ومقارنة |
| 1 | Dynamic Worker | @cloudflare/codemode | JavaScript يولّده الـLLM داخل عزلة محميّة، بلا وصول للشبكة |
| 2 | حلّ حزم NPM | @cloudflare/worker-bundler | يجلب الحزم ويحزمها بـesbuild داخل الـDynamic Worker |
| 3 | المتصفّح | Cloudflare Browser Run | تصفّح بلا واجهة، ونقر، واستخراج، ولقطات شاشة |
| 4 | بيئة معزولة كاملة | Cloudflare Sandbox | نظام تشغيل حقيقي بسلاسل أدوات — git وnpm test وcargo build |
صِل السلّم في إعداد أدوات واحد:
import { Think } from "@cloudflare/think";
import { createWorkspaceTools } from "@cloudflare/think/tools/workspace";
import { createExecuteTool } from "@cloudflare/think/tools/execute";
import { createBrowserTools } from "@cloudflare/think/tools/browser";
import { createSandboxTools } from "@cloudflare/think/tools/sandbox";
export class ResearchAgent extends Think<Env> {
extensionLoader = this.env.LOADER;
getModel() {
/* ... */
}
getTools() {
return {
execute: createExecuteTool({
tools: createWorkspaceTools(this.workspace),
loader: this.env.LOADER,
}),
...createBrowserTools(this.env.BROWSER),
...createSandboxTools(this.env.SANDBOX),
};
}
}كل مستوى بعد المستوى 0 يحتاج ربطاً خاصاً به في wrangler.jsonc — ربط Browser Run للمستوى 3، وربط Sandbox للمستوى 4. أضف فقط المستويات التي تحتاجها فعلاً؛ فكل مستوى يوسّع نطاق الضرر المحتمل من نموذج مخترَق أو مشوَّش.
Code Mode
createExecuteTool هي الأكثر إثارة للاهتمام، وهي تغيّر طريقة استخدام النموذج للأدوات.
الحلقة التقليدية هي استدعاء أداة واحد لكل رحلة ذهاب وإياب إلى النموذج. ابحث عن الملفات، انتظر. اقرأ الملف الأول، انتظر. اقرأ الملف الثاني، انتظر. مسح مئة ملف يكلّف مئة رحلة ومئة تقييم للموجّه.
يستبدل Code Mode ذلك ببرنامج واحد مولَّد يعمل داخل Dynamic Worker معزول:
// النموذج هو من يكتب هذا. ويعمل داخل Dynamic Worker معزول.
const files = await tools.find({ pattern: "**/*.ts" });
const results = [];
for (const file of files) {
const content = await tools.read({ path: file });
if (content.includes("TODO")) {
results.push({ file, todos: content.match(/\/\/ TODO:.*/g) });
}
}
return results;استدعاء واحد للنموذج، وتنفيذ واحد، ونتيجة واحدة. صياغة Cloudflare أن هذا يختصر «100 رحلة ذهاب وإياب إلى النموذج» إلى «تنفيذ برنامج واحد»، بما يستتبعه ذلك من توفير في الرموز.
وما يجعل هذا مقبولاً هو الجانب الأمني. فالشيفرة المولَّدة تعمل داخل Dynamic Worker — عزلة جديدة بلا وصول إلى الشبكة. ولا تصل إلى العالم الخارجي إلا عبر كائن tools الذي مرّرته إلى createExecuteTool. صحيح أن شيفرة ولّدها النموذج ولم تراجعها قط تُنفَّذ، لكن كامل سطح قدرتها محصور في مجموعة الأدوات التي سلّمتها إياها صراحةً.
الخطوة 9: المهام المجدولة
الوكيل الذي لا يتحرّك إلا حين تخاطبه هو نصف وكيل. تعلن getScheduledTasks() أدواراً متكرّرة:
import { defineScheduledTasks } from "@cloudflare/think";
getScheduledTasks() {
return defineScheduledTasks({
dailyBriefing: {
schedule: "every day at 09:00",
timezone: "Africa/Tunis",
prompt: "راجع ملاحظات البحث في الـworkspace واكتب في briefing.md ملخّصاً لما تغيّر منذ الأمس.",
},
hourlyCheck: {
schedule: "every hour",
handler: async ({ idempotencyKey, scheduledFor }) => {
// منطق مخصّص متعدّد الخطوات بدل موجّه واحد
},
},
});
}لغة الجدولة سهلة القراءة عن قصد: every <n> minutes وevery <n> hours وevery day at HH:mm وevery weekday at HH:mm وevery week on monday,wednesday at HH:mm.
توفّر كل مهمة واحداً فقط من prompt أو handler. يُنشئ الـprompt إرسالاً معمّراً يمرّ عبر الحلقة الوكيلية المعتادة. أما الـhandler فينفّذ شيفرتك أنت ويناسب التدفّقات متعدّدة الخطوات التي لا تحتاج إلى النموذج أصلاً.
سلوكان يجدر معرفتهما قبل الاعتماد على هذا:
جداول الساعة الفعلية تتطلّب منطقة زمنية. فأي جدول يتضمّن وقتاً محدّداً يحتاج إلى timezone مضمّن، أو منطقة زمنية على مستوى المهمة، أو تجاوز لـgetDefaultTimezone(). وبدون ذلك لن تُسوَّى المهمة. أما الجداول النسبية مثل every hour فمستثناة.
لا يوجد تعويض للفوائت. فإن كان الـWorker غير متاح في موعد الاستحقاق، ينفّذ Think الحدث المقصود مرة واحدة عند انطلاق التنبيه المتأخّر، ثم يجدول التشغيل التالي في المستقبل. الوكيل الذي بقي خارج الخدمة أسبوعاً لن يستيقظ على سبع إحاطات في الطابور.
من الخصائص الاختيارية لكل مهمة: metadata وretry: { maxAttempts }.
الخطوة 10: خطّافات دورة الحياة
يعرض Think أربع نقاط اعتراض حول كل دور، وهي الموضع الطبيعي للرصد والحواجز الوقائية والإعداد الديناميكي:
beforeTurn(ctx: TurnContext): TurnConfig | void {
console.log(`بدء الدور: ${Object.keys(ctx.tools).length} أداة متاحة`);
}
onChatResponse(result: ChatResponseResult) {
console.log(`الدور ${result.status}: ${result.message.parts.length} جزءاً`);
}تعمل beforeTurn() قبل استدعاء النموذج ويمكنها إرجاع TurnConfig لتجاوز الإعدادات في ذلك الدور فقط — التحوّل إلى نموذج أرخص للاستعلامات البسيطة، أو تقييد مجموعة الأدوات بحسب دور المستخدم، أو تعديل ميزانية الرموز. أما beforeStep() وonStepFinish() فتحيطان بكل خطوة مفردة داخل دور متعدّد الخطوات. وتنطلق onChatResponse() عند اكتمال الدور، نجح أم أخفق.
استبدل استدعاءات console.log بطبقة التسجيل أو التتبّع لديك قبل الوصول إلى الإنتاج — فسجلّات Workers عابرة، وبيانات الرموز وزمن الاستجابة لكل دور هي بالضبط ما ستحتاجه حين تفاجئك التكاليف.
اختبار تنفيذك
تحقّق من كل طبقة على حدة بدل الوثوق بالمنظومة كاملة دفعة واحدة.
البثّ والحفظ الدائم. شغّل npx vite dev، وأرسل رسالة، وتأكّد من وصول الرموز تدريجياً. حدّث المتصفّح تحديثاً قسرياً — يجب أن تُعاد المحادثة من SQLite بدل أن تبدأ فارغة.
الذاكرة. أخبر الوكيل بحقيقة عنك، ثم شغّل npx wrangler dev --remote في جلسة جديدة واسأله عنها. إن اختفت الحقيقة، فتحقّق من أن كتلة سياق memory تعلن description — فبدونها لا يستلم النموذج أداة set_context أبداً ولا يملك وسيلة للكتابة.
استئناف الـfiber. ابدأ مهمة بحث طويلة، ثم أوقف خادم التطوير في منتصف التنفيذ وأعد تشغيله. ينبغي أن تنطلق onFiberRecovered. أضف سطر تسجيل داخل الخطّاف للتأكّد، وافحص ctx.snapshot للتحقّق من أن الـstash يحوي كل ما يلزم للاستئناف.
الوكلاء الفرعيون. استدعِ handleTask() وتأكّد من اكتمال الوكيلين الفرعيين. تحقّق من العزل بالكتابة في جلسة أحدهما والتأكّد من أن الآخر لا يستطيع قراءتها.
المهام المجدولة. اضبط مهمة مؤقتاً على every 2 minutes مع handler يسجّل، ثم انشر وراقب npx wrangler tail. أعِد الجدول الحقيقي بعد ذلك.
انشر حين تجتاز كل طبقة الاختبار:
npx wrangler deployاستكشاف الأخطاء وحلّها
«Cannot use SQL storage on Durable Object class» — الصنف غائب عن new_sqlite_classes في ترحيلاتك، أو سُجّل كـDurable Object غير مدعوم بـSQLite في وسم ترحيل سابق. كل صنف وكيل ووكيل فرعي يجب أن يكون مدرجاً.
الذاكرة تُصفَّر بعد فترات خمول — خزّنت الحالة على نسخة الصنف بدل كتلة سياق. خصائص النسخة لا تنجو من السبات. وكل ما يجب أن يدوم يمرّ عبر configureSession() أو واجهة Session.
الـFibers لا تستأنف أبداً — تأكّد من أنك تستدعي ctx.stash() داخل جسم الـfiber. فالـfiber بلا نقطة استئناف لا شيء لديه ليستأنف منه، وتستقبل onFiberRecovered لقطة فارغة.
المهام المجدولة لا تنطلق أبداً — السبب في الغالب منطقة زمنية مفقودة في جدول بساعة فعلية. أضف timezone مضمّناً أو نفّذ getDefaultTimezone().
فشل اتصال WebSocket في الإنتاج — تحقّق من أن run_worker_first يتضمّن مسار وكيلك. فبدونه يعترض معالج الموارد الثابتة طلب الترقية.
تضارب أسماء الأدوات — يدمج Think الأدوات من سبعة مصادر. فإن توقّفت أداة مخصّصة عن الاستدعاء بصمت، فالأرجح أن خادم MCP أو امتداداً سجّل الاسم نفسه. استخدم نطاق تسمية لأدواتك.
تكاليف رموز أعلى من المتوقّع — أضف withCachedPrompt() إن لم تفعل، وراجع إجمالي عدد أدواتك، وفكّر في نقل تسلسلات الأدوات متعدّدة الخطوات إلى Code Mode.
الخطوات التالية
- أضف خوادم MCP. يدمج Think أدوات MCP في مجموعة الأدوات نفسها، فوصل خادم Model Context Protocol يوسّع الوكيل دون المساس بـ
getTools(). يشرح درسنا حول خوادم MCP كيفية بناء واحد. - قارن بين الهياكل. اقرأ درسنا حول Cloudflare Agents SDK لترى ما الذي يختار Think تجريده.
- أضف طبقة Workflows. للتنسيق الممتدّ عبر عدّة وكلاء وخدمات، تُكمل Cloudflare Workflows — التي رُفعت حدود التزامن فيها كثيراً خلال أسبوع الوكلاء — عملَ الـfibers.
- حصّن سطح الأدوات. الشيفرة التي يولّدها النموذج والوصول الواسع للأدوات يستلزمان حواجز وقائية؛ راجع دليلنا حول حواجز وكلاء الذكاء الاصطناعي وحقن الموجّهات.
- استكشف الامتدادات ذاتية التأليف. يستطيع وكلاء Think كتابة امتداداتهم الخاصة — برامج TypeScript تعمل داخل Dynamic Workers بصلاحيات شبكة وworkspace معلَنة، تُحزَم وتُحمَّل أثناء التشغيل. إنها أكثر زوايا المنصّة تجريبية، وتستحق المتابعة.
الخاتمة
إسهام Project Think ليس تجريداً آخر للمحادثة. إنه إدراك أن الوكيل المفيد عملية طويلة العمر، وأن العمليات طويلة العمر تحتاج بنية تحتية: نقاط استئناف، واسترجاع، وعزل، وذاكرة دائمة، وصلاحيات متدرّجة.
الأسطر الثلاثة التي يتألف منها وكيل Think الأدنى هي العنوان البرّاق، لكن الـfiber المعمّر هو البدائية الحاسمة. فهو ما يفصل بين وكيل يجيب عن أسئلة ووكيل ينجز ساعة من العمل وينجو من إعادة تشغيل المنصّة تحت قدميه.
حالة المعاينة حقيقية — ثبّت إصداراتك وتوقّع اضطراباً في الـAPI قبل الاستقرار. لكن الشكل العام صحيح، والبدائيات الكامنة تحت صنف Think تبقى قابلة للاستخدام منفردة إن لم تناسبك آراؤه.
في نقطة، نبني أنظمة وكلاء ذكاء اصطناعي إنتاجية على Cloudflare وغيرها من منصّات الحافة. إن كنت تقيّم بنية وكلاء معمّرين لفريقك، يسعدنا أن نتحدّث في الأمر.