Add invoker-aware input/output transformations for tools
### Problem/Motivation
A recurring need exists to alter the inputs and outputs of tools based on **who is invoking them**. Common examples include:
- **AI Agents/MCP** need tokens resolved to entities on input, and entities downcasted back to tokens on output.
- **AI Agents** benefit from output decoders that reduce token count sent back to the LLM.
- **MCP servers** may need similar upcasting/downcasting but with different serialization constraints.
- **Deterministic invokers** occasionally benefit from the same transformations (e.g., accepting a user email and resolving it to a user entity before tool execution). _This may be considered a per-tool flexibility and addressed in the footnote._
Currently, connector modules handle these transformations individually, but this leads to duplicated logic. Multiple invokers (AI Agents, MCP, etc.) would benefit from shared transformation infrastructure rather than each connector re-implementing upcasting, downcasting, and output reduction independently.
### Proposed resolution
Introduce an invoker-aware mechanism for tools, allowing input/output transformations to be registered and applied based on the invoker identity.
**API design** — support both passing the invoker at instantiation and setting it subsequently. The invoker argument is optional, preserving backward compatibility:
```
// Approach 1: Invoker passed on createInstance
// (precedent: Drupal\Core\ImageToolkit\ImageToolkitOperationManager)
$tool = $toolManager->createInstance('tool:my_tool', [], AiAgentsInvoker::ID);
// Approach 2: Invoker set via method after instantiation
$tool = $toolManager->createInstance('tool:my_tool');
$tool->setInvoker(AiAgentsInvoker::ID);
// Standard usage remains unchanged when no invoker is needed
$tool = $toolManager->createInstance('tool:my_tool');
$tool->setContextValue('key', 'value');
$tool->execute();
```
**Event-driven transformation** — during execute(), events are dispatched that carry the invoker identity. External modules can subscribe to these events and conditionally apply transformations based on the invoker:
- **Pre-invocation event** — allows subscribers to upcast/transform input values (e.g., resolve a token to an entity) when the invoker is a specific type.
- **Post-invocation event** — allows subscribers to downcast/reduce output (e.g., encode entities back to tokens, reduce output verbosity for LLMs) when the invoker is a specific type.
This allows modules like ai_agents or mcp to provide shared transformation subscribers without the tool itself needing awareness of invoker-specific concerns. Third-party modules can also hook into specific invokers to provide custom transformations.
```
// Example subscriber
public function onPreInvoke(ToolPreInvokeEvent $event): void {
if ($event->getInvoker() === AiAgentsInvoker::ID) {
// Upcast token to entity
$value = $event->getContextValue('user');
$event->setContextValue('user', $this->resolveToken($value));
}
}
```
### Alternatives considered
- Connector modules handling all transformations individually — current approach; leads to duplicated logic across connectors and makes shared improvements difficult.
- Decoration/middleware pattern — wrapping tools in decorator classes per-invoker. More flexible but harder to discover and debug.
- Invoker-as-plugin with built-in transformers — invokers define their own transformations directly rather than via events. Simpler but less extensible by third-party modules.
### AI usage (if applicable)
**Footnote: Per-tool type flexibility**
Per-tool input flexibility (e.g., a MixedInputDefinition allowing a parameter to accept ['entity:user', 'email', 'integer']) is a related but separate concern. It will be addressed in a follow-up issue.
- [x] **AI Assisted Issue:** This issue was generated with AI assistance, but was reviewed and refined by the creator.
- [ ] **AI Assisted Code:** This code was mainly generated by a human, with AI autocompleting or parts AI generated, but under full human supervision.
- [ ] **AI Generated Code:** This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.
- [ ] **Vibe Coded:** This code was generated by an AI and has only been functionally tested.
issue