feat: Phase 2 · 4/7 — agent substrate: tool framework, R&S grant, runner #20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p2-agent-substrate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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).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 newIWebPageFetcherport (+HttpWebPageFetcher: strip-to-readable-text, 40k cap, timeouts as errors). Web search stays OpenRouter's native:onlinecapability 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 throughGetProjectWorkspace; no Guids in the model's hands.view_pagereturns 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)
AgentToolAdaptercloses the vendor's ownTool<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) throughOpenRouter.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 toAgentSignals synchronously (aProgress<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)
data:image/part in the next request body, and the signal order; tool schemas on the wire ("type":"object", snake_casestore_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.🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 81.9%
Orihon.Domain - 100%
Orihon.Infrastructure - 92.3%
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 92%
Orihon.UseCases - 98.2%
🔮 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
projectIdnever appears in anyParameterType. The partial-merge onset_project_metadatawith its two-successive-writes regression test made me genuinely giddy. AndAgentToolAdapterclosing the vendor'sTool<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 adata: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~
[
src/Orihon.Infrastructure/Gateways/OpenRouterLlmGateway.cs:88-91] — The newRunAgentAsyncmethod adds anif (string.IsNullOrWhiteSpace(apiKey))guard returningResult.Fail("The API key is empty."), but no test exercises this branch. Every one of the 5RunAgentAsynccalls inAgentRunnerTests.cspasses"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 ofOpenRouterLlmGatewayTests.cs) pins the identical guard forValidateKeyAsync(" ")andListModelsAsync(""), asserting both returnErrANDhttp.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(orOpenRouterLlmGatewayTests.cs) shaped like the sibling:(If you prefer it in
OpenRouterLlmGatewayTests.cs, theScriptedChathandler there would need aninvocationfixture —AgentRunnerTestsis the natural home since it already has the scaffolding.)✅ What I liked~
projectIdis plain ctor state on every tool instance and never appears in any parameter record — the model cannot escape its scope. TheThe_grant_is_exactly_the_research_and_setup_allow_listtest pins all 10 tool names AND asserts no region tool exists. Least privilege by construction ♪AgentTool<TParams>.InvokeAsynccatchesJsonExceptionand returns a readable failure the model can self-correct from — exactly ADR 0014. The integration testA_failed_tool_feeds_its_reason_back_instead_of_killing_the_runproves "There is no page 99" rides back to the model instead of aborting. Beautiful.SignalRelaysynchronous mapping with the comment explaining whyProgress<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.HttpWebPageFetcherstrips markup crudely-on-purpose, caps at 40k, treats timeouts as errors, and rejects binary content-types — all 4 arms tested. TheDispose()on theHttpClientis 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)Taken in
763978a—A_blank_key_never_starts_an_agent_run, exactly your shape and inAgentRunnerTestswhere the scaffolding lives: blank key →Err<AgentOutcome>, andhttp.RequestBodiesstays 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
🔮 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_runmirrors its siblingA_blank_key_never_reaches_the_networkdown to the bone:Err<AgentOutcome>comes back, andhttp.RequestBodiesstays empty. The empty key never reaches the vendor client. The third method onILlmGatewayfinally 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
RequestBodiesemptiness 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 for763978a· Local checks: build 0 warnings/0 errors, Integration 71/71 pass (+1 = the new test)