Skip to content

Commit 8724e6b

Browse files
committed
Add AI agent detection to auto-switch to raw output
Switch from JSON to raw error format for AI agents. The raw format outputs one error per line as file:line:message, which saves ~49% tokens compared to JSON for typical error output (and 100% for zero-error runs. When an agent is detected, progress bar output is fully suppressed (equivalent to --no-progress), eliminating redraw noise that wastes tokens in agent context windows. Token comparison (3 errors across 2 files): - JSON: ~185 tokens (678 chars) with structural overhead per error - Raw: ~95 tokens (295 chars) with just file:line:message per line Zero errors: - JSON: ~18 tokens for empty structure - Raw: 0 tokens (no output) Explicit --error-format=table or config errorFormat: table always takes priority over agent detection.
1 parent a44d54e commit 8724e6b

File tree

4 files changed

+176
-4
lines changed

4 files changed

+176
-4
lines changed

src/Command/AnalyseCommand.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use OndraM\CiDetector\CiDetector;
66
use Override;
77
use PHPStan\Analyser\InternalError;
8+
use PHPStan\Command\ErrorFormatter\AgentDetectedErrorFormatter;
89
use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter;
910
use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter;
1011
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
@@ -235,11 +236,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
235236
$errorFormat = $inceptionResult->getContainer()->getParameter('errorFormat');
236237
}
237238

239+
$container = $inceptionResult->getContainer();
240+
241+
if ($errorFormat === null) {
242+
/** @var AgentDetectedErrorFormatter $agentFormatter */
243+
$agentFormatter = $container->getByType(AgentDetectedErrorFormatter::class);
244+
if ($agentFormatter->isAgentDetected()) {
245+
$errorFormat = 'raw';
246+
}
247+
}
248+
238249
if ($errorFormat === null) {
239250
$errorFormat = 'table';
240251
}
241252

242-
$container = $inceptionResult->getContainer();
243253
$errorFormatterServiceName = sprintf('errorFormatter.%s', $errorFormat);
244254
if (!$container->hasService($errorFormatterServiceName)) {
245255
$errorOutput->writeLineFormatted(sprintf(
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Command\ErrorFormatter;
4+
5+
use PHPStan\Command\AnalysisResult;
6+
use PHPStan\Command\Output;
7+
use PHPStan\DependencyInjection\AutowiredParameter;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use function getenv;
10+
use function is_string;
11+
use function trim;
12+
13+
/**
14+
* @api
15+
*/
16+
#[AutowiredService(as: AgentDetectedErrorFormatter::class)]
17+
final class AgentDetectedErrorFormatter implements ErrorFormatter
18+
{
19+
20+
public function __construct(
21+
#[AutowiredParameter(ref: '@errorFormatter.raw')]
22+
private RawErrorFormatter $rawErrorFormatter,
23+
)
24+
{
25+
}
26+
27+
public function isAgentDetected(): bool
28+
{
29+
$aiAgent = getenv('AI_AGENT');
30+
if (is_string($aiAgent) && trim($aiAgent) !== '') {
31+
return true;
32+
}
33+
34+
return getenv('CURSOR_TRACE_ID') !== false
35+
|| getenv('CURSOR_AGENT') !== false
36+
|| getenv('GEMINI_CLI') !== false
37+
|| getenv('CODEX_SANDBOX') !== false
38+
|| getenv('AUGMENT_AGENT') !== false
39+
|| getenv('OPENCODE_CLIENT') !== false
40+
|| getenv('OPENCODE') !== false
41+
|| getenv('CLAUDECODE') !== false
42+
|| getenv('CLAUDE_CODE') !== false
43+
|| getenv('REPL_ID') !== false;
44+
}
45+
46+
public function formatErrors(AnalysisResult $analysisResult, Output $output): int
47+
{
48+
return $this->rawErrorFormatter->formatErrors($analysisResult, $output);
49+
}
50+
51+
}

src/Command/ErrorsConsoleStyle.php

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
use Symfony\Component\Console\Terminal;
1414
use function array_unshift;
1515
use function explode;
16+
use function getenv;
1617
use function implode;
18+
use function is_string;
1719
use function sprintf;
1820
use function strlen;
21+
use function trim;
1922
use const DIRECTORY_SEPARATOR;
2023

2124
final class ErrorsConsoleStyle extends SymfonyStyle
@@ -29,10 +32,13 @@ final class ErrorsConsoleStyle extends SymfonyStyle
2932

3033
private ?bool $isCiDetected = null;
3134

35+
private ?bool $isAgentDetected = null;
36+
3237
public function __construct(InputInterface $input, OutputInterface $output)
3338
{
3439
parent::__construct($input, $output);
35-
$this->showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS);
40+
$showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS);
41+
$this->showProgress = $showProgress && !$this->isAgentDetected();
3642
}
3743

3844
private function isCiDetected(): bool
@@ -45,6 +51,26 @@ private function isCiDetected(): bool
4551
return $this->isCiDetected;
4652
}
4753

54+
private function isAgentDetected(): bool
55+
{
56+
if ($this->isAgentDetected === null) {
57+
$aiAgent = getenv('AI_AGENT');
58+
$this->isAgentDetected = (is_string($aiAgent) && trim($aiAgent) !== '')
59+
|| getenv('CURSOR_TRACE_ID') !== false
60+
|| getenv('CURSOR_AGENT') !== false
61+
|| getenv('GEMINI_CLI') !== false
62+
|| getenv('CODEX_SANDBOX') !== false
63+
|| getenv('AUGMENT_AGENT') !== false
64+
|| getenv('OPENCODE_CLIENT') !== false
65+
|| getenv('OPENCODE') !== false
66+
|| getenv('CLAUDECODE') !== false
67+
|| getenv('CLAUDE_CODE') !== false
68+
|| getenv('REPL_ID') !== false;
69+
}
70+
71+
return $this->isAgentDetected;
72+
}
73+
4874
/**
4975
* @param string[] $headers
5076
* @param string[][] $rows
@@ -95,9 +121,10 @@ public function createProgressBar(int $max = 0): ProgressBar
95121
}
96122

97123
$ci = $this->isCiDetected();
98-
$this->progressBar->setOverwrite(!$ci);
124+
$agent = $this->isAgentDetected();
125+
$this->progressBar->setOverwrite(!$ci && !$agent);
99126

100-
if ($ci) {
127+
if ($ci || $agent) {
101128
$this->progressBar->minSecondsBetweenRedraws(15);
102129
$this->progressBar->maxSecondsBetweenRedraws(30);
103130
} elseif (DIRECTORY_SEPARATOR === '\\') {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Command\ErrorFormatter;
4+
5+
use Override;
6+
use PHPStan\Testing\ErrorFormatterTestCase;
7+
use function putenv;
8+
9+
class AgentDetectedErrorFormatterTest extends ErrorFormatterTestCase
10+
{
11+
12+
#[Override]
13+
protected function setUp(): void
14+
{
15+
putenv('AI_AGENT');
16+
putenv('CURSOR_TRACE_ID');
17+
putenv('CURSOR_AGENT');
18+
putenv('GEMINI_CLI');
19+
putenv('CODEX_SANDBOX');
20+
putenv('AUGMENT_AGENT');
21+
putenv('OPENCODE_CLIENT');
22+
putenv('OPENCODE');
23+
putenv('CLAUDECODE');
24+
putenv('CLAUDE_CODE');
25+
putenv('REPL_ID');
26+
}
27+
28+
#[Override]
29+
protected function tearDown(): void
30+
{
31+
putenv('AI_AGENT');
32+
putenv('CLAUDE_CODE');
33+
}
34+
35+
public function testIsAgentDetectedReturnsFalse(): void
36+
{
37+
$formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter());
38+
$this->assertFalse($formatter->isAgentDetected());
39+
}
40+
41+
public function testIsAgentDetectedReturnsTrueWithAiAgent(): void
42+
{
43+
putenv('AI_AGENT=test');
44+
$formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter());
45+
$this->assertTrue($formatter->isAgentDetected());
46+
}
47+
48+
public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void
49+
{
50+
putenv('CLAUDE_CODE=1');
51+
$formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter());
52+
$this->assertTrue($formatter->isAgentDetected());
53+
}
54+
55+
public function testFormatErrorsProducesRawOutput(): void
56+
{
57+
$formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter());
58+
59+
$exitCode = $formatter->formatErrors(
60+
$this->getAnalysisResult(1, 0),
61+
$this->getOutput(),
62+
);
63+
64+
$this->assertSame(1, $exitCode);
65+
$this->assertSame(
66+
'/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n",
67+
$this->getOutputContent(),
68+
);
69+
}
70+
71+
public function testFormatErrorsNoErrors(): void
72+
{
73+
$formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter());
74+
75+
$exitCode = $formatter->formatErrors(
76+
$this->getAnalysisResult(0, 0),
77+
$this->getOutput(),
78+
);
79+
80+
$this->assertSame(0, $exitCode);
81+
$this->assertSame('', $this->getOutputContent());
82+
}
83+
84+
}

0 commit comments

Comments
 (0)