Skip to content

Add #[MapToolArguments] for flat DTO tool arguments #2509 - #2510

Open
ineersa wants to merge 2 commits into
symfony:mainfrom
ineersa:feat/map-tool-arguments
Open

ineersa wants to merge 2 commits into
symfony:mainfrom
ineersa:feat/map-tool-arguments

Conversation

@ineersa

@ineersa ineersa commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
Q A
Bug fix? no
New feature? yes
Docs? yes
Issues Fix #2509
License MIT
  • Implements new #[MapToolArguments] attribute for exposing DTO properties at the schema root.
  • Updates factories and argument resolvers to handle the new mapping functionality.
  • Includes extensive test coverage for valid and invalid use cases.
  • Documents usage and configuration of the new attribute.

@ineersa ineersa changed the title feat(agent): add #[MapToolArguments] attribute to map flat payloads [Agent] Add #[MapToolArguments] for flat DTO tool arguments #2509 Sep 11, 2026

@wachterjohannes wachterjohannes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid feature: reuses buildProperties() so nullable/enum/nested/#[Schema] handling comes for free, and the tests assert the actual flattened schema and DTO round-trip, not just presence. Two nits inline, plus one CI fix.

  • Fabbot's only red check is the commit title using Conventional Commits (feat(agent): ...). This repo doesn't use that convention, worth amending the title before merge.

Comment on lines +70 to +77
$reference = $metadata->getReference();
$method = new \ReflectionMethod($reference->getClass(), $reference->getMethod());
$mapped = MappedToolArgument::forMethod($method);
if (null !== $mapped) {
return [
$mapped->parameter->getName() => $this->denormalizer->denormalize($toolCall->getArguments(), $mapped->className, 'json'),
];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of this method wraps failures as ToolException (the class docblock promises @throws ToolException). This path denormalizes straight from the DTO and lets MissingConstructorArgumentsException escape raw instead, confirmed by the PR's own test expecting that exact class. Doesn't matter through Toolbox::execute() today, both get wrapped into ToolExecutionException the same way, but it's an inconsistent contract for anyone calling the resolver directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but below

if ($this->denormalizer->supportsDenormalization($value, $parameterType, 'json')) {
$value = $this->denormalizer->denormalize($value, $parameterType, 'json');
}
same thing happens when denormalize() being called.

Should I just add docblock to show that multiple exceptions possible or catch in both cases and produce ToolException ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that gap predates this PR. For this PR, please wrap just the new mapped-DTO denormalize call in a try/catch and rethrow as ToolException. That keeps the new code matching the docblock's promise. The pre-existing gap at the per-parameter path is real too, but it's not part of this diff. A separate follow-up issue for that works fine, no need to fix it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines +82 to +95
}

/**
* @return array<string, mixed>|null
*/
private function buildParameters(string $className, string $methodName): ?array
{
$mapped = MappedToolArgument::forMethod(new \ReflectionMethod($className, $methodName));
if (null === $mapped) {
return $this->factory->buildParameters($className, $methodName);
}

return $this->factory->buildProperties($mapped->className);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This buildParameters() helper is copy-pasted verbatim into ReflectionToolFactory.php too. Could live once, e.g. as a static helper on MappedToolArgument itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@ineersa
ineersa force-pushed the feat/map-tool-arguments branch 3 times, most recently from 5901920 to 5778617 Compare September 14, 2026 00:34

@chr-hertel chr-hertel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @ineersa - I think it's a great feature!

Please revisit how services in ToolFactory implementations, JsonSchema\Factory and MappedToolArgument work together. There are a couple of symptoms that raise a flag for me here:

  • in both ToolFactory implementations the JsonSchema\Factory is injected, but only forwarded as argument into a static method
  • it is an optional feature but feels like taking over the steering here
  • Is MappedToolArgument a state object or a service? maybe we need to decouple - not sure

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we transform this into ObjectDescriberInterface implementation and encapsulate within the Factory basically?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this looks better now.

One thing to notice is factory wiring, if we want to provide ability to use this argument attribute by default, we need to wire describer into factories.

That was IMHO a bit too invasive, I've left previous commit, so you could check how it was looking.

As a tradeoff, application side now needs to configure factory with describer to use this feature properly - https://github.com/symfony/ai/pull/2510/changes#diff-e04a1b45e14764524023222d3e64d8f72f52e20b4d3ae6d7c2691c49fe787a83R528

@carsonbot carsonbot changed the title [Agent] Add #[MapToolArguments] for flat DTO tool arguments #2509 Add #[MapToolArguments] for flat DTO tool arguments #2509 Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Agent] Add #[MapToolArguments] for flat DTO tool arguments

4 participants