ٹائپ اسکرپٹ میں شروع سے AI ایجنٹ کا استعمال کیسے کریں۔

بہت سے AI ایجنٹ ٹیوٹوریل زبان کے ماڈل کے ساتھ شروع ہوتے ہیں، اسے کچھ ٹولز دیتے ہیں، اور اس کے ذریعے اس وقت تک لوپ کرتے ہیں جب تک کہ یہ جواب نہیں بناتا۔ ٹول کال کو ظاہر کرنے کے لیے یہ کافی ہے، لیکن انجینئرنگ کے دلچسپ سوالات کے جوابات باقی ہیں۔

اگر ٹول ناکام ہو جائے تو کیا ہوگا؟ دعووں کی تصدیق کون کرتا ہے؟ ایجنٹ کو غیر معینہ مدت تک چلانے، غیر محفوظ حکموں پر عمل کرنے، یا سیاق و سباق کی ونڈو کو ختم کرنے سے کیسے روکا جائے؟ اور آپ غیر متوقع ماڈل کے جوابات پر بھروسہ کیے بغیر اس کی جانچ کیسے کریں گے؟

یہ ہے ایجنٹ کا استعمال: وہ سافٹ ویئر جو زبان کے ماڈل اور اس ماحول کے درمیان کھڑا ہے جس میں یہ کام کرتا ہے۔

اس ٹیوٹوریل میں، ہم مثال کے طور پر ایک کوڈنگ ایجنٹ کا استعمال کرتے ہوئے TypeScript میں ایک چھوٹا، فراہم کنندہ-ایگنوسٹک استعمال بناتے ہیں۔ بنیادی ایگزیکیوشن لوپ کے ساتھ شروع کرتے ہوئے، ہم بتدریج ٹول کی توثیق، ورک اسپیس آئسولیشن، ایگزیکیوشن بجٹ، سیاق و سباق کا انتظام، مشاہداتی، اور ڈیٹرمنسٹک ایرر ٹیسٹنگ شامل کرتے ہیں۔

جس کا ہم احاطہ کریں گے۔

شرطیں

اگر آپ TypeScript سے واقف ہیں اور AI ایجنٹس کے بارے میں بنیادی معلومات رکھتے ہیں تو یہ مددگار ثابت ہوگا۔ یہ Node.js 24، TypeScript، اور کم سے کم انحصار کے ساتھ بلٹ ان ٹیسٹ رنر کا استعمال کرتا ہے۔

مکمل عمل درآمد ساتھی ریپوزٹری، ایجنٹ-ہارنس-فرم سکریچ میں دستیاب ہے۔ ہر سیکشن ایک گٹ ٹیگ سے مماثل ہے، لہذا آپ اس منصوبے کی پیروی کر سکتے ہیں کیونکہ اسے حتمی کوڈ سے شروع کرنے کے بجائے تیار کیا گیا ہے۔

1. ایک بنیادی ایجنٹ لوپ بنانا

پیروی کرنے کے لیے ابتدائی سنگ میل کو دیکھیں۔

git clone https://github.com/gkoos/agent-harness-from-scratch.git
cd agent-harness-from-scratch
git switch --detach 01-model-loop
npm ci
npm run typecheck
npm test
npm run demo

علیحدہ چیک آؤٹ آپ کو شاخوں کو منتقل کیے بغیر یہاں بیان کردہ عین مطابق اسنیپ شاٹس دیکھنے کی اجازت دیتے ہیں۔ جب آپ اگلے حصے پر پہنچ جائیں تو اس ٹیگ پر جائیں۔ Node.js 24 اور npm انسٹال ہونا ضروری ہے۔

سب سے اہم آرکیٹیکچرل فیصلہ ہے جب کنٹرول چل رہا ہے، ماڈل کارروائیوں کی تجویز کرتا ہے۔. جب ماڈل ٹول کال کی درخواست کرتا ہے، تو یہ درحقیقت کچھ بھی نہیں کرتا ہے۔ ٹول کے نام اور دلائل پر مشتمل ایک منظم درخواست بنائی۔ ہارنس کو اس درخواست کی تشریح کرنا چاہیے، اس کے نفاذ کو کال کرنا چاہیے، اور نتائج کو ماڈل پر واپس کرنا چاہیے۔

تعاملات درج ذیل ہیں:

آئیے ایک معاہدے کی وضاحت کرکے شروع کریں۔ src/core/types.ts.

export type ToolCall = { id: string; name: string; arguments: unknown };

export type Message =
  | { role: 'user'; content: string }
  | { role: 'assistant'; content: string; toolCalls?: ToolCall[] }
  | { role: 'tool'; callId: string; content: string; isError: boolean };

export interface ToolDefinition {
  name: string;
  description: string;
  parameters: Record; // JSON Schema for the model
}

export interface Tool extends ToolDefinition {
  execute(argumentsValue: unknown, signal: AbortSignal): Promise;
}

export interface Model {
  generate(input: {
    messages: readonly Message[];
    tools: readonly ToolDefinition[];
    signal: AbortSignal;
  }): Promise<{ content: string; toolCalls?: ToolCall[] }>;
}

export type AgentResult = {
  status: 'completed' | 'step_limit' | 'aborted';
  answer?: string;
  messages: Message[];
  steps: number;
};

کہ Model انٹرفیس میں جان بوجھ کر OpenAI، Anthropic، یا کسی دوسرے فراہم کنندہ سے متعلق کوئی مواد شامل نہیں ہے۔ فراہم کنندہ اڈاپٹر استعمال کو تبدیل کیے بغیر APIs اور ان اقسام کے درمیان تبدیل کر سکتے ہیں۔

آلے کے دلائل ہیں: unknownنہیں any. ماڈل سے تیار کردہ ڈیٹا ایک ناقابل اعتبار ان پٹ ہے۔ ہم اسے اگلے حصے میں چلانے سے پہلے اس کی توثیق کریں گے۔

ایجنٹ لوپ src/core/agent.ts ان آسان اقدامات پر عمل کریں:

  1. ماڈل کو ڈائیلاگ اور دستیاب ٹول کی تعریفیں بھیجیں۔

  2. گفتگو میں ماڈل کا جواب شامل کریں۔

  3. اگر آپ نے کسی ٹول کی درخواست کی ہے تو اسے چلائیں اور نتائج کو شامل کریں۔

  4. اس وقت تک اعادہ کریں جب تک کہ ماڈل حتمی جواب نہ دے یا ہارنس قدم کی حد تک نہ پہنچ جائے۔

یہاں مکمل ابتدائی لوپ ہے: src/core/agent.ts. ماڈل ٹول کی تعریفیں حاصل کرتا ہے اور ہارنس قابل عمل عمل درآمد رکھتا ہے۔

import type { AgentResult, Message, Model, Tool } from './types.ts';

export async function runAgent(options: {
  model: Model;
  tools: readonly Tool[];
  prompt: string;
  maxSteps?: number;
  signal?: AbortSignal;
}): Promise {
  const { model, tools, prompt, maxSteps = 10 } = options;
  if (!Number.isSafeInteger(maxSteps) || maxSteps < 1) {
    throw new RangeError('maxSteps must be a positive safe integer');
  }
  const signal = options.signal ?? new AbortController().signal;
  const messages: Message[] = [{ role: 'user', content: prompt }];
  const registry = new Map(tools.map(tool => [tool.name, tool]));
  if (registry.size !== tools.length) throw new Error('Duplicate tool names');
  const definitions = tools.map(({ name, description, parameters }) => ({ name, description, parameters }));

  for (let step = 0; step < maxSteps; step++) {
    if (signal.aborted) return { status: 'aborted', messages, steps: step };
    let response;
    try {
      response = await model.generate({ messages: [...messages], tools: definitions, signal });
    } catch (error) {
      if (signal.aborted) return { status: 'aborted', messages, steps: step };
      throw error;
    }
    if (signal.aborted) return { status: 'aborted', messages, steps: step };
    const calls = response.toolCalls ?? [];
    messages.push({ role: 'assistant', content: response.content, ...(calls.length ? { toolCalls: calls } : {}) });
    if (!calls.length) return { status: 'completed', answer: response.content, messages, steps: step + 1 };

    for (const call of calls) {
      if (signal.aborted) return { status: 'aborted', messages, steps: step + 1 };
      const tool = registry.get(call.name);
      if (!tool) {
        messages.push({ role: 'tool', callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
        continue;
      }
      try {
        const result = await tool.execute(call.arguments, signal);
        if (signal.aborted) return { status: 'aborted', messages, steps: step + 1 };
        messages.push({ role: 'tool', callId: call.id, content: JSON.stringify(result) ?? 'null', isError: false });
      } catch (error) {
        if (signal.aborted) return { status: 'aborted', messages, steps: step + 1 };
        messages.push({
          role: 'tool', callId: call.id,
          content: error instanceof Error ? error.message : String(error),
          isError: true
        });
      }
    }
  }
  return { status: 'step_limit', messages, steps: maxSteps };
}

اگر یہ ٹول کو کال کیے بغیر جواب دیتا ہے، تو عملدرآمد ختم ہوجاتا ہے۔ بصورت دیگر، ہارنس ہر درخواست کردہ ٹول کو تلاش کرے گا اور اس پر عمل درآمد کرے گا۔

ٹول کی خرابی کی وجہ سے ایجنٹ خود بخود ختم نہیں ہوتا ہے۔ اس کے بجائے، ہارنس اصل کال ID سے متعلق ایک ایرر میسج لاگ کرتا ہے۔ یہ ماڈل کو اگلی بار لاپتہ فائلوں یا ناکام آپریشنز پر ردعمل ظاہر کرنے کی اجازت دیتا ہے۔

ہم ماڈل کی گردشوں کی زیادہ سے زیادہ تعداد بھی لگاتے ہیں۔ ماڈل کام کی درخواست کرنا جاری رکھ سکتا ہے، لیکن یہ اس کے عمل درآمد کے وقت یا تکرار کی تعداد کا تعین نہیں کر سکتا جس کا اسے حق ہے۔

جسمانی ماڈل کے بغیر ٹیسٹ کریں۔

اپنے ٹیسٹوں میں نان ڈیٹرمنسٹک API کالز کرنے (اور ٹوکنز یا GPU پاور جلانے) کے بجائے، آپ جعلی، اسکرپٹڈ ماڈلز متعارف کرواتے ہیں۔ src/models/fake.ts. پہلے سے طے شدہ جواب لوٹاتا ہے (پہلے ٹول کی درخواست، پھر حتمی جواب)۔

ایک جعلی ماڈل جوابات کی ایک سیریز کے ذریعے تیار ہوتا ہے۔ اسکرپٹ کو ختم کرنا ایک خرابی ہے، لہذا اضافی غیر متوقع ماڈل ٹرانزیشنز خود بخود پاس نہیں ہوں گی۔

// src/models/fake.ts
import type { Model } from '../core/types.ts';

export function scriptedModel(
  responses: Array>>
): Model {
  let index = 0;
  return {
    async generate() {
      const response = responses[index++];
      if (!response) throw new Error('Scripted model exhausted');
      return response;
    }
  };
}

یہاں ایک کم سے کم ٹیسٹ ہے: tests/agent.test.ts. پھر چلائیں: npm test:

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { runAgent } from '../src/core/agent.ts';
import { scriptedModel } from '../src/models/fake.ts';

test('executes a tool and returns the final answer', async () => {
  const result = await runAgent({
    prompt: 'Say hello',
    tools: [{
      name: 'hello', description: 'Return a greeting',
      parameters: { type: 'object' },
      async execute() { return 'Hello'; }
    }],
    model: scriptedModel([
      { content: '', toolCalls: [{ id: '1', name: 'hello', arguments: {} }] },
      { content: 'Done' }
    ])
  });
  assert.equal(result.status, 'completed');
  assert.equal(result.answer, 'Done');
  assert.deepEqual(result.messages[2], {
    role: 'tool', callId: '1', content: '"Hello"', isError: false
  });
});

ساتھ والا ڈیمو بھی اسی طرز کا استعمال کرتا ہے۔ اضافی ٹیسٹوں میں نامعلوم ٹولز، قدم کی حدود، اور API کلید، نیٹ ورک تک رسائی، یا ٹوکن لاگت کے بغیر منسوخی کا احاطہ کیا جاتا ہے۔

فی الحال ایک ورکنگ ایجنٹ لوپ موجود ہے، لیکن یہ اب بھی ماڈل جنریشن ٹول کے دلائل پر بھروسہ کرتا ہے اور رجسٹرڈ ٹولز کیا کر سکتے ہیں اس پر کوئی بامعنی پابندی نہیں ہے۔

پہلا لوپ ماڈل کو نام کے ذریعہ ایک ٹول کی درخواست کرنے کی اجازت دیتا ہے اور پھر ان دلائل کو براہ راست نفاذ میں منتقل کرتا ہے۔ یہ ڈیمو میں کام کرتا ہے، لیکن ٹول ٹپ صرف کہتا ہے۔ درخواست ماڈل کے لیے کوئی گارنٹی نہیں ہے کہ ماڈل درست دلائل واپس کرے گا۔

اس حصے میں، ساتھی ذخیرہ میں دوسرا سنگ میل دیکھیں۔

git switch --detach 02-tool-dispatch

یہ ٹیگ شامل کرتا ہے: src/core/registry.tsٹول رجسٹریشن اور رن ٹائم دلیل کی توثیق کو ہینڈل کرتا ہے۔ یہ ناکامی کے رویے کو بھی بدل دیتا ہے۔ src/core/agent.ts.

واضح معاہدے کے ساتھ رجسٹری

کہ ToolRegistry ابتدائی لوپ کے لیے عام کو بدل دیتا ہے۔ Map. غلط یا ڈپلیکیٹ ٹول کے ناموں کو مسترد کریں اور صرف ماڈل کے لیے درکار تعریفیں سامنے رکھیں۔

import type { Tool, ToolDefinition } from './types.ts';

export class ToolRegistry {
  private readonly tools = new Map();
  constructor(tools: readonly Tool[]) {
    for (const tool of tools) {
      if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(tool.name)) throw new Error(`Invalid tool name: ${tool.name}`);
      if (this.tools.has(tool.name)) throw new Error(`Duplicate tool name: ${tool.name}`);
      this.tools.set(tool.name, tool);
    }
  }
  definitions(): ToolDefinition[] {
    return [...this.tools.values()].map(({ name, description, parameters }) => ({ name, description, parameters }));
  }
  get(name: string): Tool | undefined { return this.tools.get(name); }
}

اسے بنائیں const registry = new ToolRegistry(tools) اس کا استعمال کرتے ہوئے ماڈل واقفیت کی تعریف حاصل کریں: registry.definitions(). جب جواب ٹول سے درخواست کرتا ہے، تو یہ فراہم کردہ دلائل کی توثیق کرتا ہے۔ پہلے کال کرنا execute:

const tool = registry.get(call.name);
if (!tool) {
  messages.push({
    role: 'tool', callId: call.id,
    content: `Unknown tool: ${call.name}`, isError: true
  });
  continue;
}

const validationErrors = validate(call.arguments, tool.parameters);
if (validationErrors.length) {
  messages.push({
    role: 'tool', callId: call.id,
    content: `Invalid arguments: ${validationErrors.join('; ')}`,
    isError: true
  });
  continue;
}

یہ ایک اہم حد ہے۔ arguments: unknown یہ اس وقت تک ناقابل اعتماد رہے گا جب تک ہارنس اس کی تصدیق نہیں کر لیتا۔ اگر توثیق ناکام ہوجاتی ہے، تو ٹول نہیں چلے گا۔ اس کے بجائے، ماڈل کو اصل کال ID سے متعلق ایک خرابی کا نتیجہ موصول ہوتا ہے۔

سبق کے validate فنکشنز JSON اسکیما کے جان بوجھ کر چھوٹے ذیلی سیٹ کی حمایت کرتے ہیں: قدیم اور کنٹینرز۔ type چیک، ریورس properties، required بیٹری، additionalProperties: falseانتظام itemsاور enum. یہ درج ذیل راستوں کی اطلاع دیتا ہے: $.flags[0] اس سے آپ کو غلط ان پٹ کی شناخت میں مدد ملے گی۔ کہ ~ نہیں یہ ایک مکمل JSON اسکیما کا نفاذ ہے اور رجسٹریشن پر ٹول اسکیما کی توثیق نہیں کرتا ہے۔

اوپر استعمال کیے گئے توثیق کار ہیں: یہ پہلے اعلان کردہ قسم کی جانچ کرتا ہے، پھر بار بار آبجیکٹ کی خصوصیات اور صفوں کی اشیاء کو چیک کرتا ہے۔

export function validate(value: unknown, schema: Record, path="$"): string[] {
  const errors: string[] = [];
  const type = schema.type;
  if (typeof type !== 'string') return [`${path}: schema must declare a type`];
  const matches = type === 'object' ? value !== null && typeof value === 'object' && !Array.isArray(value)
    : type === 'array' ? Array.isArray(value)
    : type === 'integer' ? typeof value === 'number' && Number.isInteger(value)
    : type === 'number' ? typeof value === 'number' && Number.isFinite(value)
    : type === 'null' ? value === null
    : type === 'string' || type === 'boolean' ? typeof value === type : false;
  if (!matches) return [`${path}: expected ${type}`];
  if (Array.isArray(schema.enum) && !schema.enum.some(item => Object.is(item, value))) {
    errors.push(`${path}: not an allowed value`);
  }
  if (type === 'object') {
    const obj = value as Record;
    const properties = (schema.properties ?? {}) as Record>;
    for (const key of (schema.required ?? []) as string[]) {
      if (!Object.hasOwn(obj, key)) errors.push(`${path}.${key}: required`);
    }
    for (const [key, item] of Object.entries(obj)) {
      if (Object.hasOwn(properties, key)) errors.push(...validate(item, properties[key]!, `${path}.${key}`));
      else if (schema.additionalProperties === false) errors.push(`${path}.${key}: unexpected property`);
    }
  }
  if (type === 'array' && schema.items && typeof schema.items === 'object') {
    (value as unknown[]).forEach((item, index) => {
      errors.push(...validate(
        item, schema.items as Record, `${path}[${index}]`
      ));
    });
  }
  return errors;
}

تمام غلطیوں کا مطلب ایک ہی نہیں ہے۔

سنگ میل 2 اہم فرق بھی پیش کرتا ہے: آلے کی ناکامی اور ماڈل کی ناکامی.

نامعلوم ٹولز، غلط دلائل، یا ٹولز کے ذریعے اٹھائے گئے استثنیٰ ہیں۔ role: 'tool' ایک پیغام بھیجیں isError: true. ماڈل نتائج کی جانچ کر سکتا ہے اور اگلی باری کچھ مختلف کرنے کی کوشش کر سکتا ہے۔ اس کے برعکس، اگر model.generate() جب خود ہی پھینک دیا جاتا ہے، تو استعمال اس طرح ختم ہوتا ہے: status: 'model_error' اور غلطی کے پیغام کو لاگ کرتا ہے۔

یہ صرف سوجن نہیں ہے۔ گمشدہ فائلوں کو بازیافت کیا جا سکتا ہے، لیکن ماڈل فراہم کنندہ کی عدم دستیابی کا مطلب ہے کہ بازیافت کی رہنمائی کے لیے کوئی ماڈل جواب نہیں ہے۔ ہارنس کو یہ بہانہ نہیں کرنا چاہئے کہ یہ وہی صورتحال ہے۔

لوپ میں موجود model.generate() ہڑپنے اور دوبارہ پھینکنے والے بلاکس کو ساختی نتائج میں تبدیل اور شامل کرتا ہے۔ 'model_error' اور اختیاری error?: string میدان AgentResult:

if (signal.aborted) return { status: 'aborted', messages, steps: step };
return {
  status: 'model_error', messages, steps: step,
  error: error instanceof Error ? error.message : String(error)
};

ٹول ایگزیکیوشن کیچ بلاک اب بھی ایرر میسج کو لاگ کرے گا اور لوپ کو جاری رکھے گا۔

ڈیٹرمنسٹک ٹیسٹنگ اس فرق کو ٹھوس بناتی ہے۔ میں tests/agent.test.tsغلط echo دعویٰ مسترد کر دیا جاتا ہے۔ ٹول کو کال کیے بغیرایک اٹھائے گئے ٹول کی رعایت اسکرپٹ ماڈل کو حتمی جواب واپس کرنے کا سبب بن سکتی ہے۔ tests/registry.test.ts گھریلو دلائل کی توثیق کریں اور غیر متوقع خصوصیات کی جانچ کریں۔

اب تک، ایک ایجنٹ ایک ٹول کی درخواست کر سکتا ہے اور ہارنس اس دلیل کی توثیق کرے گا۔ لیکن توثیق درخواست میں ہے۔ درست شکل. درست پڑھنے کی درخواست ../../secrets.txt یہ اب بھی ایک درخواست ہے جس سے ہمیں انکار کرنا چاہیے۔

ساتھی ذخیرہ میں سنگ میل 3 دیکھیں۔

git switch --detach 03-workspace-tools

یہ سنگ میل ورک اسپیس فائل ٹولز اور محدود کمانڈ ٹولز کا اضافہ کرتا ہے۔ نہ ہی کسی مخصوص ماڈل فراہم کنندہ پر منحصر ہے۔

فائل کی کارروائیوں کو اپنے ورک اسپیس تک محدود رکھیں

کوڈنگ ایجنٹوں کو فائلوں کو پڑھنا اور ان میں ترمیم کرنی چاہیے، لیکن انہیں خود بخود پورے فائل سسٹم تک رسائی نہیں دی جانی چاہیے۔ کہ Workspace جب کوئی کلاس بنتی ہے، تو یہ اس کی کیننیکل جڑ کی تصدیق کرتی ہے۔

const workspace = await Workspace.open('/path/to/project');
const tools = workspaceTools(workspace);

نتیجہ read_file اور write_file ٹول اس جڑ سے متعلق راستوں کو قبول کرتا ہے۔ فائل کھولنے سے پہلے، Workspace مطلق راستے، راستے جو جڑ سے بچ جاتے ہیں، اور موجودہ راستے کے اجزاء سے علامتی روابط کو مسترد کریں۔ لکھنے کے لیے پیرنٹ ڈائرکٹری کا ہونا ضروری ہے۔ یہ ٹول بے ترتیب ڈائریکٹری درخت نہیں بناتا۔

یہ دونوں طریقے اندرونی ہیں۔ Workspace راستے کی تصدیق کریں۔ فائل درآمد کریں۔ isAbsolute، relativeاور resolve سے node:pathاور lstat اور realpath سے node:fs/promises. Workspace.open() ڈپارٹمنٹ اسٹور await realpath(root) پسند this.root:

private lexical(path: string): string {
    if (!path || isAbsolute(path)) throw new Error('Expected a relative workspace path');
    const full = resolve(this.root, path);
    const rel = relative(this.root, full);
    if (!rel || rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\' : '/'}`) || isAbsolute(rel)) {
      throw new Error('Path is outside the workspace or refers to its root');
    }
    return full;
  }

  /** Reject symlinks in every existing component, not just the final file. */
  private async check(path: string, allowMissingFinal: boolean): Promise {
    const full = this.lexical(path);
    const rel = relative(this.root, full);
    const parts = rel.split(/[\/]/);
    let current = this.root;
    for (let i = 0; i < parts.length; i++) {
      current = resolve(current, parts[i]!);
      try {
        const stat = await lstat(current);
        if (stat.isSymbolicLink()) throw new Error('Symbolic links are not permitted');
        if (i < parts.length - 1 && !stat.isDirectory()) throw new Error('Parent is not a directory');
      } catch (error) {
        if (allowMissingFinal && i === parts.length - 1 && (error as NodeJS.ErrnoException).code === 'ENOENT') break;
        throw error;
      }
    }
    return full;
  }

read فون کال this.check(path, false) موجودہ فائل کو کھولنے سے پہلے۔ write فون کال this.check(path, true)صرف حتمی فائلوں کو غائب ہونے کی اجازت دیتا ہے۔ لاپتہ پیرنٹ ڈائریکٹریز یا علامتی لنکس ابھی بھی جانچ میں ناکام رہیں گے۔

حدود کو نافذ کیا جاتا ہے: ٹول پر عمل درآمدہم ماڈل کو کام کرنے کو نہیں کہہ رہے ہیں۔ ماڈل درخواست کر سکتا ہے۔ ../secret.txtتاہم، یہ ورک اسپیس میں کھلنے سے انکار کرتا ہے۔ اس کے بعد ہارنس غلطی کو ایک قابل بازیافت ٹول کے نتیجے کے طور پر رپورٹ کرتا ہے، جیسا کہ اس نے Milestone 2 میں غلط دلیل کے لیے کیا تھا۔

مزید تصدیق کے لیے، آپ کے نفاذ میں، اس کا استعمال کرتے ہوئے فائل کو کھولیں: O_NOFOLLOW اگر تعاون یافتہ ہے تو، آخری راستے کے اجزاء میں علامتی لنکس کی پیروی سے گریز کریں۔ اس سے تمام تنازعات ختم نہیں ہوتے۔ ڈائرکٹری کے اجزاء توثیق اور کھولنے کے درمیان تبدیل ہو سکتے ہیں۔ یہ ایک مفید تربیتی حد ہے۔ ~ نہیں OS سطح کا سینڈ باکس۔

ماڈل کو خود کمانڈز کو منتخب کرنے کے بجائے کمانڈز کا انتخاب کرنے کو کہیں۔

کمانڈ پر عمل درآمد بہت زیادہ حساس ہے۔ ماڈل سے تیار کردہ شیل سٹرنگ کو قبول کرنے کے بجائے ایک نامزد کمانڈ پیش سیٹ کو رجسٹر کرتا ہے۔

const tool = commandTool(projectRoot, {
  test: {
    executable: process.execPath,
    args: ['--test', 'tests/example.test.js']
  }
});

ماڈل درخواست کر سکتا ہے۔ { "command": "test" }تاہم، آپ کوئی اور قابل عمل، اضافی دلائل، یا شیل نحو فراہم نہیں کر سکتے۔ کمانڈ ٹول کے لیے ڈسپیچ کی حدود یہ ہیں: execute(value, signal) طریقہ):

signal.throwIfAborted();
const name = (value as { command: string }).command;
if (!Object.hasOwn(presets, name)) throw new Error('Command not permitted');
const preset = presets[name]!;
const timeout = preset.timeoutMs ?? 10_000;
if (!Number.isSafeInteger(timeout) || timeout < 1) {
  throw new Error('Invalid timeout');
}

// Inside the Promise that collects the command's result:
const child = spawn(preset.executable, [...preset.args], {
  cwd, shell: false, signal, windowsHide: true,
  stdio: ['ignore', 'pipe', 'pipe']
});
const timer = setTimeout(() => child.kill(), timeout);

یہاں spawn سے آتا ہے node:child_process، cwd یہ کنفیگر شدہ پروجیکٹ روٹ ہے۔ presets یہ ایک نقشہ ہے جو اوپر ڈویلپر نے فراہم کیا ہے۔ رزلٹ ہینڈلر stdout اور stderr کو اکٹھا کرتا ہے اور اپنے بچوں کو ختم اور صاف کرتا ہے جب کیپچر کیے گئے تاروں میں سے ایک 65,536 حروف سے زیادہ ہو جاتا ہے۔ timer ایک خرابی واقع ہوتی ہے یا باہر نکل جاتی ہے۔ نفاذ بائٹس کی صحیح تعداد کے بجائے JavaScript سٹرنگ کی لمبائی کی پیمائش کرتا ہے۔

اس سے ان کمانڈز کی تعداد کم ہو جاتی ہے جن سے ماڈل منتخب کر سکتا ہے، لیکن اس بات کی ضمانت نہیں دیتا کہ اس کے منتخب کردہ کمانڈز بے ضرر ہیں۔ ٹیسٹ اسکرپٹ خود فائلوں کو پڑھ سکتا ہے، نیٹ ورک تک رسائی حاصل کر سکتا ہے، یا چائلڈ پروسیس شروع کر سکتا ہے۔ وقت ختم ہونے والے بچے کو ختم کرنے سے پورے عمل کے درخت کو قابل اعتماد طریقے سے ختم نہیں کیا جاتا ہے۔ غیر بھروسہ مند ذخیروں کو چلانے کے لیے عمل کو الگ تھلگ کرنے کی ضرورت ہوتی ہے، جیسے کہ ایک مناسب طریقے سے تشکیل شدہ کنٹینر یا OS سینڈ باکس۔

سنگ میل 3 ٹیسٹ عام پڑھنے اور لکھنے، ٹراورسل کوششوں، علامتی لنکس، قابل بازیافت رسائی کی ناکامیوں، اور کمان کے ناموں کا احاطہ کرتا ہے۔

4. عمل درآمد کو محدود کریں۔

ہمارے ایجنٹ اب ثابت شدہ ٹولز کو استعمال کر سکتے ہیں، بشمول ورک اسپیس سے آگاہ فائل آپریشنز اور پہلے سے طے شدہ کمانڈز۔ تاہم، ضروری نہیں کہ درست کارروائیوں کو غیر معینہ مدت کے لیے اجازت دی جائے۔ ایک ماڈل سینکڑوں ٹول کالز کی درخواست کر سکتا ہے، یا ایجنٹ کے نتائج کا انتظار کرتے وقت کمانڈ لٹک سکتے ہیں۔

اس سنگ میل پر ہم چلانے کی ذمہ داری کو محدود کرتا ہے۔ ماڈل سے عزت مانگنے کے بجائے۔

براہ کرم درست ریپوزٹری ورژن کی پیروی کریں۔

git switch --detach 04-execution-limits

اگر آپ نے ابھی تک اپنے پروجیکٹ کو کلون نہیں کیا ہے تو اس کے ساتھ شروع کریں: git clone https://github.com/gkoos/agent-harness-from-scratch.git اور cd agent-harness-from-scratch.

انقلابات اور ٹول کالز کا الگ سے حساب لگائیں۔

متن maxSteps ترتیبات ماڈل کے جواب کو محدود کرتی ہیں، نہ کہ ٹول کالز کی تعداد۔ اکیلے گردش کی حدود کام کے بوجھ کو محدود نہیں کرتی ہیں، کیونکہ ایک جواب متعدد ٹولز کی درخواست کر سکتا ہے۔

ہم توسیع کرتے ہیں runAgent دو اضافی اختیارات ہیں:

const result = await runAgent({
  model,
  tools,
  prompt: 'Check the project',
  maxSteps: 10,
  maxToolCalls: 30,
  timeoutMs: 15_000
});

تمام درخواست کردہ کالیں، بشمول نامعلوم ٹولز والی کالز اور غلط دلائل، ٹول کال کا بجٹ استعمال کرتے ہیں۔ یہ ضروری ہے کیونکہ درخواست ناکام ہونے کی صورت میں بھی، یہ ہارنس کا کام کھاتی ہے۔ دوبارہ ترتیب دیں let toolCalls = 0 ہم اس میں اضافہ کرتے ہیں اس سے پہلے کہ ہم لوپ سے پہلے ہر اجازت یافتہ کرنسی کے لیے ٹول تلاش کریں۔ بیچ میں مسترد شدہ دعوے اور نامعلوم ٹولز بھی شامل ہیں۔

ایک ماڈل ایک ساتھ متعدد ٹول کالز بھی واپس کر سکتا ہے۔ یہ دریافت کرنے کے بجائے کہ آپ کا بجٹ پہلے چند رنز کے بعد ختم ہو گیا ہے، ہارنس یقینی بناتا ہے کہ: مکمل بیچ تندرستی:

if (calls.length > maxToolCalls - toolCalls) {
  return { status: 'tool_limit', messages, steps, toolCalls };
}

یہ روکتا ہے۔ بجٹ پر مبنی جزوی عملدرآمد۔ ہم بیچ لین دین نہیں کرتے ہیں۔ ٹول اب بھی ناکام ہو سکتا ہے یا پھانسی درمیان میں منسوخ ہو سکتی ہے۔

منسوخی اور آخری تاریخ کی تبلیغ

بھیجنے والا ہے۔ AbortSignalجبکہ timeoutMs داخلی ڈیڈ لائن بنائیں۔ ایک کنٹرول ان عناصر کو اس طرح جوڑتا ہے: AbortController ماڈلز اور ٹولز کو سگنلز دیں۔

ترتیبات اس سگنل کو تبدیل کرتی ہیں جو آپ نے ابتدائی لوپ میں براہ راست منتخب کیا ہے۔

const controller = new AbortController();
let timedOut = false;
const onAbort = () => controller.abort(options.signal?.reason);
if (options.signal?.aborted) onAbort();
else options.signal?.addEventListener('abort', onAbort, { once: true });
const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
  timedOut = true;
  controller.abort(new Error('Agent deadline exceeded'));
}, timeoutMs);
const signal = controller.signal;

یہ پاس کرو signal کو model.generate() اور tool.execute()جیسا کہ اصل لوپ میں ہے۔ لوپ لپیٹ. try/finally لہذا تکمیل اور ابتدائی واپسی کے ٹائمر اور سامعین دونوں۔

try {
  // The execution loop goes here, using controller.signal.
} finally {
  if (timer !== undefined) clearTimeout(timer);
  options.signal?.removeEventListener('abort', onAbort);
}

پھانسی کی حد پر چیک کریں۔ signal.aborted اور واپس آو timedOut ? 'timeout' : 'aborted' جیسا کہ ہے۔ نتائج بھی بتائے جاتے ہیں۔ steps اور toolCallsکال کرنے والا دیکھ سکتا ہے کہ ختم ہونے سے پہلے کتنا کام ہوا ہے۔

یہ ایک کوآپریٹو برطرفی ہے، زبردستی برطرفی نہیں۔ اگر اڈاپٹر اس سگنل کو نظر انداز کرتا ہے، تو زیر التواء کارروائیاں اپنی آخری تاریخ سے زیادہ دیر تک جاری رہ سکتی ہیں۔ اسی طرح، بچے کے عمل کو قتل کرنے سے ضروری نہیں کہ بچے کا عمل رک جائے۔ سخت عمل درآمد کی حدود کے لیے عمل یا کنٹینر تنہائی کی ضرورت ہوتی ہے۔

حقیقی ناکامی کا انتظار کیے بغیر اپنی حدود کی جانچ کریں۔

کہ budget tests ضرورت سے زیادہ بجٹ والی جگہوں کی درخواست کرنے اور تصدیق کرنے کے لیے اسکرپٹ ماڈل کا استعمال کریں۔ زیرو ٹول رن. دوسرے ٹیسٹ اس بات کا تعین کرتے ہیں کہ آیا غلط درخواستیں بجٹ کا استعمال کرتی ہیں، آیا پہلے سے اسقاط شدہ سگنلز ماڈل کالز کو روکتے ہیں، اور آیا باہمی تعاون کے ماڈل اور ٹولز ڈیڈ لائن پر رک جاتے ہیں۔

نتائج کی اقسام ہیں: types.ts اب تمیز کرو completed، step_limit، tool_limit، aborted، timeoutاور model_error. یہ نتائج جان بوجھ کر مختلف ہیں۔ عمل درآمد کسی بھی صارف کی کارروائی کو مکمل کیے بغیر درست طریقے سے روک سکتا ہے۔

5. صورتحال پر قابو پالیں۔

استعمال کرنے والوں کے پاس اب محدود آپریٹنگ وقت ہے، لیکن بات چیت میں اضافہ ہوتا جا رہا ہے۔ تمام ماڈل کے جوابات اور ٹول کے نتائج ماڈل کو واپس بھیجے گئے ایک اور پیغام بن جاتے ہیں۔ ایک فائل کو پڑھنے سے ہزاروں لائنیں واپس آ سکتی ہیں۔ متعدد تکرار ماڈل کی سیاق و سباق کی کھڑکی کو مرحلہ وار بجٹ تک پہنچنے سے بہت پہلے ختم کرنے کا سبب بن سکتی ہے۔

سنگ میل 5 درج ذیل اختلافات کو متعارف کراتا ہے: رپورٹ کارڈز جو ہم اپنے پاس رکھتے ہیں۔ اور پیغام ہم ماڈل کو بھیجتے ہیں۔. عمل درآمد کو چیک کریں۔

git switch --detach 05-context

متعلقہ فائلیں ہیں۔ src/core/context.ts، src/core/agent.tsاور tests/context.test.ts.

پابند ٹول آؤٹ پٹ اور ماڈل پر مبنی ریکارڈنگ

نیا context اختیارات نے دو الگ الگ حدود مقرر کی ہیں:

const result = await runAgent({
  prompt: 'Inspect the project',
  model,
  tools,
  context: {
    maxContextChars: 32_000,
    maxToolResultChars: 8_000
  }
});

maxToolResultChars ہر آلے کے نتائج کی حدود پہلے اپنے رپورٹ کارڈ پر جائیں۔ بڑے نتائج شروع اور اختتام کو برقرار رکھتے ہیں اور درمیان میں بیضوی ہوتا ہے۔ ضائع شدہ حصوں کو بعد میں استعمال نہیں کیا جا سکتا۔ result.messages.

ٹرنکیشن ہیلپر مارکر کے لیے جگہ محفوظ رکھتا ہے اور پھر باقی جگہ کو شروع اور اختتام کے درمیان تقسیم کرتا ہے۔

export function boundToolResult(content: string, limit: number): string {
  if (content.length <= limit) return content;
  const marker = `n...[${content.length - limit} or more characters omitted]...n`;
  if (marker.length >= limit) return marker.slice(0, limit);
  const remaining = limit - marker.length;
  const front = Math.ceil(remaining / 2);
  const back = Math.floor(remaining / 2);
  return content.slice(0, front) + marker + (back ? content.slice(-back) : '');
}

کامیاب نتائج اور غلطی کے تار دونوں لاگ کرنے کے لیے درخواست دیں۔ content: boundToolResult(content, context.maxToolResultChars). یہ کمپیکٹ ورژن واضح طور پر صفر کی لمبائی والی دموں کو ہینڈل کرتا ہے۔ slice(-0) آپ غلطی سے پورا نتیجہ نہیں رکھ سکتے۔

maxContextChars ماڈل کو بھیجے گئے سلسلہ وار پیغامات کی صف کو محدود کرتا ہے۔ ہارنس پچھلے پیغامات کو واپس کی گئی تاریخ میں رکھتا ہے، لیکن selectContext() زیادہ سے زیادہ ابتدائی صارف کی درخواستیں اور حالیہ موڑ بھیجیں۔ کوئی خلاصہ نہیں ہے۔ اس موڑ کے لیے چھوڑی گئی تفصیلات ماڈل میں استعمال نہیں کی جا سکتی ہیں۔

یہ کریکٹر کی حد ہے، ٹوکن کی حد نہیں۔ ٹول کی تعریف اور فراہم کنندہ کے مخصوص اوور ہیڈ کو چھوڑ کر، JSON سیریلائزیشن کے بعد JavaScript سٹرنگ کی لمبائی کی پیمائش کرتا ہے۔ اصل فراہم کنندہ اڈاپٹر کو ٹوکن کی شناخت کا اکاؤنٹ اور جوابات کے لیے مخصوص جگہ کی ضرورت ہوتی ہے۔

ٹول کالز کو نتائج سے الگ نہ کریں۔

سیاق و سباق کو تراشنا اتنا آسان نہیں جتنا پرانے پیغام کو ہٹانا ہے۔ معاون جواب متعدد ٹولز کی درخواست کر سکتا ہے، جن میں سے ہر ایک کی شناخت کال آئی ڈی سے ہوتی ہے۔ نتائج کا صرف ایک حصہ بھیجنے سے غلط یا گمراہ کن گفتگو ہوتی ہے۔

لہذا سلیکٹر ثانوی پیغامات کو ہینڈل کرتا ہے۔ اس بیچ کے تمام نتائج ایک لازم و ملزوم گروپ کے طور پر۔ پورے گروپ کو شامل کریں یا چھوڑ دیں۔ یہ غیر متضاد نتائج اور نامکمل تعیناتیوں کو بھی مسترد کرتا ہے، بجائے اس کے کہ خاموشی سے متضاد سیاق و سباق کی تعمیر کی جائے۔

یہاں selectContext() سے src/core/context.tsاستعمال کرتے ہوئے Message سیکشن 1 میں بیان کردہ قسم۔ پہلا لوپ ٹرانسکرپٹس کی توثیق اور گروپ کرتا ہے۔ دوسرا بجٹ مکمل ہونے تک پورے گروپ کو واپس لے جاتا ہے۔

import type { Message } from './types.ts';

const size = (messages: readonly Message[]) => JSON.stringify(messages).length;

export function selectContext(messages: readonly Message[], maxChars: number): Message[] {
  if (!Number.isSafeInteger(maxChars) || maxChars < 1) {
    throw new RangeError('maxContextChars must be a positive safe integer');
  }
  if (!messages.length) return [];
  const groups: Message[][] = [];
  for (let i = 0; i < messages.length;) {
    const message = messages[i]!;
    if (message.role === 'assistant' && message.toolCalls?.length) {
      const ids = new Set(message.toolCalls.map(call => call.id));
      if (ids.size !== message.toolCalls.length) throw new Error('Duplicate tool call IDs in assistant turn');
      const group: Message[] = [message];
      i++;
      while (i < messages.length && messages[i]!.role === 'tool') {
        const result = messages[i]!;
        if (result.role === 'tool' && !ids.delete(result.callId)) throw new Error('Orphan or duplicate tool result');
        group.push(result);
        i++;
      }
      if (ids.size) throw new Error('Incomplete tool result batch');
      groups.push(group);
    } else {
      if (message.role === 'tool') throw new Error('Orphan tool result');
      groups.push([message]);
      i++;
    }
  }
  const first = groups[0]!;
  if (first[0]?.role !== 'user') throw new Error('Conversation must start with a user message');
  const recent: Message[][] = [];
  for (let i = groups.length - 1; i >= 1; i--) {
    const group = groups[i]!;
    const next = [...first, ...[group, ...recent].flat()];
    if (size(next) > maxChars) {
      if (recent.length === 0) throw new Error('Context budget too small for latest turn');
      break;
    }
    recent.unshift(group);
  }
  const selected = [...first, ...recent.flat()];
  if (size(selected) > maxChars) throw new Error('Context budget too small for required messages');
  return selected;
}

ہر ماڈل کال سے پہلے، استعمال کریں: selectContext(messages, context.maxContextChars) اس کا messages ان پٹ اگر یا تو ابتدائی اشارہ یا سب سے حالیہ مطلوبہ گروپ مماثل نہیں ہے، سلیکٹر کو پھینک دیا جاتا ہے اور ہارنس واپس کر دیا جاتا ہے۔ context_limit. یہ عمل درآمد کا فیصلہ ہے، ٹول کی غلطی نہیں جس سے ماڈل ٹھیک ہو سکتا ہے۔

ڈیٹرمنسٹک ٹیسٹ بیچ کے تحفظ، یتیم کا پتہ لگانے، آؤٹ پٹ ٹرنکیشن، اور ماڈل اورینٹیشن پروجیکشن کو لاگو کرتا ہے۔ وہ اس بات کو یقینی بناتے ہیں کہ سیاق و سباق نہ صرف چھوٹا ہے، لیکن پھر بھی ایک مربوط گفتگو کی نمائندگی کرتا ہے۔ اگلا، ہم ان فیصلوں کو قابل مشاہدہ بنائیں گے۔

6. ایجنٹ کے عمل کو قابل مشاہدہ بنائیں

ایجنٹ غیر متوقع کام کرتے ہوئے صحیح جواب دے سکتا ہے، جیسے کہ ناکام آپریشن کی دوبارہ کوشش کرنا، غیر ضروری ماڈل کال کرنا، یا اپنا زیادہ تر وقت ایک ٹول میں گزارنا۔ اکیلے آخری جواب ہمیں نہیں بتاتا کہ کیا ہوا ہے۔ آپ کو لاگنگ کے فریم ورک میں اپنے کنٹرول کو تبدیل کیے بغیر عمل درآمد کے واقعات کی ضرورت ہے۔

ساتھی ذخیرہ میں اس سنگ میل کو دیکھیں۔

git switch --detach 06-observability

عمل درآمد src/core/events.tsانضمام src/core/agent.ts.

پھانسی کی حدود میں واقعات کا اخراج

اپنا انتخاب شامل کریں۔ onEvent کال بیک runAgent. ہارنس جاری کیا جاتا ہے۔ model_start اور model_end ماڈل کالنگ کے بارے میں tool_start اور tool_end ٹول ڈسپیچ کے ارد گرد، model_error جب ماڈل آپریشن ناکام ہوجاتا ہے۔ run_end جب پھانسی ختم ہوتی ہے۔

ہر ایونٹ میں ایک ایگزیکیوشن ID، ایک بڑھتی ہوئی ترتیب نمبر، اور ٹائم اسٹیمپ ہوتا ہے۔ ماڈل اور ٹول کی تکمیل کے واقعات بھی ایک مونوٹونک گھڑی کا استعمال کرتے ہوئے ناپے گئے دورانیے کی اطلاع دیتے ہیں۔ یہ کارروائیوں کی ترتیب کو دوبارہ ترتیب دینے اور سست کالوں کی شناخت کے لیے کافی معلومات فراہم کرتا ہے۔

const events: AgentEvent[] = [];

const result = await runAgent({
  prompt: 'Inspect the project',
  model,
  tools,
  onEvent: event => events.push(event),
});

console.log(result.runId, events);

درآمد AgentEvent سے src/core/events.ts. کال بیک اختیاری ہے۔ کال کرنے والے جن کو ٹیلی میٹری کی ضرورت نہیں ہے وہ کال بیک کو چھوڑ سکتے ہیں۔

ایک باریک تقاضا ہے۔ مشاہداتی عمل کو کنٹرول نہیں کرنا چاہئے۔. ایک کامیاب ٹول کال کسی ایونٹ سننے والے میں استثناء کی وجہ سے ناکام نہیں ہونی چاہیے۔ ایونٹ ایمیٹر سامعین کے استثناء کو پکڑتا ہے اور اختیاری طور پر ان کے ذریعے رپورٹ کرتا ہے: onObserverErrorاس ہینڈلر کی ناکامی بھی دب جاتی ہے۔ تاہم، چونکہ کال بیکس ہم آہنگ ہوتے ہیں، ایک سست سننے والا اب بھی ایجنٹ کو تاخیر کرے گا۔

emitter کی طرف سے پیدا runId ایک بار randomUUID() سے node:cryptoاور دوبارہ ترتیب دیں sequence 0. کہ emit(event) یہ طریقہ ایونٹ کو بڑھاتا ہے اور دونوں کال بیکس کو الگ کرتا ہے۔

const enriched = {
  ...event, runId, sequence: ++sequence,
  timestamp: new Date().toISOString()
};
try {
  options.onEvent?.(enriched);
} catch (error) {
  try { options.onObserverError?.(error); }
  catch { /* Observer failures cannot change the run outcome. */ }
}

ماڈل یا ٹول باؤنڈری پر محفوظ کریں۔ const start = performance.now() پھانسی سے پہلے اخراج durationMs: performance.now() - start آب و ہوا شیئر کریں finish() مددگار خارج کرتا ہے۔ run_end ہر ٹرمینل کے اپنے نتائج واپس کرنے سے پہلے مشاہدے کے لیے ابتدائی ٹرمینیشن کو بھی برقرار رکھا جاتا ہے۔

ماڈلز کو تیار کیے بغیر ان کے استعمال کی وضاحت کرتا ہے۔

آپ اختیاری فراہم کنندہ رپورٹنگ ٹوکنز کے استعمال کے ذریعے ماڈل کے جواب کو بھی بڑھا سکتے ہیں۔

return {
  content: 'Done',
  usage: { inputTokens: 125, outputTokens: 20 },
};

استعمال کی اطلاع دی گئی گنتی کو اس طرح جمع کرتا ہے: result.usageپہلو بہ پہلو modelCallsWithUsage. یہ آخری فیلڈ اہم ہے۔ 0 رپورٹ شدہ ٹوکن کے ساتھ 0 کی استعمال کی رپورٹنگ کال کا مطلب ہے: نامعلوم استعمالیہ مفت دوڑنا نہیں ہے۔ کل کو خود بخود خراب کرنے کے بجائے، غلط شمار مسترد کر دیے جاتے ہیں۔

جمع کرنے والا اختیاری ان پٹ اور آؤٹ پٹ شمار لیتا ہے اور انفرادی قدروں اور نتیجے کی رقم دونوں کی تصدیق کرتا ہے۔

export type Usage = { inputTokens?: number; outputTokens?: number };
export type UsageTotals = {
  inputTokens: number; outputTokens: number; modelCallsWithUsage: number
};

export function addUsage(totals: UsageTotals, usage: Usage | undefined): void {
  if (!usage) return;
  for (const value of [usage.inputTokens, usage.outputTokens]) {
    if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
      throw new RangeError('Model usage must contain nonnegative safe integers');
    }
  }
  if (!Number.isSafeInteger(totals.inputTokens + (usage.inputTokens ?? 0)) ||
      !Number.isSafeInteger(totals.outputTokens + (usage.outputTokens ?? 0))) {
    throw new RangeError('Accumulated model usage exceeds safe integer range');
  }
  totals.inputTokens += usage.inputTokens ?? 0;
  totals.outputTokens += usage.outputTokens ?? 0;
  totals.modelCallsWithUsage++;
}

عمل کے آغاز پر، ہم کل کو 0 سے شروع کرتے ہیں۔ usage?: Usage ماڈل جوابی معاہدوں کو کال کریں۔ addUsage(totals, response.usage) ہر جواب کی تصدیق کے بعد، نتائج میں کل کی ایک کاپی شامل کریں۔ اگر استعمال غائب ہے تو کاؤنٹر تبدیل نہیں ہوگا۔

ایونٹ پے لوڈ جان بوجھ کر پرامپٹس، ٹول آرگومنٹس اور ٹول کے نتائج کو چھوڑ دیتا ہے۔ ایکسپورٹ میں احتیاط کی ضرورت ہے کیونکہ ایرر سٹرنگز میں اب بھی حساس ڈیٹا ہو سکتا ہے۔ یہ تقسیم شدہ ٹریسنگ یا انتہائی پائیدار آڈٹ لاگ کے بجائے ہلکا پھلکا، ان میموری انسٹرومینٹیشن ہے۔

ٹیسٹ tests/events.test.ts ایونٹ آرڈرنگ، ارتباط، استعمال اکاؤنٹنگ، مبصر تنہائی، اور ٹرمینل واقعات کو چیک کریں۔

7. ناکام راستوں کے ساتھ ساتھ خوش کن راستوں کی جانچ کریں۔

ہمارے استعمال میں اب ٹول کی توثیق، عملدرآمد کی رکاوٹیں، سیاق و سباق کا انتظام، اور مشاہدہ ہے۔ تاہم، یہ میکانزم بات چیت کرتے ہیں. ماڈل غلط ٹول کالز واپس کر سکتا ہے، بیچز کو قبل از وقت منسوخ کیا جا سکتا ہے، اور مسترد شدہ بیچز گفتگو کو ماڈل کو واپس بھیجنے سے قاصر رہ سکتے ہیں۔ آخری سنگ میل براہ راست ان حدود کو جانچتا ہے۔

آخری سنگ میل کے لیے ساتھی ذخیرہ کو چیک کریں۔

git switch --detach 07-testing

نئے ٹیسٹ ہو رہے ہیں۔ tests/failures.test.ts. یہ پہلے سنگ میل کے طور پر وہی اسکرپٹ ماڈل استعمال کرتا ہے، لہذا آپ API کلید کے بغیر غلطی کو دوبارہ پیش کر سکتے ہیں۔

ان پر بھروسہ کرنے سے پہلے ماڈل کے جوابات کی تصدیق کریں۔

تصدیق کا آلہ عنصر یہ کافی نہیں ہے۔ ماڈل اڈاپٹر غلط جواب دے سکتا ہے: غائب۔ contentغیر منظم toolCallsغائب دلائل یا ڈپلیکیٹ کال IDs۔ نیا validateModelResponse() جواب کو ریکارڈ کرنے یا اس پر عمل کرنے سے پہلے ہارنس جوابی لفافے کو چیک کرتا ہے۔

import type { Model, ToolCall } from './types.ts';

/** Validate the adapter boundary before accepting a model turn or running side effects. */
export function validateModelResponse(value: unknown): asserts value is Awaited> {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    throw new TypeError('Model response must be an object');
  }
  const response = value as Record;
  if (typeof response.content !== 'string') throw new TypeError('Model response content must be a string');
  if (response.toolCalls === undefined) return;
  if (!Array.isArray(response.toolCalls)) throw new TypeError('Model toolCalls must be an array');
  const ids = new Set();
  for (const call of response.toolCalls as unknown[]) {
    if (call === null || typeof call !== 'object' || Array.isArray(call)) {
      throw new TypeError('Tool call must be an object');
    }
    const item = call as Partial;
    if (typeof item.id !== 'string' || !item.id.trim()) throw new TypeError('Tool call id must be a nonempty string');
    if (typeof item.name !== 'string' || !item.name.trim()) {
      throw new TypeError('Tool call name must be a nonempty string');
    }
    if (!Object.hasOwn(item, 'arguments')) throw new TypeError('Tool call arguments are missing');
    if (ids.has(item.id)) throw new TypeError(`Duplicate tool call id: ${item.id}`);
    ids.add(item.id);
  }
}

کال validateModelResponse(response) پھر addUsage(usage, response.usage) میں try فوراً بعد بلاک کر دیں۔ model.generate()ثانوی پیغام شامل کرنے یا ٹول چلانے سے پہلے۔ جب کوئی بھی اسے پھینکتا ہے تو وہ اسے چھوڑ دیتا ہے۔ model_error اور واپس آو finish('model_error', { error: message })کہاں message یہ ایک استثنائی پیغام ہے۔

ایک غلط جواب اس کے ساتھ ختم ہوتا ہے: model_errorآلے کے کوئی ضمنی اثرات نہیں ہیں۔ یہ غلط دلائل کے ساتھ ایک درست ٹول کال سے مختلف ہے۔ مؤخر الذکر ایک قابل بازیافت ٹول کی خرابی بن جاتا ہے، جس سے ماڈل کو دوبارہ کوشش کرنے کی اجازت ملتی ہے۔

مسترد شدہ بیچوں کو ریکارڈ سے خارج کریں۔

ہماری جانچ نے حقیقی انضمام کی خرابیوں کو بھی بے نقاب کیا۔ اس سے پہلے، ایک معاون پیغام جس میں ٹول کال شامل تھی ہارنس میں شامل کی گئی تھی۔ پہلے چیک کریں کہ آیا بیچ سے تجاوز کیا گیا ہے۔ maxToolCalls. اس پلیسمنٹ کو مسترد کرنے سے آپ کی گفتگو میں ایک غیر جوابی ٹول کال رہ جائے گی۔

اصلاحات چھوٹی لیکن اہم ہیں۔ ثانوی موڑ لاگ کرنے سے پہلے اپنا بجٹ چیک کریں۔.

if (calls.length > maxToolCalls - toolCalls) {
  return finish('tool_limit');
}
messages.push({
  role: 'assistant',
  content: response.content,
  ...(calls.length ? { toolCalls: calls } : {})
});

جب رپورٹ کارڈ کی بات آتی ہے تو یہ بجٹ کو مسترد کرنے کو جوہری بنا دیتا ہے۔ پہلے سے منظور شدہ پلیسمنٹ کے دوران منسوخی کا نتیجہ اب بھی نامکمل حتمی ریکارڈ کی صورت میں نکل سکتا ہے۔ ان طریقوں کو دوبارہ شروع کرنے کے لیے ایڈجسٹمنٹ کی ضرورت ہوگی۔

دوبارہ پیدا کرنے کے قابل متغیرات استعمال کریں۔

غلط جوابات، منسوخی، اور مبصر کی ناکامیوں کے لیے ہدف شدہ ٹیسٹوں کے ساتھ، ہم چلائیں گے: 150 بیج والے ٹول کی تعیناتی کے منظرنامے۔ بیچ کے سائز اور بجٹ مختلف ہوتے ہیں۔ ایک فکسڈ سیوڈورنڈم بیج ہر رن میں ایک ہی ترتیب پیدا کرتا ہے، ناکامیوں کو دوبارہ پیدا کرنے کے قابل بناتا ہے۔

درج ذیل مثالیں اس سے تعلق رکھتی ہیں: tests/failures.test.ts. وہ مناسب طریقے سے فارمیٹ شدہ کرنسی بنانے کے لیے ایک نون فریلز ٹول اور ایک مددگار کا اشتراک کرتے ہیں۔

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { runAgent } from '../src/core/agent.ts';
import { scriptedModel } from '../src/models/fake.ts';
import type { Tool } from '../src/core/types.ts';

const noop: Tool = {
  name: 'noop', description: 'Return a fixed result',
  parameters: { type: 'object' },
  async execute() { return 'ok'; }
};
const call = (id: string) => ({ id, name: 'noop', arguments: {} });

پہلے ٹیسٹ میں، ہم بجٹ کو 0 سے 8 اور بیچ کا سائز 0 سے 12 تک تبدیل کرتے ہیں۔ عمل درآمد کی گنتی، باہر نکلنے کی حیثیت، اور آیا مسترد شدہ کالیں تاریخ سے خارج ہیں یا نہیں۔

test('seeded batches respect tool budgets', async () => {
  let seed = 0x12345678;
  const next = () => {
    seed ^= seed << 13;
    seed ^= seed >>> 17;
    seed ^= seed << 5;
    return seed >>> 0;
  };

  for (let i = 0; i < 150; i++) {
    const limit = next() % 9;
    const batch = next() % 13;
    let executed = 0;
    const result = await runAgent({
      prompt: 'test', maxToolCalls: limit,
      tools: [{
        ...noop,
        async execute() { executed++; return 'ok'; }
      }],
      model: scriptedModel([
        {
          content: '',
          toolCalls: Array.from({ length: batch }, (_, j) => call(`call-${j}`))
        },
        { content: 'done' }
      ])
    });

    const admitted = batch <= limit;
    assert.equal(executed, admitted ? batch : 0, `case ${i}`);
    assert.equal(result.toolCalls, admitted ? batch : 0, `case ${i}`);
    assert.equal(
      result.status, admitted ? 'completed' : 'tool_limit', `case ${i}`
    );
    if (!admitted) {
      assert.deepEqual(result.messages, [{ role: 'user', content: 'test' }]);
    }
  }
});

ایک منظور شدہ تعیناتی تمام درخواست کردہ ٹولز کو چلاتی ہے، جبکہ زیادہ بجٹ کی تعیناتی صفر پر چلتی ہے۔ کہ case لیبل تیار کردہ منظرناموں میں ناکام منظرناموں کی نشاندہی کرتا ہے اور انہیں ایک مقررہ بیج کے ساتھ دوبارہ تیار کر سکتا ہے۔

ایک اضافی 60 رنز اس بات کا تعین کرنے کے لیے کیے جاتے ہیں کہ آیا ٹول کا استثنیٰ ایک قابل بازیافت غلطی بنی ہوئی ہے۔ یہ ایک مختلف غلطی کے پیغام کے ساتھ اسی ناکامی کے راستے کو دہراتا ہے۔ بیج کی تبدیلی کا استعمال نہیں کیا جاتا ہے۔

test('tool exceptions remain recoverable', async () => {
  let executed = 0;
  for (let i = 0; i < 60; i++) {
    const result = await runAgent({
      prompt: 'test',
      tools: [{
        ...noop,
        async execute() {
          executed++;
          throw new Error(`failure ${i}`);
        }
      }],
      model: scriptedModel([
        { content: '', toolCalls: [call('1')] },
        { content: 'recovered' }
      ])
    });
    assert.equal(result.status, 'completed');
    assert.equal(result.answer, 'recovered');
    assert.ok(result.messages.some(message =>
      message.role === 'tool' && message.isError &&
      message.content === `failure ${i}`
    ));
  }
  assert.equal(executed, 60);
});

بیج کے منظرنامے جائیداد کے طرز کے چیک ہوتے ہیں، جبکہ استثنیٰ ٹیسٹ بار بار مخصوص بحالی کے راستوں کو چیک کرتے ہیں۔ یہ آپریٹنگ سسٹم سینڈ باکسنگ کی درستگی کو ثابت یا تبدیل نہیں کرتا ہے، لیکن دونوں جسمانی ماڈل کے بغیر چلتے ہیں اور استعمال کے رویے کے دوبارہ قابل ثبوت فراہم کرتے ہیں۔

اب ہم نے ایک ایگزیکیوشن لیئر کے ساتھ ایک ہارنس بنایا ہے جو ایک سادہ ماڈل/ٹول لوپ میں توثیق، اجازت، پابندیاں، سیاق و سباق اور ٹیلی میٹری کا مالک ہے۔ ماڈل اب بھی کارروائی کا مشورہ دیتا ہے، لیکن اصل میں کیا ہوتا ہے اس کا تعین کرنے کے لئے کنٹرول ذمہ دار ہے۔.

درج ذیل مثال کو چلانے کی کوشش کریں: npm test. ریپوزٹری میں مکمل سنگ میل کے نفاذ کے ساتھ اوپر بیان کردہ حدود کے لیے اضافی ٹارگٹ کیسز شامل ہیں۔

اگلا مرحلہ: اصل زبان کے ماڈل کو جوڑنا

اس پورے ٹیوٹوریل کے دوران، ہم نے اصل LLM کو کال کرنے کے بجائے اسکرپٹ ماڈل کا استعمال کیا۔ یہ جان بوجھ کر ہے۔ تعییناتی جوابات آپ کو نیٹ ورک کالز، API کیز، یا غیر متوقع ماڈل کے رویے کے بغیر، ناکامی کے راستوں سمیت، اپنے استعمال کی جانچ کرنے کی اجازت دیتے ہیں۔

تاہم Model انٹرفیس فراہم کنندگان کو قابل تبادلہ ہونے کی اجازت دینے کے لیے ڈیزائن کیا گیا ہے۔ OpenAI، Anthropic، یا کسی دوسرے فراہم کنندہ کو مربوط کرنے کے لیے ایجنٹ لوپ کو دوبارہ لکھنے کی ضرورت نہیں ہے۔ آپ کو صرف اپنے فراہم کنندہ کے API اور موجودہ اقسام کو تبدیل کرنے کے لیے ایک اڈاپٹر کی ضرورت ہے۔

مثال کے طور پر، ایک OpenAI اڈاپٹر جو Responses API استعمال کرتا ہے اس طرح نظر آئے گا:

// src/models/openai.ts
import OpenAI from 'openai';
import type { Model } from '../core/types.ts';

export function openAIModel(client: OpenAI): Model {
  return {
    async generate({ messages, tools, signal }) {
      const response = await client.responses.create(
        {
          model: 'YOUR_MODEL',
          input: toOpenAIInput(messages),
          tools: toOpenAITools(tools),
        },
        { signal }
      );

      return {
        content: extractText(response),
        toolCalls: extractToolCalls(response),
        usage: extractUsage(response),
      };
    },
  };
}

یہ ایک آرکیٹیکچرل خاکہ ہے، مکمل اڈاپٹر نہیں۔ تبادلوں کا فنکشن پیغامات اور ٹول کی تعریفوں کو فراہم کنندہ کی شکل میں تبدیل کرتا ہے اور پھر جواب کو معمول بناتا ہے۔ خاص طور پر، اسے ٹول کال آئی ڈیز، پارس ٹول آرگیومینٹس، اور بعد میں آنے والی درخواستوں میں صحیح درخواست کے ساتھ ٹول کے نتائج کو منسلک کرنا چاہیے۔

اڈاپٹر ہارنس سے سگنل بھی لے جاتا ہے۔ AbortSignalمنسوخی کو API کی درخواستوں پر پھیلانے کی اجازت دیں۔ سپلائر کی غلطیاں ایک مستثنیٰ ہیں، جسے Harness پہلے سے ہی مندرجہ ذیل طور پر سنبھالتا ہے: model_error نتیجہ

ایک بار جب اڈاپٹر لاگو ہوتا ہے۔ Modelباقی نظام بدستور برقرار ہے۔ اس سے قطع نظر کہ کون سا فراہم کنندہ اگلا جواب پیش کرتا ہے، وہی ٹول رجسٹری، دلیل کی توثیق، عملدرآمد بجٹ، سیاق و سباق کا انتظام، اور مشاہدہ قابل عمل ہے۔

ایکسپلور کرنے کے قابل اضافی بہتریوں میں فی فراہم کنندہ ٹوکن اکاؤنٹنگ، API کی دوبارہ کوشش اور شرح کو محدود کرنے کی ہینڈلنگ، بات چیت کا مستقل لاگنگ، اور ناقابل اعتماد کوڈ پر عمل درآمد کے لیے مضبوط عمل تنہائی شامل ہیں۔ یہ اپنے آپ میں ایک اہم انجینئرنگ چیلنج ہے، لیکن یہ مختلف فن تعمیر کی ضرورت کے بجائے ہماری مقرر کردہ حدود میں فٹ بیٹھتا ہے۔

مکمل نفاذ یہاں پایا جا سکتا ہے: 07-testing اہم قدم۔

ہم نے صرف ایک لوپ سے زیادہ بنایا ہے جو ماڈل کو پرامپٹ بھیجتا ہے اور افعال کو انجام دیتا ہے۔ ہم نے ایک مشین بنائی ہے جو فیصلے کرتی ہے۔ کن کارروائیوں کی اجازت ہے، کتنا کام ہو سکتا ہے، ماڈل کو کون سا سیاق و سباق ملتا ہے، اور غلطیوں کو کیسے ہینڈل اور مشاہدہ کیا جاتا ہے۔.

یہ ٹولز کے ساتھ لینگویج ماڈل اور ایجنٹ کے استعمال کے درمیان ضروری فرق ہے۔

اوپر تک سکرول کریں۔