<?php
require 'examples/boot.php';
use Cognesy\Agents\AgentLoop;
use Cognesy\Agents\Continuation\AgentStopException;
use Cognesy\Agents\Continuation\StopReason;
use Cognesy\Agents\Continuation\StopSignal;
use Cognesy\Agents\Data\AgentState;
use Cognesy\Agents\Events\Support\AgentEventConsoleObserver;
use Cognesy\Agents\Tool\Tools\BaseTool;
use Cognesy\Messages\Messages;
use Cognesy\Utils\JsonSchema\JsonSchema;
use Cognesy\Utils\JsonSchema\ToolSchema;
// A tool that counts up and stops when it reaches a target
class CounterTool extends BaseTool
{
private static int $count = 0;
public function __construct(private int $stopAt = 3) {
parent::__construct(
name: 'counter',
description: 'Increments a counter and returns the current value. Call this tool repeatedly.',
);
}
#[\Override]
public function __invoke(mixed ...$args): string {
self::$count++;
echo " [Counter] Value: " . self::$count . "\n";
if (self::$count >= $this->stopAt) {
// Throw AgentStopException to halt the loop
throw new AgentStopException(
signal: new StopSignal(
reason: StopReason::StopRequested,
message: "Counter reached target: {$this->stopAt}",
),
context: ['final_count' => self::$count],
source: self::class,
);
}
return "Counter is at " . self::$count . ". Keep going — call counter again.";
}
#[\Override]
public function toToolSchema(): array {
return ToolSchema::make(
name: $this->name(),
description: $this->description(),
parameters: JsonSchema::object('parameters'),
)->toArray();
}
}
$logger = new AgentEventConsoleObserver(
useColors: true,
showTimestamps: true,
showContinuation: true,
showToolArgs: true,
);
// Create loop with the counter tool
$loop = AgentLoop::default()
->withTool(new CounterTool(stopAt: 3))
->wiretap($logger->wiretap());
$state = AgentState::empty()->withMessages(
Messages::fromString('Call the counter tool repeatedly until it stops you.')
);
echo "=== Agent with Stop Condition ===\n\n";
$finalState = $loop->execute($state);
// =========================================================================
// Reading the response after a forced stop
// =========================================================================
//
// When a tool throws AgentStopException, the last step is a ToolExecution
// (the LLM was requesting tool calls when the stop happened). This means:
//
// finalResponse() -> empty (no FinalResponse step exists)
// currentResponse() -> last step's LLM output (best available text)
//
// For stop-exception scenarios the real "answer" is typically in the stop
// signal context or agent metadata — not in the LLM's text output.
echo "\n=== Result ===\n";
// finalResponse() is empty because the agent was stopped mid-tool-execution
$final = $finalState->finalResponse()->toString();
echo "finalResponse(): " . ($final !== '' ? $final : '(empty — agent was stopped, not completed)') . "\n";
// currentResponse() falls back to the last step's output
$current = $finalState->currentResponse()->toString();
echo "currentResponse(): " . ($current !== '' ? $current : '(empty)') . "\n";
// hasFinalResponse() lets you branch on how the agent ended
echo "hasFinalResponse(): " . ($finalState->hasFinalResponse() ? 'true' : 'false') . "\n";
// The stop signal carries the reason and context set by the tool
$stopSignal = $finalState->lastStopSignal();
echo "Stop reason: " . ($stopSignal?->toString() ?? 'unknown') . "\n";
echo "Stop context: " . json_encode($stopSignal?->context ?? []) . "\n";
echo "Steps: {$finalState->stepCount()}\n";
echo "Status: {$finalState->status()->value}\n";
if ($finalState->status()->value === 'failed') {
echo "Skipping assertions because execution status is failed.\n";
return;
}
// Assertions
assert($finalState->hasFinalResponse() === false, 'Expected no final response (agent was stopped)');
assert($finalState->lastStopSignal() !== null, 'Expected a stop signal');
assert($finalState->lastStopSignal()->context['final_count'] === 3, 'Expected counter to reach 3');
assert($finalState->stepCount() >= 1, 'Expected at least 1 step');
?>