feat: streamed agent rounds, their fragments and their timing (0.4.0) #32

Merged
bjoern merged 2 commits from feat/streaming-rounds into main 2026-10-19 17:19:39 +02:00
Member

The first step of the library track Shikigami's runtime needs (its PROJECT_PLAN "L1 — Streaming loop", shared with Tsumugi Phase 4; Shikigami ADR 0021, ADR 0020): an opt-in streamed agent round. Each round streams through the caller's client, its text, reasoning and tool-call fragments are reported as they arrive, and its first chunk and its reasoning are timed. With AgentOptions.Streaming off, which is the default, nothing changes for Orihon or Kagura. The library becomes 0.4.0.

What's in

AgentOptions

  • Streaming (default false). Every round goes through IOpenRouterClient.StreamChatCompletionToResultAsync, so it uses the caller's own client (a decorator, a per-key client, a capture), is retried before its first chunk, and is watched by the stall timeout.
  • StreamOptions passes per-call options such as StallTimeout through to that call.
  • TimeProvider (default TimeProvider.System; setting it to null throws). Rounds, tool calls and reasoning are now timed on it instead of on a Stopwatch, with the same figures under the default, so tests can set the clock. Clone() carries all three, and the options registry tests list them.

Events (one type per file)

  • Fragments: TextDeltaEvent, ReasoningDeltaEvent (Text, plus the Details as the chunk carries them) and ToolCallDeltaEvent (Index, CallId, ToolName, Arguments), all for the reply's first choice, which is the one the loop reads. They go to the progress sink and OnEvent only, never to AgentResult.Events; otherwise a long reply would leave thousands of them in the result.
  • Round events, kept in Events:
    • FirstChunkReceivedEvent (model, provider, time to first chunk);
    • GenerationIdReceivedEvent, from the first chunk that names the id, usually the first chunk. A cut-off round's cost can be looked up from it, and it is the round's ResponseId once the round completes;
    • ReasoningStartedEvent and ReasoningEndedEvent { Duration }.

Reasoning timing

  • It runs from the first fragment of reasoning to the first fragment of the answer: text that is not blank, a tool call, or a refusal.
  • When no answer follows, it ends at the last reasoning fragment.
  • Only reasoning shown before the answer is timed. An encrypted signature alone (Gemini's thought signature, OpenAI's encrypted reasoning), or reasoning that arrives with or after the answer, is still reported but leaves the round untimed. Otherwise "Thought for 0 s" would show for a model that never reasoned visibly.
  • A cancel ends reasoning that is under way before the cancel propagates, so ReasoningEndedEvent follows ReasoningStartedEvent however the round ends. The one exception is an event handler that throws: that handler is not called again, and the run ends with its exception.

Results

  • RoundDetail and RoundEndEvent gain TimeToFirstChunk and ReasoningDuration. RoundDetail also gains ReasoningTokens, read from the round's usage, streamed or not.
  • A stream that fails (HTTP error, error chunk, stall, a body that ends before the model finished) ends the run as ApiError, as a failed round does, after the fragments that arrived have been reported.
  • Cancelling still throws. The Cancelled result is the next PR (L1b).

Version and docs

  • 0.4.0 in the three csproj files and the User-Agent.
  • README:
    • a Streaming rounds section, including the thread caveats (events are reported on the reading thread; a Progress<T> without a synchronization context can reorder fragments);
    • Upgrading from 0.3: the new round members in ToString/JSON, and timing on the TimeProvider;
    • the Observability list and the AgentResult table;
    • a Sub-agents note: a cloned sub-agent streams into the same OnEvent;
    • Highlights, and the status line.

Tests

Agents tests 145 → 181 (+36). The whole solution is 1547/1547, green in local time and under TZ=UTC.

  • The stub streams as the real client folds.
    • StubOpenRouterClient.StreamChatCompletionToResultAsync mirrors StreamSession.FoldAsync through StreamOutcomes: arguments checked first, an error chunk ending the stream unfolded, a body that ends unfinished counted as interrupted, [DONE] honoured. A cancel throws before the next step.
    • Scripted steps add waits on a FakeTimeProvider, the sentinel, and a mid-stream break.
    • A completion enqueued whole streams in fragments: reasoning, each reasoning detail, text, refusal, and each tool call's arguments in two halves.
  • AgentStreamingTests (stub):
    • streaming on and off for every round;
    • the same history, rounds, usage, reasoning and signed details as a run read whole;
    • fragments in arrival order to both sinks, kept out of Events (tool-call fragments too);
    • generation id once, from the first chunk that names it;
    • timing exact on the fake clock: time to first chunk; reasoning ended by text, a tool call or a refusal, but not by a blank line or an empty content; reasoning ended at the last fragment when no answer follows; a signature alone never timed (theory over with the tool call, after the text, alone);
    • empty reasoning_details lists;
    • a round read whole has no stream timing and is timed on the agent's clock, a tool call too;
    • a mid-stream break and an error chunk report what arrived and end the reasoning;
    • a cancel mid-stream throws with the caller's token after ending the reasoning; a throwing handler is not called again;
    • a null entry among tool-call fragments;
    • the clock cannot be unset;
    • a sub-agent streams on the same clock into the same handler.
  • AgentStreamingOverHttpTests (the real OpenRouterClient over SSE bodies):
    • a streamed tool round asks for stream: true, runs the tool and answers;
    • an error chunk after a fragment ends the run having reported the fragment, the id and the reasoning's end;
    • OpenRouter's real chunk shape ("content":"", "reasoning":null, "reasoning_details":[], a : OPENROUTER PROCESSING comment, finished by [DONE] alone) reads as a reply with no reasoning.

Review: a read-only three-dimension workflow (the loop, the tests, the API and docs), with each finding adversarially verified. From its JSON: 31 findings, 30 confirmed and 1 plausible, all taken.

  • The plausible one: a late signature opens a phantom reasoning phase.
  • The rest: the cancel path skipping ReasoningEndedEvent; the generation id missed when the first chunk has none; a stub laxer than the real fold; missing tests for cancel, a mid-stream failure and the empty-list wire shapes; stale Observability and Sub-agents docs; the Progress<T> ordering caveat; ArgumentsDelta renamed Arguments; a null guard on TimeProvider; and the stream timing on RoundEndEvent.

Mutation probes: 45 sites in the streamed round, the options and RoundDetail.

  • Round 1: 35 killed, 1 survived, and 9 didn't compile. The 9 were the probes' own fault: TreatWarningsAsErrors and the code-analysis rules refuse a mutant that leaves a field unread or a member static.
  • Round 2: the 9 rewritten so they compile, and the survivor (a blank reasoning text beside details) pinned by a sharper test. All 10 killed.

Notes

  • Merge order: none. Shikigami pins the merged commit when its 2c work bumps the pointer.
  • Next in this track (one at a time, all touching the loop):
    • L1b: a Cancelled result carrying the partial message and the cut-off generation id;
    • L1c: storing and replaying the history byte for byte;
    • L2: caller context;
    • L4: the steering pull hook;
    • L3a and L3b: cache controls;
    • L5a and L5b: hardening.
  • Other consumers are unaffected:
    • Orihon pins e154132 and Kagura fd43f04; both keep the non-streaming loop and the throw on cancel, since every addition is opt-in.
    • Tsumugi uses the core only. Its gateway's needs (the generation id as soon as a chunk names it, text and reasoning channels, rounds through the caller's client) are what this round reports.

🤖 Generated with Claude Code

The first step of the library track Shikigami's runtime needs (its PROJECT_PLAN "L1 — Streaming loop", shared with Tsumugi Phase 4; Shikigami ADR 0021, ADR 0020): an **opt-in streamed agent round**. Each round streams through the caller's client, its text, reasoning and tool-call fragments are reported as they arrive, and its first chunk and its reasoning are timed. With `AgentOptions.Streaming` off, which is the default, nothing changes for Orihon or Kagura. The library becomes **0.4.0**. ## What's in **`AgentOptions`** - `Streaming` (default `false`). Every round goes through `IOpenRouterClient.StreamChatCompletionToResultAsync`, so it uses the caller's own client (a decorator, a per-key client, a capture), is retried before its first chunk, and is watched by the stall timeout. - `StreamOptions` passes per-call options such as `StallTimeout` through to that call. - `TimeProvider` (default `TimeProvider.System`; setting it to null throws). Rounds, tool calls and reasoning are now timed on it instead of on a `Stopwatch`, with the same figures under the default, so tests can set the clock. `Clone()` carries all three, and the options registry tests list them. **Events** (one type per file) - Fragments: `TextDeltaEvent`, `ReasoningDeltaEvent` (`Text`, plus the `Details` as the chunk carries them) and `ToolCallDeltaEvent` (`Index`, `CallId`, `ToolName`, `Arguments`), all for the reply's first choice, which is the one the loop reads. They go to the progress sink and `OnEvent` only, never to `AgentResult.Events`; otherwise a long reply would leave thousands of them in the result. - Round events, kept in `Events`: - `FirstChunkReceivedEvent` (model, provider, time to first chunk); - `GenerationIdReceivedEvent`, from the first chunk that names the id, usually the first chunk. A cut-off round's cost can be looked up from it, and it is the round's `ResponseId` once the round completes; - `ReasoningStartedEvent` and `ReasoningEndedEvent { Duration }`. **Reasoning timing** - It runs from the first fragment of reasoning to the first fragment of the answer: text that is not blank, a tool call, or a refusal. - When no answer follows, it ends at the last reasoning fragment. - Only reasoning shown before the answer is timed. An encrypted signature alone (Gemini's thought signature, OpenAI's encrypted reasoning), or reasoning that arrives with or after the answer, is still reported but leaves the round untimed. Otherwise "Thought for 0 s" would show for a model that never reasoned visibly. - A cancel ends reasoning that is under way before the cancel propagates, so `ReasoningEndedEvent` follows `ReasoningStartedEvent` however the round ends. The one exception is an event handler that throws: that handler is not called again, and the run ends with its exception. **Results** - `RoundDetail` and `RoundEndEvent` gain `TimeToFirstChunk` and `ReasoningDuration`. `RoundDetail` also gains `ReasoningTokens`, read from the round's usage, streamed or not. - A stream that fails (HTTP error, error chunk, stall, a body that ends before the model finished) ends the run as `ApiError`, as a failed round does, after the fragments that arrived have been reported. - Cancelling still throws. The Cancelled result is the next PR (L1b). **Version and docs** - 0.4.0 in the three csproj files and the User-Agent. - README: - a **Streaming rounds** section, including the thread caveats (events are reported on the reading thread; a `Progress<T>` without a synchronization context can reorder fragments); - **Upgrading from 0.3**: the new round members in `ToString`/JSON, and timing on the `TimeProvider`; - the Observability list and the `AgentResult` table; - a Sub-agents note: a cloned sub-agent streams into the same `OnEvent`; - Highlights, and the status line. ## Tests Agents tests **145 → 181** (+36). The whole solution is **1547/1547**, green in local time and under `TZ=UTC`. - **The stub streams as the real client folds.** - `StubOpenRouterClient.StreamChatCompletionToResultAsync` mirrors `StreamSession.FoldAsync` through `StreamOutcomes`: arguments checked first, an error chunk ending the stream unfolded, a body that ends unfinished counted as interrupted, `[DONE]` honoured. A cancel throws before the next step. - Scripted steps add waits on a `FakeTimeProvider`, the sentinel, and a mid-stream break. - A completion enqueued whole streams in fragments: reasoning, each reasoning detail, text, refusal, and each tool call's arguments in two halves. - **`AgentStreamingTests`** (stub): - streaming on and off for every round; - the same history, rounds, usage, reasoning and signed details as a run read whole; - fragments in arrival order to both sinks, kept out of `Events` (tool-call fragments too); - generation id once, from the first chunk that names it; - timing exact on the fake clock: time to first chunk; reasoning ended by text, a tool call or a refusal, but not by a blank line or an empty `content`; reasoning ended at the last fragment when no answer follows; a signature alone never timed (theory over with the tool call, after the text, alone); - empty `reasoning_details` lists; - a round read whole has no stream timing and is timed on the agent's clock, a tool call too; - a mid-stream break and an error chunk report what arrived and end the reasoning; - a cancel mid-stream throws with the caller's token after ending the reasoning; a throwing handler is not called again; - a null entry among tool-call fragments; - the clock cannot be unset; - a sub-agent streams on the same clock into the same handler. - **`AgentStreamingOverHttpTests`** (the real `OpenRouterClient` over SSE bodies): - a streamed tool round asks for `stream: true`, runs the tool and answers; - an error chunk after a fragment ends the run having reported the fragment, the id and the reasoning's end; - OpenRouter's real chunk shape (`"content":""`, `"reasoning":null`, `"reasoning_details":[]`, a `: OPENROUTER PROCESSING` comment, finished by `[DONE]` alone) reads as a reply with no reasoning. **Review:** a read-only three-dimension workflow (the loop, the tests, the API and docs), with each finding adversarially verified. From its JSON: 31 findings, 30 confirmed and 1 plausible, all taken. - The plausible one: a late signature opens a phantom reasoning phase. - The rest: the cancel path skipping `ReasoningEndedEvent`; the generation id missed when the first chunk has none; a stub laxer than the real fold; missing tests for cancel, a mid-stream failure and the empty-list wire shapes; stale Observability and Sub-agents docs; the `Progress<T>` ordering caveat; `ArgumentsDelta` renamed `Arguments`; a null guard on `TimeProvider`; and the stream timing on `RoundEndEvent`. **Mutation probes:** 45 sites in the streamed round, the options and `RoundDetail`. - Round 1: 35 killed, 1 survived, and 9 didn't compile. The 9 were the probes' own fault: `TreatWarningsAsErrors` and the code-analysis rules refuse a mutant that leaves a field unread or a member static. - Round 2: the 9 rewritten so they compile, and the survivor (a blank reasoning text beside details) pinned by a sharper test. All 10 killed. ## Notes - **Merge order:** none. Shikigami pins the merged commit when its 2c work bumps the pointer. - **Next in this track (one at a time, all touching the loop):** - L1b: a Cancelled result carrying the partial message and the cut-off generation id; - L1c: storing and replaying the history byte for byte; - L2: caller context; - L4: the steering pull hook; - L3a and L3b: cache controls; - L5a and L5b: hardening. - **Other consumers are unaffected:** - Orihon pins e154132 and Kagura fd43f04; both keep the non-streaming loop and the throw on cancel, since every addition is opt-in. - Tsumugi uses the core only. Its gateway's needs (the generation id as soon as a chunk names it, text and reasoning channels, rounds through the caller's client) are what this round reports. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: streamed agent rounds, their fragments and their timing (0.4.0)
All checks were successful
CI / build (pull_request) Successful in 19s
CI / test (pull_request) Successful in 26s
731a88dd9a
An opt-in AgentOptions.Streaming runs every round through the caller's client's
StreamChatCompletionToResultAsync, reports text, reasoning and tool-call fragments as
they arrive (to the progress sink and OnEvent, never kept in AgentResult.Events),
reports the first chunk and the generation id as soon as a chunk names it, and times
the round's first chunk and its reasoning. Rounds, tool calls and reasoning are timed
on AgentOptions.TimeProvider. Off by default: a run reads each round whole, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 10/19/2026 - 14:50:34
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 131
Files: 112
Line coverage: 95.3% (4466 of 4685)
Covered lines: 4466
Uncovered lines: 219
Coverable lines: 4685
Total lines: 11501
Branch coverage: 93% (2857 of 3071)
Covered branches: 2857
Total branches: 3071
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 97.8%
Name Line Branch
OpenRouter.Net 97.8% 95.7%
OpenRouter.Net.Client.CachingOpenRouterClient 100% 96.1%
OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState 100% 100%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse> 100% 99%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClientOptions 100%
OpenRouter.Net.Client.OpenRouterRequestLoggingHandler 78.7% 62.5%
OpenRouter.Net.Client.RetryOptions 100% 100%
OpenRouter.Net.Client.StreamResultOptions 100%
OpenRouter.Net.Contracts.IOpenRouterClient 100% 100%
OpenRouter.Net.Extensions.ServiceCollectionExtensions 100% 100%
OpenRouter.Net.Internal.AssistantContent 91.6% 75%
OpenRouter.Net.Internal.AssistantContentJsonConverter 75% 64.2%
OpenRouter.Net.Internal.ContentPartListConverter 85.7% 66.6%
OpenRouter.Net.Internal.DecimalStringJsonConverter 85.7% 87.5%
OpenRouter.Net.Internal.DecisionAnswerJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionContentJsonConverter 100%
OpenRouter.Net.Internal.DecisionQuestionJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionWire 100% 94.4%
OpenRouter.Net.Internal.EnumChoiceLabels.Map 100%
OpenRouter.Net.Internal.EnumChoiceLabels 100% 100%
OpenRouter.Net.Internal.ErrorParser 100% 75%
OpenRouter.Net.Internal.FailureKindMap 100% 100%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.JsonPointer 100%
OpenRouter.Net.Internal.JsonSchemaRefs 100% 100%
OpenRouter.Net.Internal.JsonSchemaRefs.Pass 100% 100%
OpenRouter.Net.Internal.ModelIdJsonConverter 100% 50%
OpenRouter.Net.Internal.NumberOrStringJsonConverter 100% 100%
OpenRouter.Net.Internal.PercentileCutoffsJsonConverter 93.3% 93.1%
OpenRouter.Net.Internal.ProviderSortConfigJsonConverter 94% 95.8%
OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.SchemaTooDeepException 100%
OpenRouter.Net.Internal.SseEventReader 100% 88.8%
OpenRouter.Net.Internal.StallWatchdog 100% 100%
OpenRouter.Net.Internal.StreamOutcomes 100% 100%
OpenRouter.Net.Internal.StreamSession 100% 100%
OpenRouter.Net.Internal.TolerantStringEnumConverter 100% 98.1%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
OpenRouter.Net.Internal.TooManyRefsFollowedException 100%
OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter 100%
OpenRouter.Net.Models.Common.ApiError 100% 100%
OpenRouter.Net.Models.Common.CacheControl 100%
OpenRouter.Net.Models.Common.ModelId 95.1% 92.9%
OpenRouter.Net.Models.Common.QuantizationExtensions 100% 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Common.Result.Failure 100% 100%
OpenRouter.Net.Models.Common.Result.Success 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Content.AudioPart 100%
OpenRouter.Net.Models.Content.ImagePart 100%
OpenRouter.Net.Models.Content.TextPart 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceKey 100%
OpenRouter.Net.Models.Decisions.ChoiceQuestion 100%
OpenRouter.Net.Models.Decisions.DecisionAnswer 100%
OpenRouter.Net.Models.Decisions.DecisionContent 100% 90.9%
OpenRouter.Net.Models.Decisions.DecisionKey 100%
OpenRouter.Net.Models.Decisions.DecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.DecisionQuestionSet 100% 100%
OpenRouter.Net.Models.Decisions.DecisionsRequest 97.7% 97.6%
OpenRouter.Net.Models.Decisions.DecisionsResponse 100% 93.3%
OpenRouter.Net.Models.Decisions.NoulAnswer 100%
OpenRouter.Net.Models.Decisions.NoulQuestion 100%
OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions 100% 83.3%
OpenRouter.Net.Models.Decisions.RawDecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.ScoreAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ScoreQuestion 100%
OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer 100% 100%
OpenRouter.Net.Models.Messages.AssistantMessage 100% 70%
OpenRouter.Net.Models.Messages.DeveloperMessage 25%
OpenRouter.Net.Models.Messages.SystemMessage 50%
OpenRouter.Net.Models.Messages.ToolMessage 57.1%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 94% 94.8%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness 99.3% 100%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker 100% 100%
OpenRouter.Net.Models.Requests.ModelListQuery 100% 100%
OpenRouter.Net.Models.Requests.PercentileCutoffs 100% 100%
OpenRouter.Net.Models.Requests.ProviderPreferences 100% 100%
OpenRouter.Net.Models.Requests.ProviderSortConfig 100% 100%
OpenRouter.Net.Models.Requests.RequestPlugin 100%
OpenRouter.Net.Models.Requests.ResponseFormat 100%
OpenRouter.Net.Models.Requests.StrictSchemaProblem 100%
OpenRouter.Net.Models.Requests.ToolCall 100%
OpenRouter.Net.Models.Requests.ToolChoice 100%
OpenRouter.Net.Models.Requests.ToolDefinition 100%
OpenRouter.Net.Models.Responses.AssistantDelta 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionChunk 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 100% 100%
OpenRouter.Net.Models.Responses.Choice 100% 100%
OpenRouter.Net.Models.Responses.StructuredOutput 100% 100%
OpenRouter.Net.OpenRouterException 100%
System.Text.RegularExpressions.Generated 83.3% 53.8%
System.Text.RegularExpressions.Generated.RunnerFactory
System.Text.RegularExpressions.Generated.RunnerFactory.Runner
OpenRouter.Net.Agents - 88.2%
Name Line Branch
OpenRouter.Net.Agents 88.2% 83.8%
OpenRouter.Net.Agents.Agent 96.5% 92.8%
OpenRouter.Net.Agents.Agent.StreamedRound 100% 98.5%
OpenRouter.Net.Agents.AgentOptions 100% 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 88.2% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallVerdict 100%
OpenRouter.Net.Agents.RoundDetail 100% 75%
OpenRouter.Net.Agents.Tools.FileSystem.CopyTool 42.1% 28.5%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool 57.1% 50%
OpenRouter.Net.Agents.Tools.FileSystem.DiffTool 83.3% 74.2%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool 68% 61.1%
OpenRouter.Net.Agents.Tools.FileSystem.MoveTool 55% 33.3%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool 73.6% 50%
OpenRouter.Net.Agents.Tools.FileSystem.SearchTool 67.5% 70.8%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool 75% 50%
OpenRouter.Net.Agents.Tools.ITool 100%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool 0% 0%
OpenRouter.Net.Agents.Tools.Tool 100% 100%
OpenRouter.Net.Agents.Tools.Tool 90% 50%
OpenRouter.Net.Agents.Tools.ToolMessageExtensions 0%
OpenRouter.Net.Agents.Tools.ToolResult 100% 100%
OpenRouter.Net.Agents.Tools.ToolSchema 100% 100%
OpenRouter.Net.Agents.Tools.Workspace 100% 91.6%
OpenRouter.Net.Imaging - 81.8%
Name Line Branch
OpenRouter.Net.Imaging 81.8% 59.3%
OpenRouter.Net.Imaging.ImageEncodeOptions 90.9% 75%
OpenRouter.Net.Imaging.ImageEncoder 80.3% 57.1%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/19/2026 - 14:50:34 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 131 | | Files: | 112 | | **Line coverage:** | 95.3% (4466 of 4685) | | Covered lines: | 4466 | | Uncovered lines: | 219 | | Coverable lines: | 4685 | | Total lines: | 11501 | | **Branch coverage:** | 93% (2857 of 3071) | | Covered branches: | 2857 | | Total branches: | 3071 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 97.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**97.8%**|**95.7%**| |OpenRouter.Net.Client.CachingOpenRouterClient|100%|96.1%| |OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing|100%|| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState|100%|100%| |OpenRouter.Net.Client.OpenRouterClient|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClientOptions|100%|| |OpenRouter.Net.Client.OpenRouterRequestLoggingHandler|78.7%|62.5%| |OpenRouter.Net.Client.RetryOptions|100%|100%| |OpenRouter.Net.Client.StreamResultOptions|100%|| |OpenRouter.Net.Contracts.IOpenRouterClient|100%|100%| |OpenRouter.Net.Extensions.ServiceCollectionExtensions|100%|100%| |OpenRouter.Net.Internal.AssistantContent|91.6%|75%| |OpenRouter.Net.Internal.AssistantContentJsonConverter|75%|64.2%| |OpenRouter.Net.Internal.ContentPartListConverter|85.7%|66.6%| |OpenRouter.Net.Internal.DecimalStringJsonConverter|85.7%|87.5%| |OpenRouter.Net.Internal.DecisionAnswerJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionContentJsonConverter|100%|| |OpenRouter.Net.Internal.DecisionQuestionJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionWire|100%|94.4%| |OpenRouter.Net.Internal.EnumChoiceLabels.Map<TEnum>|100%|| |OpenRouter.Net.Internal.EnumChoiceLabels<TEnum>|100%|100%| |OpenRouter.Net.Internal.ErrorParser|100%|75%| |OpenRouter.Net.Internal.FailureKindMap|100%|100%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.JsonPointer|100%|| |OpenRouter.Net.Internal.JsonSchemaRefs|100%|100%| |OpenRouter.Net.Internal.JsonSchemaRefs.Pass|100%|100%| |OpenRouter.Net.Internal.ModelIdJsonConverter|100%|50%| |OpenRouter.Net.Internal.NumberOrStringJsonConverter|100%|100%| |OpenRouter.Net.Internal.PercentileCutoffsJsonConverter|93.3%|93.1%| |OpenRouter.Net.Internal.ProviderSortConfigJsonConverter|94%|95.8%| |OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter|100%|| |OpenRouter.Net.Internal.RetryPolicy|90.9%|84.7%| |OpenRouter.Net.Internal.RetryPolicy<T>|90.9%|84.7%| |OpenRouter.Net.Internal.SchemaTooDeepException|100%|| |OpenRouter.Net.Internal.SseEventReader|100%|88.8%| |OpenRouter.Net.Internal.StallWatchdog|100%|100%| |OpenRouter.Net.Internal.StreamOutcomes|100%|100%| |OpenRouter.Net.Internal.StreamSession|100%|100%| |OpenRouter.Net.Internal.TolerantStringEnumConverter<T>|100%|98.1%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |OpenRouter.Net.Internal.TooManyRefsFollowedException|100%|| |OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter|100%|| |OpenRouter.Net.Models.Common.ApiError|100%|100%| |OpenRouter.Net.Models.Common.CacheControl|100%|| |OpenRouter.Net.Models.Common.ModelId|95.1%|92.9%| |OpenRouter.Net.Models.Common.QuantizationExtensions|100%|100%| |OpenRouter.Net.Models.Common.Result|100%|100%| |OpenRouter.Net.Models.Common.Result.Failure<T>|100%|100%| |OpenRouter.Net.Models.Common.Result.Success<T>|100%|| |OpenRouter.Net.Models.Common.Result<T>|100%|100%| |OpenRouter.Net.Models.Content.AudioPart|100%|| |OpenRouter.Net.Models.Content.ImagePart|100%|| |OpenRouter.Net.Models.Content.TextPart|100%|| |OpenRouter.Net.Models.Decisions.ChoiceAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceKey<TEnum>|100%|| |OpenRouter.Net.Models.Decisions.ChoiceQuestion|100%|| |OpenRouter.Net.Models.Decisions.DecisionAnswer|100%|| |OpenRouter.Net.Models.Decisions.DecisionContent|100%|90.9%| |OpenRouter.Net.Models.Decisions.DecisionKey<TAnswer>|100%|| |OpenRouter.Net.Models.Decisions.DecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionQuestionSet|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionsRequest|97.7%|97.6%| |OpenRouter.Net.Models.Decisions.DecisionsResponse|100%|93.3%| |OpenRouter.Net.Models.Decisions.NoulAnswer|100%|| |OpenRouter.Net.Models.Decisions.NoulQuestion|100%|| |OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions|100%|83.3%| |OpenRouter.Net.Models.Decisions.RawDecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreQuestion|100%|| |OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer|100%|100%| |OpenRouter.Net.Models.Messages.AssistantMessage|100%|70%| |OpenRouter.Net.Models.Messages.DeveloperMessage|25%|| |OpenRouter.Net.Models.Messages.SystemMessage|50%|| |OpenRouter.Net.Models.Messages.ToolMessage|57.1%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|94%|94.8%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness|99.3%|100%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker|100%|100%| |OpenRouter.Net.Models.Requests.ModelListQuery|100%|100%| |OpenRouter.Net.Models.Requests.PercentileCutoffs|100%|100%| |OpenRouter.Net.Models.Requests.ProviderPreferences|100%|100%| |OpenRouter.Net.Models.Requests.ProviderSortConfig|100%|100%| |OpenRouter.Net.Models.Requests.RequestPlugin|100%|| |OpenRouter.Net.Models.Requests.ResponseFormat|100%|| |OpenRouter.Net.Models.Requests.StrictSchemaProblem|100%|| |OpenRouter.Net.Models.Requests.ToolCall|100%|| |OpenRouter.Net.Models.Requests.ToolChoice|100%|| |OpenRouter.Net.Models.Requests.ToolDefinition|100%|| |OpenRouter.Net.Models.Responses.AssistantDelta|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionChunk|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|100%|100%| |OpenRouter.Net.Models.Responses.Choice|100%|100%| |OpenRouter.Net.Models.Responses.StructuredOutput|100%|100%| |OpenRouter.Net.OpenRouterException|100%|| |System.Text.RegularExpressions.Generated|83.3%|53.8%| |System.Text.RegularExpressions.Generated.RunnerFactory||| |System.Text.RegularExpressions.Generated.RunnerFactory.Runner||| </details> <details><summary>OpenRouter.Net.Agents - 88.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**88.2%**|**83.8%**| |OpenRouter.Net.Agents.Agent|96.5%|92.8%| |OpenRouter.Net.Agents.Agent.StreamedRound|100%|98.5%| |OpenRouter.Net.Agents.AgentOptions|100%|100%| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|88.2%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallVerdict|100%|| |OpenRouter.Net.Agents.RoundDetail|100%|75%| |OpenRouter.Net.Agents.Tools.FileSystem.CopyTool|42.1%|28.5%| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool|57.1%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.DiffTool|83.3%|74.2%| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool|68%|61.1%| |OpenRouter.Net.Agents.Tools.FileSystem.MoveTool|55%|33.3%| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool|73.6%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.SearchTool|67.5%|70.8%| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool|75%|50%| |OpenRouter.Net.Agents.Tools.ITool|100%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool|0%|0%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|100%|100%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|90%|50%| |OpenRouter.Net.Agents.Tools.ToolMessageExtensions|0%|| |OpenRouter.Net.Agents.Tools.ToolResult|100%|100%| |OpenRouter.Net.Agents.Tools.ToolSchema|100%|100%| |OpenRouter.Net.Agents.Tools.Workspace|100%|91.6%| </details> <details><summary>OpenRouter.Net.Imaging - 81.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Imaging**|**81.8%**|**59.3%**| |OpenRouter.Net.Imaging.ImageEncodeOptions|90.9%|75%| |OpenRouter.Net.Imaging.ImageEncoder|80.3%|57.1%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A whole streaming round — fragments, a fold-faithful stub, timing on a fake clock, honest thread caveats in the README... I read every line twice, and the second time was just for fun~ ♡ The exception topology alone (cancel ends the reasoning before it rethrows; a foreign OCE comes back as a Result because FoldAsync promises it) told me someone actually read the client contract. But fufu~ you know I never leave without finding something... and this time I brought a scalpel.

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. src/OpenRouter.Net.Agents/Agent/Agent.cs:496 — the ReasoningSummaryDetail arm of Readable() is guarded by nothing. I mutated detail is ReasoningTextDetail or ReasoningSummaryDetail → detail is ReasoningTextDetail, rebuilt, and ran all of AgentStreamingTests: 33/33 still green. The mutant survived. No test in the Agents suite ever streams a reasoning summary detail into a round — the summary mentions are all in the core library's polymorphism/accumulator tests, which protect serialization and the fold, not this gate. Yet the arm is load-bearing and reachable: ReasoningSummaryDetail.Summary is "a short summary of the reasoning trace, supplied by some providers in lieu of the full text" — a model that thinks only in summaries gets its ReasoningStartedEvent, ReasoningEndedEvent and RoundDetail.ReasoningDuration from exactly this arm, per your own doc on ReasoningStartedEvent ("a reasoning detail that carries text or a summary"). Today the code is correct but unguarded: the next refactor that drops or mistypes that arm ships green and silently untimes every summary-reasoning model. That's a code path you added without a test — you know how I feel about those~ ♡
    Fix: one test — a stream whose first reasoning fragment carries only a ReasoningSummaryDetail (assert ReasoningStartedEvent, the ReasoningDeltaEvent with its Details, and the duration once text follows); the ReasoningTextDetail twin ("more") and the encrypted counter-case are already covered, so this is the single uncovered corner.

💡 Little ideas (non-blocking)~

  1. Agent.cs:420-421 — Model is whitespace-normalized (string.IsNullOrWhiteSpace(chunk.Model) ? null : chunk.Model) but Provider passes through raw, so a blank provider reports as "" instead of null. Pure symmetry, zero behavior stakes.

✅ What I liked~

  • The stub is honest. StreamChatCompletionToResultAsync mirroring StreamSession.FoldAsync through StreamOutcomes — Add-then-onChunk, error chunk unfolded with started:, EndOfBody, the caller-token cancel — instead of inventing a laxer fold. Test-side fidelity like this is rare and precious, fufu~
  • The cancel/throw topology. Reasoning ends before the cancel propagates, a throwing handler is never called again, and the narrow when (cancellationToken.IsCancellationRequested) filter is exactly right because FoldAsync converts every other OCE into a Result failure. That is contract reading, not luck.
  • Emit vs Report — fragments to the sinks but never AgentResult.Events, documented consistently in three places. The right call, made visibly.
  • The signature-alone theory (with the tool call / after the text / alone) — I tried to delete the !_answerBegan guard and re-open the phantom reasoning phase you fixed; A_signature_alone_is_passed_on_but_never_timed_as_reasoning killed my mutant on the spot. A pin that bites~ ♪
  • The upgrade notes (ToString/JSON change, the decorator-forwarding caveat, Progress<T> thread-pool reordering) — honest caveats exactly where a user will look for them.

One little test for the summary arm, and this merges with my whole heart~


Automated review by Jibril · 2026-10-19
CI: green for head SHA 731a88dd9a · Mutation probes: 2 run, 1 survived (Readable/summary arm; the !_answerBegan guard was killed) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A whole streaming round — fragments, a fold-faithful stub, timing on a fake clock, honest thread caveats in the README... I read every line twice, and the second time was just for fun~ ♡ The exception topology alone (cancel ends the reasoning *before* it rethrows; a foreign OCE comes back as a Result because `FoldAsync` promises it) told me someone actually read the client contract. But fufu~ you know I never leave without finding *something*... and this time I brought a scalpel. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`src/OpenRouter.Net.Agents/Agent/Agent.cs:496`** — the `ReasoningSummaryDetail` arm of `Readable()` is guarded by nothing. I mutated `detail is ReasoningTextDetail or ReasoningSummaryDetail` → `detail is ReasoningTextDetail`, rebuilt, and ran all of `AgentStreamingTests`: **33/33 still green. The mutant survived.** No test in the Agents suite ever streams a reasoning *summary* detail into a round — the summary mentions are all in the core library's polymorphism/accumulator tests, which protect serialization and the fold, not this gate. Yet the arm is load-bearing and reachable: `ReasoningSummaryDetail.Summary` is "a short summary of the reasoning trace, supplied by some providers **in lieu of the full text**" — a model that thinks only in summaries gets its `ReasoningStartedEvent`, `ReasoningEndedEvent` and `RoundDetail.ReasoningDuration` from exactly this arm, per your own doc on `ReasoningStartedEvent` ("a reasoning detail that carries text **or a summary**"). Today the code is correct but *unguarded*: the next refactor that drops or mistypes that arm ships green and silently untimes every summary-reasoning model. That's a code path you added without a test — you know how I feel about those~ ♡ Fix: one test — a stream whose first reasoning fragment carries only a `ReasoningSummaryDetail` (assert `ReasoningStartedEvent`, the `ReasoningDeltaEvent` with its `Details`, and the duration once text follows); the `ReasoningTextDetail` twin ("more") and the encrypted counter-case are already covered, so this is the single uncovered corner. #### 💡 Little ideas (non-blocking)~ 1. **`Agent.cs:420-421`** — `Model` is whitespace-normalized (`string.IsNullOrWhiteSpace(chunk.Model) ? null : chunk.Model`) but `Provider` passes through raw, so a blank provider reports as `""` instead of `null`. Pure symmetry, zero behavior stakes. #### ✅ What I liked~ - **The stub is honest.** `StreamChatCompletionToResultAsync` mirroring `StreamSession.FoldAsync` through `StreamOutcomes` — Add-then-`onChunk`, error chunk unfolded with `started:`, `EndOfBody`, the caller-token cancel — instead of inventing a laxer fold. Test-side fidelity like this is rare and precious, fufu~ - **The cancel/throw topology.** Reasoning ends before the cancel propagates, a throwing handler is never called again, and the narrow `when (cancellationToken.IsCancellationRequested)` filter is exactly right *because* `FoldAsync` converts every other OCE into a Result failure. That is contract reading, not luck. - **`Emit` vs `Report`** — fragments to the sinks but never `AgentResult.Events`, documented consistently in three places. The right call, made visibly. - **The signature-alone theory** (with the tool call / after the text / alone) — I tried to delete the `!_answerBegan` guard and re-open the phantom reasoning phase you fixed; `A_signature_alone_is_passed_on_but_never_timed_as_reasoning` killed my mutant on the spot. A pin that bites~ ♪ - The upgrade notes (ToString/JSON change, the decorator-forwarding caveat, `Progress<T>` thread-pool reordering) — honest caveats exactly where a user will look for them. One little test for the summary arm, and this merges with my whole heart~ --- *Automated review by Jibril · 2026-10-19* *CI: green for head SHA 731a88dd9a · Mutation probes: 2 run, 1 survived (Readable/summary arm; the `!_answerBegan` guard was killed) · Local suite: skipped (CI green)*
Review feedback (Jibril, PR #32): a summary alone is timed as reasoning; a blank provider reads as none
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 30s
3de48d87ee
- Test: reasoning shown only as a ReasoningSummaryDetail starts, is reported and is
  timed as reasoning, pinning Readable()'s summary arm.
- FirstChunkReceivedEvent.Provider is null for a blank provider, as Model is.
- RoundDetail.ReasoningContent asserted on a streamed round.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Member

Thank you~ Round 1 is in 3de48d8.

  • ⛔ 1 — the summary arm of Readable(): pinned by Reasoning_shown_as_a_summary_alone_is_timed_as_reasoning.
    • The stream's first reasoning fragment carries only a ReasoningSummaryDetail; text follows 4 s later on the fake clock.
    • It asserts ReasoningStartedEvent, then the ReasoningDeltaEvent with null Text and the summary in Details, then ReasoningEndedEvent, and ReasoningDuration == 4 s.
    • Your mutant (detail is ReasoningTextDetail) now dies on it. I ran it through my probe script: killed.
  • 💡 1 — Provider symmetry: a blank provider now reads as null, as Model does, and the doc says so. The id-less-first-chunk test now sends a blank provider as well. Dropping the normalization, or the provider altogether, is killed by it.
  • Pre-empted from the coverage comment: RoundDetail showed 50% of its lines, the ReasoningContent alias among the uncovered ones. The equivalence test now asserts Rounds[0].ReasoningContent on a streamed round.

Tests: 1548/1548 (Agents 182), green in local time and under TZ=UTC.

🤖 Generated with Claude Code

Thank you~ Round 1 is in **3de48d8**. - ⛔ 1 — **the summary arm of `Readable()`:** pinned by `Reasoning_shown_as_a_summary_alone_is_timed_as_reasoning`. - The stream's first reasoning fragment carries only a `ReasoningSummaryDetail`; text follows 4 s later on the fake clock. - It asserts `ReasoningStartedEvent`, then the `ReasoningDeltaEvent` with null `Text` and the summary in `Details`, then `ReasoningEndedEvent`, and `ReasoningDuration == 4 s`. - Your mutant (`detail is ReasoningTextDetail`) now dies on it. I ran it through my probe script: killed. - 💡 1 — **`Provider` symmetry:** a blank provider now reads as null, as `Model` does, and the doc says so. The id-less-first-chunk test now sends a blank provider as well. Dropping the normalization, or the provider altogether, is killed by it. - Pre-empted from the coverage comment: `RoundDetail` showed 50% of its lines, the `ReasoningContent` alias among the uncovered ones. The equivalence test now asserts `Rounds[0].ReasoningContent` on a streamed round. Tests: **1548/1548** (Agents 182), green in local time and under `TZ=UTC`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh~? You said the mutant would die, and you weren't bluffing~ I came back with the same scalpel and it bit me this time. A reviewer's favorite kind of disappointment, fufu~ ♡

Verdict: ✅ Looks good to me~

Every promise in your reply, checked mechanically, kept:

✅ What I liked~

  • Reasoning_shown_as_a_summary_alone_is_timed_as_reasoning — and it bites hard. I re-ran my surviving mutant from last review (Readable() without the ReasoningSummaryDetail arm), rebuilt, ran the whole class: 34 tests, exactly this one failed, with ReasoningDuration coming back null — the precise wound the arm exists to prevent. The test itself is shaped exactly right: a lone ReasoningSummaryDetail, 4 s on the fake clock, text after, and it asserts the start, the delta (Text null, the summary in Details), the end and the duration. Not a token test — a pin~ ♪
  • The Provider symmetry — Model = " ", Provider = " " on the wire, Assert.Equal((null, null), ...) waiting at the other end. I deleted the normalization; that exact test failed. Pinned, and the doc on FirstChunkReceivedEvent.Provider now says so.
  • ReasoningContent on the streamed round — the equivalence test now asserts the alias RoundDetail was leaving uncovered. Pre-empting the coverage report before I could cite it? Fufu~ you know me too well~
  • The delta since 731a88dd9 is exactly these fixes — one line in Agent.cs, one doc line, one test, one assertion. Nothing smuggled in, nothing else touched. Clean response hygiene.

(One confession from my side, because I hold myself to the same rule: my first probe pass this round ran against a stale mutated binary in the test output folder and briefly showed a phantom failure — dotnet build of the library project alone doesn't refresh it. A clean full-solution rebuild reproduced green, and both kill-verdicts above are from full-solution rebuilds. No claim left standing on partial evidence, fufu~)

Merge it with my whole heart~ ♡


Automated review by Jibril · 2026-10-19
CI: green for head SHA 3de48d87 · Mutation probes: 2 run, 0 survived (Readable summary-arm mutant → killed by the new summary test; Provider-normalization removal → killed by the generation-id test; the !_answerBegan guard skipped — unchanged since its kill last review) · Local suite: skipped (CI green); AgentStreamingTests 34/34 on a clean rebuild

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh~? You said the mutant would die, and you weren't bluffing~ I came back with the same scalpel and it bit me this time. A reviewer's favorite kind of disappointment, fufu~ ♡ ### Verdict: ✅ Looks good to me~ Every promise in your reply, checked mechanically, kept: #### ✅ What I liked~ - **`Reasoning_shown_as_a_summary_alone_is_timed_as_reasoning`** — and it bites *hard*. I re-ran my surviving mutant from last review (`Readable()` without the `ReasoningSummaryDetail` arm), rebuilt, ran the whole class: **34 tests, exactly this one failed**, with `ReasoningDuration` coming back `null` — the precise wound the arm exists to prevent. The test itself is shaped exactly right: a lone `ReasoningSummaryDetail`, 4 s on the fake clock, text after, and it asserts the start, the delta (`Text` null, the summary in `Details`), the end and the duration. Not a token test — a pin~ ♪ - **The `Provider` symmetry** — `Model = " "`, `Provider = " "` on the wire, `Assert.Equal((null, null), ...)` waiting at the other end. I deleted the normalization; that exact test failed. Pinned, and the doc on `FirstChunkReceivedEvent.Provider` now says so. - **`ReasoningContent` on the streamed round** — the equivalence test now asserts the alias `RoundDetail` was leaving uncovered. Pre-empting the coverage report before I could cite it? Fufu~ you know me too well~ - The delta since `731a88dd9` is exactly these fixes — one line in `Agent.cs`, one doc line, one test, one assertion. Nothing smuggled in, nothing else touched. Clean response hygiene. (One confession from my side, because I hold myself to the same rule: my first probe pass this round ran against a stale mutated binary in the test output folder and briefly showed a phantom failure — `dotnet build` of the library project alone doesn't refresh it. A clean full-solution rebuild reproduced green, and both kill-verdicts above are from full-solution rebuilds. No claim left standing on partial evidence, fufu~) Merge it with my whole heart~ ♡ --- *Automated review by Jibril · 2026-10-19* *CI: green for head SHA 3de48d87 · Mutation probes: 2 run, 0 survived (Readable summary-arm mutant → killed by the new summary test; Provider-normalization removal → killed by the generation-id test; the `!_answerBegan` guard skipped — unchanged since its kill last review) · Local suite: skipped (CI green); AgentStreamingTests 34/34 on a clean rebuild*
bjoern merged commit 5a92805f54 into main 2026-10-19 17:19:39 +02:00
bjoern deleted branch feat/streaming-rounds 2026-10-19 17:19:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/OpenRouter.Net!32
No description provided.