Skip to content

Pattern: Tool-calling Agent

Build an AI agent that can call server-side tools, handle multi-step reasoning, and stream results back to the client.

SvelteKit Implementation

Server

ts
// src/routes/api/agent/+server.ts
import { ServerAgent } from "@aibind/sveltekit/agent";
import { tool, stepCountIs } from "ai";
import { z } from "zod";
import { models } from "../../../models.server";

const agent = new ServerAgent({
  model: models.smart,
  system: "You are a helpful assistant. Use tools when relevant.",
  tools: {
    search_docs: tool({
      description: "Search the documentation for a topic",
      inputSchema: z.object({
        query: z.string().describe("Search query"),
      }),
      execute: async ({ query }) => {
        // Your search logic here
        return { results: [`Result for: ${query}`] };
      },
    }),
    create_ticket: tool({
      description: "Create a support ticket",
      inputSchema: z.object({
        title: z.string(),
        priority: z.enum(["low", "medium", "high"]),
        description: z.string(),
      }),
      execute: async ({ title, priority, description }) => {
        // Your ticket creation logic
        return { ticketId: "TICK-" + Math.random().toString(36).slice(2, 8) };
      },
    }),
  },
  stopWhen: stepCountIs(10), // Safety limit
});

export async function POST({ request }) {
  const { messages } = await request.json();
  const lastMessage = messages[messages.length - 1];

  const result = agent.stream(lastMessage.content, {
    messages: messages.slice(0, -1),
  });

  return result.toTextStreamResponse();
}

Client

svelte
<script lang="ts">
  import { useAgent } from '@aibind/sveltekit/agent';

  const agent = useAgent({ endpoint: '/api/agent' });
  let prompt = $state('');
</script>

<div class="chat">
  {#each agent.messages as msg}
    <div class="message {msg.role}">
      <strong>{msg.role}:</strong>
      {#if msg.toolCalls}
        {#each msg.toolCalls as call}
          <div class="tool-call">
            Called <code>{call.toolName}</code> with {JSON.stringify(call.args)}
          </div>
        {/each}
      {:else}
        <p>{msg.content}</p>
      {/if}
    </div>
  {/each}

  {#if agent.status === 'thinking'}
    <p class="thinking">Thinking...</p>
  {/if}

  {#if agent.status === 'tool_calling'}
    <p class="thinking">Calling tool...</p>
  {/if}
</div>

<form onsubmit={(e) => { e.preventDefault(); agent.send(prompt); prompt = ''; }}>
  <input bind:value={prompt} placeholder="Ask the agent..." />
  <button disabled={agent.status !== 'idle'}>Send</button>
</form>

Key Patterns

Tool Design

  • Keep tool descriptions clear and specific
  • Use Zod .describe() on parameters to help the AI understand what to pass
  • Return structured data from tools — the AI will interpret it for the user

Step Limits

Always set stopWhen: stepCountIs(n) to prevent infinite tool-calling loops:

ts
import { stepCountIs } from "ai";

const agent = new ServerAgent({
  // ...
  stopWhen: stepCountIs(5), // Max 5 tool calls per request
});

Tool Approval

For sensitive tools, you can require user approval:

svelte
{#if agent.pendingApproval}
  <div class="approval">
    <p>Agent wants to call <code>{agent.pendingApproval.toolName}</code></p>
    <p>Args: {JSON.stringify(agent.pendingApproval.args)}</p>
    <button onclick={() => agent.approve(agent.pendingApproval.id)}>Approve</button>
    <button onclick={() => agent.deny(agent.pendingApproval.id, 'Not now')}>Deny</button>
  </div>
{/if}

Released under the MIT License.