واجهة الوكلاء
وكلاء يستدعون النماذج والأدوات نيابة عنك، تشغّلهم المنصة
الإصدار التجريبي
لا تستقبل واجهة الوكلاء الطلبات بعد: حتى يفتحها فريق MindLab تُجيب مساراتها بالرمز 503. تصف هذه الصفحة العقد الذي ستعمل به.
واجهة الوكلاء في مرحلة تجريبية. يحتاج المفتاح إلى النطاق agents، ويمنحه فريق MindLab. والبتّ في الموافقات عبر الواجهة يحتاج النطاق agents.approve على مفتاح غير المفتاح الذي بدأ التشغيل.
في الإصدار التجريبي (0.1.0) تضيف التغييرات حقولاً ومسارات، ولا تعيد تسمية شيء ولا تحذفه. ويسرد سجل التغييرات ما تغيّر.
عنوان القاعدة https://api.mindlabsa.com/v1 والمصادقة هما نفسهما في واجهة المحادثة: Authorization: Bearer <API key> مع كل طلب، عدا الخطاف الموقّع POST /v1/hooks/{trigger_id}. التشغيلات التي يبدؤها مفتاح تتصرف باسمه: لا تستخدم إلا النماذج التي يحق له استدعاؤها.
يُرفض دائماً المفتاح الذي لا يملك agents.approve عند البتّ في موافقة، ويُرفض دائماً المفتاح الذي لا يملك agents في كل مسارات الـ Webhooks وفي كل مسارات المشغّلات (القراءة والكتابة على السواء؛ أما الخطاف الموقّع الوارد فلا يأخذ مفتاحاً) وفي ملخّص الاستخدام. أما في بقية المسارات فرفض المفتاح الذي لا يملك agents قيد التفعيل وقد لا يسري بعد: وحتى يسري يُخدَم هذا المفتاح. امنح كل مفتاح النطاقات التي يحتاجها فقط، ولا تعتمد على هذا الرفض لإبعاد مفتاح عن تشغيلات وكلائك.
المفاهيم
| المفهوم | ما هو |
|---|---|
| agent | وكيل له اسم وslug وحالة (active أو archived) وإصدار حالي تستخدمه التشغيلات الجديدة. وحين يُوقف فريق تشغيل المنصة الوكيل أو حسابك كله مؤقتاً، يذكر ذلك الحقل operator_hold ولا تبدأ تشغيلاته (agent_held). |
| version | لقطة ثابتة لا تتغير من سلوك الوكيل: موجّه النظام والنموذج والأدوات والسياسة والميزانيات. يستخدم التشغيل الإصدار الحالي عند بدئه، والإصدار الجديد يرث كل حقل لم ترسله. |
| thread | محادثة تضيف إليها كل التشغيلات التي تبدأ عليها. تخص وكيلاً واحداً، ولا يجري فيها إلا تشغيل واحد في كل مرة (thread_busy). التشغيل الذي يبدأ دون thread_id يبدأ محادثة جديدة. |
| run | تنفيذ واحد لإصدار: يبدأ queued، ثم running، وقد ينتظر waiting_approval، وينتهي succeeded أو failed أو budget_exceeded أو cancelled. القيمتان waiting_input وsleeping محجوزتان. |
| step | خطوة محفوظة من التشغيل بترتيب seq، ونوعها assistant (رد النموذج) أو tool_call أو tool_result أو system أو approval. |
| approval | استدعاء أداة ينتظر قرار شخص، فيتوقف التشغيل في waiting_approval حتى يُوافَق عليه أو يُرفض، أو تنتهي مهلته بعد approval_ttl_seconds من سياسة الإصدار. |
| tool | أداة من المنصة (builtin) أو أداة HTTP تسجّلها أنت (http)، ولكلٍّ مستوى خطورة: read أو write أو irreversible. |
| connection | سر محفوظ ترسله أداة HTTP مع كل استدعاء (bearer أو ترويسة تسمّيها). لا يُعاد السر أبداً. |
| trigger | يبدأ تشغيلات وكيل وفق جدول (cron) أو عند وصول طلب موقّع (webhook). كل انطلاق يُسجَّل مع نتيجته. |
| webhook | نقطة استقبال تُرسل إليها المنصة الأحداث التي تختارها، موقّعة بسرّها. |
مستويات الخطورة ومتى يلزم الموافقة
| المستوى | الأثر |
|---|---|
| read | يجري دون موافقة، إلا إن ورد اسمه في require_approval. |
| write | يحتاج موافقة، إلا إن كانت unattended_writes تساوي true. وبعد أن يقرأ التشغيل محتوى من خارج المنصة تحتاج كتاباته موافقة ما لم تكن writes_after_external_content تساوي allow. |
| irreversible | يحتاج موافقة دائماً. |
البداية السريعة
أنشئ وكيلاً، ثم ابدأ تشغيلاً وتابعه. لا تحتاج أي حزمة: الواجهة HTTP عادية.
حتى تُفتح الواجهة تُجاب هذه الطلبات بالرمز 503.
# 1. Create an agent (it gets its version 1)
curl https://api.mindlabsa.com/v1/agents \
-H "Authorization: Bearer $QIRNAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support triage",
"system_prompt": "صنّف رسالة العميل واقترح رداً قصيراً."
}'
# 2. Start a run: answered 202 at once, with the queued run's id
curl https://api.mindlabsa.com/v1/agents/$AGENT_ID/runs \
-H "Authorization: Bearer $QIRNAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "input": { "message": "فاتورتي الأخيرة بمبلغ خاطئ." } }'
# 3. Follow it as server-sent events (or poll GET /v1/runs/$RUN_ID)
curl -N https://api.mindlabsa.com/v1/runs/$RUN_ID/events \
-H "Authorization: Bearer $QIRNAS_API_KEY"الوكلاء وإصداراتهم
/v1/agentsagents/v1/agentsagents/v1/agents/{id}agents/v1/agents/{id}/versionsagents/v1/agents/{id}/versionsagentsالتشغيلات
/v1/agents/{id}/runsagents/v1/agents/{id}/runsagents/v1/runsagents/v1/runs/{id}agents/v1/runs/{id}/stepsagents/v1/runs/{id}/cancelagents/v1/runs/{id}/eventsagentsالاستخدام
/v1/agents/usageagentsالمحادثات
/v1/threads/{id}agents/v1/threads/{id}/messagesagentsالموافقات
/v1/approvalsagents/v1/approvals/{id}agents/v1/approvals/{id}/approveagents.approve/v1/approvals/{id}/rejectagents.approveقواعد البتّ في الموافقة
| الحالة | الرد |
|---|---|
| الموافقة غير موجودة أو تخص مالكاً آخر | 404 approval_not_found |
المفتاح لا يملك agents.approve | 403 scope_required |
| المفتاح هو الذي بدأ التشغيل | 403 permission_denied: يجب أن يقرر مفتاح آخر أو شخص |
| القرار نفسه مرة أخرى | 200 بالموافقة دون تغيير |
| القرار الآخر بعد البتّ | 409 approval_decided |
| بعد انتهاء المهلة | 409 approval_expired |
| أُلغيت أو انتهى تشغيلها | 409 approval_closed |
# With a key that holds agents.approve and did not start the run
curl https://api.mindlabsa.com/v1/approvals/$APPROVAL_ID/approve \
-H "Authorization: Bearer $APPROVER_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Checked the amount." }'الأدوات والاتصالات
/v1/toolsagents/v1/toolsagents/v1/tools/{name}/revisionsagents/v1/tools/{name}/disableagents/v1/tools/{name}agents/v1/tools/connectionsagents/v1/tools/connectionsagents/v1/tools/connections/{id}agents/v1/tools/connections/{id}agents/v1/tools/connections/{id}/secretagentsالمشغّلات والخطافات
/v1/triggersagents/v1/triggersagents/v1/triggers/{id}agents/v1/triggers/{id}agents/v1/triggers/{id}agents/v1/triggers/{id}/rotate-secretagents/v1/triggers/{id}/firesagents/v1/hooks/{trigger_id}مشغّل cron يحمل جدولاً بخمسة حقول ومنطقة زمنية IANA. ومشغّل webhook يُطلَق بطلب موقّع إلى POST /v1/hooks/{trigger_id} (انظر التوقيع أدناه). قيمة overlap تحدد ما يحدث إن كان التشغيل السابق ما زال جارياً: skip أو allow.
نقاط الاستقبال وعمليات التسليم
/v1/webhooksagents/v1/webhooksagents/v1/webhooks/{id}agents/v1/webhooks/{id}agents/v1/webhooks/{id}agents/v1/webhooks/{id}/rotate-secretagents/v1/webhooks/{id}/testagents/v1/webhooks/{id}/deliveriesagents/v1/webhooks/{id}/deliveries/{delivery_id}agents/v1/webhooks/{id}/deliveries/{delivery_id}/replayagentsتستقبل نقطة الاستقبال الأحداث run.completed وrun.failed وapproval.requested وapproval.decided وtrigger.fired، وwebhook.test للتجربة. جسم كل حدث {id, type, created_at, data}. تحمل كل محاولة وكل إعادة إرسال نفس webhook-id، فاستخدمه لتجاهل ما وصلك من قبل.
البث (SSE)
يرسل GET /v1/runs/{id}/events إطار data: <JSON> لكل حدث:
| الإطار | متى |
|---|---|
| run.status | أولاً، ثم عند كل تغيّر في الحالة. |
| step.completed | لكل خطوة محفوظة، بترتيب seq. |
| approval.requested | بعد خطوة approval تطلب موافقة (تظهر الموافقة كما كانت حينها: معلّقة). |
| approval.decided | بعد خطوة approval تغلقها. |
| run.completed | نجح التشغيل؛ يليه data: [DONE]. |
| run.failed | انتهى التشغيل بأي حالة نهائية أخرى؛ يليه data: [DONE]. |
: keep-alive
data: {"type":"run.status","status":"running"}
data: {"type":"step.completed","step":{"seq":1,"kind":"assistant", ...}}
data: {"type":"run.completed","run":{"id":"…","status":"succeeded", ...}}
data: [DONE]- إن لم يحدث شيء لمدة 15 ثانية يرسل البث سطر تعليق
: keep-alive: تجاهل الأسطر التي تبدأ بـ:. - ينتهي البث دون
[DONE]بعد ساعة، أو عند إعادة تشغيل الخادم: أعد الاتصال. - كل اتصال يعيد إرسال كل الخطوات من الأولى، فاحتفظ بآخر
seqوصلك وتجاهل ما سبقه. - يُبقي كل خادم 20 بثاً مفتوحاً على الأكثر لكل مفتاح؛ وبعدها يُجاب 429
too_many_streams.
// Follow a run to its end. A stream that closes without [DONE] is reconnected;
// each connection replays every step, so the steps already seen are dropped by seq.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function follow(runId, onEvent) {
let lastSeq = 0;
for (;;) {
const res = await fetch(`https://api.mindlabsa.com/v1/runs/${runId}/events`, {
headers: { Authorization: `Bearer ${process.env.QIRNAS_API_KEY}` },
});
if (res.status === 429) {
await sleep(Number(res.headers.get("retry-after") ?? 1) * 1000);
continue;
}
if (!res.ok) {
const body = await res.json().catch(() => null); // a 503 page from in front of the API is not JSON
throw new Error(`${res.status} ${body?.error?.code ?? body?.error?.type ?? res.statusText}`);
}
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
let replayed = false; // the last step frame was one already seen
for (;;) {
const { value, done } = await reader.read();
if (done) break; // closed without [DONE]: reconnect
buffer += value;
let end;
while ((end = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
for (const line of frame.split("\n")) {
if (!line.startsWith("data: ")) continue; // ": keep-alive" and other comments
const data = line.slice(6);
if (data === "[DONE]") return;
const event = JSON.parse(data);
if (event.type === "step.completed") {
replayed = event.step.seq <= lastSeq;
if (!replayed) lastSeq = event.step.seq;
} else if (!event.type.startsWith("approval.")) {
replayed = false; // approval frames follow their approval step
}
if (!replayed) onEvent(event);
}
}
}
}
}التوقيع
الأحداث التي تُرسل إلى نقاط الاستقبال والطلبات التي ترسلها أنت إلى POST /v1/hooks/{trigger_id} موقّعة بطريقة Standard Webhooks، بثلاث ترويسات:
| الترويسة | القيمة |
|---|---|
| webhook-id | معرّف الحدث، نفسه في كل إعادة محاولة (في الخطاف: من 1 إلى 128 محرف ASCII قابل للطباعة تختاره أنت). |
| webhook-timestamp | ثواني Unix. يقبل الخطاف طابعاً زمنياً في حدود 300 ثانية من ساعة الخادم. |
| webhook-signature | v1,<base64>، وقد تُرسل عدة توقيعات تفصل بينها مسافات (توقيع لكل سر أثناء التدوير). |
التوقيع هو HMAC-SHA256 للنص <webhook-id>.<webhook-timestamp>.<body> بمفتاح هو بايتات السر بعد whsec_ (جزء base64 بعد فك ترميزه)، مرمّزاً بـ base64. احسبه على الجسم الخام كما وصل، قبل تحليل JSON، وقارن بزمن ثابت.
التحقق من حدث وصل إلى نقطة استقبالك
بمكتبة crypto في Node ومكتبة hmac في Python، دون أي حزمة إضافية:
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: the endpoint's whsec_... secret. body: the raw request body, before JSON.parse.
export function verifyWebhook(secret, headers, body) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures || !/^[0-9]+$/.test(timestamp)) return false;
// Refuse stale timestamps (the platform allows 300 s on hooks).
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest();
// Several space-separated signatures can arrive (one per secret during a rotation).
return signatures.split(" ").some((entry) => {
const [version, value] = entry.split(",");
if (version !== "v1" || !value) return false;
const given = Buffer.from(value, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}إطلاق مشغّل webhook من خدمتك
الجسم JSON بحجم 64 KiB على الأكثر، بنوع application/json. التوقيع الخاطئ أو المفقود، والطابع الزمني القديم، والمشغّل غير المعروف كلها تُجاب بنفس 401 invalid_signature. وإرسال نفس webhook-id مرة أخرى يُجاب 200 بالانطلاق الأول ولا يبدأ شيئاً.
import { createHmac, randomUUID } from "node:crypto";
// Fire a webhook trigger from your service. No API key: the request is signed with the
// trigger's whsec_... secret (shown once, when the trigger was created or rotated).
const secret = process.env.TRIGGER_SECRET;
const body = JSON.stringify({ order_id: "A-1042", status: "delayed" });
const id = randomUUID(); // send the same webhook-id again when you retry
const timestamp = String(Math.floor(Date.now() / 1000));
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signature = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
const res = await fetch(`https://api.mindlabsa.com/v1/hooks/${process.env.TRIGGER_ID}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": id,
"webhook-timestamp": timestamp,
"webhook-signature": `v1,${signature}`,
},
body,
});
// 202: { fire: { id, outcome, run_id } }; 200: this webhook-id was already received.
console.log(res.status, await res.json());التكلفة
- المبالغ بالريال السعودي (SAR) كنصوص عشرية بست خانات، مثل
"0.012500". فيcostللتشغيل:modelوtoolsوtotal. - المبلغ
nullيعني غير معروف: استُخدم شيء لا سعر له. لا يُحسب صفراً أبداً، وtotalيكونnullإن كانmodelأوtoolsكذلك. - تُجمَّد الأسعار عند بدء التشغيل (
cost.prices): السعر الذي يتغير بعدها لا يغيّر تكلفته. - يُفحص
max_cost_sarقبل كل جولة للنموذج مقابل الأجزاء المسعّرة من التكلفة فقط، فقد يتجاوزه التشغيل بتكلفة جولة واحدة قبل أن ينتهيbudget_exceeded. - يجمع
GET /v1/agents/usageتشغيلاتك المنتهية حسب اليوم (بتوقيت UTC) أو حسب الوكيل، على مدى 92 يوماً على الأكثر: عدد التشغيلات وكيف انتهت، والرموز، واستدعاءات الأدوات، وcost_sarوهي تكلفة التشغيلات المسعّرة وحدها؛ أما التشغيل المجهولة تكلفته فيُعدّ فيunpriced_runs.
الحدود والأخطاء
لكل خطأ الجسم نفسه: {"error": {"message", "type", "code", "param"}}. تعامل مع error.code (تُضاف الرموز ولا يُعاد تسميتها) لا مع الرسالة. الرفض بسبب التحقق من الصحة أو غياب المفتاح أو حد الطلبات يحمل type فقط، وparam يسمّي الحقل المخطئ إن وُجد.
الحد 120 طلباً في الدقيقة لكل مفتاح، مع حد لكل عنوان عميل. ردود 429 تحمل Retry-After (ثوانٍ كاملة) حيث يذكرها المسار: انتظرها ثم أعد المحاولة. حدود التشغيلات والمشغّلات ونقاط الاستقبال وعمليات التسليم تُعرف برموزها أدناه، وقيمها ليست نهائية بعد.
| الرمز | HTTP | المعنى |
|---|---|---|
| invalid_request | 400 | حقل يخالف قاعدة في المسار (يسمّيه param)، أو الجسم ليس JSON في الخطاف. |
| url_refused | 400 | العنوان غير مسموح (تسمّي الرسالة القاعدة): https والمنفذ 443 واسم DNS عام. |
| invalid_signature | 401 | تعذّر التحقق من توقيع الخطاف أو طابعه الزمني أو المشغّل. |
| scope_required | 403 | المفتاح لا يملك النطاق المطلوب: agents، أو agents.approve للقرار في موافقة. |
| agent_held | 403, 503 | فريق تشغيل المنصة يُوقف الوكيل أو حسابك مؤقتاً (operator_hold): يُرفض بدء التشغيل (403)، ولا يبدأ انطلاق مشغّل webhook شيئاً (503؛ أعد المحاولة بنفس webhook-id بعد Retry-After). |
| permission_denied | 403 | مفتاح لا تتصرف به المنصة لهذا الطلب: مفتاح داخلي، أو مفتاح بلا مالك، أو المفتاح الذي بدأ التشغيل يحاول البتّ في موافقته. |
| model_not_permitted | 403 | المفتاح لا يحق له استدعاء النموذج (يسمّيه param). |
| not_enabled | 403, 503 | الميزة غير مفعّلة على المنصة: أدوات HTTP أو الـ Webhooks الصادرة أو المشغّلات. |
| agent_not_found | 404 | الوكيل غير موجود أو ليس لك. |
| agent_version_not_found | 404 | لا إصدار للوكيل يمكن تشغيله. |
| approval_not_found | 404 | الموافقة غير موجودة أو ليست لك. |
| connection_not_found | 404 | الاتصال غير موجود أو ليس لك. |
| delivery_not_found | 404 | لا توجد عملية التسليم هذه لنقطة الاستقبال. |
| model_not_found | 404 | النموذج ليس من النماذج التي تعرفها المنصة. |
| run_not_found | 404 | التشغيل غير موجود أو ليس لك. |
| thread_not_found | 404 | المحادثة غير موجودة أو ليست لك. |
| tool_not_found | 404 | الأداة غير موجودة أو ليست لك. |
| trigger_not_found | 404 | المشغّل غير موجود أو ليس لك. |
| webhook_not_found | 404 | نقطة الاستقبال غير موجودة أو ليست لك. |
| approval_closed | 409 | أُلغيت الموافقة أو انتهى تشغيلها. |
| approval_decided | 409 | سبق البتّ في الموافقة بالقرار الآخر. |
| approval_expired | 409 | انتهت مهلة الموافقة. |
| connection_limit_reached | 409 | بلغت الحد الأعلى لعدد الاتصالات. |
| connection_name_taken | 409 | لديك اتصال بهذا الاسم. |
| endpoint_disabled | 409 | نقطة الاستقبال معطّلة؛ فعّلها أولاً. |
| event_expired | 409 | لم يعد الحدث محفوظاً. |
| name_taken | 409 | لديك مشغّل بهذا الاسم. |
| slug_taken | 409 | لديك وكيل بهذا المعرّف المختصر (slug). |
| thread_agent_mismatch | 409 | المحادثة تخص وكيلاً آخر. |
| thread_busy | 409 | في المحادثة تشغيل قيد التنفيذ. |
| tool_exists | 409 | لديك أداة بهذا الاسم: أضف مراجعة لها. |
| tool_limit_reached | 409 | بلغت الحد الأعلى لأسماء الأدوات. |
| trigger_paused | 409 | المشغّل متوقف مؤقتاً (رفضُ انطلاقٍ بسبب مفتاحه أو وكيله أو نموذجه يوقفه أيضاً). |
| unsupported_media_type | 415 | جسم الخطاف ليس application/json. |
| active_runs_limit | 429 | لديك تشغيلات كثيرة في الانتظار أو قيد التنفيذ؛ أعد المحاولة حين ينتهي أحدها. |
| admission_busy | 429 | بدايات كثيرة في وقت واحد لحسابك؛ أعد المحاولة بعد قليل. |
| daily_runs_limit | 429 | تشغيلات كثيرة بدأت خلال آخر 24 ساعة. |
| deliveries_limit | 429 | عمليات تسليم كثيرة تنتظر الإرسال؛ أعد المحاولة بعد إرسالها. |
| endpoints_limit | 429 | بلغت الحد الأعلى لنقاط الاستقبال. |
| rate_limited | 429 | طلبات كثيرة لهذا المشغّل، أو تشغيلاته عند حدٍّ ما؛ أعد المحاولة لاحقاً. |
| too_many_streams | 429 | للمفتاح عدد البثوث الأقصى مفتوحاً على هذا الخادم؛ أغلق أحدها أو أعد المحاولة لاحقاً. |
| triggers_limit | 429 | بلغت الحد الأعلى للمشغّلات. |
| internal_error | 500 | تعذّرت معالجة الخطاف؛ أعد المحاولة بنفس webhook-id. |
| connections_unavailable | 503 | لا يمكن حفظ الأسرار على هذا النشر حالياً. |
| no_model_available | 503 | تعذّر التحقق من قائمة نماذج المفتاح حالياً؛ أعد المحاولة لاحقاً. |
| triggers_unavailable | 503 | لا يمكن حفظ أسرار المشغّلات على هذا النشر حالياً. |
| webhooks_unavailable | 503 | لا يمكن حفظ أسرار نقاط الاستقبال على هذا النشر حالياً. |