Skip to content

Implement job.result_chunk event kind #19

@nficano

Description

@nficano

ARCP v1.1 §8.4 introduces result_chunk event for streamed results. Not in PHP SDK.

Steps:

  1. Add ResultChunk message in src/Messages/Job/
  2. Register @SerialName('result_chunk') (or PHP equivalent attribute)
  3. Implement client-side stream assembler concatenating chunks until final: true
  4. Add samples/result_chunk/ demo
  5. Tests

Reference: spec §8.4.


Audit reference: ARCP SDK Audit v1.1 (2026-05-19)

Implementation prompt

Largely landed; this is a "verify + harden" pass. PSR-4 root: Arcp\ -> src/. Tests: Arcp\Tests\ -> tests/. Spec ref: spec/docs/draft-arcp-1.1.md §8.4. Note: in this codebase the message lives under src/Messages/Execution/, not src/Messages/Job/.

Files to touch

  • src/Messages/Execution/ResultChunk.php — existing. Verify only.
  • src/Envelope/MessageCatalog.php — confirm ResultChunk::class in CORE_CLASSES.
  • src/Client/ResultChunkAssembler.php — existing assembler. Verify only.
  • src/Client/ARCPClient.php — confirm public readonly ResultChunkAssembler $resultChunks and that runReadLoop() pushes ResultChunk payloads into it before routing.
  • src/Runtime/JobContext.php — confirm emitResultChunk(string $resultId, string $data, bool $more = true, string $encoding = 'utf8'): void increments $job->nextResultChunkSeq($resultId).
  • src/Runtime/Job.php — confirm nextResultChunkSeq() is per-result-id.
  • samples/result_chunk/main.php — existing. Verify runnable.
  • tests/Unit/Client/ResultChunkAssemblerTest.php (new) — unit.
  • tests/Integration/ResultChunkTest.php (new) — integration.

Public API additions

Namespace Arcp\Messages\Execution:

final readonly class ResultChunk extends MessageType {
    public function __construct(
        public string $resultId,
        public int $chunkSeq,            // monotonically increasing per resultId, starts at 0
        public string $data,
        public string $encoding = 'utf8', // 'utf8' | 'base64'
        public bool $more = true,         // false on the final chunk
    );
}

Namespace Arcp\Client:

final class ResultChunkAssembler {
    public function push(ResultChunk $chunk): void;
    public function isComplete(string $resultId): bool;
    public function assemble(string $resultId): string;   // throws InvalidArgumentException if unknown
}

On JobContext:

public function emitResultChunk(
    string $resultId,
    string $data,
    bool $more = true,
    string $encoding = 'utf8',
): void;

Step-by-step changes

  1. src/Messages/Execution/ResultChunk.php: confirm typeName() returns 'job.result_chunk'. Confirm fromArray() validates encoding in {utf8, base64} and rejects negative chunk_seq. No edits expected.
  2. src/Envelope/MessageCatalog.php: confirm ResultChunk::class is in the Execution block of CORE_CLASSES.
  3. src/Client/ResultChunkAssembler.php: confirm assemble() sorts chunks by chunkSeq, decodes base64 chunks via base64_decode(..., strict: true), and rejects malformed base64 with InvalidArgumentException. If chunks arrive out of order, ksort() must put them back in order before concatenation.
  4. src/Client/ARCPClient.php runReadLoop(): confirm if ($env->payload instanceof ResultChunk) { $this->resultChunks->push($env->payload); } runs before $this->router->handle($env). Spec §8.4 requires the assembler to see every chunk even when the application also subscribes via the router.
  5. src/Runtime/Job.php: confirm nextResultChunkSeq(string $resultId): int returns a fresh counter per resultId (0, 1, 2, ...).
  6. src/Runtime/JobContext.php emitResultChunk(): confirm it pulls the Job via $this->runtime->jobs->tryGet($this->jobId), calls $job->nextResultChunkSeq($resultId), and emits the ResultChunk envelope with job_id + trace_id envelope hints.
  7. samples/result_chunk/main.php: open, confirm it streams (e.g.) 5 chunks then a final chunk with more=false, and the sample client calls $assembler->assemble($resultId) after receiving the final chunk. Run php samples/result_chunk/main.php.
  8. tests/Unit/Client/ResultChunkAssemblerTest.php (new):
    • testAssemblesUtf8ChunksInOrder() — push chunks 0..3, last with more=false, assert assemble() returns concatenation.
    • testAssemblesOutOfOrderChunks() — push in random order (seq 2, 0, 3, 1), assert correct concatenation.
    • testBase64ChunksDecoded() — push base64-encoded chunks, assert binary output.
    • testRejectsMalformedBase64() — push invalid base64 then call assemble(), expect InvalidArgumentException.
    • testIsCompleteOnlyAfterFinal() — assert isComplete() is false until a chunk with more=false arrives.
    • testAssembleUnknownResultIdThrows().
  9. tests/Integration/ResultChunkTest.php (new):
    • Register a tool that emits 4 chunks via $ctx->emitResultChunk(...) then returns. Capture client side: after invokeTool resolves, assert $client->resultChunks->isComplete($rid) and $client->resultChunks->assemble($rid) equals expected joined payload.
    • Add a test that interleaves two resultIds in one job — assert each id's chunks assemble independently.

Tests to add

  • tests/Unit/Client/ResultChunkAssemblerTest.php (cases above).
  • tests/Integration/ResultChunkTest.php:
    • testStreamingResultAssembles()
    • testInterleavedResultIdsAssembleIndependently()
  • Add a round-trip case in tests/Unit/MessageCatalogRoundTripTest.php if not already covered.

Verification commands

cd /Users/nficano/code/arpc/php-sdk
composer test -- --filter ResultChunk
composer stan
composer psalm
php samples/result_chunk/main.php

Acceptance

  • [task] ResultChunk message registered in catalog with type job.result_chunk.
  • [task] chunk_seq monotonic per result_id (Job-level counter).
  • [task] encoding validated to utf8 or base64 at deserialization.
  • [task] Client read-loop unconditionally pushes ResultChunk into ResultChunkAssembler.
  • [task] ResultChunkAssembler::assemble() produces correct bytes even when chunks arrive out of order.
  • [task] Base64 chunks decode strictly; malformed payload throws InvalidArgumentException.
  • [task] samples/result_chunk/main.php runs to completion.
  • [task] Unit and integration tests added.
  • [task] composer stan && composer psalm && composer test all pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature implementationv1.1ARCP v1.1 feature work

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions