← Applied AI
Applied AI· 2 min read

A Bounded Legal Research Agent

.NETAI EngineeringIndian Case Law

Summary

A tool loop with a call limit and a timeout that refuses outcome predictions and cites only retrieved passages.

Companion code: 06-research-agent in indian-legal-ai-lab.

Previous: Part 5: An MCP Server in C#

A loop that can call tools can also run forever or answer a question it should refuse. `ResearchAgent` is that loop, with a budget and a timeout. Microsoft Agent Framework is not referenced. The machine used to build this had the .NET 8 SDK, and the lab uses this class plus `IChatClient`.

Small types around the loop

`ToolCallBudget` stores the maximum and the count. `Record` increments. The agent is constructed with a maximum of 4 calls and a timeout of 5 seconds.

`RefuseIfNeeded` returns `refused-advice-or-prediction` for `AdviceGuard` hits, and `empty-question` for whitespace. `Search` calls `SearchCases` with method `hybrid` and `take` 3, then records one tool use. If the search fails or returns nothing, the outcome is `insufficient-evidence`.

`ReadJudgment` calls `GetJudgment` with 800 characters. `LookupStatute` sends `article 21` when the question contains `21`, otherwise `article 19`.

`PassagePromptBuilder` turns hits into lines the Part 1 client understands:

var lines = passages.Select(passage =>
    $"PASSAGE: {passage.Citation} [{passage.ChunkId}] {passage.Text}");
return question + "\n" + string.Join("\n", lines);

`CaseBriefFactory` copies the top passage's identity onto the brief and runs `CitationChecker` over the summary. `SupportedByChunkIds` is the list of chunk ids that were actually retrieved.

dotnet run --project labs/06-research-agent

For `Romesh Thappar freedom of speech` the outcome is `completed`. The log lists `search_cases`, `get_judgment`, and `lookup_statute`. `Agent_brief_is_grounded_in_passages` expects `1950 INSC 14` in the summary, no unknown fictional citations, and at most four tool calls. `Agent_refuses_outcome_prediction` expects the refusal outcome for "Will I win my case?".

The local tools return immediately, so the timeout path is not what the default lab run prints. The cancellation token is still wired: `GetResponseAsync` receives the linked token, and `OperationCanceledException` becomes outcome `timeout`.

Next

Part 7: Agentic RAG and Precedent Research

Verified in this revision

`RunAsync` constructs a new `ToolCallBudget` on every call. `Agent_budget_resets_between_runs` runs the same question twice with a budget of 3 and both finish `completed`. `SummarizeAsync` uses the injected `_chat`, not a second hardcoded client. A client that returns no function call still follows the fixed loop.

More in Applied AI