From 9f00cf25999239be164cbb857813cf9bef8d9f9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:55:55 +0000 Subject: [PATCH 01/10] Initial plan From 9f160dbd684c5b8a8b3d6ec7b38d1e412722cf7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:12:09 +0000 Subject: [PATCH 02/10] Fix: Propagate metadata (sources) in streaming responses Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/.phpunit.result.cache | 1 + src/agent/src/Toolbox/StreamResult.php | 9 +++- .../tests/Toolbox/AgentProcessorTest.php | 49 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/agent/.phpunit.result.cache diff --git a/src/agent/.phpunit.result.cache b/src/agent/.phpunit.result.cache new file mode 100644 index 000000000..003df7cfa --- /dev/null +++ b/src/agent/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"Symfony\\AI\\Agent\\Tests\\Toolbox\\AgentProcessorTest::testSourcesEndUpInResultMetadataWithStreaming":4},"times":{"Symfony\\AI\\Agent\\Tests\\Toolbox\\AgentProcessorTest::testSourcesEndUpInResultMetadataWithStreaming":0}} \ No newline at end of file diff --git a/src/agent/src/Toolbox/StreamResult.php b/src/agent/src/Toolbox/StreamResult.php index 63efcaa93..e16bc738a 100644 --- a/src/agent/src/Toolbox/StreamResult.php +++ b/src/agent/src/Toolbox/StreamResult.php @@ -31,7 +31,14 @@ public function getContent(): \Generator $streamedResult = ''; foreach ($this->generator as $value) { if ($value instanceof ToolCallResult) { - yield from ($this->handleToolCallsCallback)($value, Message::ofAssistant($streamedResult))->getContent(); + $innerResult = ($this->handleToolCallsCallback)($value, Message::ofAssistant($streamedResult)); + + // Propagate metadata from inner result to this result + foreach ($innerResult->getMetadata()->all() as $key => $metadataValue) { + $this->getMetadata()->add($key, $metadataValue); + } + + yield from $innerResult->getContent(); break; } diff --git a/src/agent/tests/Toolbox/AgentProcessorTest.php b/src/agent/tests/Toolbox/AgentProcessorTest.php index 44465ae99..222102efe 100644 --- a/src/agent/tests/Toolbox/AgentProcessorTest.php +++ b/src/agent/tests/Toolbox/AgentProcessorTest.php @@ -26,6 +26,7 @@ use Symfony\AI\Platform\PlatformInterface; use Symfony\AI\Platform\Result\DeferredResult; use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\Result\StreamResult as GenericStreamResult; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Result\ToolCallResult; @@ -233,4 +234,52 @@ public function testSourcesGetCollectedAcrossConsecutiveToolCalls() $this->assertCount(2, $metadata->get('sources')); $this->assertSame([$source1, $source2], $metadata->get('sources')); } + + public function testSourcesEndUpInResultMetadataWithStreaming() + { + $toolCall = new ToolCall('call_1234', 'tool_sources', ['arg1' => 'value1']); + $source1 = new Source('Relevant Article 1', 'http://example.com/article1', 'Content of article about the topic'); + $source2 = new Source('Relevant Article 2', 'http://example.com/article2', 'More content of article about the topic'); + $toolbox = $this->createMock(ToolboxInterface::class); + $toolbox + ->expects($this->once()) + ->method('execute') + ->willReturn(new ToolResult($toolCall, 'Response based on the two articles.', [$source1, $source2])); + + $messageBag = new MessageBag(); + + // Create a generator that yields chunks and then a ToolCallResult + $generator = (function () use ($toolCall) { + yield 'chunk1'; + yield 'chunk2'; + yield new ToolCallResult($toolCall); + })(); + + $result = new GenericStreamResult($generator); + + $agent = $this->createMock(AgentInterface::class); + $agent + ->expects($this->once()) + ->method('call') + ->willReturn(new TextResult('Final response based on the two articles.')); + + $processor = new AgentProcessor($toolbox, includeSources: true); + $processor->setAgent($agent); + + $output = new Output('gpt-4', $result, $messageBag); + + $processor->processOutput($output); + + // Consume the stream + $content = ''; + foreach ($output->getResult()->getContent() as $chunk) { + $content .= $chunk; + } + + // After consuming the stream, metadata should be available + $metadata = $output->getResult()->getMetadata(); + $this->assertTrue($metadata->has('sources')); + $this->assertCount(2, $metadata->get('sources')); + $this->assertSame([$source1, $source2], $metadata->get('sources')); + } } From 9b1f76c42e41126468f2e14ec10365b55911a7bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:13:14 +0000 Subject: [PATCH 03/10] Add .phpunit.result.cache to .gitignore Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- .gitignore | 1 + src/agent/.phpunit.result.cache | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 src/agent/.phpunit.result.cache diff --git a/.gitignore b/.gitignore index 5d204c615..3b3481f11 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock vendor +.phpunit.result.cache diff --git a/src/agent/.phpunit.result.cache b/src/agent/.phpunit.result.cache deleted file mode 100644 index 003df7cfa..000000000 --- a/src/agent/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Symfony\\AI\\Agent\\Tests\\Toolbox\\AgentProcessorTest::testSourcesEndUpInResultMetadataWithStreaming":4},"times":{"Symfony\\AI\\Agent\\Tests\\Toolbox\\AgentProcessorTest::testSourcesEndUpInResultMetadataWithStreaming":0}} \ No newline at end of file From 2bb7b3a870a1216cbc0ecc10a0abfe9f4d590bd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:25:51 +0000 Subject: [PATCH 04/10] Fix trailing whitespace in test Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/tests/Toolbox/AgentProcessorTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent/tests/Toolbox/AgentProcessorTest.php b/src/agent/tests/Toolbox/AgentProcessorTest.php index 222102efe..628d535ef 100644 --- a/src/agent/tests/Toolbox/AgentProcessorTest.php +++ b/src/agent/tests/Toolbox/AgentProcessorTest.php @@ -247,14 +247,14 @@ public function testSourcesEndUpInResultMetadataWithStreaming() ->willReturn(new ToolResult($toolCall, 'Response based on the two articles.', [$source1, $source2])); $messageBag = new MessageBag(); - + // Create a generator that yields chunks and then a ToolCallResult $generator = (function () use ($toolCall) { yield 'chunk1'; yield 'chunk2'; yield new ToolCallResult($toolCall); })(); - + $result = new GenericStreamResult($generator); $agent = $this->createMock(AgentInterface::class); @@ -275,7 +275,7 @@ public function testSourcesEndUpInResultMetadataWithStreaming() foreach ($output->getResult()->getContent() as $chunk) { $content .= $chunk; } - + // After consuming the stream, metadata should be available $metadata = $output->getResult()->getMetadata(); $this->assertTrue($metadata->has('sources')); From 74f8a8a6cd701ba2b5eaedeb82d8b68be428db64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:12:39 +0000 Subject: [PATCH 05/10] Fix: Handle non-iterable content from inner result Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/src/Toolbox/StreamResult.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/agent/src/Toolbox/StreamResult.php b/src/agent/src/Toolbox/StreamResult.php index e16bc738a..519598de6 100644 --- a/src/agent/src/Toolbox/StreamResult.php +++ b/src/agent/src/Toolbox/StreamResult.php @@ -32,13 +32,18 @@ public function getContent(): \Generator foreach ($this->generator as $value) { if ($value instanceof ToolCallResult) { $innerResult = ($this->handleToolCallsCallback)($value, Message::ofAssistant($streamedResult)); - + // Propagate metadata from inner result to this result foreach ($innerResult->getMetadata()->all() as $key => $metadataValue) { $this->getMetadata()->add($key, $metadataValue); } - - yield from $innerResult->getContent(); + + $content = $innerResult->getContent(); + if (is_iterable($content)) { + yield from $content; + } else { + yield $content; + } break; } From edbdfdd5c478b45816e576f3015f50f5c0bf80ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:14:17 +0000 Subject: [PATCH 06/10] Improve content type check to avoid string iteration Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/src/Toolbox/StreamResult.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/src/Toolbox/StreamResult.php b/src/agent/src/Toolbox/StreamResult.php index 519598de6..c6d4b49fd 100644 --- a/src/agent/src/Toolbox/StreamResult.php +++ b/src/agent/src/Toolbox/StreamResult.php @@ -39,7 +39,7 @@ public function getContent(): \Generator } $content = $innerResult->getContent(); - if (is_iterable($content)) { + if ($content instanceof \Generator || $content instanceof \Iterator || (is_array($content))) { yield from $content; } else { yield $content; From d5da6c9bc4c90a8b1ab1520075e360568c0c3cf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:16:14 +0000 Subject: [PATCH 07/10] Simplify content type check for better maintainability Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/src/Toolbox/StreamResult.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/agent/src/Toolbox/StreamResult.php b/src/agent/src/Toolbox/StreamResult.php index c6d4b49fd..034dc4376 100644 --- a/src/agent/src/Toolbox/StreamResult.php +++ b/src/agent/src/Toolbox/StreamResult.php @@ -39,10 +39,12 @@ public function getContent(): \Generator } $content = $innerResult->getContent(); - if ($content instanceof \Generator || $content instanceof \Iterator || (is_array($content))) { - yield from $content; - } else { + // Strings are iterable in PHP but should be yielded as single values + // null should also be yielded as-is + if (is_string($content) || null === $content) { yield $content; + } else { + yield from $content; } break; From 4976370e18e4b1e7bd376e1f33991d9341311504 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:17:31 +0000 Subject: [PATCH 08/10] Improve code comments and comparison consistency Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/src/Toolbox/StreamResult.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/agent/src/Toolbox/StreamResult.php b/src/agent/src/Toolbox/StreamResult.php index 034dc4376..7a654684f 100644 --- a/src/agent/src/Toolbox/StreamResult.php +++ b/src/agent/src/Toolbox/StreamResult.php @@ -39,9 +39,10 @@ public function getContent(): \Generator } $content = $innerResult->getContent(); - // Strings are iterable in PHP but should be yielded as single values - // null should also be yielded as-is - if (is_string($content) || null === $content) { + // Strings are iterable in PHP but yield from would iterate character-by-character. + // We need to yield the complete string as a single value to preserve streaming behavior. + // null should also be yielded as-is. + if (is_string($content) || $content === null) { yield $content; } else { yield from $content; From e5f59aaf8416b7c7a9d0b6ba0159eef957ac7afa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:19:04 +0000 Subject: [PATCH 09/10] Add safety check for non-iterable objects Co-authored-by: OskarStark <995707+OskarStark@users.noreply.github.com> --- src/agent/src/Toolbox/StreamResult.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/src/Toolbox/StreamResult.php b/src/agent/src/Toolbox/StreamResult.php index 7a654684f..7239f4f0c 100644 --- a/src/agent/src/Toolbox/StreamResult.php +++ b/src/agent/src/Toolbox/StreamResult.php @@ -42,7 +42,7 @@ public function getContent(): \Generator // Strings are iterable in PHP but yield from would iterate character-by-character. // We need to yield the complete string as a single value to preserve streaming behavior. // null should also be yielded as-is. - if (is_string($content) || $content === null) { + if (is_string($content) || $content === null || !is_iterable($content)) { yield $content; } else { yield from $content; From 4dac1e3cbc96f44bfeedec8a23a5f161fef0c19c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:07:29 +0000 Subject: [PATCH 10/10] Revert unnecessary .gitignore change Co-authored-by: chr-hertel <2852185+chr-hertel@users.noreply.github.com> --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3b3481f11..5d204c615 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ composer.lock vendor -.phpunit.result.cache