adachsoft/ai-agent

Stateless AI Agent Orchestrator Library for tool-calling chats.

Installs: 17

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/adachsoft/ai-agent

v0.6.0 2026-01-10 07:20 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)

1) Create ports (PublicApi facades from external libs):

  • ToolCallingChatFacade (e.g. Deepseek/OpenAI provider via their builders),
  • AiToolCallFacade (provides tool catalog and execution). 2) Build the AiAgent facade via AiAgentBuilder with config and policies. 3) Call startConversation($conversationId) once to create an empty conversation in the configured repository. 4) 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;

// 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,
    ))
    ->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

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,
          'thinkingLevel' => 'high',
      ],
      '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):
    • Run from repo: php examples/agent-publicapi-quickstart/run.php "Your prompt"
    • Run from installed package: php vendor/adachsoft/ai-agent/examples/agent-publicapi-quickstart/run.php "Your prompt"
  • Interactive chat (stateful over a single conversationId):
    • Run from repo: php examples/agent-publicapi-chat/run.php
    • Run from installed package: php vendor/adachsoft/ai-agent/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): void
  • AiAgentFacadeInterface::ask(string $conversationId, AskRequestDto $request): AskResponseDto

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 - use finalText, ignore toolCalls.
  • prefer_tool_calls - ignore finalText, continue with tool execution.
  • error_on_conflict - treat the situation as a domain error.

Example:

$facade = (new AiAgentBuilder())
    ->withPorts($ports)
    ->withAgentConfig($config)
    ->withPolicies($policies)
    ->withChatTurnResolutionMode('prefer_final_text')
    ->build();

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:

use AdachSoft\\AiAgent\\Spi\\Conversation\\ChatTurnResolutionStrategyInterface as SpiStrategy;

final class MyResolutionStrategy implements SpiStrategy
{
    public function decide(ChatTurnResult $turn): string
    {
        // your custom policy here
        return 'use_tool_calls';
    }
}

$facade = (new AiAgentBuilder())
    ->withPorts($ports)
    ->withAgentConfig($config)
    ->withPolicies($policies)
    ->withSpiChatTurnResolutionStrategy(new MyResolutionStrategy())
    ->build();

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.

Changelog

See CHANGELOG.md. Source of truth is changelog.json (generated via adachsoft/changelog-linter).

License

MIT (see composer.json).