Skip to content
This repository was archived by the owner on Mar 15, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(
public string $runId = '',
public ?string $agentApiKeyOverride = null,
public ?string $agentProviderOverride = null,
public int $retryCount = 0,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
use App\Workflow\Facade\WorkflowConcurrencyFacadeInterface;
use App\WorkspaceManagement\Facade\WorkspaceManagementFacadeInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Uid\Uuid;
use Throwable;

readonly class ImplementationRunExecutor implements ImplementationRunExecutorInterface
{
private const int MAX_RETRIES = 2;

private const string OUTPUT_FORMAT_INSTRUCTIONS = <<<'INSTRUCTIONS'
## Required Output Format

Expand Down Expand Up @@ -61,6 +65,7 @@ public function __construct(
private LlmIntegrationFacadeInterface $llmIntegrationFacade,
private LoggerInterface $logger,
private MessengerLogContext $logContext,
private MessageBusInterface $messageBus,
) {
}

Expand Down Expand Up @@ -113,6 +118,9 @@ private function doExecute(ImplementIssueMessage $message, ProductConfigDto $con
return;
}

$agentErrorText = null;
$exceptionMessage = null;

try {
$issueDto = $this->findIssue($config->githubUrl, $config->githubToken, $message->issueNumber);
if ($issueDto === null) {
Expand Down Expand Up @@ -181,30 +189,27 @@ private function doExecute(ImplementIssueMessage $message, ProductConfigDto $con
'durationMs' => $agentResult->durationMs,
]);

$this->handleOutcome(
$outcome,
$agentResult->resultText,
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
$issueDto,
$message->prNumber,
);
if ($outcome === ImplementationOutcome::Error) {
$agentErrorText = $this->stripMarkers($agentResult->resultText);
} else {
$this->handleOutcome(
$outcome,
$agentResult->resultText,
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
$issueDto,
$message->prNumber,
);
}
} catch (Throwable $e) {
$this->logger->error('[ImplementationAgent] Unhandled exception', [
'issueNumber' => $message->issueNumber,
'isRevision' => $message->isRevision,
'error' => $e->getMessage(),
]);

$this->applyErrorLabels($config->githubUrl, $config->githubToken, $message->issueNumber);

$this->githubIntegrationFacade->postIssueComment(
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
"**ProductBuilder Implementation Error**\n\nAn unexpected error occurred during implementation:\n\n> " . $e->getMessage(),
);
$exceptionMessage = $e->getMessage();
} finally {
if ($workspaceInfo !== null) {
$this->workspaceManagementFacade->releaseWorkspace($workspaceInfo);
Expand All @@ -217,6 +222,53 @@ private function doExecute(ImplementIssueMessage $message, ProductConfigDto $con
$message->runId,
);
}

if ($agentErrorText !== null || $exceptionMessage !== null) {
if ($message->retryCount < self::MAX_RETRIES) {
$newRunId = Uuid::v4()->toRfc4122();

if ($this->workflowConcurrencyFacade->tryCreateRunClaim(
$message->productConfigId,
$message->issueNumber,
WorkflowRunPhase::Implementation,
$newRunId,
)) {
$this->logger->warning('[ImplementationAgent] Error on attempt, scheduling retry', [
'issueNumber' => $message->issueNumber,
'attempt' => $message->retryCount + 1,
]);

$this->messageBus->dispatch(new ImplementIssueMessage(
$message->productConfigId,
$message->issueNumber,
$message->isRevision,
$message->prBranchName,
$message->prNumber,
$newRunId,
$message->agentApiKeyOverride,
$message->agentProviderOverride,
$message->retryCount + 1,
));

return;
}
}

$this->githubIntegrationFacade->postIssueComment(
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
$exceptionMessage !== null
? "**ProductBuilder Implementation Error**\n\nAn unexpected error occurred during implementation:\n\n> " . $exceptionMessage
: "**ProductBuilder Implementation Error**\n\nThe implementation agent encountered an error:\n\n" . $agentErrorText,
);

$this->applyErrorLabels($config->githubUrl, $config->githubToken, $message->issueNumber);

$this->logger->error('[ImplementationAgent] Implementation failed after all retries', [
'issueNumber' => $message->issueNumber,
]);
}
}

private function findIssue(string $githubUrl, string $githubToken, int $issueNumber): ?GithubIssueDto
Expand Down Expand Up @@ -328,7 +380,7 @@ private function handleOutcome(
match ($outcome) {
ImplementationOutcome::PrCreated => $this->handlePrCreated($resultText, $githubUrl, $githubToken, $issueNumber, $issueDto),
ImplementationOutcome::RevisionPushed => $this->handleRevisionPushed($githubUrl, $githubToken, $issueNumber, $prNumber),
ImplementationOutcome::Error => $this->handleError($resultText, $githubUrl, $githubToken, $issueNumber),
ImplementationOutcome::Error => null, // unreachable; error is captured and handled after finally blocks
};
}

Expand All @@ -341,14 +393,7 @@ private function handlePrCreated(
): void {
$metadata = ImplementationOutcome::extractPrMetadata($resultText);
if ($metadata === null) {
$this->handleError(
'Agent indicated PR readiness but did not return valid PR metadata markers.',
$githubUrl,
$githubToken,
$issueNumber,
);

return;
throw new \RuntimeException('Agent indicated PR readiness but did not return valid PR metadata markers.');
}

$body = $metadata->body;
Expand Down Expand Up @@ -416,24 +461,6 @@ private function handleRevisionPushed(string $githubUrl, string $githubToken, in
]);
}

private function handleError(string $resultText, string $githubUrl, string $githubToken, int $issueNumber): void
{
$cleanedText = $this->stripMarkers($resultText);

$this->githubIntegrationFacade->postIssueComment(
$githubUrl,
$githubToken,
$issueNumber,
"**ProductBuilder Implementation Error**\n\nThe implementation agent encountered an error:\n\n" . $cleanedText,
);

$this->applyErrorLabels($githubUrl, $githubToken, $issueNumber);

$this->logger->error('[ImplementationAgent] Implementation errored', [
'issueNumber' => $issueNumber,
]);
}

private function stripMarkers(string $text): string
{
$cleaned = str_replace(['[PR_READY]', '[REVISION_PUSHED]'], '', $text);
Expand Down
1 change: 1 addition & 0 deletions src/PlanningAgent/Facade/Message/PlanIssueMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function __construct(
public string $runId,
public ?string $agentApiKeyOverride = null,
public ?string $agentProviderOverride = null,
public int $retryCount = 0,
) {
}
}
98 changes: 66 additions & 32 deletions src/PlanningAgent/Infrastructure/Service/PlanningRunExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
use App\Workflow\Facade\WorkflowConcurrencyFacadeInterface;
use App\WorkspaceManagement\Facade\WorkspaceManagementFacadeInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Uid\Uuid;
use Throwable;

readonly class PlanningRunExecutor implements PlanningRunExecutorInterface
{
private const int MAX_RETRIES = 2;

private const string OUTPUT_FORMAT_INSTRUCTIONS = <<<'INSTRUCTIONS'
## Required Output Format

Expand All @@ -50,6 +54,7 @@ public function __construct(
private LlmIntegrationFacadeInterface $llmIntegrationFacade,
private LoggerInterface $logger,
private MessengerLogContext $logContext,
private MessageBusInterface $messageBus,
) {
}

Expand Down Expand Up @@ -99,6 +104,9 @@ private function doExecute(PlanIssueMessage $message, ProductConfigDto $config):
return;
}

$agentErrorText = null;
$exceptionMessage = null;

try {
$issueDto = $this->findIssue($config->githubUrl, $config->githubToken, $message->issueNumber);
if ($issueDto === null) {
Expand Down Expand Up @@ -149,27 +157,24 @@ private function doExecute(PlanIssueMessage $message, ProductConfigDto $config):
'durationMs' => $agentResult->durationMs,
]);

$this->handleOutcome(
$outcome,
$agentResult->resultText,
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
);
if ($outcome === PlanningOutcome::Error) {
$agentErrorText = $this->stripMarkers($agentResult->resultText);
} else {
$this->handleOutcome(
$outcome,
$agentResult->resultText,
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
);
}
} catch (Throwable $e) {
$this->logger->error('[PlanningAgent] Unhandled exception', [
'issueNumber' => $message->issueNumber,
'error' => $e->getMessage(),
]);

$this->applyErrorLabels($config->githubUrl, $config->githubToken, $message->issueNumber);

$this->githubIntegrationFacade->postIssueComment(
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
"**ProductBuilder Planning Error**\n\nAn unexpected error occurred during planning:\n\n> " . $e->getMessage(),
);
$exceptionMessage = $e->getMessage();
} finally {
if ($workspaceInfo !== null) {
$this->workspaceManagementFacade->releaseWorkspace($workspaceInfo);
Expand All @@ -182,6 +187,51 @@ private function doExecute(PlanIssueMessage $message, ProductConfigDto $config):
$message->runId,
);
}

if ($agentErrorText !== null || $exceptionMessage !== null) {
if ($message->retryCount < self::MAX_RETRIES) {
$newRunId = Uuid::v4()->toRfc4122();

if ($this->workflowConcurrencyFacade->tryCreateRunClaim(
$message->productConfigId,
$message->issueNumber,
WorkflowRunPhase::Planning,
$newRunId,
)) {
$this->logger->warning('[PlanningAgent] Error on attempt, scheduling retry', [
'issueNumber' => $message->issueNumber,
'attempt' => $message->retryCount + 1,
]);

$this->messageBus->dispatch(new PlanIssueMessage(
$message->productConfigId,
$message->issueNumber,
$message->isResume,
$newRunId,
$message->agentApiKeyOverride,
$message->agentProviderOverride,
$message->retryCount + 1,
));

return;
}
}

$this->githubIntegrationFacade->postIssueComment(
$config->githubUrl,
$config->githubToken,
$message->issueNumber,
$exceptionMessage !== null
? "**ProductBuilder Planning Error**\n\nAn unexpected error occurred during planning:\n\n> " . $exceptionMessage
: "**ProductBuilder Planning Error**\n\nThe planning agent encountered an error:\n\n" . $agentErrorText,
);

$this->applyErrorLabels($config->githubUrl, $config->githubToken, $message->issueNumber);

$this->logger->error('[PlanningAgent] Planning failed after all retries', [
'issueNumber' => $message->issueNumber,
]);
}
}

private function findIssue(string $githubUrl, string $githubToken, int $issueNumber): ?GithubIssueDto
Expand Down Expand Up @@ -246,7 +296,7 @@ private function handleOutcome(
match ($outcome) {
PlanningOutcome::FeedbackNeeded => $this->handleFeedbackNeeded($cleanedText, $githubUrl, $githubToken, $issueNumber),
PlanningOutcome::PlanProduced => $this->handlePlanProduced($cleanedText, $githubUrl, $githubToken, $issueNumber),
PlanningOutcome::Error => $this->handleError($cleanedText, $githubUrl, $githubToken, $issueNumber),
PlanningOutcome::Error => null, // unreachable; error is captured and handled after finally blocks
};
}

Expand Down Expand Up @@ -290,22 +340,6 @@ private function handlePlanProduced(string $text, string $githubUrl, string $git
]);
}

private function handleError(string $text, string $githubUrl, string $githubToken, int $issueNumber): void
{
$this->githubIntegrationFacade->postIssueComment(
$githubUrl,
$githubToken,
$issueNumber,
"**ProductBuilder Planning Error**\n\nThe planning agent encountered an error:\n\n" . $text,
);

$this->applyErrorLabels($githubUrl, $githubToken, $issueNumber);

$this->logger->error('[PlanningAgent] Planning errored', [
'issueNumber' => $issueNumber,
]);
}

private function applyErrorLabels(string $githubUrl, string $githubToken, int $issueNumber): void
{
$this->githubIntegrationFacade->removeLabelFromIssue($githubUrl, $githubToken, $issueNumber, GithubLabel::PlanningOngoing);
Expand Down
Loading
Loading