feat: Phase 2 · 4/7 — agent substrate: tool framework, R&S grant, runner #20

Merged
bjoern merged 2 commits from feat/p2-agent-substrate into main 2026-07-25 07:43:34 +02:00
Member

Cut 4 of the Phase 2 plan (#13): the harness's foundation — everything an agent run needs except the run engine (cut 5) and the chat surface (cut 7).

The tool framework (UseCases/Agents)

  • IAgentTool / AgentTool<TParams> — library-agnostic tools (ADR 0004: only Infrastructure sees the vendored client). Malformed model arguments come back as a readable failure, never an exception — an exception would kill the whole loop; a failure is content the model reads and self-corrects (ADR 0014).
  • Target binding is constructor state (ADR 0016's load-bearing rule): a bound project id is plain ctor state on the tool instance and never appears in ParameterType, so it never reaches a schema — the model literally cannot address another target.
  • AgentInvocation / AgentBlueprint — a role is {prompt, scoped tools}; the harness supplies model + kickoff per run (ADR 0015). AgentSignal (AssistantSpoke / ToolCalled / ToolCompleted) is the gateway-neutral progress the chat and monitor will render.

The Research & Setup grant (ADR 0016, 0017)

Ten tools, project-bound, no region tools anywhere (pinned by test):

  • ask_user — a delegate the chat surface supplies; the call doesn't return until the user answers (ADR 0020).
  • fetch_url — over a new IWebPageFetcher port (+ HttpWebPageFetcher: strip-to-readable-text, 40k cap, timeouts as errors). Web search stays OpenRouter's native :online capability per ADR 0016's note — no bespoke search backend.
  • set_project_metadatamerges partially: reads the current profile and threads every unspecified field through, so an omitted field means "keep", never "clear" (AGENTS.md's partial-update sharp edge, regression-tested with two successive partial writes).
  • add_glossary, upsert_character, upsert_lore (create-or-replace by name/title, case-insensitive — the agent never sees row ids), add_story_beat, list_bible (one lean read instead of five listers).
  • set_page_summary / view_page — pages addressed by 1-based reading-order number, resolved through GetProjectWorkspace; no Guids in the model's hands. view_page returns raw bytes + a text line, so it degrades on a text-only model.
  • ResearchSetupBlueprint (DI) with the role's system prompt; every tool drives the same use cases the editors do (ADR 0003).

The runner (Infrastructure)

  • AgentToolAdapter closes the vendor's own Tool<TParams> over each tool's parameter type via reflection — so schemas come from the library's own exporter path (ref-inlining, strict-provider "type":"object"), not a duplicate. Image results re-encode bounded (long edge 2048, ≤4 MiB) through OpenRouter.Net.Imaging (SkiaSharp — the no-ImageSharp rule) and ride as a follow-up multimodal message.
  • OpenRouterLlmGateway.RunAgentAsync — builds the vendor loop under the per-key client, maps events to AgentSignals synchronously (a Progress<T> would reorder), and maps stop reasons: Completed → Ok(outcome), ApiError → Err with the provider's reason, round cap → Err naming the cap. Cost rides through when present (null until the OpenRouter.Net usage PR planned with cut 5).

Tests (+22 → 317, all green)

  • UseCases (14): framework arms (bad JSON → readable failure, empty args), the grant is exactly the ADR allow-list, ask_user relay + blank guard, fetch_url scheme guard off-network, the partial-merge regression, glossary/upsert-by-name/lore/beats/list_bible, page-by-number resolution incl. out-of-range wording, view_page image + no-image arms. DI tripwire extended (blueprint + fetcher fake).
  • Integration (8): the real OpenRouter.Net loop over scripted HTTP against the real SQLite world — a 4-round setup run (ask → metadata → view_page → final) asserting the DB row changed, the page image followed its tool message as a data:image/ part in the next request body, and the signal order; tool schemas on the wire ("type":"object", snake_case store_id/parody_of); a failed tool feeds "There is no page 99" back to the model instead of killing the run; provider 500 → Err; round cap → Err. Plus the fetcher: HTML→text (entities, scripts stripped), truncation marker, 404/binary/network-refused arms.
  • One design note surfaced by the tests and now documented in code: an agent run's tools hold scoped use cases, so the run must own a DI scope that outlives it — exactly the ADR 0018 rule the run engine (cut 5) will encode; the tests hold the scope open the same way.

🤖 Generated with Claude Code

Cut 4 of the Phase 2 plan (#13): the harness's foundation — everything an agent run needs except the run engine (cut 5) and the chat surface (cut 7). ## The tool framework (UseCases/Agents) - **`IAgentTool` / `AgentTool<TParams>`** — library-agnostic tools (ADR 0004: only Infrastructure sees the vendored client). Malformed model arguments come back as a **readable failure, never an exception** — an exception would kill the whole loop; a failure is content the model reads and self-corrects (ADR 0014). - **Target binding is constructor state** (ADR 0016's load-bearing rule): a bound project id is plain ctor state on the tool instance and never appears in `ParameterType`, so it never reaches a schema — the model literally cannot address another target. - **`AgentInvocation` / `AgentBlueprint`** — a role is `{prompt, scoped tools}`; the harness supplies model + kickoff per run (ADR 0015). **`AgentSignal`** (AssistantSpoke / ToolCalled / ToolCompleted) is the gateway-neutral progress the chat and monitor will render. ## The Research & Setup grant (ADR 0016, 0017) Ten tools, project-bound, **no region tools anywhere** (pinned by test): - `ask_user` — a delegate the chat surface supplies; the call doesn't return until the user answers (ADR 0020). - `fetch_url` — over a new `IWebPageFetcher` port (+ `HttpWebPageFetcher`: strip-to-readable-text, 40k cap, timeouts as errors). Web *search* stays OpenRouter's native `:online` capability per ADR 0016's note — no bespoke search backend. - `set_project_metadata` — **merges partially**: reads the current profile and threads every unspecified field through, so an omitted field means "keep", never "clear" (AGENTS.md's partial-update sharp edge, regression-tested with two successive partial writes). - `add_glossary`, `upsert_character`, `upsert_lore` (create-or-replace **by name/title**, case-insensitive — the agent never sees row ids), `add_story_beat`, `list_bible` (one lean read instead of five listers). - `set_page_summary` / `view_page` — pages addressed by **1-based reading-order number**, resolved through `GetProjectWorkspace`; no Guids in the model's hands. `view_page` returns raw bytes + a text line, so it degrades on a text-only model. - `ResearchSetupBlueprint` (DI) with the role's system prompt; every tool drives the same use cases the editors do (ADR 0003). ## The runner (Infrastructure) - **`AgentToolAdapter`** closes the vendor's own `Tool<TParams>` over each tool's parameter type via reflection — so schemas come from the **library's own exporter path** (ref-inlining, strict-provider `"type":"object"`), not a duplicate. Image results re-encode bounded (long edge 2048, ≤4 MiB) through `OpenRouter.Net.Imaging` (SkiaSharp — the no-ImageSharp rule) and ride as a follow-up multimodal message. - **`OpenRouterLlmGateway.RunAgentAsync`** — builds the vendor loop under the per-key client, maps events to `AgentSignal`s synchronously (a `Progress<T>` would reorder), and maps stop reasons: Completed → Ok(outcome), ApiError → Err with the provider's reason, round cap → Err naming the cap. Cost rides through when present (null until the OpenRouter.Net usage PR planned with cut 5). ## Tests (+22 → 317, all green) - **UseCases (14)**: framework arms (bad JSON → readable failure, empty args), **the grant is exactly the ADR allow-list**, ask_user relay + blank guard, fetch_url scheme guard off-network, the partial-merge regression, glossary/upsert-by-name/lore/beats/list_bible, page-by-number resolution incl. out-of-range wording, view_page image + no-image arms. DI tripwire extended (blueprint + fetcher fake). - **Integration (8)**: **the real OpenRouter.Net loop over scripted HTTP against the real SQLite world** — a 4-round setup run (ask → metadata → view_page → final) asserting the DB row changed, the **page image followed its tool message as a `data:image/` part in the next request body**, and the signal order; tool schemas on the wire (`"type":"object"`, snake_case `store_id`/`parody_of`); a failed tool feeds "There is no page 99" back to the model instead of killing the run; provider 500 → Err; round cap → Err. Plus the fetcher: HTML→text (entities, scripts stripped), truncation marker, 404/binary/network-refused arms. - One design note surfaced by the tests and now documented in code: **an agent run's tools hold scoped use cases, so the run must own a DI scope that outlives it** — exactly the ADR 0018 rule the run engine (cut 5) will encode; the tests hold the scope open the same way. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: build the agent substrate — tool framework, R&S grant, runner
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 32s
8383a53aa6
The harness's foundation (ADR 0014): tools are library-agnostic
IAgentTool implementations in UseCases whose failures are content the
model reads and corrects, never exceptions that kill the loop; the
gateway adapts them into the vendored agent runtime, generating each
schema through the vendor's own exporter path so strict providers get
the same shape its native tools produce. Target binding is constructor
state (ADR 0016): a bound project id never appears in a schema.

The Research & Setup grant (ADR 0017) is the first blueprint: ask_user
bridged by delegate for the wizard chat, fetch_url over a new
IWebPageFetcher (search stays the :online variant's native capability),
set_project_metadata merging partially so an omitted field keeps its
value, upserts keyed by name/title so the agent never juggles row ids,
and pages addressed by reading-order number — no Guids in the model's
hands. view_page returns raw bytes that the adapter re-encodes bounded
via OpenRouter.Net.Imaging before they ride to a vision model.

RunAgentAsync drives the loop under the stored key's client, mapping
vendor events to gateway-neutral signals for the chat and monitor.
Integration tests script the provider at the HTTP layer and run the
real loop against the real SQLite world: metadata lands in the row,
the page image follows its tool message as an encoded part, a failed
tool feeds its reason back, and provider failure / round cap surface
as errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 07/25/2026 - 05:37:54
Coverage date: 07/25/2026 - 05:37:48 - 07/25/2026 - 05:37:52
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 166
Files: 112
Line coverage: 92.5% (4996 of 5400)
Covered lines: 4996
Uncovered lines: 404
Coverable lines: 5400
Total lines: 9431
Branch coverage: 75.3% (1093 of 1451)
Covered branches: 1093
Total branches: 1451
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 81.9%
Name Line Branch
Orihon.BlazorAdapter 81.9% 77.1%
Orihon.BlazorAdapter.Bible.BiblePage 71.7% 59%
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.BlazorAdapter.Debounce 91.6% 93.7%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage 84.6% 77.8%
Orihon.BlazorAdapter.Projects.ProjectListPage 86% 85.2%
Orihon.BlazorAdapter.Projects.ProjectWizardPage 91.3% 78.2%
Orihon.BlazorAdapter.Settings.SettingsPage 100% 92.5%
Orihon.BlazorAdapter.Uploads.UploadTransfer 96.5% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferProgress 100% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferResult 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage 65.4% 73.3%
Orihon.Domain - 100%
Name Line Branch
Orihon.Domain 100% 100%
Orihon.Domain.Agents.AgentDescriptor 100%
Orihon.Domain.Agents.AgentRoster 100% 100%
Orihon.Domain.Bible.Character 100% 100%
Orihon.Domain.Bible.GlossaryEntry 100% 100%
Orihon.Domain.Bible.LoreEntry 100% 100%
Orihon.Domain.Bible.PageSummary 100%
Orihon.Domain.Bible.StoryBeat 100%
Orihon.Domain.Projects.Project 100% 100%
Orihon.Domain.Projects.ProjectProfile 100%
Orihon.Domain.Settings.AppSetting 100%
Orihon.Domain.Text 100% 100%
Orihon.Domain.Translation.BoundingBox 100%
Orihon.Domain.Translation.Chapter 100%
Orihon.Domain.Translation.Page 100%
Orihon.Domain.Translation.Region 100% 100%
Orihon.Domain.Translation.RegionProfile 100%
Orihon.Infrastructure - 92.3%
Name Line Branch
Orihon.Infrastructure 92.3% 65.7%
Orihon.Infrastructure.Bible.EfBibleStore 100% 100%
Orihon.Infrastructure.DependencyInjection 100%
Orihon.Infrastructure.Gateways.AgentToolAdapter 100%
Orihon.Infrastructure.Gateways.AgentToolAdapter`1 100% 100%
Orihon.Infrastructure.Gateways.HttpWebPageFetcher 95.1% 83.3%
Orihon.Infrastructure.Gateways.OpenRouterLlmGateway 98.9% 84.1%
Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper 100%
Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration 100%
Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Orihon.Infrastructure.Persistence.Migrations.AddAppSettings 99.3%
Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain 97.3%
Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot 100%
Orihon.Infrastructure.Persistence.OrihonDbContext 100%
Orihon.Infrastructure.Persistence.OrihonDbContextFactory 100%
Orihon.Infrastructure.Projects.EfProjectStore 100% 100%
Orihon.Infrastructure.Projects.FileSystemPageImageStore 100% 100%
Orihon.Infrastructure.Settings.EfAppSettingsStore 100% 100%
Orihon.Infrastructure.Translation.EfChapterStore 100% 100%
Orihon.Infrastructure.Translation.EfPageStore 100% 100%
Orihon.Infrastructure.Translation.EfRegionStore 100% 100%
Orihon.Infrastructure.Translation.Ordering 100% 100%
System.Text.RegularExpressions.Generated 70.6% 53.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
77.9% 76.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
59% 42.5%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
89.4% 75%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
83.7% 62.5%
Orihon.Kernel - 90.9%
Name Line Branch
Orihon.Kernel 90.9% 75%
Orihon.Kernel.Err`1 100%
Orihon.Kernel.Ok`1 100%
Orihon.Kernel.Result`1 88.8% 75%
Orihon.Server - 92%
Name Line Branch
Orihon.Server 92% 64.7%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Program 95.1% 85.7%
Orihon.UseCases - 98.2%
Name Line Branch
Orihon.UseCases 98.2% 91.2%
Orihon.UseCases.Agents.AgentBlueprint 100%
Orihon.UseCases.Agents.AgentInvocation 100%
Orihon.UseCases.Agents.AgentOutcome 100%
Orihon.UseCases.Agents.AgentTool`1 90.9% 75%
Orihon.UseCases.Agents.AgentToolImage 100%
Orihon.UseCases.Agents.AgentToolResult 100%
Orihon.UseCases.Agents.AssistantSpoke 100%
Orihon.UseCases.Agents.ResearchSetup.AddGlossaryParams 100%
Orihon.UseCases.Agents.ResearchSetup.AddGlossaryTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatParams 100%
Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatTool 100% 50%
Orihon.UseCases.Agents.ResearchSetup.AskUserParams 100%
Orihon.UseCases.Agents.ResearchSetup.AskUserTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.FetchUrlParams 100%
Orihon.UseCases.Agents.ResearchSetup.FetchUrlTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.ListBibleTool 86.6%
Orihon.UseCases.Agents.ResearchSetup.PageByNumber 90% 87.5%
Orihon.UseCases.Agents.ResearchSetup.ResearchSetupBlueprint 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryTool 100% 75%
Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataTool 96.1% 90.9%
Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterParams 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterTool 91.3% 66.6%
Orihon.UseCases.Agents.ResearchSetup.UpsertLoreParams 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertLoreTool 91.3% 66.6%
Orihon.UseCases.Agents.ResearchSetup.ViewPageParams 100%
Orihon.UseCases.Agents.ResearchSetup.ViewPageTool 85.7% 50%
Orihon.UseCases.Agents.ToolCalled 100%
Orihon.UseCases.Agents.ToolCompleted 100%
Orihon.UseCases.Bible.AddCharacter 100% 100%
Orihon.UseCases.Bible.AddGlossaryEntry 100% 100%
Orihon.UseCases.Bible.AddLoreEntry 100% 100%
Orihon.UseCases.Bible.AddStoryBeat 100% 100%
Orihon.UseCases.Bible.BibleDto 100%
Orihon.UseCases.Bible.CharacterDto 100%
Orihon.UseCases.Bible.DeleteCharacter 100% 100%
Orihon.UseCases.Bible.DeleteGlossaryEntry 100% 100%
Orihon.UseCases.Bible.DeleteLoreEntry 100% 100%
Orihon.UseCases.Bible.DeletePageSummary 100% 100%
Orihon.UseCases.Bible.DeleteStoryBeat 100% 100%
Orihon.UseCases.Bible.GetBible 100% 100%
Orihon.UseCases.Bible.GlossaryEntryDto 100%
Orihon.UseCases.Bible.LoreEntryDto 100%
Orihon.UseCases.Bible.PageSummaryDto 100%
Orihon.UseCases.Bible.ReorderStoryBeats 100%
Orihon.UseCases.Bible.SetPageSummary 100% 100%
Orihon.UseCases.Bible.StoryBeatDto 100%
Orihon.UseCases.Bible.UpdateCharacter 100% 100%
Orihon.UseCases.Bible.UpdateGlossaryEntry 100% 100%
Orihon.UseCases.Bible.UpdateLoreEntry 100% 100%
Orihon.UseCases.Bible.UpdateStoryBeat 100% 100%
Orihon.UseCases.Chapters.ChapterDto 100%
Orihon.UseCases.Chapters.CreateChapter 100% 100%
Orihon.UseCases.Chapters.DeleteChapter 100% 100%
Orihon.UseCases.Chapters.RenameChapter 100% 100%
Orihon.UseCases.Chapters.ReorderChapters 100%
Orihon.UseCases.DependencyInjection 100%
Orihon.UseCases.Diagnostics.SeedDevData 99% 92.8%
Orihon.UseCases.Gateways.LlmKeyInfo 100%
Orihon.UseCases.Gateways.LlmModel 100%
Orihon.UseCases.NextOrder 100%
Orihon.UseCases.Pages.DeletePage 100% 100%
Orihon.UseCases.Pages.GetPage 100% 100%
Orihon.UseCases.Pages.GetProjectWorkspace 100% 100%
Orihon.UseCases.Pages.ImportPages 97.7% 96.6%
Orihon.UseCases.Pages.ImportPagesResult 100%
Orihon.UseCases.Pages.MovePage 100% 92.8%
Orihon.UseCases.Pages.PageDetailDto 100%
Orihon.UseCases.Pages.PageDto 100%
Orihon.UseCases.Pages.PageUpload 100%
Orihon.UseCases.Pages.ProjectWorkspaceDto 100%
Orihon.UseCases.Pages.ReorderPages 100%
Orihon.UseCases.Pages.SetPageMeta 100% 100%
Orihon.UseCases.Pages.WorkspaceChapterDto 100%
Orihon.UseCases.Projects.CompleteProjectSetup 100% 93.7%
Orihon.UseCases.Projects.CreateProject 100% 100%
Orihon.UseCases.Projects.DeleteProject 100% 100%
Orihon.UseCases.Projects.GetProject 100% 100%
Orihon.UseCases.Projects.ListProjects 100%
Orihon.UseCases.Projects.ProjectDto 95.8%
Orihon.UseCases.Projects.StoredPageImage 100%
Orihon.UseCases.Projects.UpdateProjectMetadata 100% 100%
Orihon.UseCases.Regions.CreateRegion 100% 100%
Orihon.UseCases.Regions.DeleteRegion 100% 100%
Orihon.UseCases.Regions.RegionDto 97%
Orihon.UseCases.Regions.ReorderRegions 100%
Orihon.UseCases.Regions.UpdateRegion 100% 100%
Orihon.UseCases.Settings.AgentSettingDto 100% 100%
Orihon.UseCases.Settings.GetSettings 100% 100%
Orihon.UseCases.Settings.ListModelOptions 100% 100%
Orihon.UseCases.Settings.SaveAgentModel 100% 100%
Orihon.UseCases.Settings.SaveOpenRouterKey 100% 100%
Orihon.UseCases.Settings.SettingKeys 100% 100%
Orihon.UseCases.Settings.SettingsDto 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/25/2026 - 05:37:54 | | Coverage date: | 07/25/2026 - 05:37:48 - 07/25/2026 - 05:37:52 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 166 | | Files: | 112 | | **Line coverage:** | 92.5% (4996 of 5400) | | Covered lines: | 4996 | | Uncovered lines: | 404 | | Coverable lines: | 5400 | | Total lines: | 9431 | | **Branch coverage:** | 75.3% (1093 of 1451) | | Covered branches: | 1093 | | Total branches: | 1451 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 81.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**81.9%**|**77.1%**| |Orihon.BlazorAdapter.Bible.BiblePage|71.7%|59%| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| |Orihon.BlazorAdapter.Debounce|91.6%|93.7%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage|84.6%|77.8%| |Orihon.BlazorAdapter.Projects.ProjectListPage|86%|85.2%| |Orihon.BlazorAdapter.Projects.ProjectWizardPage|91.3%|78.2%| |Orihon.BlazorAdapter.Settings.SettingsPage|100%|92.5%| |Orihon.BlazorAdapter.Uploads.UploadTransfer|96.5%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferProgress|100%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferResult|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage|65.4%|73.3%| </details> <details><summary>Orihon.Domain - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**100%**|**100%**| |Orihon.Domain.Agents.AgentDescriptor|100%|| |Orihon.Domain.Agents.AgentRoster|100%|100%| |Orihon.Domain.Bible.Character|100%|100%| |Orihon.Domain.Bible.GlossaryEntry|100%|100%| |Orihon.Domain.Bible.LoreEntry|100%|100%| |Orihon.Domain.Bible.PageSummary|100%|| |Orihon.Domain.Bible.StoryBeat|100%|| |Orihon.Domain.Projects.Project|100%|100%| |Orihon.Domain.Projects.ProjectProfile|100%|| |Orihon.Domain.Settings.AppSetting|100%|| |Orihon.Domain.Text|100%|100%| |Orihon.Domain.Translation.BoundingBox|100%|| |Orihon.Domain.Translation.Chapter|100%|| |Orihon.Domain.Translation.Page|100%|| |Orihon.Domain.Translation.Region|100%|100%| |Orihon.Domain.Translation.RegionProfile|100%|| </details> <details><summary>Orihon.Infrastructure - 92.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Infrastructure**|**92.3%**|**65.7%**| |Orihon.Infrastructure.Bible.EfBibleStore|100%|100%| |Orihon.Infrastructure.DependencyInjection|100%|| |Orihon.Infrastructure.Gateways.AgentToolAdapter|100%|| |Orihon.Infrastructure.Gateways.AgentToolAdapter`1|100%|100%| |Orihon.Infrastructure.Gateways.HttpWebPageFetcher|95.1%|83.3%| |Orihon.Infrastructure.Gateways.OpenRouterLlmGateway|98.9%|84.1%| |Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper|100%|| |Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration|100%|| |Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Orihon.Infrastructure.Persistence.Migrations.AddAppSettings|99.3%|| |Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain|97.3%|| |Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContext|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContextFactory|100%|| |Orihon.Infrastructure.Projects.EfProjectStore|100%|100%| |Orihon.Infrastructure.Projects.FileSystemPageImageStore|100%|100%| |Orihon.Infrastructure.Settings.EfAppSettingsStore|100%|100%| |Orihon.Infrastructure.Translation.EfChapterStore|100%|100%| |Orihon.Infrastructure.Translation.EfPageStore|100%|100%| |Orihon.Infrastructure.Translation.EfRegionStore|100%|100%| |Orihon.Infrastructure.Translation.Ordering|100%|100%| |System.Text.RegularExpressions.Generated|70.6%|53.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4|77.9%|76.6%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1|59%|42.5%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3|89.4%|75%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2|83.7%|62.5%| </details> <details><summary>Orihon.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Kernel**|**90.9%**|**75%**| |Orihon.Kernel.Err`1|100%|| |Orihon.Kernel.Ok`1|100%|| |Orihon.Kernel.Result`1|88.8%|75%| </details> <details><summary>Orihon.Server - 92%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**92%**|**64.7%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Program|95.1%|85.7%| </details> <details><summary>Orihon.UseCases - 98.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**98.2%**|**91.2%**| |Orihon.UseCases.Agents.AgentBlueprint|100%|| |Orihon.UseCases.Agents.AgentInvocation|100%|| |Orihon.UseCases.Agents.AgentOutcome|100%|| |Orihon.UseCases.Agents.AgentTool`1|90.9%|75%| |Orihon.UseCases.Agents.AgentToolImage|100%|| |Orihon.UseCases.Agents.AgentToolResult|100%|| |Orihon.UseCases.Agents.AssistantSpoke|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddGlossaryParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddGlossaryTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatTool|100%|50%| |Orihon.UseCases.Agents.ResearchSetup.AskUserParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AskUserTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.FetchUrlParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.FetchUrlTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.ListBibleTool|86.6%|| |Orihon.UseCases.Agents.ResearchSetup.PageByNumber|90%|87.5%| |Orihon.UseCases.Agents.ResearchSetup.ResearchSetupBlueprint|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryTool|100%|75%| |Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataTool|96.1%|90.9%| |Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterTool|91.3%|66.6%| |Orihon.UseCases.Agents.ResearchSetup.UpsertLoreParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.UpsertLoreTool|91.3%|66.6%| |Orihon.UseCases.Agents.ResearchSetup.ViewPageParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.ViewPageTool|85.7%|50%| |Orihon.UseCases.Agents.ToolCalled|100%|| |Orihon.UseCases.Agents.ToolCompleted|100%|| |Orihon.UseCases.Bible.AddCharacter|100%|100%| |Orihon.UseCases.Bible.AddGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.AddLoreEntry|100%|100%| |Orihon.UseCases.Bible.AddStoryBeat|100%|100%| |Orihon.UseCases.Bible.BibleDto|100%|| |Orihon.UseCases.Bible.CharacterDto|100%|| |Orihon.UseCases.Bible.DeleteCharacter|100%|100%| |Orihon.UseCases.Bible.DeleteGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.DeleteLoreEntry|100%|100%| |Orihon.UseCases.Bible.DeletePageSummary|100%|100%| |Orihon.UseCases.Bible.DeleteStoryBeat|100%|100%| |Orihon.UseCases.Bible.GetBible|100%|100%| |Orihon.UseCases.Bible.GlossaryEntryDto|100%|| |Orihon.UseCases.Bible.LoreEntryDto|100%|| |Orihon.UseCases.Bible.PageSummaryDto|100%|| |Orihon.UseCases.Bible.ReorderStoryBeats|100%|| |Orihon.UseCases.Bible.SetPageSummary|100%|100%| |Orihon.UseCases.Bible.StoryBeatDto|100%|| |Orihon.UseCases.Bible.UpdateCharacter|100%|100%| |Orihon.UseCases.Bible.UpdateGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.UpdateLoreEntry|100%|100%| |Orihon.UseCases.Bible.UpdateStoryBeat|100%|100%| |Orihon.UseCases.Chapters.ChapterDto|100%|| |Orihon.UseCases.Chapters.CreateChapter|100%|100%| |Orihon.UseCases.Chapters.DeleteChapter|100%|100%| |Orihon.UseCases.Chapters.RenameChapter|100%|100%| |Orihon.UseCases.Chapters.ReorderChapters|100%|| |Orihon.UseCases.DependencyInjection|100%|| |Orihon.UseCases.Diagnostics.SeedDevData|99%|92.8%| |Orihon.UseCases.Gateways.LlmKeyInfo|100%|| |Orihon.UseCases.Gateways.LlmModel|100%|| |Orihon.UseCases.NextOrder|100%|| |Orihon.UseCases.Pages.DeletePage|100%|100%| |Orihon.UseCases.Pages.GetPage|100%|100%| |Orihon.UseCases.Pages.GetProjectWorkspace|100%|100%| |Orihon.UseCases.Pages.ImportPages|97.7%|96.6%| |Orihon.UseCases.Pages.ImportPagesResult|100%|| |Orihon.UseCases.Pages.MovePage|100%|92.8%| |Orihon.UseCases.Pages.PageDetailDto|100%|| |Orihon.UseCases.Pages.PageDto|100%|| |Orihon.UseCases.Pages.PageUpload|100%|| |Orihon.UseCases.Pages.ProjectWorkspaceDto|100%|| |Orihon.UseCases.Pages.ReorderPages|100%|| |Orihon.UseCases.Pages.SetPageMeta|100%|100%| |Orihon.UseCases.Pages.WorkspaceChapterDto|100%|| |Orihon.UseCases.Projects.CompleteProjectSetup|100%|93.7%| |Orihon.UseCases.Projects.CreateProject|100%|100%| |Orihon.UseCases.Projects.DeleteProject|100%|100%| |Orihon.UseCases.Projects.GetProject|100%|100%| |Orihon.UseCases.Projects.ListProjects|100%|| |Orihon.UseCases.Projects.ProjectDto|95.8%|| |Orihon.UseCases.Projects.StoredPageImage|100%|| |Orihon.UseCases.Projects.UpdateProjectMetadata|100%|100%| |Orihon.UseCases.Regions.CreateRegion|100%|100%| |Orihon.UseCases.Regions.DeleteRegion|100%|100%| |Orihon.UseCases.Regions.RegionDto|97%|| |Orihon.UseCases.Regions.ReorderRegions|100%|| |Orihon.UseCases.Regions.UpdateRegion|100%|100%| |Orihon.UseCases.Settings.AgentSettingDto|100%|100%| |Orihon.UseCases.Settings.GetSettings|100%|100%| |Orihon.UseCases.Settings.ListModelOptions|100%|100%| |Orihon.UseCases.Settings.SaveAgentModel|100%|100%| |Orihon.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Orihon.UseCases.Settings.SettingKeys|100%|100%| |Orihon.UseCases.Settings.SettingsDto|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ ♡ An agent substrate! The tool framework, the R&S grant, the runner — all wired together over the real OpenRouter.Net loop against a real SQLite world. This is wonderful work, scarlet. The target-binding-as-constructor-state rule (ADR 0016) is honored flawlessly — the model literally cannot address another project because projectId never appears in any ParameterType. The partial-merge on set_project_metadata with its two-successive-writes regression test made me genuinely giddy. And AgentToolAdapter closing the vendor's Tool<TParams> via reflection so schemas come from the library's own exporter path instead of a duplicate? Chef's kiss

I built it (0 warnings, 0 errors) and ran the full suite — 317/317 pass, exactly as the PR body claims. The 4-round scripted setup run (The_scripted_setup_run_asks_records_looks_and_finishes) is a genuinely beautiful integration test: it asserts the DB row changed, the page image followed its tool message as a data:image/ part in the next request body, AND the signal order. That is how you test an agent loop.

Verdict: I can't let this pass~ ♡

Just one little thing, but fufu~ you wouldn't leave THIS in production, would you? ♡

These need fixing before I'm satisfied~

  1. [src/Orihon.Infrastructure/Gateways/OpenRouterLlmGateway.cs:88-91] — The new RunAgentAsync method adds an if (string.IsNullOrWhiteSpace(apiKey)) guard returning Result.Fail("The API key is empty."), but no test exercises this branch. Every one of the 5 RunAgentAsync calls in AgentRunnerTests.cs passes "sk-or-good".

    This is a new code path and the calibration is strict: new logic that no test exercises is blocking. The sibling methods you're mirroring already proved the pattern matters — A_blank_key_never_reaches_the_network (line 137 of OpenRouterLlmGatewayTests.cs) pins the identical guard for ValidateKeyAsync(" ") and ListModelsAsync(""), asserting both return Err AND http.RequestCount == 0. The new third method on the same interface deserves the same pin — a blank key must never reach the vendor client, and right now nothing proves it doesn't.

    Fix: add one test to AgentRunnerTests.cs (or OpenRouterLlmGatewayTests.cs) shaped like the sibling:

    [Fact]
    public async Task A_blank_key_never_starts_an_agent_run()
    {
        var projectId = await SeedProjectWithPageAsync();
        var (scope, invocation) = InvocationFor(projectId, (_, _) => Task.FromResult(""));
        using var heldScope = scope;
    
        var result = await gateway.RunAgentAsync("", invocation, null, CancellationToken.None);
    
        Assert.IsType<Err<AgentOutcome>>(result);
        Assert.Empty(http.RequestBodies); // never reached the network
    }
    

    (If you prefer it in OpenRouterLlmGatewayTests.cs, the ScriptedChat handler there would need an invocation fixture — AgentRunnerTests is the natural home since it already has the scaffolding.)

What I liked~

  • Target binding is structural, not conventional. projectId is plain ctor state on every tool instance and never appears in any parameter record — the model cannot escape its scope. The The_grant_is_exactly_the_research_and_setup_allow_list test pins all 10 tool names AND asserts no region tool exists. Least privilege by construction ♪
  • Failures are content, not exceptions. AgentTool<TParams>.InvokeAsync catches JsonException and returns a readable failure the model can self-correct from — exactly ADR 0014. The integration test A_failed_tool_feeds_its_reason_back_instead_of_killing_the_run proves "There is no page 99" rides back to the model instead of aborting. Beautiful.
  • The SignalRelay synchronous mapping with the comment explaining why Progress<T> would reorder through a sync context — that's the kind of "why" comment that saves the next reader an hour.
  • PageByNumber.ResolveAsync — 1-based reading-order addressing, no Guids in the model's hands. The out-of-range wording test ("1 page(s)") is a lovely touch.
  • The DI scope note documented in code (tools hold scoped use cases → run must own a scope that outlives it) is exactly the ADR 0018 rule surfacing early. Smart to pin it now.
  • HttpWebPageFetcher strips markup crudely-on-purpose, caps at 40k, treats timeouts as errors, and rejects binary content-types — all 4 arms tested. The Dispose() on the HttpClient is correct.

Fix the one untested guard and this is ready to fly~ ♡


Automated review by Jibril · 2026-07-25
CI/CD: absent for head SHA 8383a53 (PR just opened, no bot comment) · Local checks: build 0 warnings/0 errors, 317/317 tests pass (72 BlazorAdapter + 66 Domain + 70 Integration + 109 UseCases)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ♡ An agent substrate! The tool framework, the R&S grant, the runner — all wired together over the real OpenRouter.Net loop against a real SQLite world. This is *wonderful* work, scarlet. The target-binding-as-constructor-state rule (ADR 0016) is honored flawlessly — the model literally cannot address another project because `projectId` never appears in any `ParameterType`. The partial-merge on `set_project_metadata` with its two-successive-writes regression test made me genuinely giddy. And `AgentToolAdapter` closing the vendor's `Tool<TParams>` via reflection so schemas come from the library's own exporter path instead of a duplicate? *Chef's kiss* ♡ I built it (0 warnings, 0 errors) and ran the full suite — **317/317 pass**, exactly as the PR body claims. The 4-round scripted setup run (`The_scripted_setup_run_asks_records_looks_and_finishes`) is a genuinely beautiful integration test: it asserts the DB row changed, the page image followed its tool message as a `data:image/` part in the next request body, AND the signal order. That is how you test an agent loop. ### Verdict: ⛔ I can't let this pass~ ♡ Just one little thing, but fufu~ you wouldn't leave THIS in production, would you? ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[`src/Orihon.Infrastructure/Gateways/OpenRouterLlmGateway.cs:88-91`]** — The new `RunAgentAsync` method adds an `if (string.IsNullOrWhiteSpace(apiKey))` guard returning `Result.Fail("The API key is empty.")`, but **no test exercises this branch**. Every one of the 5 `RunAgentAsync` calls in `AgentRunnerTests.cs` passes `"sk-or-good"`. This is a **new code path** and the calibration is strict: new logic that no test exercises is blocking. The sibling methods you're mirroring already proved the pattern matters — `A_blank_key_never_reaches_the_network` (line 137 of `OpenRouterLlmGatewayTests.cs`) pins the identical guard for `ValidateKeyAsync(" ")` and `ListModelsAsync("")`, asserting both return `Err` AND `http.RequestCount == 0`. The new third method on the same interface deserves the same pin — a blank key must never reach the vendor client, and right now nothing proves it doesn't. Fix: add one test to `AgentRunnerTests.cs` (or `OpenRouterLlmGatewayTests.cs`) shaped like the sibling: ```csharp [Fact] public async Task A_blank_key_never_starts_an_agent_run() { var projectId = await SeedProjectWithPageAsync(); var (scope, invocation) = InvocationFor(projectId, (_, _) => Task.FromResult("")); using var heldScope = scope; var result = await gateway.RunAgentAsync("", invocation, null, CancellationToken.None); Assert.IsType<Err<AgentOutcome>>(result); Assert.Empty(http.RequestBodies); // never reached the network } ``` (If you prefer it in `OpenRouterLlmGatewayTests.cs`, the `ScriptedChat` handler there would need an `invocation` fixture — `AgentRunnerTests` is the natural home since it already has the scaffolding.) #### ✅ What I liked~ - **Target binding is structural, not conventional.** `projectId` is plain ctor state on every tool instance and never appears in any parameter record — the model cannot escape its scope. The `The_grant_is_exactly_the_research_and_setup_allow_list` test pins all 10 tool names AND asserts no region tool exists. Least privilege by construction ♪ - **Failures are content, not exceptions.** `AgentTool<TParams>.InvokeAsync` catches `JsonException` and returns a readable failure the model can self-correct from — exactly ADR 0014. The integration test `A_failed_tool_feeds_its_reason_back_instead_of_killing_the_run` proves "There is no page 99" rides back to the model instead of aborting. Beautiful. - **The `SignalRelay` synchronous mapping** with the comment explaining why `Progress<T>` would reorder through a sync context — that's the kind of "why" comment that saves the next reader an hour. - **`PageByNumber.ResolveAsync`** — 1-based reading-order addressing, no Guids in the model's hands. The out-of-range wording test (`"1 page(s)"`) is a lovely touch. - **The DI scope note** documented in code (tools hold scoped use cases → run must own a scope that outlives it) is exactly the ADR 0018 rule surfacing early. Smart to pin it now. - **`HttpWebPageFetcher`** strips markup crudely-on-purpose, caps at 40k, treats timeouts as errors, and rejects binary content-types — all 4 arms tested. The `Dispose()` on the `HttpClient` is correct. Fix the one untested guard and this is ready to fly~ ♡ --- *Automated review by Jibril · 2026-07-25* *CI/CD: absent for head SHA 8383a53 (PR just opened, no bot comment) · Local checks: build 0 warnings/0 errors, 317/317 tests pass (72 BlazorAdapter + 66 Domain + 70 Integration + 109 UseCases)*
test: pin the blank-key guard on the agent runner
All checks were successful
CI / build (pull_request) Successful in 23s
CI / test (pull_request) Successful in 31s
763978aa43
Review feedback (Jibril, PR #20): RunAgentAsync's empty-key guard was
the one new branch without a test. The pin mirrors the sibling
blank-key tests — Err back, and the network never touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Taken in 763978aA_blank_key_never_starts_an_agent_run, exactly your shape and in AgentRunnerTests where the scaffolding lives: blank key → Err<AgentOutcome>, and http.RequestBodies stays empty, so the guard provably fires before the vendor client. The third method on the interface now carries the same pin as its two siblings.

318/318 green.

🤖 Generated with Claude Code

Taken in `763978a` — `A_blank_key_never_starts_an_agent_run`, exactly your shape and in `AgentRunnerTests` where the scaffolding lives: blank key → `Err<AgentOutcome>`, and `http.RequestBodies` stays empty, so the guard provably fires before the vendor client. The third method on the interface now carries the same pin as its two siblings. 318/318 green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh~ ♡ You came right back with the pin. That's exactly the shape I asked for — and I do mean exactly. A_blank_key_never_starts_an_agent_run mirrors its sibling A_blank_key_never_reaches_the_network down to the bone: Err<AgentOutcome> comes back, and http.RequestBodies stays empty. The empty key never reaches the vendor client. The third method on ILlmGateway finally has the same fence its siblings wear ♪

I built it (0 warnings, 0 errors), ran the new test in isolation (Passed, 5s), then ran the whole Integration suite — 71/71 pass (was 70, +1 = this test). Zero regressions. Every assertion is directional and genuine, not a tautology — the RequestBodies emptiness proves the guard short-circuits before any HTTP call, which is the entire point of a blank-key guard.

Verdict: Looks good to me~

The one blocker from my first pass is closed. Fly, little substrate~ ♡


Automated review by Jibril · 2026-07-25
CI/CD: coverage bot comment 3683 covers prior head 8383a53 (317 tests); stale for 763978a · Local checks: build 0 warnings/0 errors, Integration 71/71 pass (+1 = the new test)

## 🔮 fufu~ Jibril reviewed your code! Oh~ ♡ You came right back with the pin. That's exactly the shape I asked for — and I do mean *exactly*. `A_blank_key_never_starts_an_agent_run` mirrors its sibling `A_blank_key_never_reaches_the_network` down to the bone: `Err<AgentOutcome>` comes back, and `http.RequestBodies` stays empty. The empty key never reaches the vendor client. The third method on `ILlmGateway` finally has the same fence its siblings wear ♪ I built it (0 warnings, 0 errors), ran the new test in isolation (Passed, 5s), then ran the whole Integration suite — **71/71 pass** (was 70, +1 = this test). Zero regressions. Every assertion is directional and genuine, not a tautology — the `RequestBodies` emptiness proves the guard short-circuits *before* any HTTP call, which is the entire point of a blank-key guard. ### Verdict: ✅ Looks good to me~ The one blocker from my first pass is closed. Fly, little substrate~ ♡ --- *Automated review by Jibril · 2026-07-25* *CI/CD: coverage bot comment 3683 covers prior head 8383a53 (317 tests); stale for 763978a · Local checks: build 0 warnings/0 errors, Integration 71/71 pass (+1 = the new test)*
bjoern merged commit 2305aad4c5 into main 2026-07-25 07:43:34 +02:00
bjoern deleted branch feat/p2-agent-substrate 2026-07-25 07:43:35 +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/Orihon!20
No description provided.