adachsoft/ai-tool-call-plugin

Composer plugin that discovers ai-tool-call providers and generates a deterministic registry.

Maintainers

Package info

gitlab.com/a.adach/ai-tool-call-plugin

Issues

Type:composer-plugin

pkg:composer/adachsoft/ai-tool-call-plugin

Statistics

Installs: 14

Dependents: 0

Suggesters: 0

Stars: 0

v0.3.0 2026-02-21 10:10 UTC

This package is not auto-updated.

Last update: 2026-03-02 04:36:34 UTC


README

Composer plugin that discovers ai-tool-call tooling across installed packages and generates deterministic registry files used at runtime.

Requirements

  • PHP >= 8.3
  • Composer 2.x
  • composer-plugin-api ^2.0, psr/log ^3.0, adachsoft/collection ^3.0

ToolFactory autodiscovery (recommended)

From version 0.3.0 the main integration path is ToolFactory-based autodiscovery.

This plugin:

  • scans installed packages for implementations of AdachSoft\\AiToolCall\\SPI\\Factory\\ToolFactoryInterface using Composer metadata and AST,
  • generates a PHP registry file with discovered factories, grouped by Composer package name,
  • enriches the registry with constructor argument metadata for each factory (for debugging/inspection),
  • exposes a public runtime facade so that your application never needs to touch Internal / Infrastructure classes.

1) Enable plugin in your project composer.json

{
  "config": {
    "allow-plugins": {
      "adachsoft/ai-tool-call-plugin": true
    }
  }
}

2) Implement ToolFactory in your extension package

In your package that provides tools:

  • implement AdachSoft\\AiToolCall\\SPI\\Factory\\ToolFactoryInterface,
  • point to your package via normal Composer autoload (autoload.psr-4).

Example:

namespace Vendor\\Pkg;

use AdachSoft\\AiToolCall\\SPI\\Collection\\ConfigMap;
use AdachSoft\\AiToolCall\\SPI\\Collection\\TagsCollection;
use AdachSoft\\AiToolCall\\SPI\\Dto\\ToolCallRequestDto;
use AdachSoft\\AiToolCall\\SPI\\Dto\\ToolCallResultDto;
use AdachSoft\\AiToolCall\\SPI\\Dto\\ToolDefinitionDto;
use AdachSoft\\AiToolCall\\SPI\\Factory\\ToolFactoryInterface;
use AdachSoft\\AiToolCall\\SPI\\ToolInterface;

final class MyToolFactory implements ToolFactoryInterface
{
    public function getToolClass(): string
    {
        return MyTool::class;
    }

    public function create(ConfigMap $config): ToolInterface
    {
        // resolve dependencies from your container / config map
        return new MyTool();
    }
}

final class MyTool implements ToolInterface
{
    public static function getDefinition(): ToolDefinitionDto
    {
        return new ToolDefinitionDto(
            name: 'my_tool',
            description: 'My tool description',
            parametersSchema: [],
            tags: new TagsCollection([]),
            enabled: true,
        );
    }

    public function callTool(ToolCallRequestDto $request): ToolCallResultDto
    {
        // ... implement your tool logic
    }
}

3) Configure scanning and registry (optional)

You can control how the plugin scans for ToolFactory implementations and where it writes the registry file using an optional JSON config file in your project root:

  • ai-tool-call.json

Supported keys (all optional):

{
  "registry_output_path": "/absolute/path/to/ai-tool-call-registry.php", // default: null (project-root/.generated/ai-tool-call-registry.php)
  "additional_scan_directories": [
    "src/ExtraTools"
  ],
  "excluded_scan_directories": [
    ".generated", // default is [".generated"] when not overridden
    "var/cache"
  ],
  "failure_mode": "fail_fast", // or "fail_soft"; default: "fail_fast"
  "max_php_files": 10000,
  "max_total_bytes": 104857600,
  "max_file_bytes": 1048576
}

These values are mapped to the AdachSoft\\AiToolCallPlugin\\PublicApi\\ValueObject\\AiToolCallConfig value object and used consistently by the registry generator, debug tools and runtime discovery.

4) Build ToolFactory registry

The plugin can build the registry in two ways:

  • Automatically via Composer plugin hooks (post-install-cmd, post-update-cmd).
  • Manually via CLI:
# default path under project root
$ ./vendor/bin/ai-tool-call-tool-factory-registry-build
[ai-tool-call-plugin] ToolFactory registry written

# override registry_output_path via ai-tool-call.json
$ cat ai-tool-call.json
{
  "registry_output_path": "/var/cache/ai-tool-call-registry.php"
}

$ ./vendor/bin/ai-tool-call-tool-factory-registry-build
[ai-tool-call-plugin] ToolFactory registry written

The generated file has the following conceptual shape:

<?php
// .generated/ai-tool-call-registry.php (or custom absolute path from config)
return [
    'packages' => [
        'vendor/pkg-a' => [
            'Vendor\\Pkg\\MyToolFactory',
        ],
        'vendor/other' => [
            'Vendor\\Other\\AnotherFactory',
        ],
    ],
    'meta' => [
        'factories' => [
            // class-string<ToolFactoryInterface> => ctor args metadata
            'Vendor\\Pkg\\MyToolFactory' => [
                'ctor_args' => [
                    ['name' => 'dependency', 'type' => 'Vendor\\Pkg\\Dependency'],
                ],
            ],
        ],
        'errors' => [
            // reflection errors collected during build (non-fatal in fail-soft modes)
            [
                'code' => 'reflection_error',
                'message' => 'Could not autoload ...',
                'package' => 'vendor/pkg-a',
                'factory_fqcn' => 'Vendor\\Pkg\\BrokenFactory',
            ],
        ],
    ],
];

The exact structure is represented in code by AdachSoft\\AiToolCallPlugin\\PublicApi\\Dto\\ToolFactoryRegistryDataDto.

5) Runtime discovery (Public API + SPI)

At runtime the recommended entrypoint is the Public API facade:

  • AdachSoft\\AiToolCallPlugin\\PublicApi\\Facade\\ToolingDiscoveryFacade (implements ToolingDiscoveryFacadeInterface).

Your application (host) MUST provide implementations for two SPI interfaces:

  • AdachSoft\\AiToolCallPlugin\\Spi\\Runtime\\ServiceLocatorInterface – simple locator that can return services by class-string id;
  • AdachSoft\\AiToolCallPlugin\\Spi\\Runtime\\ToolConfigProviderInterface – provides a ConfigMap for a given tool name.

Typical flow:

use AdachSoft\\AiToolCall\\SPI\\Collection\\ConfigMap;
use AdachSoft\\AiToolCallPlugin\\PublicApi\\Dto\\ToolFactoryAutodiscoveryOptionsDto;
use AdachSoft\\AiToolCallPlugin\\PublicApi\\Enum\\FailureModeEnum;
use AdachSoft\\AiToolCallPlugin\\PublicApi\\Facade\\ToolingDiscoveryFacade;
use AdachSoft\\AiToolCallPlugin\\Spi\\Runtime\\ServiceLocatorInterface;
use AdachSoft\\AiToolCallPlugin\\Spi\\Runtime\\ToolConfigProviderInterface;

final class AppServiceLocator implements ServiceLocatorInterface
{
    public function get(string $id): object
    {
        // integrate with your container or service locator
    }

    public function has(string $id): bool
    {
        // ...
    }
}

final class AppToolConfigProvider implements ToolConfigProviderInterface
{
    public function getConfigForToolName(string $toolName): ConfigMap
    {
        // return ConfigMap with configuration for a given tool
        return new ConfigMap([]);
    }
}

$facade = ToolingDiscoveryFacade::create(
    new AppServiceLocator(),
    new AppToolConfigProvider(),
);

$options = new ToolFactoryAutodiscoveryOptionsDto(FailureModeEnum::FAIL_SOFT);

// 1) Discover using project root + optional registry path override
$result = $facade->discoverFromProjectRoot(
    $projectRoot,
    $options,
    // null => default project-root/.generated/ai-tool-call-registry.php
    null,
);

// 2) Or discover directly from an absolute registry path
$result = $facade->discoverFromRegistryPath(
    '/absolute/path/to/ai-tool-call-registry.php',
    $options,
);

// $result is ToolingDiscoveryResultDto
foreach ($result->factories as $factory) {
    $toolClass = $factory->getToolClass();
    // ... use $toolClass or create tools
}

$configsByToolName = $result->configsByToolName; // array<string, ConfigMap>
$errors = $result->errors;                      // DiscoveryErrorCollection

Under the hood the facade composes the existing building blocks:

  • ToolFactoryRegistryReaderInterface / internal ToolFactoryRegistryReader (loads ToolFactoryRegistryDataDto from the generated registry file),
  • DefaultToolFactoryDiscovery (turns factory FQCNs into ToolFactoryInterface instances using ToolFactoryInstantiator + your ServiceLocatorInterface),
  • DefaultToolingDiscovery (combines discovered factories with configuration from ToolConfigProviderInterface into ToolingDiscoveryResultDto).

You can choose failure mode via ToolFactoryAutodiscoveryOptionsDto:

  • FailureModeEnum::FAIL_FAST – stop on first error, return partial data.
  • FailureModeEnum::FAIL_SOFT – collect all errors and continue.

When the registry file is missing or invalid, the facade returns an empty ToolingDiscoveryResultDto with a single DiscoveryErrorDto (code = REGISTRY_READ_FAILED) instead of throwing.

Debug helpers (bin)

ToolFactory registry debug

Use bin/ai-tool-call-registry-debug to inspect the ToolFactory registry.

  • Prints absolute path to the registry (resolved from project root and optional override argument).
  • For each entry prints: package name, factory FQCN and constructor argument types (when available in meta.factories).
  • Exits with non-zero when registry is missing/invalid.

Examples:

# default registry path (project-root/.generated/ai-tool-call-registry.php or override from ai-tool-call.json)
$ ./vendor/bin/ai-tool-call-registry-debug
[ai-tool-call-plugin] Registry path: /path/to/project/.generated/ai-tool-call-registry.php
vendor/pkg-a | Vendor\\Pkg\\MyToolFactory | dependency: Vendor\\Pkg\\Dependency
vendor/other | Vendor\\Other\\AnotherFactory | (no ctor args)

# inspect a custom absolute registry path
$ ./vendor/bin/ai-tool-call-registry-debug /var/cache/ai-tool-call-registry.php
[ai-tool-call-plugin] Registry path: /var/cache/ai-tool-call-registry.php
...

ToolFactory scan debug

Use bin/ai-tool-call-factory-scan-debug to run a raw scan of ToolFactory classes for a given project root and directories.

$ ./vendor/bin/ai-tool-call-factory-scan-debug src
[ai-tool-call-plugin] Found factories:
Vendor\\Pkg\\MyToolFactory
...

Notes

  • No scanning of vendor/ at runtime – only the generated registry file is read.
  • Deterministic content: packages and factories are sorted in the generated file.
  • Legacy providers-based registry (entry-file resources/ai-tool-call.php and related PublicApi/Infrastructure types) has been removed in current development versions; only ToolFactory-based autodiscovery is supported going forward.
  • Runtime SPI (Spi\\Runtime\\ServiceLocatorInterface, Spi\\Runtime\\ToolConfigProviderInterface) is now a separate namespace from PublicApi; hosts are expected to implement SPI, while application code should depend primarily on the PublicApi facades and DTOs.

Versioning

  • Semantic Versioning via Git tags (do not set version in composer.json).