Stark-Kit / Docs

Make Your First Agent

A full walkthrough of defining typed tools with Zod, constructing an Agent, running it with run(), and upgrading to real-time streaming with runStream().

Defining a Tool

Tools are the primary way agents interact with the outside world — APIs, databases, file systems, anything. Use defineTool() to declare a tool with a name, a description (used by the model to decide when to call it), a Zod parameter schema, and anexecute function.

tools.ts
import "dotenv/config";
import { defineTool } from "@mehularora/stark-kit";
import z from "zod";
const searchTool = defineTool({
name: "searchWeb",
description: "Search the web for information on a given topic.",
parameters: z.object({
query: z.string().describe("The search query string"),
maxResults: z.number().int().min(1).max(10).optional(),
}),
execute: async ({ query, maxResults = 5 }) => {
// Your actual implementation here
return { results: [`Result for "${query}" (top ${maxResults})`] };
},
});

Returning structured errors with ToolResult

Instead of throwing errors (which would crash the run loop), you can return a ToolResult with success: false. The model receives the error message and can attempt to self-correct — for example, by rewriting a broken SQL query.

tools.ts
import { defineTool, ToolResult } from "@mehularora/stark-kit";
import z from "zod";
const dbQueryTool = defineTool({
name: "queryDatabase",
description: "Execute read-only SQL queries.",
parameters: z.object({ sql: z.string() }),
execute: async ({ sql }): Promise<ToolResult> => {
try {
const data = await db.query(sql);
return { success: true, data };
} catch (err) {
// The model receives this and can self-correct its query
return {
success: false,
error: err instanceof Error ? err.message : String(err),
};
}
},
});

Building an Agent

Pass your tools and a provider instance to new Agent(). The instructions field becomes the system prompt — be specific about when tools should be used.

agent.ts
import "dotenv/config";
import { Agent, OpenAIProvider, defineTool } from "@mehularora/stark-kit";
import z from "zod";
const provider = new OpenAIProvider({ model: "gpt-4o" });
const searchTool = defineTool({
name: "searchWeb",
description: "Search the web for a query.",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => `Results for "${query}": ...`,
});
const agent = new Agent({
name: "ResearchAssistant",
provider,
instructions: "You are a research assistant. Always search before answering factual questions.",
tools: [searchTool],
maxSteps: 15, // stop after 15 steps (default: 10)
temperature: 0.3, // lower = more deterministic
model: "gpt-4o-mini", // overrides provider default for this agent
});
Agent Configuration Options
namestring

Required. Identifier for this agent (shown in handoff events).

instructionsstring

Required. The system prompt given to the model on every step.

providerProvider

Required. An OpenAIProvider, ClaudeProvider, GeminiProvider, or MistralProvider instance.

toolsIToolOptions[]

Optional. Array of tools defined with defineTool().

maxStepsnumber

Optional (default: 10). Maximum number of run loop iterations.

temperaturenumber

Optional. Sampling temperature passed to the provider.

modelstring

Optional. Model name that overrides the provider's default.

hooksAgentHooks

Optional. beforeChat, beforeTool, afterTool lifecycle callbacks.

outputTypez.ZodType

Optional. A Zod schema that forces structured JSON output.

Running the Agent

Call run() with the agent and a user message. The message can be a plain string or a CanonicalMessage[] array for multi-turn conversations.

run.ts
import { run, isHITLPause } from "@mehularora/stark-kit";
const result = await run({
agent,
messages: "What are the latest advancements in quantum computing?",
maxSteps: 10, // optional override
temperature: 0.5, // optional override
});
// result is either a RunResult or a HITLPause
if (isHITLPause(result)) {
// A tool with requiresApproval: true was triggered — see HITL docs
console.log("Waiting for approval:", result.pendingToolCalls);
} else if (result.status === "complete") {
console.log("Final answer:", result.content);
// If agent used outputType (structured output):
// console.log("Structured data:", result.finalOutput);
}
Note: run() returns either a RunResult (when the loop completes normally) or a HITLPause (when a tool with requiresApproval: true was triggered). Always check with isHITLPause(result) before reading the result.

Streaming with runStream()

Use runStream() instead of run() to receive events as they happen. This is useful for streaming text to the user in real-time, showing tool call progress, or building reactive UIs.

stream.ts
import "dotenv/config";
import { runStream } from "@mehularora/stark-kit";
const stream = runStream({
agent,
messages: "Tell me about quantum computing and then search for recent news.",
});
for await (const event of stream) {
switch (event.type) {
case "chunk":
// Text delta from the model — write to terminal or UI
if (event.chunk.type === "text") {
process.stdout.write(event.chunk.delta);
}
break;
case "tool_start":
console.log(`\n[→ Calling: ${event.toolName}]`);
break;
case "tool_end":
console.log(`[← Finished: ${event.toolName}]`);
break;
case "handoff":
// Only emitted during multi-agent handoffs
console.log(`[Handing off to: ${event.targetAgentName}]`);
break;
case "step_complete":
// One full step of the loop completed
break;
case "hitl_pause":
// A tool with requiresApproval triggered mid-stream
console.log("Paused for approval:", event.result.pendingToolCalls);
break;
case "done":
// Stream finished — event.result is the final RunResult
console.log("\n\nFinal answer:", event.result.content);
break;
}
}

The generator yields RunStreamEvent objects. The possible event types are: chunk, tool_start, tool_end, handoff, step_complete, hitl_pause, and done.

Tip: The done event is always emitted last. Its event.result is the complete RunResult — the same object you would get from a non-streaming run() call.