connect('sampling_tools', $this->clientSamplingWithTools()); $result = $client->callTool('weather_report', ['city' => 'Paris']); $this->assertInstanceOf(TextContent::class, $result->content[0]); $this->assertSame('Paris: 18 C is the answer. (endTurn)', $result->content[0]->text); } #[TestDox('the tools the server offered arrive at the client')] public function testToolsReachTheClient(): void { /** @var \ArrayObject $seen */ $seen = new \ArrayObject(); $client = $this->connect('sampling_tools', $this->clientSamplingWithTools($seen)); $client->callTool('weather_report', ['city' => 'Paris']); $this->assertCount(2, $seen); $this->assertSame('get_weather', $seen[0]->tools[0]->name); // Second turn carries the assistant's tool use and the server's tool result. $this->assertCount(3, $seen[1]->messages); $this->assertInstanceOf(ToolUseContent::class, $seen[1]->messages[1]->getContentBlocks()[0]); $toolResult = $seen[1]->messages[2]->getContentBlocks()[0]; $this->assertInstanceOf(ToolResultContent::class, $toolResult); $this->assertSame('call-1', $toolResult->toolUseId); } #[TestDox('a client that did not advertise sampling.tools is never sent tools')] public function testClientWithoutSamplingToolsIsNotOfferedTools(): void { $client = $this->connect('sampling_tools', $this->clientBuilder() ->setCapabilities(new ClientCapabilities(sampling: true)) ->addRequestHandler(new SamplingRequestHandler($this->neverCalled()))); $result = $client->callTool('weather_report', ['city' => 'Paris']); $this->assertInstanceOf(TextContent::class, $result->content[0]); $this->assertSame('client cannot use tools during sampling', $result->content[0]->text); } /** * @param \ArrayObject|null $seen collects what the server asked for */ private function clientSamplingWithTools(?\ArrayObject $seen = null): ClientBuilder { $callback = new class($seen ?? new \ArrayObject()) implements SamplingCallbackInterface { /** @param \ArrayObject $seen */ public function __construct(private readonly \ArrayObject $seen) { } public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult { $this->seen[] = $request; $lastMessage = $request->messages[\count($request->messages) - 1]; $answeredTools = array_filter( $lastMessage->getContentBlocks(), static fn ($block): bool => $block instanceof ToolResultContent, ); // First turn: ask for the tool. Second: answer from its result. if ([] === $answeredTools) { return new CreateSamplingMessageResult( Role::Assistant, [new ToolUseContent('call-1', 'get_weather', ['city' => 'Paris'])], 'test-model', 'toolUse', ); } $toolResult = reset($answeredTools); $text = $toolResult->content[0]; \assert($text instanceof TextContent); return new CreateSamplingMessageResult( Role::Assistant, new TextContent(\sprintf('%s is the answer.', $text->text)), 'test-model', 'endTurn', ); } }; return $this->clientBuilder() ->setCapabilities(new ClientCapabilities(sampling: true, samplingTools: true)) ->addRequestHandler(new SamplingRequestHandler($callback)); } private function neverCalled(): SamplingCallbackInterface { return new class implements SamplingCallbackInterface { public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult { throw new \LogicException('The server must not sample a client that cannot use tools.'); } }; } }