adachsoft / ai-tool-call-plugin
Composer plugin that discovers ai-tool-call providers and generates a deterministic registry.
Package info
gitlab.com/a.adach/ai-tool-call-plugin
Type:composer-plugin
pkg:composer/adachsoft/ai-tool-call-plugin
Requires
- php: ^8.3
- composer-plugin-api: ^2.0
- adachsoft/ai-tool-call: ^2.0
- adachsoft/collection: ^3.0
- nikic/php-parser: ^5.0
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.4
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\\ToolFactoryInterfaceusing 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/Infrastructureclasses.
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(implementsToolingDiscoveryFacadeInterface).
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 aConfigMapfor 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/ internalToolFactoryRegistryReader(loadsToolFactoryRegistryDataDtofrom the generated registry file),DefaultToolFactoryDiscovery(turns factory FQCNs intoToolFactoryInterfaceinstances usingToolFactoryInstantiator+ yourServiceLocatorInterface),DefaultToolingDiscovery(combines discovered factories with configuration fromToolConfigProviderInterfaceintoToolingDiscoveryResultDto).
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.phpand 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).