Skip to content

Commit 1d0e72e

Browse files
author
Dariusz Debowczyk
committed
refactor: add typed Claude stream event collection
1 parent b2af1aa commit 1d0e72e

File tree

6 files changed

+78
-6
lines changed

6 files changed

+78
-6
lines changed

.beads/issues.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@
263263
{"id":"instructor-php-s8g.1","title":"State change: status → in_progress","description":"Set status to in_progress\n\nReason: Started InferenceExecutionId migration after completing InferenceAttemptId","status":"closed","priority":4,"issue_type":"event","created_at":"2026-02-21T19:28:23.902257+01:00","created_by":"Dariusz Debowczyk","updated_at":"2026-02-21T19:32:09.359861+01:00","closed_at":"2026-02-21T19:32:09.359861+01:00","close_reason":"Housekeeping: close state-change event beads after parent task completion","dependencies":[{"issue_id":"instructor-php-s8g.1","depends_on_id":"instructor-php-s8g","type":"parent-child","created_at":"2026-02-21T19:28:23.902815+01:00","created_by":"Dariusz Debowczyk"}]}
264264
{"id":"instructor-php-t5y","title":"Introduce InferenceResponseId value object and migrate polyglot response identity","description":"Create InferenceResponseId and migrate inference response identity usage and serialization.","acceptance_criteria":"- Introduce the new XxxId value object as a thin wrapper delegating UUID generation/validation to Cognesy\\Utils\\Uuid.\\n- Refactor the target bounded context to use the typed ID internally (entities, state objects, repositories/stores, serializers).\\n- Do boundary normalization in one place only; avoid scattering XxxId|string across domain internals and store contracts.\\n- Remove temporary normalization helpers made obsolete by full typing.\\n- Update/adjust tests and ensure relevant test suites pass.\\n- Update package docs under ./packages/*/docs and root ./examples to reflect typed IDs and migration usage patterns.","status":"closed","priority":2,"issue_type":"task","owner":"ddebowczyk@guidewire.com","created_at":"2026-02-21T10:34:22.65846+01:00","created_by":"Dariusz Debowczyk","updated_at":"2026-02-21T19:31:02.091732+01:00","closed_at":"2026-02-21T19:31:02.091732+01:00","close_reason":"Completed InferenceResponseId migration with typed response identity, serialization boundary, docs update, and passing polyglot tests","labels":["status:in_progress"]}
265265
{"id":"instructor-php-t5y.1","title":"State change: status → in_progress","description":"Set status to in_progress\n\nReason: Started InferenceResponseId migration after completing InferenceExecutionId","status":"closed","priority":4,"issue_type":"event","created_at":"2026-02-21T19:30:07.485849+01:00","created_by":"Dariusz Debowczyk","updated_at":"2026-02-21T19:32:19.3989+01:00","closed_at":"2026-02-21T19:32:19.3989+01:00","close_reason":"Housekeeping: close state-change event beads after parent task completion","dependencies":[{"issue_id":"instructor-php-t5y.1","depends_on_id":"instructor-php-t5y","type":"parent-child","created_at":"2026-02-21T19:30:07.486428+01:00","created_by":"Dariusz Debowczyk"}]}
266-
{"id":"instructor-php-tpq","title":"DecodedObject is too generic - needs semantic structure","description":"DecodedObject is just array\u003cstring,mixed\u003e wrapper with no semantic meaning. Should create typed DTOs for Claude CLI response structure (messages, cost, duration, tool calls, etc.) instead of generic array access. Current design loses type safety benefits.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T03:43:14.818957549+01:00","updated_at":"2025-12-02T03:43:14.818957549+01:00"}
266+
{"id":"instructor-php-tpq","title":"DecodedObject is too generic - needs semantic structure","description":"DecodedObject is just array\u003cstring,mixed\u003e wrapper with no semantic meaning. Should create typed DTOs for Claude CLI response structure (messages, cost, duration, tool calls, etc.) instead of generic array access. Current design loses type safety benefits.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-02T03:43:14.818957549+01:00","updated_at":"2026-02-21T21:04:40.536616+01:00","closed_at":"2026-02-21T21:04:40.536616+01:00","close_reason":"Replaced generic Claude decoded-object usage with semantic typed stream-event collection in ClaudeResponse/ResponseParser and bridge consumption, while retaining decoded raw data for compatibility."}
267267
{"id":"instructor-php-u3x","title":"Introduce ExecutionId value object and fully migrate execution identity","description":"Create ExecutionId and migrate execution identity across execution state, serialization, and orchestration usage.","acceptance_criteria":"- Introduce the new XxxId value object as a thin wrapper delegating UUID generation/validation to Cognesy\\Utils\\Uuid.\\n- Refactor the target bounded context to use the typed ID internally (entities, state objects, repositories/stores, serializers).\\n- Do boundary normalization in one place only; avoid scattering XxxId|string across domain internals and store contracts.\\n- Remove temporary normalization helpers made obsolete by full typing.\\n- Update/adjust tests and ensure relevant test suites pass.\\n- Update package docs under ./packages/*/docs and root ./examples to reflect typed IDs and migration usage patterns.","status":"closed","priority":2,"issue_type":"task","owner":"ddebowczyk@guidewire.com","created_at":"2026-02-21T10:34:18.881884+01:00","created_by":"Dariusz Debowczyk","updated_at":"2026-02-21T10:44:07.76599+01:00","closed_at":"2026-02-21T10:44:07.76599+01:00","close_reason":"ExecutionId migration completed","labels":["status:in_progress"]}
268268
{"id":"instructor-php-u3x.1","title":"State change: status → in_progress","description":"Set status to in_progress\n\nReason: Started ExecutionId migration after completing AgentId","status":"closed","priority":4,"issue_type":"event","created_at":"2026-02-21T10:42:03.403783+01:00","created_by":"Dariusz Debowczyk","updated_at":"2026-02-21T19:32:09.343842+01:00","closed_at":"2026-02-21T19:32:09.343842+01:00","close_reason":"Housekeeping: close state-change event beads after parent task completion","dependencies":[{"issue_id":"instructor-php-u3x.1","depends_on_id":"instructor-php-u3x","type":"parent-child","created_at":"2026-02-21T10:42:03.405052+01:00","created_by":"Dariusz Debowczyk"}]}
269269
{"id":"instructor-php-xsh","title":"Move REFACTORING_PLAN.md to tmp/","description":"Move REFACTORING_PLAN.md from root to tmp/REFACTORING_PLAN.md since it's active WIP documentation, not permanent internal docs.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-28T12:26:19.820866135+01:00","updated_at":"2025-11-28T12:40:02.058807122+01:00","closed_at":"2025-11-28T12:40:02.058807122+01:00","dependencies":[{"issue_id":"instructor-php-xsh","depends_on_id":"instructor-php-ypb","type":"parent-child","created_at":"2025-11-28T12:26:37.799098689+01:00","created_by":"daemon","metadata":"{}"}]}

packages/agent-ctrl/src/Bridge/ClaudeCodeBridge.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,8 @@ public function executeStreaming(string $prompt, ?StreamHandler $handler): Agent
182182

183183
if ($collectedText === '' && $handler === null) {
184184
$extractStart = microtime(true);
185-
foreach ($response->decoded()->all() as $event) {
185+
foreach ($response->events()->all() as $streamEvent) {
186186
$eventCount++;
187-
$streamEvent = StreamEvent::fromArray($event->data());
188187
if ($streamEvent instanceof MessageEvent) {
189188
foreach ($streamEvent->message->textContent() as $textContent) {
190189
$collectedText .= $textContent->text;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Cognesy\AgentCtrl\ClaudeCode\Application\Dto;
4+
5+
use Cognesy\AgentCtrl\ClaudeCode\Domain\Dto\StreamEvent\StreamEvent;
6+
7+
final readonly class ClaudeEventCollection
8+
{
9+
/** @var list<StreamEvent> */
10+
private array $items;
11+
12+
/**
13+
* @param list<StreamEvent> $items
14+
*/
15+
private function __construct(array $items) {
16+
$this->items = array_values($items);
17+
}
18+
19+
public static function empty() : self {
20+
return new self([]);
21+
}
22+
23+
/**
24+
* @param list<StreamEvent> $items
25+
*/
26+
public static function of(array $items) : self {
27+
return new self($items);
28+
}
29+
30+
/**
31+
* @return list<StreamEvent>
32+
*/
33+
public function all() : array {
34+
return $this->items;
35+
}
36+
37+
public function count() : int {
38+
return count($this->items);
39+
}
40+
41+
public function isEmpty() : bool {
42+
return $this->count() === 0;
43+
}
44+
}

packages/agent-ctrl/src/ClaudeCode/Application/Dto/ClaudeResponse.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77

88
final readonly class ClaudeResponse
99
{
10+
private ClaudeEventCollection $events;
11+
1012
public function __construct(
1113
private ExecResult $result,
1214
private DecodedObjectCollection $decoded,
13-
) {}
15+
?ClaudeEventCollection $events = null,
16+
) {
17+
$this->events = $events ?? ClaudeEventCollection::empty();
18+
}
1419

1520
public function result() : ExecResult {
1621
return $this->result;
@@ -19,4 +24,8 @@ public function result() : ExecResult {
1924
public function decoded() : DecodedObjectCollection {
2025
return $this->decoded;
2126
}
27+
28+
public function events() : ClaudeEventCollection {
29+
return $this->events;
30+
}
2231
}

packages/agent-ctrl/src/ClaudeCode/Application/Parser/ResponseParser.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Cognesy\AgentCtrl\ClaudeCode\Application\Parser;
44

55
use Cognesy\AgentCtrl\ClaudeCode\Application\Dto\ClaudeResponse;
6+
use Cognesy\AgentCtrl\ClaudeCode\Application\Dto\ClaudeEventCollection;
7+
use Cognesy\AgentCtrl\ClaudeCode\Domain\Dto\StreamEvent\StreamEvent;
68
use Cognesy\AgentCtrl\ClaudeCode\Domain\Enum\OutputFormat;
79
use Cognesy\AgentCtrl\Common\Collection\DecodedObjectCollection;
810
use Cognesy\AgentCtrl\Common\Value\DecodedObject;
@@ -24,12 +26,18 @@ private function fromJson(ExecResult $result) : ClaudeResponse {
2426
return new ClaudeResponse($result, DecodedObjectCollection::empty());
2527
}
2628
$items = [];
29+
$events = [];
2730
foreach ($this->normalizeToList($decoded) as $entry) {
2831
if (is_array($entry)) {
2932
$items[] = new DecodedObject($entry);
33+
$events[] = StreamEvent::fromArray($entry);
3034
}
3135
}
32-
return new ClaudeResponse($result, DecodedObjectCollection::of($items));
36+
return new ClaudeResponse(
37+
result: $result,
38+
decoded: DecodedObjectCollection::of($items),
39+
events: ClaudeEventCollection::of($events),
40+
);
3341
}
3442

3543
private function fromStreamJson(ExecResult $result) : ClaudeResponse {
@@ -38,6 +46,7 @@ private function fromStreamJson(ExecResult $result) : ClaudeResponse {
3846
return new ClaudeResponse($result, DecodedObjectCollection::empty());
3947
}
4048
$items = [];
49+
$events = [];
4150
foreach ($lines as $line) {
4251
$trimmed = trim($line);
4352
if ($trimmed === '') {
@@ -46,9 +55,14 @@ private function fromStreamJson(ExecResult $result) : ClaudeResponse {
4655
$decoded = json_decode($trimmed, true);
4756
if (is_array($decoded)) {
4857
$items[] = new DecodedObject($decoded);
58+
$events[] = StreamEvent::fromArray($decoded);
4959
}
5060
}
51-
return new ClaudeResponse($result, DecodedObjectCollection::of($items));
61+
return new ClaudeResponse(
62+
result: $result,
63+
decoded: DecodedObjectCollection::of($items),
64+
events: ClaudeEventCollection::of($events),
65+
);
5266
}
5367

5468
/**

packages/agent-ctrl/tests/Unit/ClaudeCode/ClaudeCodeCliResponseParserTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use Cognesy\AgentCtrl\ClaudeCode\Application\Parser\ResponseParser;
6+
use Cognesy\AgentCtrl\ClaudeCode\Domain\Dto\StreamEvent\UnknownEvent;
67
use Cognesy\AgentCtrl\ClaudeCode\Domain\Enum\OutputFormat;
78
use Cognesy\Sandbox\Data\ExecResult;
89

@@ -15,6 +16,8 @@
1516
expect($response->decoded()->count())->toBe(2);
1617
$first = $response->decoded()->all()[0]->data();
1718
expect($first['text'])->toBe('hi');
19+
expect($response->events()->count())->toBe(2);
20+
expect($response->events()->all()[0])->toBeInstanceOf(UnknownEvent::class);
1821
});
1922

2023
it('parses stream-json output line by line', function () {
@@ -26,6 +29,8 @@
2629
expect($response->decoded()->count())->toBe(2);
2730
$last = $response->decoded()->all()[1]->data();
2831
expect($last['event'])->toBe('final');
32+
expect($response->events()->count())->toBe(2);
33+
expect($response->events()->all()[1])->toBeInstanceOf(UnknownEvent::class);
2934
});
3035

3136
it('returns empty collection on invalid json', function () {
@@ -34,4 +39,5 @@
3439
$response = (new ResponseParser())->parse($result, OutputFormat::Json);
3540

3641
expect($response->decoded()->isEmpty())->toBeTrue();
42+
expect($response->events()->isEmpty())->toBeTrue();
3743
});

0 commit comments

Comments
 (0)