Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phpaisdk/core/llms.txt

Use this file to discover all available pages before exploring further.

aisdk/core supports multi-step agentic loops where the model can call tools, receive results, and continue reasoning across several back-and-forth cycles before delivering a final answer. This is the foundation for building research agents, function-calling pipelines, and autonomous task runners — all controlled through the single ->maxSteps() method on Generate::text().

How maxSteps Works

By default, ->run() allows only one step: the model responds and the result is returned, even if the model emits tool calls. Setting ->maxSteps(n) raises that ceiling to n cycles.
Because maxSteps defaults to 1, tool calls in the first response are executed but the loop stops immediately afterwards — the model never sees the tool results unless you set maxSteps to at least 2. Always set maxSteps explicitly when you want the model to act on tool results.
1
Step 1 — Build the initial request
2
Generate::text()->prompt(...)->tool(...)->maxSteps(n)->run() is called. The SDK assembles the first TextModelRequest and sends it to the model.
3
Step 2 — Model responds
4
The model returns a response. The SDK inspects it for tool calls.
5
  • No tool calls: The loop ends and TextResult is returned immediately, regardless of how many steps remain.
  • Tool calls present: Execution continues to the next step.
  • 6
    Step 3 — Execute tool calls
    7
    Each tool call in the response is resolved by name, its arguments are validated, and the handler is invoked. Results are collected into ToolResult objects.
    8
    Step 4 — Feed results back
    9
    An assistant message containing the tool calls, and individual tool messages containing each result, are appended to the conversation. The updated message list is used for the next model call.
    10
    Step 5 — Repeat or finish
    11
    If the step counter has not reached maxSteps, the SDK sends the next request with the extended message history (back to Step 2). Once maxSteps is reached — or the model responds with no tool calls — the final TextResult is assembled from the last response plus all accumulated tool calls and results.

    Basic Example

    use AiSdk\Generate;
    use AiSdk\OpenAI;
    use AiSdk\Schema;
    use AiSdk\Tool;
    
    $search = Tool::make('search', 'Search the web for current information')
        ->input(Schema::string(name: 'query')->required())
        ->run(fn (string $query): string => mySearchApi($query));
    
    $summarise = Tool::make('summarise', 'Summarise a block of text in one sentence')
        ->input(Schema::string(name: 'text')->required())
        ->run(fn (string $text): string => mySummariseApi($text));
    
    $result = Generate::text()
        ->model(OpenAI::model('gpt-4o'))
        ->prompt('Find the latest PHP release and summarise the key changes.')
        ->tool($search)
        ->tool($summarise)
        ->maxSteps(5)
        ->run();
    
    echo $result->text;
    
    The model can call search to find the release notes, then call summarise on the content, and finally compose an answer — all within the five-step budget.

    Research Agent Example

    Here is a fuller agent that uses ToolExecutionContext to log every tool invocation:
    use AiSdk\Generate;
    use AiSdk\OpenAI;
    use AiSdk\Schema;
    use AiSdk\Tool;
    use AiSdk\ToolExecutionContext;
    
    $fetchPage = Tool::make('fetch_page', 'Retrieve the text content of a URL')
        ->input(Schema::string(name: 'url')->required())
        ->run(function (string $url, ToolExecutionContext $ctx): string {
            // $ctx->toolCallId  — unique ID for this call
            // $ctx->toolName    — 'fetch_page'
            // $ctx->arguments   — ['url' => $url]
            // $ctx->messages    — full message history up to this point
    
            echo "[{$ctx->toolCallId}] Fetching {$url}" . PHP_EOL;
    
            return myHttpGet($url);
        });
    
    $extractFacts = Tool::make('extract_facts', 'Pull key facts from raw page text')
        ->input(Schema::string(name: 'content')->required())
        ->run(fn (string $content): string => myExtractFacts($content));
    
    $result = Generate::text()
        ->model(OpenAI::model('gpt-4o'))
        ->instructions('You are a research agent. Use the tools to gather information before answering.')
        ->prompt('What are the headline features of PHP 8.4?')
        ->tool($fetchPage)
        ->tool($extractFacts)
        ->maxSteps(6)
        ->run();
    
    echo $result->text;
    
    // Inspect everything the agent did
    foreach ($result->toolCalls as $call) {
        echo "Called: {$call->name}" . PHP_EOL;
    }
    
    foreach ($result->toolResults as $toolResult) {
        echo "Result for {$toolResult->toolName}: " . substr((string) $toolResult->output, 0, 80) . PHP_EOL;
    }
    

    ToolExecutionContext

    When a tool handler declares a ToolExecutionContext parameter as its last argument, the SDK injects it automatically. You do not need to include it in the tool’s input schema — it is always appended after the declared inputs.
    use AiSdk\ToolExecutionContext;
    
    $tool = Tool::make('lookup', 'Look up a record by ID')
        ->input(Schema::string(name: 'id')->required())
        ->run(function (string $id, ToolExecutionContext $ctx): string {
            // Available on every injected context:
            $ctx->toolCallId; // string — unique ID assigned by the model
            $ctx->toolName;   // string — name of the tool being called
            $ctx->arguments;  // array<string, mixed> — raw arguments from the model
            $ctx->messages;   // array<int, Message> — conversation so far
    
            return myLookup($id);
        });
    
    The $messages array contains the full conversation including all prior tool calls and results for that step, which lets you implement stateful or context-aware tools.

    Inspecting Tool Calls and Results

    TextResult accumulates every tool interaction across all steps:
    PropertyTypeDescription
    $toolCallsToolCall[]Every tool call made across all steps, in order.
    $toolResultsToolResult[]Every tool result, in the same order as $toolCalls.
    $result = Generate::text()
        ->model(OpenAI::model('gpt-4o'))
        ->prompt('What is 12 factorial?')
        ->tool($calculatorTool)
        ->maxSteps(3)
        ->run();
    
    echo count($result->toolCalls) . ' tool calls made' . PHP_EOL;
    
    foreach ($result->toolCalls as $i => $call) {
        $output = $result->toolResults[$i]->output;
        echo "{$call->name}(" . json_encode($call->arguments) . ") => {$output}" . PHP_EOL;
    }
    

    Class-Based Tools

    Class-based tools work identically with multi-step loops. Declare ToolExecutionContext as the last parameter of __invoke to receive context:
    use AiSdk\Schema;
    use AiSdk\Tool;
    use AiSdk\ToolExecutionContext;
    
    final class DatabaseLookupTool extends Tool
    {
        public function __construct()
        {
            $this->as('db_lookup')
                ->for('Look up a record in the database by table and ID')
                ->inputs([
                    Schema::string(name: 'table')->required(),
                    Schema::string(name: 'id')->required(),
                ]);
        }
    
        public function __invoke(string $table, string $id, ToolExecutionContext $ctx): string
        {
            // $ctx is injected automatically as the last argument
            return myDb()->find($table, $id) ?? 'not found';
        }
    }
    
    $result = Generate::text()
        ->model(OpenAI::model('gpt-4o'))
        ->prompt('Find the user with ID 42 and summarise their profile.')
        ->tool(Tool::make(DatabaseLookupTool::class))
        ->maxSteps(4)
        ->run();
    

    Streaming and Multi-Step

    Multi-step loops are only driven by ->run(). Calling ->stream() dispatches a single request and does not execute tool calls or iterate further — the streamed response is returned as-is. If you need agentic tool execution, use ->run().

    Best Practices

    The model decides which tool to call and with what arguments based entirely on the name and description you provide. Vague descriptions like 'Do stuff' lead to incorrect or missing tool calls. Write descriptions as imperative actions: 'Search the web for a query string and return the top results'.
    Every step is a model API call. Start with a low value (2–4) and raise it only if your task genuinely requires more cycles. Unbounded loops are prevented by the hard maxSteps ceiling, but unnecessarily high values increase latency and cost on every invocation.
    The SDK passes tool return values directly back to the model as strings. If your tool can return null, empty strings, or structured data, normalise the output to a meaningful string so the model has actionable information to work with.
    Injecting ToolExecutionContext costs nothing and gives you the full message history at each step. Use it to log tool calls, record metrics, or implement circuit-breakers that bail out early if the same tool is called too many times.

    Build docs developers (and LLMs) love