Skip to content

Commit e68b91e

Browse files
ddebowczykclaude
andcommitted
Fix PHPStan errors and agent bugs
Bug fixes: - Fix AgentFinished event reporting incorrect status (in_progress → completed) - Fix duplicate ContinuationEvaluated events in StepByStep - Fix AgentBuilder $maxRetries not being used (now passes to ToolCallingDriver) - Fix AbstractAgentDefinition::build() returning stale agent without event handler PHPStan fixes: - Add callable signatures to ContentParts and MessageList map/reduce/filter - Add Closure signature to MockTool and IdempotencyMiddleware - Add class-string annotations to SchemaDefinition - Add array type annotations to Task and TaskList - Fix return type in TodoReminderProcessor::stepsSinceTodo() - Fix ReActDriver::withCachedContext() to use accessor methods - Fix DeterministicDriver array key types with array_values() - Remove redundant int casts in retry policies - Add Agent::isStateChanged() to prevent extra iterator yields on status change - Add phpstan.neon ignores for trait property and forward-compatibility checks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e365407 commit e68b91e

File tree

138 files changed

+2599
-727
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+2599
-727
lines changed

.beads/issues.jsonl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
{"id":"instructor-6l0","title":"Update FinishReasonCheck criterion","description":"Return appropriate ContinuationDecision based on finish reason.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-05T23:45:38.618842247+01:00","created_by":"ddebowczyk","updated_at":"2026-01-05T23:51:17.17827818+01:00","closed_at":"2026-01-05T23:51:17.17827818+01:00","close_reason":"Updated to use ContinuationDecision enum","dependencies":[{"issue_id":"instructor-6l0","depends_on_id":"instructor-amp","type":"blocks","created_at":"2026-01-05T23:45:56.881004218+01:00","created_by":"daemon"}]}
1515
{"id":"instructor-6uo","title":"Add HTTP RetryMiddleware with backoff + jitter and config","description":"Implement RetryPolicy + RetryMiddleware in http-client, wire into HttpClientBuilder config (retryOnStatus/Exceptions, base/max delay, jitter).","notes":"Added RetryPolicy + RetryMiddleware (backoff+jitter) and HttpClientBuilder::withRetryPolicy convenience.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-05T03:30:00.402240885+01:00","created_by":"ddebowczyk","updated_at":"2026-01-05T03:37:04.08848893+01:00","closed_at":"2026-01-05T03:37:04.088495142+01:00"}
1616
{"id":"instructor-72a","title":"Refactor ContinuationCriteria resolution","description":"Remove ALL/ANY/NONE modes. Implement flat resolution: Forbid wins, then Allow continues, else Stop. Remove nested composition.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-05T23:45:27.735789547+01:00","created_by":"ddebowczyk","updated_at":"2026-01-05T23:48:40.332058272+01:00","closed_at":"2026-01-05T23:48:40.332058272+01:00","close_reason":"Implemented ContinuationDecision enum, updated interface, refactored ContinuationCriteria","dependencies":[{"issue_id":"instructor-72a","depends_on_id":"instructor-amp","type":"blocks","created_at":"2026-01-05T23:45:55.582381862+01:00","created_by":"daemon"}]}
17-
{"id":"instructor-7e2","title":"[enhancement] AgentFinished event should reflect termination reason in status","description":"## Problem\n\nWhen an agent is forcibly stopped by a guard criterion (e.g., TokenUsageLimit), the AgentFinished event reports `status: 'in_progress'` which is confusing since the agent has actually finished.\n\n## Evidence\n\nLog output:\n```\nAgentFinished • status =\u003e 'in_progress' • steps =\u003e 1\n```\n\nThis occurs because:\n1. TokenUsageLimit stopped continuation after step 1\n2. AgentState.status was never transitioned from 'in_progress'\n3. AgentFinished event just reflects the state's status field\n\n## Expected Behavior\n\nWhen an agent terminates due to a guard criterion, the status should indicate the termination was abnormal, e.g.:\n- `status: 'interrupted'` or `status: 'stopped'`\n- Or add a separate `stopReason` field alongside status\n\n## Current Behavior\n\nStatus remains 'in_progress' even after forced termination, making it hard to distinguish between:\n- Agent that completed naturally\n- Agent interrupted by token limit\n- Agent interrupted by time limit\n- Agent interrupted by step limit\n\n## Suggested Fix\n\nOption A: Add `terminationReason` field to AgentFinished event (non-breaking)\nOption B: Update AgentState.status to 'interrupted' when guards force stop\nOption C: Add `wasInterrupted` boolean to AgentFinished event\n\n## Impact\n\nLow - debugging/observability improvement only","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-17T00:18:47.965056+01:00","created_by":"ddebowczyk","updated_at":"2026-01-17T00:18:47.965056+01:00"}
18-
{"id":"instructor-7hd","title":"[bug] Duplicate ContinuationEvaluated events emitted per iteration in StepByStep","description":"## Problem\n\nThe StepByStep::iterator() emits duplicate ContinuationEvaluated events per iteration because hasNextStep() is called twice:\n\n1. In iterator() while condition (line 73)\n2. In nextStep() match statement (line 31)\n\n## Evidence\n\nLog shows two identical events at step 0:\n- event_id: c1fad037... ContinuationEvaluated step=0, shouldContinue=true\n- event_id: 8a9ad928... ContinuationEvaluated step=0, shouldContinue=true\n\n## Root Cause\n\n```php\n// StepByStep.php:73 - FIRST call\nwhile ($this-\u003ehasNextStep($state)) {\n $state = $this-\u003enextStep($state);\n}\n\n// StepByStep.php:31 - SECOND call (redundant)\npublic function nextStep(object $state): object {\n return match(true) {\n !$this-\u003ehasNextStep($state) =\u003e $this-\u003eonNoNextStep($state),\n // ...\n };\n}\n```\n\n## Impact\n\n- Redundant event emission (2x events per iteration)\n- Potential performance impact for event listeners\n- Confusing debug logs\n\n## Suggested Fix\n\nOption A: Cache continuation result in iterator() and pass to nextStep()\nOption B: Add internal flag to skip re-evaluation when called from iterator()\nOption C: Remove the hasNextStep check from nextStep() since iterator() already verified","status":"open","priority":2,"issue_type":"bug","created_at":"2026-01-17T00:18:37.709319+01:00","created_by":"ddebowczyk","updated_at":"2026-01-17T00:18:37.709319+01:00"}
17+
{"id":"instructor-7e2","title":"[enhancement] AgentFinished event should reflect termination reason in status","description":"## Problem\n\nWhen an agent is forcibly stopped by a guard criterion (e.g., TokenUsageLimit), the AgentFinished event reports `status: 'in_progress'` which is confusing since the agent has actually finished.\n\n## Evidence\n\nLog output:\n```\nAgentFinished • status =\u003e 'in_progress' • steps =\u003e 1\n```\n\nThis occurs because:\n1. TokenUsageLimit stopped continuation after step 1\n2. AgentState.status was never transitioned from 'in_progress'\n3. AgentFinished event just reflects the state's status field\n\n## Expected Behavior\n\nWhen an agent terminates due to a guard criterion, the status should indicate the termination was abnormal, e.g.:\n- `status: 'interrupted'` or `status: 'stopped'`\n- Or add a separate `stopReason` field alongside status\n\n## Current Behavior\n\nStatus remains 'in_progress' even after forced termination, making it hard to distinguish between:\n- Agent that completed naturally\n- Agent interrupted by token limit\n- Agent interrupted by time limit\n- Agent interrupted by step limit\n\n## Suggested Fix\n\nOption A: Add `terminationReason` field to AgentFinished event (non-breaking)\nOption B: Update AgentState.status to 'interrupted' when guards force stop\nOption C: Add `wasInterrupted` boolean to AgentFinished event\n\n## Impact\n\nLow - debugging/observability improvement only","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-17T00:18:47.965056+01:00","created_by":"ddebowczyk","updated_at":"2026-01-17T00:27:39.858382+01:00","closed_at":"2026-01-17T00:27:39.858382+01:00","close_reason":"Fixed: AgentFinished now sets status to Completed/Failed based on StopReason"}
18+
{"id":"instructor-7hd","title":"[bug] Duplicate ContinuationEvaluated events emitted per iteration in StepByStep","description":"## Problem\n\nThe StepByStep::iterator() emits duplicate ContinuationEvaluated events per iteration because hasNextStep() is called twice:\n\n1. In iterator() while condition (line 73)\n2. In nextStep() match statement (line 31)\n\n## Evidence\n\nLog shows two identical events at step 0:\n- event_id: c1fad037... ContinuationEvaluated step=0, shouldContinue=true\n- event_id: 8a9ad928... ContinuationEvaluated step=0, shouldContinue=true\n\n## Root Cause\n\n```php\n// StepByStep.php:73 - FIRST call\nwhile ($this-\u003ehasNextStep($state)) {\n $state = $this-\u003enextStep($state);\n}\n\n// StepByStep.php:31 - SECOND call (redundant)\npublic function nextStep(object $state): object {\n return match(true) {\n !$this-\u003ehasNextStep($state) =\u003e $this-\u003eonNoNextStep($state),\n // ...\n };\n}\n```\n\n## Impact\n\n- Redundant event emission (2x events per iteration)\n- Potential performance impact for event listeners\n- Confusing debug logs\n\n## Suggested Fix\n\nOption A: Cache continuation result in iterator() and pass to nextStep()\nOption B: Add internal flag to skip re-evaluation when called from iterator()\nOption C: Remove the hasNextStep check from nextStep() since iterator() already verified","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-17T00:18:37.709319+01:00","created_by":"ddebowczyk","updated_at":"2026-01-17T00:27:39.761431+01:00","closed_at":"2026-01-17T00:27:39.761431+01:00","close_reason":"Fixed: Removed redundant hasNextStep() check from nextStep() in StepByStep.php"}
1919
{"id":"instructor-7z1","title":"Update RetryLimit criterion","description":"Return ForbidContinuation when max retries exceeded, AllowContinuation otherwise.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-05T23:45:37.705196582+01:00","created_by":"ddebowczyk","updated_at":"2026-01-05T23:51:17.171182338+01:00","closed_at":"2026-01-05T23:51:17.171182338+01:00","close_reason":"Updated to use ContinuationDecision enum","dependencies":[{"issue_id":"instructor-7z1","depends_on_id":"instructor-amp","type":"blocks","created_at":"2026-01-05T23:45:56.525651001+01:00","created_by":"daemon"}]}
2020
{"id":"instructor-8e4","title":"Update ErrorPresenceCheck criterion","description":"Return ForbidContinuation on error, AllowContinuation otherwise.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-05T23:45:38.16304774+01:00","created_by":"ddebowczyk","updated_at":"2026-01-05T23:51:17.174787029+01:00","closed_at":"2026-01-05T23:51:17.174787029+01:00","close_reason":"Updated to use ContinuationDecision enum","dependencies":[{"issue_id":"instructor-8e4","depends_on_id":"instructor-amp","type":"blocks","created_at":"2026-01-05T23:45:56.704789339+01:00","created_by":"daemon"}]}
2121
{"id":"instructor-8eq","title":"Comprehensive PHPStan level 8 compliance project","description":"Master tracking issue for achieving PHPStan level 8 compliance across all packages.\n\n🎉 MAJOR SUCCESS: Reduced from 214 errors to 24 errors (89% reduction!)\n\n✅ COMPLETED PHASES:\n1. **Redundant Type Casting** (4 errors): Fixed in Usage.php, SandboxCommandExecutor.php \n2. **Console Command Issues** (12+ errors): Fixed properties, JSON encoding, unused dependencies\n3. **HTTP Client Issues** (13+ errors): Added getters, fixed impossible comparisons, removed dead code\n4. **Code Duplication Bug** (1 critical): Fixed resolveToolKey() method\n5. **Test Class Conflicts** (3+ errors): Renamed conflicting TestEvent classes\n6. **Laravel Model Issues** (6+ errors): Added type annotations for properties\n7. **Method Signature Issues** (3+ errors): Added proper callable return type signatures\n8. **Framework Integration** (140+ errors): Fixed Laravel/Illuminate integration issues\n\n🔄 FINAL PHASE: 24 remaining errors to fix\n\n📊 CURRENT BREAKDOWN (24 errors total):\n- HTTP Client package: 7 errors (parameter types, unused properties)\n- Instructor package: 3 errors (nullable return types)\n- Stream handling: 4 errors (iterable type specifications)\n- Miscellaneous: 10 errors (various type and parameter issues)\n\n🚀 TARGET: Complete PHPStan level 8 compliance (0 errors)\n\nPROVEN APPROACH CONTINUES:\n✅ Systematic categorization and batch fixes\n✅ Analyze before removing - check legitimate usage\n✅ Add proper type annotations\n✅ Fix root causes, not just symptoms\n\nERROR REDUCTION PROGRESS:\n- Start: 214 errors \n- Previous update: 164 errors (50 fixed)\n- CURRENT: 24 errors (190 total fixed = 89% reduction!)\n\n📈 EXCELLENCE ACHIEVED: From 214 → 24 errors through systematic approach","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-07T01:04:44.785037059+01:00","updated_at":"2025-12-07T02:23:15.197434703+01:00","closed_at":"2025-12-07T02:23:15.197434703+01:00"}

docs-build/http/1-overview.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ HttpClient
9494
└── withRequest() - Create pending request for execution
9595
└── pool() - Execute multiple requests concurrently
9696
└── withPool() - Create pending pool for deferred execution
97-
// @doctest id="b599"
97+
// @doctest id="744f"
9898
```
9999

100100
### Middleware Layer
@@ -105,7 +105,7 @@ The middleware system allows for processing requests and responses through a cha
105105
Request -> Middleware 1 -> Middleware 2 -> ... -> Driver -> External API
106106
107107
Response <- Middleware 1 <- Middleware 2 <- ... <- Driver <- HTTP Response
108-
// @doctest id="dec5"
108+
// @doctest id="87c2"
109109
```
110110

111111
Key components:
@@ -123,7 +123,7 @@ CanHandleHttpRequest (interface)
123123
├── SymfonyDriver
124124
├── LaravelDriver
125125
└── MockHttpDriver (for testing)
126-
// @doctest id="fd15"
126+
// @doctest id="86e9"
127127
```
128128

129129
### Adapter Layer
@@ -136,7 +136,7 @@ HttpResponse (interface)
136136
├── SymfonyHttpResponse
137137
├── LaravelHttpResponse
138138
└── MockHttpResponse
139-
// @doctest id="ddd3"
139+
// @doctest id="98a5"
140140
```
141141

142142
## Supported HTTP Clients

docs-build/http/10-middleware.mdx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface HttpMiddleware
3434
{
3535
public function handle(HttpClientRequest $request, CanHandleHttpRequest $next): HttpResponse;
3636
}
37-
// @doctest id="92ea"
37+
// @doctest id="eaa5"
3838
```
3939

4040
The `handle` method takes two parameters:
@@ -85,7 +85,7 @@ abstract class BaseMiddleware implements HttpMiddleware
8585
return $response;
8686
}
8787
}
88-
// @doctest id="b0b9"
88+
// @doctest id="6fc2"
8989
```
9090

9191
By extending `BaseMiddleware`, you only need to override the methods relevant to your middleware's functionality, making the code more focused and maintainable.
@@ -120,7 +120,7 @@ $client->withMiddleware(
120120
new RetryMiddleware(),
121121
new TimeoutMiddleware()
122122
);
123-
// @doctest id="8e29"
123+
// @doctest id="30c4"
124124
```
125125

126126
Named middleware are useful when you need to reference them later, for example, to remove or replace them.
@@ -132,7 +132,7 @@ You can remove middleware from the stack by name:
132132
```php
133133
// Remove a middleware by name
134134
$client->middleware()->remove('cache');
135-
// @doctest id="b668"
135+
// @doctest id="3a85"
136136
```
137137

138138
### Replacing Middleware
@@ -142,7 +142,7 @@ You can replace a middleware with another one:
142142
```php
143143
// Replace a middleware with a new one
144144
$client->middleware()->replace('cache', new ImprovedCachingMiddleware());
145-
// @doctest id="df60"
145+
// @doctest id="df86"
146146
```
147147

148148
### Clearing Middleware
@@ -152,7 +152,7 @@ You can remove all middleware from the stack:
152152
```php
153153
// Clear all middleware
154154
$client->middleware()->clear();
155-
// @doctest id="029d"
155+
// @doctest id="7f07"
156156
```
157157

158158
### Checking Middleware
@@ -164,7 +164,7 @@ You can check if a middleware exists in the stack:
164164
if ($client->middleware()->has('rate-limit')) {
165165
// The 'rate-limit' middleware exists
166166
}
167-
// @doctest id="f847"
167+
// @doctest id="023f"
168168
```
169169

170170
### Getting Middleware
@@ -177,7 +177,7 @@ $rateLimitMiddleware = $client->middleware()->get('rate-limit');
177177

178178
// Get a middleware by index
179179
$firstMiddleware = $client->middleware()->get(0);
180-
// @doctest id="f570"
180+
// @doctest id="c1a7"
181181
```
182182

183183
### Middleware Order
@@ -229,7 +229,7 @@ $request = new HttpRequest(
229229
// 6. RetryMiddleware processes the response (may retry on certain status codes)
230230
// 7. LoggingMiddleware processes the response (logs incoming response)
231231
$response = $client->withRequest($request)->get();
232-
// @doctest id="7fc0"
232+
// @doctest id="fbea"
233233
```
234234

235235
## Built-in Middleware
@@ -248,7 +248,7 @@ $client->withMiddleware(new DebugMiddleware());
248248

249249
// Or use the convenience method
250250
$client->withDebugPreset('on');
251-
// @doctest id="ce67"
251+
// @doctest id="0ac4"
252252
```
253253

254254
The debug middleware logs:
@@ -275,7 +275,7 @@ return [
275275
'responseStreamByLine' => true, // Dump stream as full lines or raw chunks
276276
],
277277
];
278-
// @doctest id="6dd6"
278+
// @doctest id="dd73"
279279
```
280280

281281
### StreamByLine Middleware
@@ -287,7 +287,7 @@ use Cognesy\Http\Middleware\ServerSideEvents\StreamSSEsMiddleware;
287287

288288
// Add stream by line middleware
289289
$client->withMiddleware(new StreamSSEsMiddleware());
290-
// @doctest id="a530"
290+
// @doctest id="f920"
291291
```
292292

293293
You can customize how lines are processed by providing a parser function:
@@ -302,7 +302,7 @@ $lineParser = function (string $line) {
302302
};
303303

304304
$client->withMiddleware(new StreamByLineMiddleware($lineParser));
305-
// @doctest id="2fb9"
305+
// @doctest id="ce58"
306306
```
307307

308308

@@ -318,7 +318,7 @@ $client->withMiddleware(
318318
new BufferResponseMiddleware(), // Buffer responses for reuse
319319
new DebugMiddleware() // Log requests and responses
320320
);
321-
// @doctest id="87b4"
321+
// @doctest id="9e87"
322322
```
323323

324324
#### API Client Setup
@@ -331,7 +331,7 @@ $client->withMiddleware(
331331
new RateLimitingMiddleware(maxRequests: 100), // Respect rate limits
332332
new LoggingMiddleware() // Log API interactions
333333
);
334-
// @doctest id="8ec6"
334+
// @doctest id="677a"
335335
```
336336

337337
#### Testing Setup
@@ -341,7 +341,7 @@ $client = new HttpClient();
341341
$client->withMiddleware(
342342
new RecordReplayMiddleware(RecordReplayMiddleware::MODE_REPLAY) // Replay recorded responses
343343
);
344-
// @doctest id="fafe"
344+
// @doctest id="33de"
345345
```
346346

347347
#### Streaming Setup
@@ -352,7 +352,7 @@ $client->withMiddleware(
352352
new StreamByLineMiddleware(), // Process streaming responses line by line
353353
new BufferResponseMiddleware() // Buffer responses for reuse
354354
);
355-
// @doctest id="56a2"
355+
// @doctest id="c4b7"
356356
```
357357

358358
By combining middleware components, you can create a highly customized HTTP client that handles complex requirements while keeping your application code clean and focused.

docs-build/http/11-processing-with-middleware.mdx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ $client->withMiddleware(new class implements HttpMiddleware {
7070
return $response;
7171
}
7272
});
73-
// @doctest id="6c74"
73+
// @doctest id="6a06"
7474
```
7575

7676
This approach is concise but less reusable than defining a named class.
@@ -131,7 +131,7 @@ Then add the middleware to your client:
131131
```php
132132
$client = new HttpClient();
133133
$client->withMiddleware(new JsonStreamMiddleware());
134-
// @doctest id="16b0"
134+
// @doctest id="3162"
135135
```
136136

137137
### Response Decoration for Transforming Content
@@ -169,7 +169,7 @@ class XmlToJsonDecorator extends BaseResponseDecorator
169169
return $headers;
170170
}
171171
}
172-
// @doctest id="205c"
172+
// @doctest id="9e60"
173173
```
174174

175175
And the corresponding middleware:
@@ -199,7 +199,7 @@ class XmlToJsonMiddleware extends BaseMiddleware
199199
return new XmlToJsonDecorator($request, $response);
200200
}
201201
}
202-
// @doctest id="4758"
202+
// @doctest id="9370"
203203
```
204204

205205
## Advanced Middleware Examples
@@ -257,7 +257,7 @@ class AnalyticsMiddleware extends BaseMiddleware
257257
return $response;
258258
}
259259
}
260-
// @doctest id="31cc"
260+
// @doctest id="4261"
261261
```
262262

263263
### Circuit Breaker Middleware
@@ -347,7 +347,7 @@ class CircuitBreakerMiddleware extends BaseMiddleware
347347
}
348348
}
349349
}
350-
// @doctest id="a330"
350+
// @doctest id="9590"
351351
```
352352

353353
### Conditional Middleware
@@ -387,7 +387,7 @@ class ConditionalMiddleware implements HttpMiddleware
387387
return $next->handle($request);
388388
}
389389
}
390-
// @doctest id="28a5"
390+
// @doctest id="602b"
391391
```
392392

393393
Usage example:
@@ -401,7 +401,7 @@ $conditionalCaching = new ConditionalMiddleware(
401401
);
402402

403403
$client->withMiddleware($conditionalCaching);
404-
// @doctest id="c294"
404+
// @doctest id="e79d"
405405
```
406406

407407
### Request ID Middleware
@@ -454,7 +454,7 @@ class RequestIdMiddleware extends BaseMiddleware
454454
return $response;
455455
}
456456
}
457-
// @doctest id="6ede"
457+
// @doctest id="8ef7"
458458
```
459459

460460
### OpenTelemetry Tracing Middleware
@@ -537,7 +537,7 @@ class TracingMiddleware extends BaseMiddleware
537537
}
538538
}
539539
}
540-
// @doctest id="94b9"
540+
// @doctest id="f9e9"
541541
```
542542

543543
### Customizing Middleware for LLM APIs
@@ -643,7 +643,7 @@ class LlmStreamingMiddleware extends BaseMiddleware
643643
};
644644
}
645645
}
646-
// @doctest id="1e0d"
646+
// @doctest id="0301"
647647
```
648648

649649
### Combining Multiple Middleware Components
@@ -687,7 +687,7 @@ $client->withMiddleware(
687687

688688
// Now the client is ready to use with a complete middleware pipeline
689689
$response = $client->withRequest($request)->get();
690-
// @doctest id="5884"
690+
// @doctest id="7874"
691691
```
692692

693693
With this setup, requests and responses flow through the middleware in the following order:

0 commit comments

Comments
 (0)