إعادة صياغة Durable Agents: اختبار استدعاءات الأدوات المكررة (2026)
٢ أكتوبر ٢٠٢٦

ملخص: عميل Restate الذي يتعرض للانهيار بين التأثير الجانبي للأداة واللحظة التي يقوم فيها ctx.run بتسجيل نتيجته يمكن أن يكرر ذلك التأثير الجانبي. في اختباراتي، أدى قتل العامل (worker) في تلك النقطة تحديداً إلى جعل نظام دفع وهمي API يستقبل رسومي في 3 من أصل 3 محاولات.1
أما عمليات القتل قبل التأثير، أو بعد عودة ctx.run، فقد أسفرت عن رسوم واحدة في كل مرة. وقد جعل تمرير مفتاح من ctx.rand.uuidv4() إلى طلب الدفع الطلب الثاني بلا تأثير (no-op) في 3 من أصل 3 محاولات، طالما أن المستلم يحترم هذا المفتاح.
تقول وثائق Restate الخاصة بالذكاء الاصطناعي أن "التأثيرات الجانبية للأدوات لا تتكرر (لا حجوزات مزدوجة، ولا رسائل بريد إلكتروني مكررة)".2 وهذا ينطبق بمجرد تدوين الخطوة في السجل (journaled). يوضح هذا المنشور أين توجد الفجوة، وكيفية قياسها باستخدام SIGKILL، وما يجب إضافته إلى أدواتك.
عملاء Restate المستدامون: الإجابة المختصرة
Restate هو محرك تنفيذ مستدام: يعمل العميل الخاص بك كمعالج (handler)، ويتم تسجيل كل خطوة ctx.run في سجل بحيث يمكن للمعالج الذي تمت إعادة تشغيله تخطي العمل المكتمل.23 هو يحمي الخطوات المكتملة، لكنه لا يستطيع حماية خطوة حدث تأثيرها ولكن لم يتم تسجيل نتيجتها أبداً.
بالنسبة للأدوات التي تنفق أموالاً أو ترسل رسائل، فإن الحل هو مفتاح التكرار (idempotency key). يعيد ctx.rand.uuidv4() نفس القيمة عند إعادة المحاولة، لذا يمكن للنظام API في الطرف الآخر التعرف على التكرار.3
ما ستتعلمه
- ما تعد به وثائق Restate بالنسبة لاستدعاءات أدوات العميل، والمشكلة المفتوحة التي تتحدى ذلك
- كيفية بناء منصة اختبار الانهيار باستخدام LLM وهمي، ونظام دفع API وهمي و SIGKILL
- نتيجة تسعة سيناريوهات، من حلقة بسيطة إلى عميل Restate يستخدم المفاتيح
- لماذا يتكرر التأثير خارج
ctx.runواستدعاء LLM غير المدون في السجل أيضاً - قائمة مراجعة قصيرة لجعل أدوات العميل آمنة لإعادة المحاولة
- ما لم تغطِه هذه الاختبارات
لماذا يتصدر عملاء Restate المستدامون الأخبار
أعلنت Restate عن جولة تمويل Series A بقيمة 20 مليون دولار في 30 سبتمبر 2026، بقيادة Singular، مع استثمارات من Redpoint Ventures و Capital One Ventures أيضاً.4 وقد غطتها الصحافة التجارية في 1 أكتوبر.4
الفكرة الأساسية هي التعافي من الانهيار للعملاء. يقول إعلان Restate أن عميلاً واحداً يمكنه إجراء مئات من استدعاءات النماذج، واستدعاءات الأدوات، والانتظارات وإعادة المحاولات، لذا فإن الانهيار في منتصف الطريق يكون مكلفاً إذا كان يجب بدء التشغيل من جديد.4
تصف وثائق Restate هذه الآلية. تقول صفحة العملاء المستدامين: "يتم إعادة تشغيل الخطوات المكتملة من السجل (بدون إعادة تنفيذ)".2
ثم تذكر نتيجتين:2
- "استدعاءات LLM لا تتكرر (مما يوفر التكلفة والوقت)"
- "التأثيرات الجانبية للأدوات لا تتكرر (لا حجوزات مزدوجة، ولا رسائل بريد إلكتروني مكررة)"
بالنسبة لحلقة TypeScript المكتوبة يدويًا، تقول الصفحة نفسها: "يتم تنفيذ التأثيرات الجانبية مرة واحدة بالضبط. عند الاسترداد، يتم إعادة تشغيل النتيجة."2 تم تغيير تلك الصفحة لآخر مرة في 23 يونيو 2026.2
المشكلة المفتوحة: نافذة زمنية بين التأثير والسجل
في 24 سبتمبر 2026، فتح أحد المستخدمين المشكلة رقم 410 في مستودع وثائق Restate، بعنوان: الوكلاء المستدامون: "التأثيرات الجانبية للأدوات لا تتكرر"، ولكن إجراء ctx.run يُعاد تنفيذه عندما تتوقف العملية بعد الإجراء وقبل تسجيل نتيجته في السجل.5
استخدم المُبلغ وكيل Pydantic AI في Python ومستقبل لا يمكنه إزالة التكرار. قاموا بإنهاء العامل باستخدام SIGKILL بعد أن طبق المستقبل الطلب، وأفادوا بأنه تم تطبيقه مرتين في 30 تجربة من أصل 30.5
كتبوا أن "في تلك الفترة الزمنية، يكون السجل الوحيد للإجراء هو التأثير الذي أحدثه."5 وحتى 2 أكتوبر 2026، لا تزال المشكلة مفتوحة دون أي تعليقات.5
صفحة البنية الخاصة بـ Restate تتوافق مع هذا. فهي تقول إن اللحظة التي يتم فيها "نسخ إدخال سجل الخطوة إلى النصاب القانوني (quorum) تحدد 'أن الخطوة قد حدثت'. ومنذ ذلك الحين، سيتم استرداد الخطوة عند إعادة المحاولة ولن يتم إعادة تنفيذها."6
يصف تعليق في دليل قواعد البيانات على نفس الموقع، حول تحديث غير مغلف بـ ctx.run، "نافذة زمنية صغيرة جدًا حيث يتم إعادة تنفيذ الاستعلام بعد النجاح."6
إذن الضمان حقيقي، ويبدأ عند كتابة السجل. بحثت في صفحة الذكاء الاصطناعي عن كلمات "window"، و"at-least-once" و"idempotency" ولم أجد أيًا منها. أردت أن أرى ذلك بنفسي، في TypeScript، مع الإصدار الحالي.
منصة الاختبار: LLM وهمي، مدفوعات وهمية، SIGKILL
استخدمت @restatedev/restate-server 1.7.13 (نُشر في 1 أكتوبر 2026) و @restatedev/restate-sdk 1.17.2 (نُشر في 21 سبتمبر 2026).1 لا يوجد نموذج حقيقي في الحلقة. يطلب "LLM" وهمي مبرمج إجراء charge، ثم send_email، ثم يجيب.
يقوم خادم صغير بدور النموذج الوهمي وواجهات برمجة تطبيقات المدفوعات والبريد الإلكتروني الوهمية. يقوم بتسجيل كل طلب في events.jsonl، ولا يقوم بإزالة التكرار إلا عندما يحمل الطلب ترويسة Idempotency-Key.
mkdir restate-agent-test && cd restate-agent-test
npm init -y
npm i @restatedev/restate-server@1.7.13 @restatedev/restate-sdk@1.17.2 tsx typescript @types/node
احفظ هذا باسم mocks.ts:
// Mock LLM + mock "payments/email" receiver on one port. Every hit is appended to events.jsonl.
import http from "node:http";
import fs from "node:fs";
const LOG = process.env.EVENTS_LOG!;
const seen = new Map<string, unknown>();
const log = (e: object) => fs.appendFileSync(LOG, JSON.stringify({ ...e, at: Date.now() }) + "\n");
http
.createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
const input = body ? JSON.parse(body) : {};
const send = (o: object) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(o));
};
if (req.url === "/llm") {
// scripted "model": 0 tool results -> charge, 1 -> send_email, 2 -> final answer
const n = input.toolResults as number;
log({ t: "llm", toolResults: n });
if (n === 0) return send({ type: "tool", tool: { name: "charge", args: { amount: 49 } } });
if (n === 1) return send({ type: "tool", tool: { name: "send_email", args: { to: "a@example.com" } } });
return send({ type: "final", text: "Charged $49 and emailed the receipt." });
}
if (req.url === "/charge" || req.url === "/send_email") {
const kind = req.url.slice(1);
const key = req.headers["idempotency-key"] as string | undefined;
if (key && seen.has(key)) {
log({ t: kind, key, applied: false });
return send(seen.get(key) as object);
}
const out = { id: `${kind}_${Math.random().toString(36).slice(2, 8)}` };
if (key) seen.set(key, out);
log({ t: kind, key: key ?? null, applied: true });
return send(out);
}
res.statusCode = 404;
res.end();
});
})
.listen(7001, "127.0.0.1");
الآن الوكيل، agent.ts. له نفس شكل حلقة TypeScript في وثائق Restate: إجراء ctx.run واحد لكل استدعاء للنموذج وواحد لكل استدعاء للأداة.2 يتحكم متغيران للبيئة في التجربة: يحدد MODE كيفية تغليف الأدوات، ويحدد CRASH المكان الذي تنهي فيه العملية نفسها، لمرة واحدة.
import * as restate from "@restatedev/restate-sdk";
import fs from "node:fs";
const MODE = process.env.MODE ?? "journaled"; // journaled | key | bare
const CRASH = process.env.CRASH ?? "none"; // none | before_effect | after_effect | after_tool_returned | llm_after_call
const MARKER = process.env.CRASH_MARKER!;
const BASE = "http://127.0.0.1:7001";
function crashOnce(point: string) {
if (CRASH !== point || fs.existsSync(MARKER)) return;
fs.writeFileSync(MARKER, point);
process.kill(process.pid, "SIGKILL"); // no cleanup, no goodbye
}
async function post(path: string, body: object, key?: string) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "content-type": "application/json", ...(key ? { "idempotency-key": key } : {}) },
body: JSON.stringify(body),
});
return (await r.json()) as any;
}
async function callTool(name: string, args: object, key?: string) {
if (name === "charge") crashOnce("before_effect");
const out = await post("/" + name, args, key);
if (name === "charge") crashOnce("after_effect");
return out;
}
const agent = restate.service({
name: "agent",
handlers: {
run: async (ctx: restate.Context, input: { task: string }) => {
let toolResults = 0;
for (let turn = 0; turn < 6; turn++) {
const decision = await ctx.run(`llm turn ${turn}`, async () => {
const d = await post("/llm", { task: input.task, toolResults });
if (turn === 0) crashOnce("llm_after_call");
return d;
});
if (decision.type === "final") return decision.text as string;
const { name, args } = decision.tool;
if (MODE === "bare") {
await callTool(name, args); // NOT wrapped in ctx.run: re-executes on every replay
} else {
const key = MODE === "key" ? ctx.rand.uuidv4() : undefined;
await ctx.run(`tool ${name}`, () => callTool(name, args, key));
}
if (name === "charge") crashOnce("after_tool_returned");
toolResults++;
}
return "gave up";
},
},
});
restate.serve({ services: [agent], port: 9080 });
يجعل ملف العلامة (marker file) كل انهيار يحدث مرة واحدة، بحيث يكمل العامل الذي تمت إعادة تشغيله عملية التشغيل. بالنسبة للخط المرجعي بدون بيئة تشغيل مستدامة، فإن naive.ts هو نفس الحلقة ككود عادي:
// The same loop with no durable runtime: a crash means "start over".
import fs from "node:fs";
const CRASH = process.env.CRASH ?? "none";
const MARKER = process.env.CRASH_MARKER!;
const BASE = "http://127.0.0.1:7001";
const post = async (p: string, b: object) =>
(await (await fetch(BASE + p, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(b) })).json()) as any;
async function loop(task: string) {
let toolResults = 0;
for (let turn = 0; turn < 6; turn++) {
const d = await post("/llm", { task, toolResults });
if (d.type === "final") return d.text;
await post("/" + d.tool.name, d.tool.args);
if (d.tool.name === "charge" && CRASH === "after_tool_returned" && !fs.existsSync(MARKER)) {
fs.writeFileSync(MARKER, "x");
process.kill(process.pid, "SIGKILL");
}
toolResults++;
}
}
loop("refund order 1042").then((t) => { console.log("RESULT " + t); });
كيفية تشغيل سيناريو انهيار واحد
استخدم أربع نوافذ terminal في مجلد المشروع. في الأولى، ابدأ الخادم. لم يكن لدى بيئة الاختبار (sandbox) الخاصة بي IPv6، لذا قمت بربط كل شيء بعنوان loopback؛ أما في الجهاز العادي، فإن npx restate-server يعمل بشكل طبيعي.
RESTATE_BIND_ADDRESS=127.0.0.1:5122 RESTATE_INGRESS__BIND_ADDRESS=127.0.0.1:8080 \
RESTATE_ADMIN__BIND_ADDRESS=127.0.0.1:9070 npx restate-server --no-logo
في الثانية، ابدأ المستقبل الوهمي (mock receiver). في الثالثة، قم بتشغيل العميل (agent) ضمن حلقة تكرارية تعيد تشغيله، بالطريقة التي يفعلها المنسق (orchestrator) بعد حدوث عطل:
# terminal 2
EVENTS_LOG=events.jsonl npx tsx mocks.ts
# terminal 3
export MODE=journaled CRASH=after_effect CRASH_MARKER=/tmp/crashed
rm -f "$CRASH_MARKER"
while true; do npx tsx agent.ts; echo "worker exited, restarting"; sleep 0.4; done
في الرابعة، قم بتسجيل الخدمة، واستدعِ العميل، واحسب ما رآه المستقبل:
curl -s 127.0.0.1:9070/deployments -H 'content-type: application/json' -d '{"uri":"http://127.0.0.1:9080"}' > /dev/null
curl -s 127.0.0.1:8080/agent/run -H 'content-type: application/json' -d '{"task":"refund order 1042"}'
jq -s '{llm: map(select(.t=="llm"))|length, charges_received: map(select(.t=="charge"))|length, charges_applied: map(select(.t=="charge" and .applied))|length}' events.jsonl
يستخدم دليل البدء السريع الخاص بـ Restate نفس المنافذ: واجهة الإدارة على 9070، والخدمة على 9080، والمدخل (ingress) على 8080.2 لتجربة صف آخر من الجدول أدناه، أوقف terminal 3، وقم بتغيير MODE و CRASH، واحذف events.jsonl والعلامة (marker)، ثم ابدأ من جديد.
النتائج: تسعة سيناريوهات، ثلاث تجارب لكل منها
لقد قمت بتشغيل المصفوفة بأكملها ثلاث مرات، مع مجلد بيانات Restate جديد وخوادم وهمية جديدة لكل سيناريو.1 أعطى كل صف نفس الأعداد في جميع التجارب الثلاث.

المخرجات الحقيقية لأداة الاختبار في 2 أكتوبر 2026، مع حذف أعمدة النتيجة وإعادة التشغيل. "الخصومات المستلمة" تحسب الطلبات التي وصلت إلى نظام الدفع الوهمي API. المربع الأحمر: خصم مكرر. الأخضر: إصلاح المفتاح. البرتقالي: تكرارات أخرى.1
| السيناريو | MODE / CRASH | استدعاءات LLM | الخصومات المستلمة | الخصومات المطبقة |
|---|---|---|---|---|
| S0 حلقة عادية، بدون عطل | n/a | 3 | 1 | 1 |
| S1 حلقة عادية، إيقاف بعد الخصم | after_tool_returned | 4 | 2 | 2 |
| S2 Restate، بدون عطل | journaled / none | 3 | 1 | 1 |
| S3 Restate، إيقاف قبل طلب الخصم | journaled / before_effect | 3 | 1 | 1 |
| S4 Restate، إيقاف بعد الخصم، قبل السجل | journaled / after_effect | 3 | 2 | 2 |
S5 Restate، إيقاف بعد عودة ctx.run | journaled / after_tool_returned | 3 | 1 | 1 |
| S6 نفس إيقاف S4، مع مفتاح idempotency | key / after_effect | 3 | 2 | 1 |
S7 Restate، خصم خارج ctx.run، إيقاف بعده | bare / after_tool_returned | 3 | 2 | 2 |
| S8 Restate، إيقاف بعد الرد على استدعاء LLM 0 | journaled / llm_after_call | 4 | 1 | 1 |
كل صف من صفوف Restate قام أيضاً بتطبيق رسالة إلكترونية واحدة بالضبط، وأعادت كل عملية تشغيل الإجابة النهائية. في كل صف من صفوف أعطال Restate، اكتمل الاستدعاء من تلقاء نفسه بمجرد أن أعادت حلقة إعادة التشغيل الخاصة بي تشغيل العامل (worker).1
ماذا تظهر النتائج
الحلقة العادية تبدأ من جديد. بعد الإيقاف الذي يلي الخصم، طلبت الحلقة المعاد تشغيلها من النموذج مرة أخرى، وخصمت مرة أخرى وانتهت بأربعة استدعاءات للنموذج وخصمين. هذه هي المشكلة التي وجد التنفيذ المستدام (durable execution) لحلها.
تم استئناف Restate من السجل (journal). مع حدوث الإيقاف (kill) قبل طلب الخصم، أو بعد عودة ctx.run، لم يكرر العامل (worker) الذي أعيد تشغيله أي خطوة مكتملة. ظلت استدعاءات النموذج عند ثلاثة وتم تطبيق الخصم مرة واحدة.
الفجوة تكمن بين التأثير والسجل. في صف "الإيقاف بعد التأثير"، وصل الخصم إلى المستلم، ومات العامل قبل أن يتمكن ctx.run من تسجيل النتيجة، وقام المعالج الذي أعيد تشغيله بتنفيذ الإجراء مرة أخرى. تلقى نظام الدفع API خصمين وطبقهما معاً.
هذا يتطابق مع المشكلة رقم 410، التي وُجدت هناك باستخدام عميل Python و30 تجربة.5 تجربتي تعيد إنتاج ذلك باستخدام SDK الخاص بـ TypeScript وحلقة تكرارية مكتوبة يدوياً، في 3 من أصل 3 تجارب، بالإضافة إلى 5 من أصل 5 تجارب سابقة أثناء بناء النظام.1
نوافذ الانهيار الثلاث
إليك استدعاء أداة واحد، مع تحديد نقاط الإيقاف.

نقاط الإيقاف الثلاث في المصفوفة. النافذة B فقط هي التي تكرر التأثير.
النافذة A تكون قبل مغادرة الطلب للعامل. لم يحدث شيء، لذا فإن إعادة المحاولة هي المحاولة الأولى. النافذة C تكون بعد عودة await ctx.run(...). تم تسجيل الخطوة في السجل، لذا يتخطاها إعادة التشغيل (replay).
النافذة B هي التي تقع في المنتصف. الدليل الوحيد على الخصم هو الخصم نفسه، وهذا هو محور نقطة مُبلغ المشكلة.5 مدى اتساع هذه النافذة في الحياة الواقعية يعتمد على شبكتك، والمضيف الخاص بك، وكيفية موت العامل. تشير المشكلة إلى أن الوسيط كان حوالي 16 مللي ثانية في إعداداتهم.5
تضع صفحة بنية Restate الخط في نفس المكان: الخطوة تكون قد "حدثت" بمجرد نسخ إدخال السجل الخاص بها إلى النصاب القانوني (quorum)، وليس قبل ذلك.6 اختباراتي لم تقِس عرض النافذة، بل فقط أنها موجودة.
الحل: مفتاح تكرار (idempotency key) من ctx.rand.uuidv4()
تقول وثائق Restate أن المساعدات العشوائية الخاصة بها "يتم تحديد بذرها (seeded) بواسطة معرف الاستدعاء" وتعيد "نفس النتيجة عند إعادة المحاولة". ويقترحون استخدام ctx.rand.uuidv4() للحصول على "معرفات UUID مستقرة لأشياء مثل مفاتيح التكرار".3
هذا بالضبط ما يحتاجه استدعاء الأداة عند إعادة المحاولة. في الصف الذي يستخدم المفتاح، يقوم العميل بتوليد المفتاح خارج ctx.run، ثم يرسله كترويسة Idempotency-Key:
const key = MODE === "key" ? ctx.rand.uuidv4() : undefined;
await ctx.run(`tool ${name}`, () => callTool(name, args, key));
مع نفس الإيقاف السابق، رأى المستلم طلبي خصم بنفس المفتاح. قام بتطبيق الأول وأجاب على الثاني من سجله، لذا تم تطبيق خصم واحد في 3 من أصل 3 تجارب.1
أعاد المعالج الذي تم تشغيله استخدام المفتاح دون أي تخزين من جانبي. هذه هي الخاصية التي تريدها: يقوم إعادة التشغيل (replay) بحساب نفس المفتاح، لذا يمكن للخدمة النهائية التمييز بين التكرار وبين خصم جديد.
هناك ملاحظة واحدة. المفتاح يساعد فقط إذا كانت الخدمة في الطرف الآخر تحترمه. المستلم الوهمي الخاص بي يفعل ذلك؛ أما نظام دفع أو بريد إلكتروني أو نظام تذاكر API حقيقي فقد يفعل ذلك أو لا، لذا تحقق من الوثائق الخاصة به قبل الاعتماد على هذا الحل.
فخان آخران: التأثيرات المجردة واستدعاءات النماذج غير المسجلة
التأثيرات خارج ctx.run تتكرر عند كل إعادة تشغيل (replay). في الصف المجرد، كانت عملية الخصم عبارة عن fetch بسيطة في المعالج (handler). بعد عملية القتل (kill)، قام Restate بإعادة تشغيل المعالج من البداية وتم تنفيذ عملية الخصم مرة أخرى. الخطوات المسجلة في السجل (Journaled steps) يتم تخطيها، ولكن الكود الموجود خارجها يعمل مرة أخرى.
استدعاء LLM غير مسجل يتم دفعه مرتين. في الصف الأخير، قمت بقتل العامل (worker) بعد عودة استدعاء النموذج الأول ولكن قبل أن يقوم ctx.run بتسجيله. قام المعالج الذي أعيد تشغيله باستدعاء النموذج مرة أخرى، لذا سجل المستقبل أربعة استدعاءات للنموذج بدلاً من ثلاثة.
هذه هي نفس النافذة الزمنية الخاصة بالخصم، ولكنها طبقت على النموذج. تقول الوثائق "استدعاءات LLM لا تتكرر"؛ وبالنسبة للاستدعاءات التي تم تسجيلها، فهذا ما رأيته.21
نموذجي الوهمي (mock model) مبرمج مسبقاً، لذا أعطى نفس الإجابة في المرة الثانية. قد يجيب نموذج حقيقي بشكل مختلف عند الاستدعاء المكرر. لم أختبر ذلك، ولم أستخدم نموذجاً حقيقياً.
إذا كنت تحسب تكلفة هذا، فإن منشوري حول التحكم في تكلفة وكيل الذكاء الاصطناعي وحدود الجلسة يغطي جانب الإنفاق. استدعاء واحد مكرر للنموذج هو أمر بسيط، لكنه ليس صفراً.
قائمة مراجعة قصيرة لأدوات الوكيل الآمنة لإعادة المحاولة
- غلف كل تأثير جانبي بـ
ctx.run. أي شيء خارجها يمكن أن يعمل مرة أخرى عند إعادة التشغيل. - امنح كل أداة غير متماثلة (non-idempotent) مفتاح تماثل (idempotency key) من
ctx.rand.uuidv4()، يتم توليده خارج إغلاق (closure)ctx.run. - تأكد من أن الـ API في الطرف الآخر يحترم المفتاح. إذا لم يفعل، أضف عملية بحث خاصة بك، مثل سجل مفتاحه نفس القيمة، قبل تكرار الإجراء.
- ضع ميزانية لاستدعاء نموذج مكرر بعد الانهيار. في اختباراتي، كلف الانهيار استدعاءً واحداً.
- اختبر باستخدام SIGKILL حقيقي عند كل نقطة. الجهاز المستخدم أعلاه يتكون من 123 سطراً من TypeScript.
- راقب إعدادات إعادة المحاولة. يقوم Restate بإعادة محاولة خطوات
ctx.runالفاشلة ويمكنه تحديدها باستخدامmaxRetryAttemptsوالخيارات ذات الصلة.3
بالنسبة لإعادة المحاولات، والمهل الزمنية (timeouts) والتوجيه في إطار عمل مختلف، أجريت مجموعة مماثلة من الاختبارات على Google ADK 2.0 Workflow. وللجانب الهيكلي لموثوقية الوكيل، راجع موثوقية وكيل الذكاء الاصطناعي وحلقات التحقق.
حدود هذه الاختبارات
هذه اختبارات على عقدة واحدة (single-node) في بيئة Linux sandbox واحدة مع نموذج مبرمج، و API دفع وهمي، و SIGKILL كخطأ وحيد. اختبرت خادم Restate إصدار 1.7.13 و TypeScript SDK إصدار 1.17.2 فقط.
لم أختبر Python SDK، أو تكامل Vercel AI SDK، أو عنقود متعدد العقد (multi-node cluster)، أو عاملاً متجمداً، أو انهيار خادم Restate نفسه. يصف مقدم البلاغ حالة عامل متجمد؛ لم أقم بتشغيلها.5
كذلك لم أختبر محركات تنفيذ مستدامة (durable execution engines) أخرى، لذا لا يمكنني القول كيف تتصرف في نفس النافذة الزمنية.
الخلاصة
يقوم Restate بما يعد به بالنسبة للخطوات المكتملة: في اختباراتي، لم يتم تشغيل أي شيء تم تسجيله في السجل مرتين. الفجوة تكمن في الخطوة التي نفذت الفعل ولكن لم يتم تسجيلها بعد.
إذا كان وكيلك يقوم بخصم مبالغ من البطاقات، أو إرسال رسائل بريد إلكتروني، أو فتح تذاكر دعم، فأضف مفتاح تماثل إلى تلك الأدوات وقم بإيقاف العامل الخاص بك لإثبات أنها تعمل. بالنسبة للأدوات المخصصة للقراءة فقط واستدعاءات النماذج، فإن هذه الفجوة تكلف وقتاً ورموزاً (tokens)، وليس مالاً.
Footnotes
-
Author's measurement, October 2, 2026: Node 22.22.0 on Linux x86_64;
@restatedev/restate-server1.7.13 (npm publish date 2026-10-01) and@restatedev/restate-sdk1.17.2 (npm publish date 2026-09-21), dates fromnpm view <package> time. A fresh Restate server data folder and fresh mock servers per scenario; the full nine-scenario matrix run three times, with identical counts in every trial. The two key rows (kill after the effect, with and without a key) were also run five times each earlier while building the rig, with the same counts. The code in this post was extracted from the finished post and re-run in a fresh folder. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 -
Restate docs, "Durable Agents" page (docs.restate.dev/ai/patterns/durable-agents); quotations checked against its source,
docs/ai/patterns/durable-agents.mdxon the restatedev/docs-restate GitHub main branch, read 2026-10-02. The last commit to that file was dated June 23, 2026. Port numbers: Agent Quickstart, fetched 2026-10-02. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Restate docs, "Durable Steps" (TypeScript), quotations checked against
docs/develop/ts/durable-steps.mdxon GitHub main, read 2026-10-02. ↩ ↩2 ↩3 ↩4 ↩5 -
Restate raises $20M Series A — Restate blog, dated September 30, 2026 as displayed, fetched 2026-10-02; investor list and date also checked against The AI Insider's October 1, 2026 report, fetched 2026-10-02. ↩ ↩2 ↩3
-
Issue #410, restatedev/docs-restate, opened 2026-09-24 by keshav9926; state, comment count and text read through the GitHub API on 2026-10-02 (open, 0 comments). The 30-of-30 result and the roughly 16 ms median are the reporter's own measurements on Restate server 1.7.10 and Python SDK 1.0.5; I did not reproduce the Python setup. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Restate docs, "Architecture" (step "Durable step commit (ctx.run)") and the database guide's code comment in
docs/guides/databases.mdx, both read on the docs-restate GitHub main branch on 2026-10-02. The quoted database-guide sentence spans two comment lines in the source; I removed the comment markers. ↩ ↩2 ↩3 ↩4



