POST /agent/chatConversational agent مع:
POST https://toolkit.rork.com/agent/chat
Content-Type: application/json
الاستجابة المقاسة:
HTTP/2 200
Content-Type: application/octet-stream
Content-Encoding: none
Cache-Control: no-cache
X-Accel-Buffering: no
X-Vercel-Ai-UI-Message-Stream: v1
الـbody عبارة عن SSE-style frames:
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"gen-..."}
data: {"type":"text-delta","id":"gen-...","delta":"Hello"}
data: {"type":"text-end","id":"gen-..."}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"stop"}
data: [DONE]
هذا هو Vercel AI SDK UI Message Stream v1، وليس OpenAI SSE.
{
"messages": [
{
"id": "user-1",
"role": "user",
"parts": [
{
"type": "text",
"text": "Hello"
}
]
}
],
"tools": {}
}
الـSDK الرسمي ينشئ هذه الرسائل عبر useChat وDefaultChatTransport.
الصيغة الخارجية المطلوبة من Rork:
{
"tools": {
"lookup_order": {
"description": "Look up an order and return its current status",
"jsonSchema": {
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "The order identifier"
}
},
"required": ["orderId"],
"additionalProperties": false
}
}
}
}
jsonSchema وليس inputSchema؟Vercel AI SDK داخليًا يستخدم inputSchema، لكن Rork يعرّف Contract خارجيًا خاصًا به باسم jsonSchema.
اختبار inputSchema مباشرة أعاد validation error يطلب:
tools.<toolName>.jsonSchema
الـSDK الرسمي يحول Zod إلى JSON Schema:
const toolsEntries = Object.entries(options.tools).map(
([name, tool]) => [
name,
{
description: tool.description,
jsonSchema: z.toJSONSchema(tool.zodSchema)
}
]
);
عندما يقرر الـAgent استدعاء Tool، التسلسل المقاس يكون قريبًا من:
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"reasoning-start","id":"gen-..."}
data: {"type":"reasoning-delta","id":"gen-...","delta":"[REDACTED]"}
data: {
"type":"tool-input-start",
"toolCallId":"call_123",
"toolName":"lookup_order"
}
data: {
"type":"tool-input-delta",
"toolCallId":"call_123",
"inputTextDelta":"{\"orderId\":\"ORD-42\"}"
}
data: {
"type":"tool-input-available",
"toolCallId":"call_123",
"toolName":"lookup_order",
"input":{"orderId":"ORD-42"}
}
data: {"type":"reasoning-end","id":"gen-..."}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"tool-calls"}
data: [DONE]
نفّذ الأداة فقط بعد tool-input-available. لا تعتمد على tool-input-delta كـJSON كامل.
const result = await lookupOrder({
orderId: "ORD-42"
});
مثال output:
{
"orderId": "ORD-42",
"status": "shipped",
"trackingNumber": "TRK-9001"
}
أضف Assistant tool part إلى history ثم أرسل request جديدًا:
{
"id": "assistant-tool-1",
"role": "assistant",
"parts": [
{
"type": "tool-lookup_order",
"toolCallId": "call_123",
"state": "output-available",
"input": {
"orderId": "ORD-42"
},
"output": {
"orderId": "ORD-42",
"status": "shipped",
"trackingNumber": "TRK-9001"
}
}
]
}
الـSDK الرسمي يوفر:
addToolResult({
toolCallId,
tool: toolName,
output
});
ومع lastAssistantMessageIsCompleteWithToolCalls يرسل الرسالة التالية تلقائيًا للحصول على الرد النهائي.
يمكن تمثيل الفشل:
{
"type": "tool-lookup_order",
"toolCallId": "call_123",
"state": "output-error",
"input": {
"orderId": "ORD-42"
},
"errorText": "Order service timed out"
}
يجب على الـAdapter:
الصيغة التي نجحت كانت UI file part:
{
"id": "user-image-1",
"role": "user",
"parts": [
{
"type": "text",
"text": "Read the order ID"
},
{
"type": "file",
"mediaType": "image/png",
"url": "data:image/png;base64,..."
}
]
}
الصيغة القديمة {type:"image", image:"..."} لم تُقبل كـUIMessage في الاختبار المباشر للـAgent.
الـEndpoint Stateless من ناحية السيرفر بالنسبة للـhistory المرسلة: تذكّر قيمة سابقة فقط عندما تم إرسال الرسائل السابقة معه. لذلك الـAdapter مسؤول عن تخزين:
export async function readRorkStream(
response: Response,
onEvent: (event: unknown) => Promise<void> | void
): Promise<void> {
if (!response.ok || !response.body) {
throw new Error(`Rork error: ${response.status}`);
}
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
const frames = buffer.split("\n\n");
buffer = frames.pop() ?? "";
for (const frame of frames) {
const line = frame
.split("\n")
.find(item => item.startsWith("data:"));
if (!line) continue;
const payload = line.slice(5).trim();
if (payload === "[DONE]") return;
await onEvent(JSON.parse(payload));
}
}
}
Production parser يجب أن يضع limits لحجم frame، يتعامل مع malformed events، ويلغي الـrequest عند timeout أو client disconnect.