
আমাদের আগের পর্বে আমরা যখন Agent যখন 'ভাবছে' তখন UI-তে Thinking/Loading State দেখানোর সঠিক উপায় নিয়ে কাজ করেছিলাম, তখন AgentThinkingTimeline কম্পোনেন্ট বানিয়ে এজেন্টের প্রতিটি ধাপ লাইভ ট্র্যাক করেছিলাম। কিন্তু সেই কোড লেখার সময় আপনি নিশ্চয়ই একটা জিনিস খেয়াল করেছেন—React-এর useChat হুক থেকে যখন m.toolInvocations বা m.annotations আসে, তখন সেগুলোর ডিফল্ট TypeScript টাইপ থাকে unknown বা জেনেরিক any-এর কাছাকাছি। ফলে তাড়াহুড়ো করে কোড মিলাতে গিয়ে অনেক ডেভেলপারই as any বা as MyType লিখে কম্পাইলারের মুখ বন্ধ করে দেন!
একটি পূর্ণাঙ্গ AI Agent সিস্টেমকে শুরু থেকে শেষ পর্যন্ত (End-to-End) TypeScript দিয়ে শতভাগ Type-safe রাখার মূল কৌশল হলো—Zod স্কিমাকে Single Source of Truth হিসেবে ব্যবহার করা এবং z.infer<typeof schema> ও Discriminated Union দিয়ে এজেন্টের ৪টি প্রধান বাউন্ডারি সুরক্ষিত করা: ১) টুলের ইনপুট প্যারামিটার, ২) টুলের রিটার্ন এনভেলপ ({ ok: true, data: T } | { ok: false, agentInstruction: string }), ৩) মডেলের স্ট্রাকচার্ড আউটপুট (generateObject), এবং ৪) React UI-এর ToolInvocation টাইপ গার্ড (isToolInvocationFor)। এই আর্কিটেকচার ব্যবহার করলে পুরো প্রজেক্টে একটিও বিপজ্জনক as any কাস্ট লিখতে হয় না এবং রানটাইমে কখনোই Cannot read properties of undefined ক্র্যাশ ঘটে না।
আজকের এই ২১তম পর্বের মাধ্যমে আমাদের DevPulse প্রজেক্টের দ্বিতীয় ধাপ (Phase 2: Tool Calling, Multi-step Reasoning & Type Safety, পর্ব ১৭–২১) সম্পন্ন হতে যাচ্ছে। পরের পর্ব থেকে প্রোডাকশন রেট-লিমিটিং ও ডেটাবেস মেমোরিতে (Phase 3) যাওয়ার আগে আজ আমরা আমাদের পুরো এজেন্ট কোডবেসকে এমনভাবে টাইপ-লক করব যেন কম্পাইল টাইম এবং রানটাইম—কোথাও কোনো ফাঁক না থাকে।
মূল সমস্যাটা কোথায়: অনিশ্চিত LLM আউটপুট বনাম স্ট্যাটিক TypeScript
সাধারণ ফুল-স্ট্যাক ওয়েব অ্যাপে ব্যাকএন্ড এবং ফ্রন্টএন্ড দুটোই আপনার নিজের কোড। ডেটাবেস থেকে কী কলাম আসবে এবং API কী রিটার্ন করবে তা আগে থেকেই ফিক্সড থাকে, তাই সাধারণ TypeScript interface লিখে দিলেই কাজ হয়ে যায়।
কিন্তু AI Agent সিস্টেমে মূল ব্রেইন হলো একটি LLM (যেমন Claude 3.5 Sonnet বা GPT-4o), যা রানটাইমে ডায়নামিকভাবে টেক্সট এবং JSON তৈরি করে। মডেল কখনো সঠিক আর্গুমেন্ট পাঠায়, কখনো আংশিক স্ট্রিম (partial-call) পাঠায়, আবার কখনো এক্সটার্নাল গিটহাব API থেকে 404 এরর আসে।
এখন আপনি যদি শুধু ফাইলের উপরে একটি TypeScript interface লিখে ফ্রন্টএন্ডে const issue = inv.result as GitHubIssueDTO লিখে বসে থাকেন, তবে npm run build হয়তো কোনো এরর ছাড়াই পাস করে যাবে—কিন্তু রানটাইমে যেই মুহূর্তে টুল থেকে ডেটার বদলে এরর অবজেক্ট আসবে, সাথে সাথে আপনার React পেজ ক্র্যাশ করবে! তাই এজেন্ট সিস্টেমে সত্যিকারের টাইপ-সেফটি পেতে হলে Compile-time Type Inference এবং Runtime Schema Narrowing একসাথে ব্যবহার করতে হয়।

একটি ফুল-স্ট্যাক AI Agent-এর ৪টি Type Boundary
আমাদের DevPulse এজেন্টকে ব্যাকএন্ড থেকে ফ্রন্টএন্ড পর্যন্ত পুরোপুরি টাইপ-সেফ রাখার জন্য আমরা ৪টি নির্দিষ্ট বাউন্ডারিতে (সীমানায়) টাইপ কন্ট্রাক্ট বসাব:
১. বাউন্ডারি ১ — টুল ইনপুট কন্ট্রাক্ট (LLM -> Server Tool): আলাদা করে ম্যানুয়াল interface না লিখে সরাসরি Zod স্কিমা থেকে z.infer দিয়ে টাইপ তৈরি করা, যাতে প্রম্পটের স্কিমা এবং ফাংশনের আর্গুমেন্ট টাইপ কখনোই আলাদা না হয়ে যায়।
২. বাউন্ডারি ২ — Discriminated Tool Output Envelope (Server Tool -> Agent Loop): সব টুলের আউটপুটকে একটি কেন্দ্রীয় ToolContractRegistry ম্যাপে বাঁধা এবং ok: true বনাম ok: false ডিসক্রিমিনেটেড ইউনিয়ন ব্যবহার করা—যাতে if (result.ok) চেক না করে কেউ ভুলেও result.data রিড করতে না পারে।
৩. বাউন্ডারি ৩ — স্ট্রাকচার্ড LLM আউটপুট (generateObject): এজেন্টকে দিয়ে যখন কোনো বাগ রিপোর্টের ক্যাটাগরি বা সিভিয়ারিটি নির্ণয় করাতে হবে, তখন সাধারণ টেক্সট পার্স না করে generateObject ও Zod স্কিমা দিয়ে শতভাগ টাইপ-ভ্যালিডেটেড অবজেক্ট জেনারেট করা।
৪. বাউন্ডারি ৪ — ফ্রন্টএন্ড UI Type Guard (Stream -> React JSX): কাস্টম TypeScript Type Predicate (inv is NarrowedToolInvocation<'inspectLiveGitHubIssue'>) তৈরি করা, যাতে JSX-এর ভেতরে টুলের নাম চেক করলেই সাথে সাথে VS Code-এ inv.args এবং inv.result.data-এর শতভাগ অটো-কমপ্লিট পাওয়া যায়।
নিচের ফ্লোচার্টে দেখুন কীভাবে একটিমাত্র কেন্দ্রীয় Zod স্কিমা থেকে এই ৪টি বাউন্ডারিতেই টাইপ সেফটি প্রবাহিত হয়:
`mermaid flowchart TD ZodContract[Single Source of Truth: Zod Schemas in type-contracts.ts] -->|z.infer কম্পাইল-টাইম + .safeParse রানটাইম| B1[বাউন্ডারি ১: টুল ইনপুট ভ্যালিডেশন] ZodContract -->|Mapped ToolContractRegistry + Discriminated Union| B2[বাউন্ডারি ২: টুল আউটপুট এনভেলপ ok: true বা false] ZodContract -->|generateObject স্কিমা গ্রামার| B3[বাউন্ডারি ৩: স্ট্রাকচার্ড LLM ট্রায়াজ আউটপুট] ZodContract -->|Type Predicate Narrowing Guard| B4[বাউন্ডারি ৪: React UI ToolInvocation অটো-কমপ্লিট]
B1 --> SafeApp[শূন্য 'any' কাস্ট ও শূন্য রানটাইম TypeError ক্র্যাশ]
B2 --> SafeApp
B3 --> SafeApp
B4 --> SafeApp
`
আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): আমাদের কেন্দ্রীয় Zod স্কিমা (type-contracts.ts) একাধারে ৪টি বাউন্ডারিকে নিয়ন্ত্রণ করে: ১ম বাউন্ডারিতে z.infer ও .safeParse দিয়ে টুলের ইনপুট যাচাই করে, ২য় বাউন্ডারিতে ToolContractRegistry ও ডিসক্রিমিনেটেড ইউনিয়ন দিয়ে টুলের আউটপুট টাইপ বাঁধে, ৩য় বাউন্ডারিতে generateObject দিয়ে LLM-এর আউটপুট নিশ্চিত করে এবং ৪র্থ বাউন্ডারিতে টাইপ গার্ড ফাংশনের মাধ্যমে React UI-কে শতভাগ টাইপ-সেফ অটো-কমপ্লিট দেয়।
ধাপ ১: কেন্দ্রীয় ToolContractRegistry এবং Type Guard তৈরি করা (type-contracts.ts)
আগের পর্বগুলোতে আমাদের AgentStepTrace এবং টুল আউটপুটের টাইপ রাখা হয়েছিল unknown। ফলে ফ্রন্টএন্ডে গিয়ে আমাদের অনুমান করতে হতো কোন টুল কী ডেটা পাঠাচ্ছে।
চলুন এবার src/lib/agent/type-contracts.ts ফাইলটি তৈরি করি। এখানে আমরা TypeScript-এর Mapped Types এবং Discriminated Union (SuccessEnvelope<T> | ErrorEnvelope) ব্যবহার করে আমাদের চারটি টুলের ইনপুট ও আউটপুটকে এক সুতোয় বেঁধে দেব:
`typescript // src/lib/agent/type-contracts.ts import { z } from 'zod'; import { type ToolInvocation } from 'ai'; import { searchDocsSchema, checkIssuesSchema, draftTicketSchema, } from './blueprint'; import { inspectGitHubIssueSchema, type SanitizedGitHubIssueDTO, } from './custom-github-tool';
export type SuccessEnvelope = { ok: true; toolName: ToolName; durationMs: number; data: T; };
export type ErrorEnvelope = { ok: false; toolName: ToolName; retryable: boolean; durationMs: number; data: null; agentInstruction: string; };
export type ToolResultEnvelope = SuccessEnvelope | ErrorEnvelope;
export interface SearchDocsOutputDTO { queryUsed: string; topK: number; matchedChunks: Array<{ docPath: string; score: number; excerpt: string; }>; }
export interface CheckIssuesOutputDTO { keyword: string; statusFilter: 'open' | 'closed' | 'all'; issues: Array<{ id: string; title: string; status: 'open' | 'closed'; component: string; }>; }
export interface DraftTicketOutputDTO { draftId: string; status: 'awaiting_user_approval'; ticketPreview: z.infer; }
export interface ToolContractRegistry { searchKnowledgeBase: { input: z.infer; output: ToolResultEnvelope; }; checkIssueTracker: { input: z.infer; output: ToolResultEnvelope; }; inspectLiveGitHubIssue: { input: z.infer; output: ToolResultEnvelope; }; draftIssueTicket: { input: z.infer; output: ToolResultEnvelope; }; }
export type ToolName = keyof ToolContractRegistry; export type ToolInput = ToolContractRegistry[K]['input']; export type ToolOutput = ToolContractRegistry[K]['output'];
export type TypedAgentStepTrace = { [K in ToolName]: { stepIndex: number; thoughtSummary: string; selectedTool: K; toolInput: ToolInput; toolOutput: ToolOutput; durationMs: number; }; }[ToolName];
export type NarrowedToolInvocation = ( | { state: 'partial-call' | 'call'; toolCallId: string; toolName: K; args: Partial<ToolInput>; } | { state: 'result'; toolCallId: string; toolName: K; args: ToolInput; result: ToolOutput; } ) & ToolInvocation;
export function isToolInvocationFor( invocation: ToolInvocation, expectedToolName: K ): invocation is NarrowedToolInvocation { return invocation.toolName === expectedToolName; } `
এই ফাইলটির তিনটি জায়গা ভালোভাবে লক্ষ্য করুন:
ToolResultEnvelope<T>(Discriminated Union): যখনok: trueহয় কেবল তখনইdata: Tপাওয়া যায়, আর যখনok: falseহয় তখনdataহয়ে যায়nullএবং সাথে থাকেagentInstruction: string। এর ফলে কেউ ভুলেও এরর চেক না করেdata-এর ভেতরের প্রপার্টি রিড করতে পারবে না।- স্ট্রিমিং অবস্থায়
Partial<ToolInput<K>>: যখন টুলটি'partial-call'বা'call'অবস্থায় থাকে, তখনো মডেল পুরো আর্গুমেন্ট স্ট্রিম করা শেষ করেনি। তাই আমাদের টাইপটি স্বয়ংক্রিয়ভাবে তখনargs-কেPartialহিসেবে ধরে, যাতে আনডিফাইন্ড ফিল্ড নিয়ে কোনো ক্র্যাশ না হয়! isToolInvocationForটাইপ গার্ড: এই ছোট্ট ফাংশনটি আমাদের React কম্পোনেন্টে জেনেরিকToolInvocation-কে নিমেষেই নির্দিষ্ট টুলের টাইপে সংকুচিত (Narrow) করে ফেলবে।
ধাপ ২: generateObject দিয়ে স্ট্রাকচার্ড LLM আউটপুট টাইপ-সেফ করা (structured-triage.ts)
অনেক সময় আমাদের এজেন্টের ভেতরেই কোনো ইউজার মেসেজ পড়ে সেটিকে নির্দিষ্ট ক্যাটাগরিতে ভাগ করতে হয় (যেমন: বাগটির সিভিয়ারিটি কত, কোন টুল আগে কল করা উচিত ইত্যাদি)। সাধারণ generateText দিয়ে JSON চেয়ে সেটিকে JSON.parse() করা খুবই ঝুঁকিপূর্ণ।
চলুন src/lib/agent/structured-triage.ts ফাইলটি তৈরি করি, যেখানে Vercel AI SDK-এর generateObject সরাসরি আমাদের Zod স্কিমা মেনে ১০০% টাইপ-সেফ অবজেক্ট রিটার্ন করবে:
`typescript // src/lib/agent/structured-triage.ts import 'server-only'; import { generateObject } from 'ai'; import { createAnthropic } from '@ai-sdk/anthropic'; import { z } from 'zod';
export const bugTriageClassificationSchema = z.object({ category: z.enum([ 'rate-limiting', 'authentication', 'database-connection', 'ui-rendering', 'general-question', ]), estimatedSeverity: z.enum(['low', 'medium', 'high', 'critical']), requiresExternalGitHubCheck: z.boolean(), recommendedFirstTool: z.enum([ 'searchKnowledgeBase', 'checkIssueTracker', 'inspectLiveGitHubIssue', ]), extractedKeywords: z.array(z.string()).min(1).max(4), reasoningSummary: z.string().max(200), });
export type BugTriageClassification = z.infer< typeof bugTriageClassificationSchema
;
export async function classifyDeveloperQuery( userQuery: string ): Promise { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { throw new Error('Missing ANTHROPIC_API_KEY in server environment.'); }
const anthropic = createAnthropic({ apiKey });
const { object } = await generateObject({ model: anthropic('claude-3-5-sonnet-latest'), schema: bugTriageClassificationSchema, temperature: 0, system: 'Classify incoming developer support queries into strict triage metadata for the DevPulse agent router.', prompt: userQuery, });
return object; } `
এখানে classifyDeveloperQuery ফাংশনটি যা রিটার্ন করছে (object), তা কম্পাইল টাইমে যেমন BugTriageClassification টাইপের, তেমনি রানটাইমেও Zod দিয়ে পুরোপুরি যাচাইকৃত।
ধাপ ৩: কোনো any ছাড়া React UI-তে টুল কার্ড রেন্ডার করা (TypeSafeToolRenderer.tsx)
এবার চলুন দেখি আমাদের তৈরি করা isToolInvocationFor টাইপ গার্ড ব্যবহার করলে React ফ্রন্টএন্ডের কোড কতটা পরিষ্কার এবং নিরাপদ হয়ে যায়। নিচের src/components/TypeSafeToolRenderer.tsx ফাইলে একটিও as any বা as unknown কাস্ট ব্যবহার করা হয়নি, অথচ প্রতিটি প্রপার্টিতে আপনি পূর্ণ অটো-কমপ্লিট পাবেন:
`tsx // src/components/TypeSafeToolRenderer.tsx 'use client';
import { type ToolInvocation } from 'ai'; import { isToolInvocationFor } from '@/lib/agent/type-contracts';
interface TypeSafeToolRendererProps { invocation: ToolInvocation; }
export default function TypeSafeToolRenderer({
invocation,
}: TypeSafeToolRendererProps) {
if (isToolInvocationFor(invocation, 'inspectLiveGitHubIssue')) {
if (invocation.state !== 'result') {
return (
⚡ Inspecting GitHub: {invocation.args.repository ?? '...'}{' '}
{invocation.args.issueNumber ? #${invocation.args.issueNumber} : ''}
);
}
const envelope = invocation.result;
if (!envelope.ok) {
return (
<div className='p-3 rounded-lg bg-amber-950/40 border border-amber-800/60 text-xs text-amber-200'>
⚠ GitHub Tool Alert ({envelope.durationMs}ms): {envelope.agentInstruction}
</div>
);
}
const issue = envelope.data;
return (
<div className='p-3.5 rounded-lg bg-slate-950 border border-cyan-800/60 text-xs space-y-1.5'>
<div className='flex items-center justify-between font-mono'>
<span className='text-cyan-400 font-semibold'>
{issue.repository} #{issue.issueNumber}
</span>
<span className='text-emerald-400 uppercase text-[10px]'>
{issue.state} • {issue.commentsCount} comments
</span>
</div>
<div className='text-slate-100 font-medium'>{issue.title}</div>
<p className='text-slate-400 line-clamp-2'>{issue.bodyExcerpt}</p>
</div>
);
}
if (isToolInvocationFor(invocation, 'searchKnowledgeBase')) { if (invocation.state !== 'result') { return ( 🔍 Searching Docs for: '{invocation.args.query ?? '...'}' ); }
const envelope = invocation.result;
if (!envelope.ok) {
return (
<div className='p-3 rounded-lg bg-red-950/40 border border-red-800 text-xs text-red-200'>
Docs search failed: {envelope.agentInstruction}
</div>
);
}
return (
<div className='p-3 rounded-lg bg-slate-950 border border-purple-800/60 text-xs space-y-1'>
<div className='text-purple-400 font-mono font-semibold'>
📚 Retrieved {envelope.data.matchedChunks.length} doc chunk(s) for '{envelope.data.queryUsed}'
</div>
{envelope.data.matchedChunks.map((chunk) => (
<div key={chunk.docPath} className='text-slate-300 truncate'>
• {chunk.docPath} (Score: {chunk.score})
</div>
))}
</div>
);
}
return null; } `

আমার নিজের একটি ডিবাগিং শিক্ষা: যখন একটি অলস as টাইপ কাস্ট পুরো চ্যাট পেজ ক্র্যাশ করিয়ে দিয়েছিল!
আমি যখন আমার Movie Explorer এবং GitHub Issues Tracker প্রজেক্টে এক্সটার্নাল ডেটা নিয়ে কাজ করেছিলাম, তখন সবসময় অপশনাল চেইনিং ব্যবহার করতাম। কিন্তু DevPulse এজেন্টের প্রথম প্রোটোটাইপে যখন আমি ফ্রন্টএন্ডে টুল কার্ড রেন্ডার করতে যাই, তখন তাড়াহুড়োর মাথায় একটা শর্টকাট নিয়েছিলাম যার মাশুল আমাকে দিতে হয়েছিল মাত্র দশ মিনিটের মাথায়।
হয়েছিল কী—useChat হুকের ভেতরে toolInvocation.result-এর ডিফল্ট টাইপ ছিল any। আমি জানতাম আমার গিটহাব টুলটি সফল হলে SanitizedGitHubIssueDTO রিটার্ন করে। তাই আমি কম্পাইলারের ওয়ার্নিং থামানোর জন্য সোজা লিখে দিয়েছিলাম: const issue = toolInvocation.result as SanitizedGitHubIssueDTO; এবং তার নিচেই JSX-এ লিখেছিলাম issue.labels.map(...)। আমার টার্মিনালে tsc --noEmit কোনো এরর ছাড়াই পাস করল এবং সঠিক ইস্যু নম্বর দিয়ে টেস্ট করার সময় কার্ডটি চমৎকার রেন্ডার হলো।
কিন্তু এর ঠিক দশ মিনিট পর আমি যখন টেস্ট করার জন্য একটি ভুয়া ইস্যু নম্বর (#999999) দিয়ে প্রম্পট পাঠালাম, তখন সার্ভারে আমার কাস্টম টুলটি গিটহাব থেকে 404 পেয়ে নিয়মমতো { ok: false, data: null, agentInstruction: 'Issue does not exist' } রিটার্ন করল। যেই মুহূর্তে সেই রেজাল্ট ব্রাউজারে পৌঁছাল, আমার React কম্পোনেন্ট অন্ধের মতো সেটিকেও as SanitizedGitHubIssueDTO ধরে নিল এবং undefined-এর ওপর .labels.map() চালাতে গিয়ে পুরো চ্যাট ইন্টারফেসটি সাদা স্ক্রিন (Unhandled Runtime TypeError) হয়ে ক্র্যাশ করল!
সেদিনই আমি কোড থেকে সব as টাইপ কাস্ট মুছে ফেলে ToolResultEnvelope<T> (ডিসক্রিমিনেটেড ইউনিয়ন) এবং isToolInvocationFor টাইপ গার্ড তৈরি করি। এটি বসানোর সাথে সাথেই TypeScript কম্পাইলার নিজে থেকেই আমার কোডের ৩টি জায়গায় লাল দাগ দিয়ে দেখিয়ে দিল কোথায় কোথায় আমি if (!envelope.ok) চেক করতে ভুলে গিয়েছিলাম! কম্পাইল টাইমে সেই ৩টি চেক বসানোর পর থেকে আজ পর্যন্ত কোনো 404 বা নেটওয়ার্ক টাইমআউটে আমাদের UI একবারও ক্র্যাশ করেনি।
এজেন্ট সিস্টেমে TypeScript ব্যবহারের সময় যে ৪টি ভুল এড়িয়ে চলবেন
আমাদের রোডম্যাপের দ্বিতীয় ধাপ (Phase 2) শেষ করার আগে নিশ্চিত হয়ে নিন আপনার কোডবেসে যেন এই ৪টি ভুল না থাকে:
১. LLM আউটপুটে JSON.parse(text) as MyType ব্যবহার করা
মনে রাখবেন, TypeScript-এর as কি-ওয়ার্ড রানটাইমে কোনো ডেটা চেক করে না; এটি শুধু কম্পাইলারকে চোখ বন্ধ রাখতে বলে। মডেল যদি কোনো কারণে একটা ফিল্ড মিস করে বা নামের বানান ভুল করে, তবে as MyType আপনার অ্যাপকে সরাসরি ক্র্যাশের দিকে ঠেলে দেবে। সবসময় schema.safeParse() অথবা generateObject() ব্যবহার করবেন।
২. আলাদাভাবে TypeScript interface এবং Zod স্কিমা মেইনটেইন করা
আপনি যদি এক ফাইলে হাত দিয়ে interface লেখেন আর আরেক ফাইলে Zod স্কিমা লেখেন, তবে কয়েক দিন পর কোনো একটি ফিল্ড পরিবর্তন করলে দুটির মধ্যে অমিল (Schema Drift) তৈরি হবেই। তাই সবসময় শুধুমাত্র Zod স্কিমা লিখবেন এবং সেখান থেকেই z.infer<typeof mySchema> দিয়ে TypeScript টাইপ বের করে নেবেন।
৩. Discriminated Union বাদ দিয়ে সব ফিল্ড Optional (?) রাখা
টুল রেজাল্টের টাইপ { ok: boolean; data?: IssueDTO; error?: string } এভাবে লেখা একটি বড় ভুল। কারণ এতে if (res.ok) চেক করার পরও TypeScript নিশ্চিত হতে পারে না যে res.data আসলেই আছে কি না। আমাদের SuccessEnvelope<T> | ErrorEnvelope-এর মতো ok: true এবং ok: false আলাদা করে ইউনিয়ন টাইপ লিখলে কম্পাইলার শতভাগ নিখুঁতভাবে টাইপ ন্যারো (Narrow) করতে পারে।
৪. 'partial-call' অবস্থায় আর্গুমেন্টকে সম্পূর্ণ (Required) ধরে নেওয়া
যখন invocation.state === 'partial-call' থাকে, তখনো মডেল প্যারামিটার স্ট্রিম করা শেষ করেনি—ফলে invocation.args-এর ভেতরের যেকোনো ফিল্ড তখন undefined থাকতে পারে। তাই আমাদের NarrowedToolInvocation টাইপের মতো স্ট্রিমিং চলাকালীন আর্গুমেন্টকে সবসময় Partial<ToolInput<K>> হিসেবে টাইপ করবেন।
প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)
পুরো এজেন্ট সিস্টেমে Zod ভ্যালিডেশন এবং TypeScript টাইপ গার্ড ব্যবহার করলে কি স্ট্রিমিং ধীরগতির হয়ে যাবে?
একদমই না। TypeScript-এর সমস্ত টাইপ (ToolContractRegistry, z.infer, এবং isToolInvocationFor টাইপ গার্ড) বিল্ড হওয়ার সময় পুরোপুরি মুছে যায়, তাই রানটাইমে এর খরচ শূন্য মিলিসেকেন্ড। আর সার্ভার সাইডে ৫-৬টি ফিল্ডের একটি অবজেক্ট Zod দিয়ে .safeParse() করতে সময় লাগে মাত্র ০.০৫ মিলিসেকেন্ডেরও কম—যা ৪০০ মিলিসেকেন্ডের LLM কলের তুলনায় ধর্তব্যের মধ্যেই পড়ে না।
সাধারণ TypeScript interface না লিখে আমরা কেন সব জায়গায় আগে Zod স্কিমা লিখে z.infer ব্যবহার করছি?
কারণ TypeScript-এর interface শুধুমাত্র কোড লেখার সময় (Compile time) কাজ করে; কোড যখন জাভাস্ক্রিপ্টে রান হয় তখন ইন্টারফেসের কোনো অস্তিত্ব থাকে না। কিন্তু OpenAI বা Claude-কে টুল চেনানোর জন্য রানটাইমে JSON Schema পাঠাতে হয় এবং মডেলের পাঠানো ডেটা রানটাইমে ভ্যালিডেট করতে হয়। আগে Zod স্কিমা লিখলে একই কোড থেকে আপনি রানটাইম ভ্যালিডেশন, LLM-এর JSON Schema এবং z.infer দিয়ে স্ট্যাটিক TypeScript টাইপ—তিনটিই একসাথে পেয়ে যাচ্ছেন।
useChat হুকের ভেতরে থাকা message.annotations-কে কীভাবে Type-safe করব?
Vercel AI SDK-তে message.annotations-এর ডিফল্ট টাইপ থাকে JSONValue[] | undefined। এটিকে টাইপ-সেফ রাখার সবচেয়ে ভালো উপায় হলো একটি ছোট Zod স্কিমা বা Type Predicate ফাংশন (function isTelemetryAnnotation(a: unknown): a is MessageTelemetryAnnotation) তৈরি করে JSX-এ রেন্ডার করার আগে চেক করে নেওয়া—যাতে ভুল ফরম্যাটের কোনো মেটাডেটা এলে কম্পোনেন্ট কখনোই না ভাঙে।
কাস্টম ওয়েব অ্যাপ বা এআই অটোমেশন বানাতে চান?
আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (Next.js, React, Node, PostgreSQL) এবং ইন্টেলিজেন্ট এআই এজেন্ট সলিউশন তৈরি করি। আপনার প্রজেক্টের আইডিয়া নিয়ে কথা বলা যাক!
আর্টিকেলটি কি আপনার ভালো লেগেছে?
তন্ময়ের কাজকে সাপোর্ট করতে তালি (Clap) দিয়ে উৎসাহিত করুন!
জাহিদ হাসান তন্ময়
সফটওয়্যার ডেভেলপার
MERN ফুল-স্ট্যাক ডেভেলপার এবং AI এজেন্ট ডেভেলপার, ঢাকা, বাংলাদেশ। ওয়েব ডেভেলপমেন্ট, রিঅ্যাক্ট, লারাভেল এবং আমার লার্নিং জার্নি নিয়ে লিখছি।