adachsoft/ai-agent
Stateful AI Agent Orchestrator Library for tool-calling chats.
- PHP: >= 8.3
- Namespace:
AdachSoft\\AiAgent\\
Overview
This library orchestrates tool-calling chats in stateful mode. The client identifies a conversation by conversationId, the library owns and persists full conversation history behind a ConversationRepositoryInterface, and each ask() call executes a single agent turn over the stored history.
High-level responsibilities:
- maintain conversation history and correlate messages with tool calls,
- control the tool-calling loop under retry and budget policies,
- integrate exclusively with external PublicApi facades for chat and tools (no HTTP transport in this library),
- expose a small, stable PublicApi facade for starting conversations and executing turns.
See detailed docs:
docs/PROJECT_OVERVIEW_AI_AGENT.md (EN)
docs/PRZEGLAD_PROJEKTU_AI_AGENT.md (PL)
Installation
Install via Composer:
composer require adachsoft/ai-agent
This library relies on external PublicApi facades for chat and tools (installed by you):
adachsoft/ai-integration (ToolCallingChatFacade)
adachsoft/ai-tool-call (AiToolCallFacade)
Quick Start (Stateful)
- Create ports (PublicApi facades from external libs):
- ToolCallingChatFacade (e.g. Deepseek/OpenAI provider via their builders),
- AiToolCallFacade (provides tool catalog and execution).
- Build the AiAgent facade via
AiAgentBuilder with config and policies.
- Call
startConversation($conversationId) once to create an empty conversation in the configured repository.
- For each user turn call
ask($conversationId, AskRequestDto) and persist nothing yourself – the library appends new messages internally.
Minimal Example (stateful)
use AdachSoft\AiAgent\PublicApi\Builder\AiAgentBuilder;
use AdachSoft\AiAgent\PublicApi\Dto\AgentConfigDto;
use AdachSoft\AiAgent\PublicApi\Dto\AskRequestDto;
use AdachSoft\AiAgent\PublicApi\Dto\Collection\ToolIdCollection;
use AdachSoft\AiAgent\PublicApi\Dto\PoliciesConfigDto;
use AdachSoft\AiAgent\PublicApi\Dto\PortsConfigDto;
use AdachSoft\AiAgent\PublicApi\Vo\ToolId;
use AdachSoft\AiAgent\Spi\Conversation\ConversationRepositoryInterface as SpiConversationRepositoryInterface;
$toolCalling = ;
$aiTools = ;
$ports = new PortsConfigDto(
toolCallingChatFacade: $toolCalling,
aiToolCallFacade: $aiTools,
);
$facade = (new AiAgentBuilder())
->withPorts($ports)
->withAgentConfig(new AgentConfigDto(
name: 'MyAgent',
description: 'Demo stateful agent',
providerId: 'deepseek',
modelId: 'deepseek-chat',
systemPrompt: 'You are a helpful assistant.',
parameters: [
'temperature' => 0.2,
'max_tokens' => 1024,
],
timeoutSeconds: 60,
tools: new ToolIdCollection([new ToolId('current_datetime')]),
))
->withPolicies(new PoliciesConfigDto(
maxSteps: 6,
maxToolCallsPerTurn: 2,
maxDurationSeconds: 60,
))
->build();
$conversationId = 'example-conversation-1';
try {
$facade->startConversation($conversationId);
} catch (\AdachSoft\AiAgent\PublicApi\Exception\ConversationAlreadyExistsPublicApiException $e) {
}
$request = new AskRequestDto(prompt: 'What time is it in Warsaw? Reply as HH:MM.');
$response = $facade->ask($conversationId, $request);
$finalText = $response->finalText;
$history = $response->fullConversation;
Conversation Repository Configuration
By default the builder uses an in-memory repository (InMemoryConversationRepository). To provide a persistent implementation:
- Implement
AdachSoft\AiAgent\Spi\Conversation\ConversationRepositoryInterface
- Register it via
AiAgentBuilder::withSpiConversationRepository(SpiConversationRepositoryInterface $repository)
The builder automatically wraps your SPI implementation with SpiBackedConversationRepository (which implements the internal Domain\Conversation\ConversationRepositoryInterface). This prevents domain leakage into the PublicApi layer.
The facade methods (startConversation, ask, getRawConversation, listConversationIds, deleteConversation) work with any repository backend.
Conversation Management
The AiAgentFacadeInterface now provides dedicated methods for managing conversations:
getRawConversation(string $conversationId): ChatMessageDtoCollection – returns the complete raw history without applying any context trimming strategy (useful for auditing, export, debugging or admin panels).
listConversationIds(): ConversationIdDtoCollection – returns all known conversation identifiers (empty collection when none exist).
deleteConversation(string $conversationId): void – permanently deletes a conversation and its entire history. Throws ConversationNotFoundPublicApiException when the conversation does not exist.
These methods work with any implementation of ConversationRepositoryInterface (in-memory or SPI-backed).
Example:
$rawHistory = $facade->getRawConversation($conversationId);
$allIds = $facade->listConversationIds();
foreach ($allIds as $id) {
echo "Conversation: $id\n";
}
$facade->deleteConversation('old-conversation-123');
Configuring system prompt and generation parameters
Agent behaviour is configured via two fields on AgentConfigDto:
systemPrompt – high level, stable instruction injected as the first system role message in every request to adachsoft/ai-integration.
parameters – free-form array of provider-specific generation knobs forwarded 1:1 to ToolCallingChatRequestDto::$parameters (no renaming or filtering).
Examples of parameters values:
- OpenAI-style:
['temperature' => 0.7, 'max_tokens' => 200, 'top_p' => 0.9, 'presence_penalty' => 0.6, 'frequency_penalty' => 0.3]
- Gemini-style:
['temperature' => 0.5, 'thinkingConfig' => ['includeThoughts' => false], 'maxOutputTokens' => 8192]
You are responsible for choosing parameter names that match the selected provider (providerId, modelId). The agent library treats this array as opaque data and passes it straight to adachsoft/ai-integration.
Examples
Runnable examples are provided in the examples/ directory. Each example has its own folder with run.php and README.md.
- Quickstart (single stateful turn):
php examples/agent-publicapi-quickstart/run.php "Your prompt"
- Interactive chat (stateful over a single conversationId):
php examples/agent-publicapi-chat/run.php
Note: bin/ scripts are thin wrappers that redirect to example entrypoints.
Environment variables:
API Highlights
AiAgentFacadeInterface::startConversation(string $conversationId): void
AiAgentFacadeInterface::ask(string $conversationId, AskRequestDto $request): AskResponseDto
AiAgentFacadeInterface::getRawConversation(string $conversationId): ChatMessageDtoCollection
AiAgentFacadeInterface::listConversationIds(): ConversationIdDtoCollection
AiAgentFacadeInterface::deleteConversation(string $conversationId): void
AiAgentFacadeInterface::getConversationContext(string $conversationId): ChatMessageDtoCollection (applies configured trimming strategy)
AiAgentBuilder::withSpiConversationRepository(SpiConversationRepositoryInterface $repository): self (new – replaces previous domain-leaking method)
AskResponseDto contains:
fullConversation (ChatMessageDtoCollection) – full conversation as seen by the agent after the turn,
tokensUsed (TokensUsageDto: promptTokens, completionTokens, totalTokens),
status (AskStatusEnum) – orchestration outcome for the turn: Completed, Limited, or Failed,
finalText (string|null) – final assistant text for this turn,
limitInfo (LimitInfoDto|null) – present only when status is Limited; contains the limit reason (e.g. steps, tool calls per turn, duration), execution metrics (executedSteps, executedToolCalls, elapsedSeconds) and the configured limits from PoliciesConfigDto.
SPI: Chat turn resolution
When the underlying provider returns an ambiguous chat turn (both finalText and at least one toolCall), the library uses a strategy to decide how to interpret the result.
Built-in strategies
You can select one of the built-in domain strategies via AiAgentBuilder::withChatTurnResolutionMode(string $mode):
prefer_final_text – use finalText, ignore toolCalls.
prefer_tool_calls – ignore finalText, continue with tool execution.
error_on_conflict – treat the situation as a domain error.
Custom SPI strategy
For full control you can implement the SPI:
- Interface:
AdachSoft\AiAgent\Spi\Conversation\ChatTurnResolutionStrategyInterface
- Method:
public function decide(ChatTurnResult $turn): string
- Expected return values:
'use_final_text', 'use_tool_calls', 'error'
Register your SPI strategy via the builder (example omitted for brevity – see full docs).
Internally the library adapts the SPI decision to a domain enum and uses it inside the orchestrator; the public API of AiAgentFacadeInterface remains unchanged.
Design Notes
- Stateful: the library owns conversation history keyed by
conversationId and appends only new messages via ConversationRepositoryInterface.
- Ports only: integrates via PublicApi facades (no HTTP details in this lib).
- Collections: uses
adachsoft/collection; no public arrays in DTOs. New collections (ConversationIdCollection, ConversationIdDtoCollection) follow the modern isValidItem() pattern.
- No domain leakage: PublicApi builder now accepts only SPI interfaces for repositories (internal adapter is created automatically).
Changelog
See CHANGELOG.md. Source of truth is changelog.json (generated via adachsoft/changelog-linter).
License
MIT (see composer.json).