adachsoft / ai-agent
Stateless AI Agent Orchestrator Library for tool-calling chats.
Requires
- php: ^8.3
- adachsoft/ai-integration: ^0.6.1 || ^0.7.0
- adachsoft/ai-tool-call: ^2.0
- adachsoft/collection: ^3.0
- adachsoft/console-io: ^0.2
Requires (Dev)
- adachsoft/changelog-linter: ^0.3.0
- adachsoft/php-code-style: 0.2.1
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.1.50
- phpunit/phpunit: ^12.4
- rector/rector: ^2.4
- twig/twig: ^3.0
- vlucas/phpdotenv: ^5.6
This package is auto-updated.
Last update: 2026-04-21 08:53:42 UTC
README
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
AiAgentBuilderwith 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;
// 1) Create external facades (using your provider credentials)
$toolCalling = /* ToolCallingChatFacadeInterface from adachsoft/ai-integration */;
$aiTools = /* AiToolCallFacadeInterface from adachsoft/ai-tool-call */;
$ports = new PortsConfigDto(
toolCallingChatFacade: $toolCalling,
aiToolCallFacade: $aiTools,
);
// 2) Build AiAgent facade in stateful mode
$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,
))
// Optional: provide your own SPI-backed repository implementation
// ->withSpiConversationRepository($mySpiRepository)
->build();
$conversationId = 'example-conversation-1';
// 3) Start a new empty conversation once
try {
$facade->startConversation($conversationId);
} catch (\AdachSoft\AiAgent\PublicApi\Exception\ConversationAlreadyExistsPublicApiException $e) {
// conversation already exists - reuse it
}
// 4) Ask a question (one stateful turn)
$request = new AskRequestDto(prompt: 'What time is it in Warsaw? Reply as HH:MM.');
$response = $facade->ask($conversationId, $request);
$finalText = $response->finalText; // assistant final message
$history = $response->fullConversation; // full conversation as seen by the agent
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. ThrowsConversationNotFoundPublicApiExceptionwhen 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";
}
// Delete when no longer needed
$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 firstsystemrole message in every request toadachsoft/ai-integration.parameters– free-form array of provider-specific generation knobs forwarded 1:1 toToolCallingChatRequestDto::$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:
DEEPSEEK_API_KEY
API Highlights
AiAgentFacadeInterface::startConversation(string $conversationId): voidAiAgentFacadeInterface::ask(string $conversationId, AskRequestDto $request): AskResponseDtoAiAgentFacadeInterface::getRawConversation(string $conversationId): ChatMessageDtoCollectionAiAgentFacadeInterface::listConversationIds(): ConversationIdDtoCollectionAiAgentFacadeInterface::deleteConversation(string $conversationId): voidAiAgentFacadeInterface::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(enum value describing orchestration outcome),finalText(string|null) – final assistant text for this turn.
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– usefinalText, ignoretoolCalls.prefer_tool_calls– ignorefinalText, 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
conversationIdand appends only new messages viaConversationRepositoryInterface. - 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 modernisValidItem()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).