[{"data":1,"prerenderedAt":851},["ShallowReactive",2],{"navigation_docs":3,"-tools-adding-custom-tools":139,"-tools-adding-custom-tools-surround":846},[4,8,12,16,20,24,28,32,59,63,117,121,135],{"title":5,"path":6,"stem":7},"Introduction","\u002Fintroduction","1.introduction",{"title":9,"path":10,"stem":11},"Memory","\u002Fmemory","10.memory",{"title":13,"path":14,"stem":15},"Customization","\u002Fcustomization","11.customization",{"title":17,"path":18,"stem":19},"Deployment","\u002Fdeployment","12.deployment",{"title":21,"path":22,"stem":23},"Reference","\u002Freference","13.reference",{"title":25,"path":26,"stem":27},"Quickstart","\u002Fquickstart","2.quickstart",{"title":29,"path":30,"stem":31},"Installation","\u002Finstallation","3.installation",{"title":33,"icon":34,"path":35,"stem":36,"children":37,"page":58},"Connectors","i-lucide-cable","\u002Fconnectors","4.connectors",[38,42,46,50,54],{"title":39,"path":40,"stem":41},"Telegram","\u002Fconnectors\u002Ftelegram","4.connectors\u002F1.telegram",{"title":43,"path":44,"stem":45},"Slack","\u002Fconnectors\u002Fslack","4.connectors\u002F2.slack",{"title":47,"path":48,"stem":49},"Email","\u002Fconnectors\u002Femail","4.connectors\u002F3.email",{"title":51,"path":52,"stem":53},"API","\u002Fconnectors\u002Fapi","4.connectors\u002F4.api",{"title":55,"path":56,"stem":57},"Terminal","\u002Fconnectors\u002Fterminal","4.connectors\u002F5.terminal",false,{"title":60,"path":61,"stem":62},"Attachments","\u002Fattachments","5.attachments",{"title":64,"icon":65,"path":66,"stem":67,"children":68,"page":58},"Tools","i-lucide-wrench","\u002Ftools","6.tools",[69,73,77,81,85,89,93,97,101,105,109,113],{"title":70,"path":71,"stem":72},"File Manager","\u002Ftools\u002Ffile-manager","6.tools\u002F1.file-manager",{"title":74,"path":75,"stem":76},"Text-to-Speech","\u002Ftools\u002Ftext-to-speech","6.tools\u002F10.text-to-speech",{"title":78,"path":79,"stem":80},"Confirmations","\u002Ftools\u002Fconfirmations","6.tools\u002F11.confirmations",{"title":82,"path":83,"stem":84},"Adding Custom Tools","\u002Ftools\u002Fadding-custom-tools","6.tools\u002F12.adding-custom-tools",{"title":86,"path":87,"stem":88},"Image Manager","\u002Ftools\u002Fimage-manager","6.tools\u002F2.image-manager",{"title":90,"path":91,"stem":92},"Web Request","\u002Ftools\u002Fweb-request","6.tools\u002F3.web-request",{"title":94,"path":95,"stem":96},"Reminders","\u002Ftools\u002Freminders","6.tools\u002F4.reminders",{"title":98,"path":99,"stem":100},"Heartbeats","\u002Ftools\u002Fheartbeats","6.tools\u002F5.heartbeats",{"title":102,"path":103,"stem":104},"Email Manager","\u002Ftools\u002Femail-manager","6.tools\u002F6.email-manager",{"title":106,"path":107,"stem":108},"Calendar","\u002Ftools\u002Fcalendar","6.tools\u002F7.calendar",{"title":110,"path":111,"stem":112},"Read Database","\u002Ftools\u002Fread-database","6.tools\u002F8.read-database",{"title":114,"path":115,"stem":116},"Headless Browser","\u002Ftools\u002Fbrowser","6.tools\u002F9.browser",{"title":118,"path":119,"stem":120},"Tinker","\u002Ftinker","7.tinker",{"title":122,"icon":123,"path":124,"stem":125,"children":126,"page":58},"Skills","i-lucide-sparkles","\u002Fskills","8.skills",[127,131],{"title":128,"path":129,"stem":130},"Overview","\u002Fskills\u002Foverview","8.skills\u002F1.overview",{"title":132,"path":133,"stem":134},"Adding Custom Skills","\u002Fskills\u002Fadding-custom-skills","8.skills\u002F2.adding-custom-skills",{"title":136,"path":137,"stem":138},"Personas","\u002Fpersonas","9.personas",{"id":140,"title":82,"body":141,"description":840,"extension":841,"links":842,"meta":843,"navigation":219,"path":83,"seo":844,"stem":84,"__hash__":845},"docs\u002F6.tools\u002F12.adding-custom-tools.md",{"type":142,"value":143,"toc":829},"minimark",[144,148,152,164,168,171,194,197,427,434,438,445,484,487,493,497,508,533,543,548,551,603,610,614,617,698,701,705,711,762,773,776,780,825],[145,146,5],"h2",{"id":147},"introduction",[149,150,151],"p",{},"The built-in tools cover general-purpose needs. Custom tools are how you give the agent access to code that knows about your specific application.",[149,153,154,155,159,160,163],{},"A tool is any class implementing ",[156,157,158],"code",{},"Laravel\\Ai\\Contracts\\Tool",". Laraclaw also provides a ",[156,161,162],{},"BaseTool"," class that handles operation dispatch, schema definition, and confirmation flows.",[145,165,167],{"id":166},"anatomy-of-a-tool","Anatomy of a Tool",[149,169,170],{},"Every tool has three things:",[172,173,174,182,188],"ul",{},[175,176,177,181],"li",{},[178,179,180],"strong",{},"A description"," — natural-language text the agent reads to decide whether to use the tool",[175,183,184,187],{},[178,185,186],{},"A schema"," — a JSON schema describing the tool's input parameters",[175,189,190,193],{},[178,191,192],{},"A handler"," — the method that runs when the agent calls the tool",[149,195,196],{},"A tool that fetches the current weather for a city:",[198,199,204],"pre",{"className":200,"code":201,"language":202,"meta":203,"style":203},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003C?php\n\nnamespace App\\Laraclaw\\Tools;\n\nuse Illuminate\\Contracts\\JsonSchema\\JsonSchema;\nuse Illuminate\\Support\\Facades\\Http;\nuse Laravel\\Ai\\Contracts\\Tool;\nuse Laravel\\Ai\\Tools\\Request;\nuse Stringable;\n\nclass WeatherTool implements Tool\n{\n    public function description(): Stringable|string\n    {\n        return 'Get the current weather for a city. Returns temperature, conditions, and humidity.';\n    }\n\n    public function schema(JsonSchema $schema): array\n    {\n        return [\n            'city' => $schema->string()->required()->description('The city name'),\n            'units' => $schema->string()->description('\"metric\" or \"imperial\" (default: metric)'),\n        ];\n    }\n\n    public function handle(Request $request): Stringable|string\n    {\n        $city = $request['city'];\n        $units = $request['units'] ?? 'metric';\n\n        $response = Http::get('https:\u002F\u002Fapi.example.com\u002Fweather', [\n            'city' => $city,\n            'units' => $units,\n        ]);\n\n        return $response->body();\n    }\n}\n","php","",[156,205,206,214,221,227,232,238,244,250,256,262,267,273,279,285,291,297,303,308,314,319,325,331,337,343,348,353,359,364,370,376,381,387,393,399,405,410,416,421],{"__ignoreMap":203},[207,208,211],"span",{"class":209,"line":210},"line",1,[207,212,213],{},"\u003C?php\n",[207,215,217],{"class":209,"line":216},2,[207,218,220],{"emptyLinePlaceholder":219},true,"\n",[207,222,224],{"class":209,"line":223},3,[207,225,226],{},"namespace App\\Laraclaw\\Tools;\n",[207,228,230],{"class":209,"line":229},4,[207,231,220],{"emptyLinePlaceholder":219},[207,233,235],{"class":209,"line":234},5,[207,236,237],{},"use Illuminate\\Contracts\\JsonSchema\\JsonSchema;\n",[207,239,241],{"class":209,"line":240},6,[207,242,243],{},"use Illuminate\\Support\\Facades\\Http;\n",[207,245,247],{"class":209,"line":246},7,[207,248,249],{},"use Laravel\\Ai\\Contracts\\Tool;\n",[207,251,253],{"class":209,"line":252},8,[207,254,255],{},"use Laravel\\Ai\\Tools\\Request;\n",[207,257,259],{"class":209,"line":258},9,[207,260,261],{},"use Stringable;\n",[207,263,265],{"class":209,"line":264},10,[207,266,220],{"emptyLinePlaceholder":219},[207,268,270],{"class":209,"line":269},11,[207,271,272],{},"class WeatherTool implements Tool\n",[207,274,276],{"class":209,"line":275},12,[207,277,278],{},"{\n",[207,280,282],{"class":209,"line":281},13,[207,283,284],{},"    public function description(): Stringable|string\n",[207,286,288],{"class":209,"line":287},14,[207,289,290],{},"    {\n",[207,292,294],{"class":209,"line":293},15,[207,295,296],{},"        return 'Get the current weather for a city. Returns temperature, conditions, and humidity.';\n",[207,298,300],{"class":209,"line":299},16,[207,301,302],{},"    }\n",[207,304,306],{"class":209,"line":305},17,[207,307,220],{"emptyLinePlaceholder":219},[207,309,311],{"class":209,"line":310},18,[207,312,313],{},"    public function schema(JsonSchema $schema): array\n",[207,315,317],{"class":209,"line":316},19,[207,318,290],{},[207,320,322],{"class":209,"line":321},20,[207,323,324],{},"        return [\n",[207,326,328],{"class":209,"line":327},21,[207,329,330],{},"            'city' => $schema->string()->required()->description('The city name'),\n",[207,332,334],{"class":209,"line":333},22,[207,335,336],{},"            'units' => $schema->string()->description('\"metric\" or \"imperial\" (default: metric)'),\n",[207,338,340],{"class":209,"line":339},23,[207,341,342],{},"        ];\n",[207,344,346],{"class":209,"line":345},24,[207,347,302],{},[207,349,351],{"class":209,"line":350},25,[207,352,220],{"emptyLinePlaceholder":219},[207,354,356],{"class":209,"line":355},26,[207,357,358],{},"    public function handle(Request $request): Stringable|string\n",[207,360,362],{"class":209,"line":361},27,[207,363,290],{},[207,365,367],{"class":209,"line":366},28,[207,368,369],{},"        $city = $request['city'];\n",[207,371,373],{"class":209,"line":372},29,[207,374,375],{},"        $units = $request['units'] ?? 'metric';\n",[207,377,379],{"class":209,"line":378},30,[207,380,220],{"emptyLinePlaceholder":219},[207,382,384],{"class":209,"line":383},31,[207,385,386],{},"        $response = Http::get('https:\u002F\u002Fapi.example.com\u002Fweather', [\n",[207,388,390],{"class":209,"line":389},32,[207,391,392],{},"            'city' => $city,\n",[207,394,396],{"class":209,"line":395},33,[207,397,398],{},"            'units' => $units,\n",[207,400,402],{"class":209,"line":401},34,[207,403,404],{},"        ]);\n",[207,406,408],{"class":209,"line":407},35,[207,409,220],{"emptyLinePlaceholder":219},[207,411,413],{"class":209,"line":412},36,[207,414,415],{},"        return $response->body();\n",[207,417,419],{"class":209,"line":418},37,[207,420,302],{},[207,422,424],{"class":209,"line":423},38,[207,425,426],{},"}\n",[149,428,429,430,433],{},"The ",[156,431,432],{},"description"," is the most important field. The agent decides whether to call the tool based on it, so make it clear and specific.",[145,435,437],{"id":436},"registering-a-tool","Registering a Tool",[149,439,440,441,444],{},"Name the class in ",[156,442,443],{},"config\u002Flaraclaw.php",":",[198,446,448],{"className":200,"code":447,"language":202,"meta":203,"style":203},"use App\\Laraclaw\\Tools\\WeatherTool;\n\n'tools' => [\n    'custom' => [\n        WeatherTool::class,\n    ],\n],\n",[156,449,450,455,459,464,469,474,479],{"__ignoreMap":203},[207,451,452],{"class":209,"line":210},[207,453,454],{},"use App\\Laraclaw\\Tools\\WeatherTool;\n",[207,456,457],{"class":209,"line":216},[207,458,220],{"emptyLinePlaceholder":219},[207,460,461],{"class":209,"line":223},[207,462,463],{},"'tools' => [\n",[207,465,466],{"class":209,"line":229},[207,467,468],{},"    'custom' => [\n",[207,470,471],{"class":209,"line":234},[207,472,473],{},"        WeatherTool::class,\n",[207,475,476],{"class":209,"line":240},[207,477,478],{},"    ],\n",[207,480,481],{"class":209,"line":246},[207,482,483],{},"],\n",[149,485,486],{},"That's the whole wiring. No service provider, no closure, no command to run.",[149,488,489,490,492],{},"A class that doesn't exist or doesn't implement ",[156,491,158],{}," is skipped with a warning in your log rather than throwing, so a typo costs you one tool instead of taking down every reply.",[145,494,496],{"id":495},"accessing-the-current-message","Accessing the Current Message",[149,498,499,500,503,504,507],{},"Tools that depend on context (current connector, inbound attachments, thread's persona) accept ",[156,501,502],{},"IncomingMessage"," and ",[156,505,506],{},"Thread"," in their constructor:",[198,509,511],{"className":200,"code":510,"language":202,"meta":203,"style":203},"public function __construct(\n    private readonly IncomingMessage $message,\n    private readonly Thread $thread,\n) {}\n",[156,512,513,518,523,528],{"__ignoreMap":203},[207,514,515],{"class":209,"line":210},[207,516,517],{},"public function __construct(\n",[207,519,520],{"class":209,"line":216},[207,521,522],{},"    private readonly IncomingMessage $message,\n",[207,524,525],{"class":209,"line":223},[207,526,527],{},"    private readonly Thread $thread,\n",[207,529,530],{"class":209,"line":229},[207,531,532],{},") {}\n",[149,534,535,536,503,539,542],{},"Tools are built once per message, and the container fills in constructor arguments named ",[156,537,538],{},"$message",[156,540,541],{},"$thread"," for you. Everything else resolves from the container as usual, so you can inject your own services alongside them.",[544,545,547],"h3",{"id":546},"registering-at-runtime","Registering at Runtime",[149,549,550],{},"Config is a static list. When the tools themselves need to vary per message — a full tool in a DM, a read-only one in a group — register a factory instead. It runs on every turn and receives the message and thread:",[198,552,554],{"className":200,"code":553,"language":202,"meta":203,"style":203},"use Laraclaw\\Tools\\ToolRegistry;\n\npublic function boot(): void\n{\n    $this->app->make(ToolRegistry::class)->register(\n        fn (IncomingMessage $message, ?Thread $thread) => $thread?->is_direct_message\n            ? new WeatherTool($message)\n            : new PublicForecastTool($message),\n    );\n}\n",[156,555,556,561,565,570,574,579,584,589,594,599],{"__ignoreMap":203},[207,557,558],{"class":209,"line":210},[207,559,560],{},"use Laraclaw\\Tools\\ToolRegistry;\n",[207,562,563],{"class":209,"line":216},[207,564,220],{"emptyLinePlaceholder":219},[207,566,567],{"class":209,"line":223},[207,568,569],{},"public function boot(): void\n",[207,571,572],{"class":209,"line":229},[207,573,278],{},[207,575,576],{"class":209,"line":234},[207,577,578],{},"    $this->app->make(ToolRegistry::class)->register(\n",[207,580,581],{"class":209,"line":240},[207,582,583],{},"        fn (IncomingMessage $message, ?Thread $thread) => $thread?->is_direct_message\n",[207,585,586],{"class":209,"line":246},[207,587,588],{},"            ? new WeatherTool($message)\n",[207,590,591],{"class":209,"line":252},[207,592,593],{},"            : new PublicForecastTool($message),\n",[207,595,596],{"class":209,"line":258},[207,597,598],{},"    );\n",[207,600,601],{"class":209,"line":264},[207,602,426],{},[149,604,605,606,609],{},"Reach for this only when the tool list genuinely varies. Config is easier to find and survives ",[156,607,608],{},"config:cache",", which closures do not.",[145,611,613],{"id":612},"returning-files-in-the-reply","Returning Files in the Reply",[149,615,616],{},"If your tool produces a file for the user, write it to the outbound attachments folder for the current message UUID:",[198,618,620],{"className":200,"code":619,"language":202,"meta":203,"style":203},"use Laraclaw\\Services\\Attachments;\n\npublic function __construct(\n    private readonly IncomingMessage $message,\n    private readonly Attachments $attachments,\n) {}\n\npublic function handle(Request $request): string\n{\n    $report = $this->generateReport();\n\n    $this->attachments\n        ->outbound($this->message->uuid)\n        ->set('report.pdf', $report);\n\n    return 'Report generated. It will be attached to your reply.';\n}\n",[156,621,622,627,631,635,639,644,648,652,657,661,666,670,675,680,685,689,694],{"__ignoreMap":203},[207,623,624],{"class":209,"line":210},[207,625,626],{},"use Laraclaw\\Services\\Attachments;\n",[207,628,629],{"class":209,"line":216},[207,630,220],{"emptyLinePlaceholder":219},[207,632,633],{"class":209,"line":223},[207,634,517],{},[207,636,637],{"class":209,"line":229},[207,638,522],{},[207,640,641],{"class":209,"line":234},[207,642,643],{},"    private readonly Attachments $attachments,\n",[207,645,646],{"class":209,"line":240},[207,647,532],{},[207,649,650],{"class":209,"line":246},[207,651,220],{"emptyLinePlaceholder":219},[207,653,654],{"class":209,"line":252},[207,655,656],{},"public function handle(Request $request): string\n",[207,658,659],{"class":209,"line":258},[207,660,278],{},[207,662,663],{"class":209,"line":264},[207,664,665],{},"    $report = $this->generateReport();\n",[207,667,668],{"class":209,"line":269},[207,669,220],{"emptyLinePlaceholder":219},[207,671,672],{"class":209,"line":275},[207,673,674],{},"    $this->attachments\n",[207,676,677],{"class":209,"line":281},[207,678,679],{},"        ->outbound($this->message->uuid)\n",[207,681,682],{"class":209,"line":287},[207,683,684],{},"        ->set('report.pdf', $report);\n",[207,686,687],{"class":209,"line":293},[207,688,220],{"emptyLinePlaceholder":219},[207,690,691],{"class":209,"line":299},[207,692,693],{},"    return 'Report generated. It will be attached to your reply.';\n",[207,695,696],{"class":209,"line":305},[207,697,426],{},[149,699,700],{},"The connector picks the file up automatically when it sends the reply.",[145,702,704],{"id":703},"requiring-approval","Requiring Approval",[149,706,707,708,710],{},"If your tool performs a destructive action, extend ",[156,709,162],{}," and declare which operations require approval:",[198,712,714],{"className":200,"code":713,"language":202,"meta":203,"style":203},"use Laraclaw\\Tools\\BaseTool;\n\nclass InvoiceTool extends BaseTool\n{\n    protected array $requiresApproval = [\n        'delete' => 'Delete invoice {id}?',\n    ];\n\n    \u002F\u002F ...\n}\n",[156,715,716,721,725,730,734,739,744,749,753,758],{"__ignoreMap":203},[207,717,718],{"class":209,"line":210},[207,719,720],{},"use Laraclaw\\Tools\\BaseTool;\n",[207,722,723],{"class":209,"line":216},[207,724,220],{"emptyLinePlaceholder":219},[207,726,727],{"class":209,"line":223},[207,728,729],{},"class InvoiceTool extends BaseTool\n",[207,731,732],{"class":209,"line":229},[207,733,278],{},[207,735,736],{"class":209,"line":234},[207,737,738],{},"    protected array $requiresApproval = [\n",[207,740,741],{"class":209,"line":240},[207,742,743],{},"        'delete' => 'Delete invoice {id}?',\n",[207,745,746],{"class":209,"line":246},[207,747,748],{},"    ];\n",[207,750,751],{"class":209,"line":252},[207,752,220],{"emptyLinePlaceholder":219},[207,754,755],{"class":209,"line":258},[207,756,757],{},"    \u002F\u002F ...\n",[207,759,760],{"class":209,"line":264},[207,761,426],{},[149,763,764,765,768,769,772],{},"The placeholder ",[156,766,767],{},"{id}"," is filled from the request parameters. When the agent calls a gated operation, the connector asks the user before the handler runs, and the paused run survives a worker restart or a deploy. See ",[770,771,78],"a",{"href":79}," for the full flow.",[149,774,775],{},"Approval works the same whether the tool was registered through config or through the registry.",[145,777,779],{"id":778},"best-practices","Best Practices",[172,781,782,797,803,813,819],{},[175,783,784,787,788,792,793,796],{},[178,785,786],{},"Keep descriptions concrete."," ",[789,790,791],"em",{},"\"Get the current stock price for a ticker symbol\""," beats ",[789,794,795],{},"\"Stock tool\"",".",[175,798,799,802],{},[178,800,801],{},"Return strings the agent can reason about."," Plain text or JSON are both fine. Avoid raw binary.",[175,804,805,808,809,812],{},[178,806,807],{},"Fail loudly, in plain English."," Returning ",[156,810,811],{},"\"City 'Atlantis' not found.\""," is more useful than throwing an exception.",[175,814,815,818],{},[178,816,817],{},"Limit output size."," The agent has a context window. If your data is large, summarize or paginate.",[175,820,821,824],{},[178,822,823],{},"One job per tool."," Build five sharp blades, not one Swiss Army knife.",[826,827,828],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":203,"searchDepth":216,"depth":216,"links":830},[831,832,833,834,837,838,839],{"id":147,"depth":216,"text":5},{"id":166,"depth":216,"text":167},{"id":436,"depth":216,"text":437},{"id":495,"depth":216,"text":496,"children":835},[836],{"id":546,"depth":223,"text":547},{"id":612,"depth":216,"text":613},{"id":703,"depth":216,"text":704},{"id":778,"depth":216,"text":779},"Build your own tools and register them with the agent.","md",null,{},{"title":82,"description":840},"O3qHe9ZyKSBbHULXzFKHf4JNXjaONtZf9mkxVoOqJ2I",[847,849],{"title":78,"path":79,"stem":80,"description":848,"children":-1},"How tools ask the user to confirm a destructive action before running it.",{"title":86,"path":87,"stem":88,"description":850,"children":-1},"Read image metadata and perform transformations like resize, crop, convert, and optimize.",1786661906119]