Skip to content

Commit 8536d19

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 8536d19

File tree

4 files changed

+180
-4
lines changed

4 files changed

+180
-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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 file_exists;
10+
use function getenv;
11+
use function is_string;
12+
use function trim;
13+
14+
/**
15+
* @api
16+
*/
17+
#[AutowiredService(as: AgentDetectedErrorFormatter::class)]
18+
final class AgentDetectedErrorFormatter implements ErrorFormatter
19+
{
20+
21+
public function __construct(
22+
#[AutowiredParameter(ref: '@errorFormatter.raw')]
23+
private RawErrorFormatter $rawErrorFormatter,
24+
)
25+
{
26+
}
27+
28+
public function isAgentDetected(): bool
29+
{
30+
$aiAgent = getenv('AI_AGENT');
31+
if (is_string($aiAgent) && trim($aiAgent) !== '') {
32+
return true;
33+
}
34+
35+
return getenv('CURSOR_TRACE_ID') !== false
36+
|| getenv('CURSOR_AGENT') !== false
37+
|| getenv('GEMINI_CLI') !== false
38+
|| getenv('CODEX_SANDBOX') !== false
39+
|| getenv('AUGMENT_AGENT') !== false
40+
|| getenv('OPENCODE_CLIENT') !== false
41+
|| getenv('OPENCODE') !== false
42+
|| getenv('CLAUDECODE') !== false
43+
|| getenv('CLAUDE_CODE') !== false
44+
|| getenv('REPL_ID') !== false
45+
|| file_exists('/opt/.devin');
46+
}
47+
48+
public function formatErrors(AnalysisResult $analysisResult, Output $output): int
49+
{
50+
return $this->rawErrorFormatter->formatErrors($analysisResult, $output);
51+
}
52+
53+
}

src/Command/ErrorsConsoleStyle.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
use function explode;
1616
use function implode;
1717
use function sprintf;
18+
use function file_exists;
19+
use function getenv;
20+
use function is_string;
1821
use function strlen;
22+
use function trim;
1923
use const DIRECTORY_SEPARATOR;
2024

2125
final class ErrorsConsoleStyle extends SymfonyStyle
@@ -29,10 +33,13 @@ final class ErrorsConsoleStyle extends SymfonyStyle
2933

3034
private ?bool $isCiDetected = null;
3135

36+
private ?bool $isAgentDetected = null;
37+
3238
public function __construct(InputInterface $input, OutputInterface $output)
3339
{
3440
parent::__construct($input, $output);
35-
$this->showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS);
41+
$showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS);
42+
$this->showProgress = $showProgress && !$this->isAgentDetected();
3643
}
3744

3845
private function isCiDetected(): bool
@@ -45,6 +52,27 @@ private function isCiDetected(): bool
4552
return $this->isCiDetected;
4653
}
4754

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

97125
$ci = $this->isCiDetected();
98-
$this->progressBar->setOverwrite(!$ci);
126+
$agent = $this->isAgentDetected();
127+
$this->progressBar->setOverwrite(!$ci && !$agent);
99128

100-
if ($ci) {
129+
if ($ci || $agent) {
101130
$this->progressBar->minSecondsBetweenRedraws(15);
102131
$this->progressBar->maxSecondsBetweenRedraws(30);
103132
} 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)