Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,11 @@
<PackageVersion Include="NSec.Cryptography" Version="25.4.0" />
<PackageVersion Include="pocketken.H3" Version="4.0.0" />
</ItemGroup>
<!-- MCP / A2A protocol adapters (Tier-2 adapter projects and hosts only; the kernel
spine — Nexo.Abstractions / Nexo.Core.* / Nexo.Contracts / Nexo.Hosting — must
never reference these packages, or every AddNexo consumer inherits them. -->
<ItemGroup>
<PackageVersion Include="ModelContextProtocol" Version="2.2.0" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="2.2.0" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions LICENSING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Rationale: this tier protects two moats at once — an extensible SDK and a sing
| OPEN | `src/Nexo.Transport.Grpc/Nexo.Transport.Grpc.csproj` |
| OPEN | `src/Nexo.Transport.Grpc.Server/Nexo.Transport.Grpc.Server.csproj` |
| OPEN | `src/Nexo.Transport.Grpc.Server.Host/Nexo.Transport.Grpc.Server.Host.csproj` |
| OPEN | `src/Nexo.Mcp.Server/Nexo.Mcp.Server.csproj` |
| OPEN | `src/Nexo.Mcp.Server.Host/Nexo.Mcp.Server.Host.csproj` |
| OPEN | `src/Nexo.Mcp.Server.Tests/Nexo.Mcp.Server.Tests.csproj` |
| OPEN | `src/Nexo.Tests.Application/Nexo.Tests.Application.csproj` |
| OPEN | `src/Nexo.Tests.BackgroundAgents/Nexo.Tests.BackgroundAgents.csproj` |
| OPEN | `src/Nexo.Tests.Contracts/Nexo.Tests.Contracts.csproj` |
Expand Down
45 changes: 45 additions & 0 deletions Nexo.sln
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Analyzers", "src\Nexo.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Analyzers.Tests", "src\Nexo.Analyzers.Tests\Nexo.Analyzers.Tests.csproj", "{0ED1235E-0E34-400E-B6CD-1C22D0560D2E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Mcp.Server", "src\Nexo.Mcp.Server\Nexo.Mcp.Server.csproj", "{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Mcp.Server.Host", "src\Nexo.Mcp.Server.Host\Nexo.Mcp.Server.Host.csproj", "{97B79E4E-E40C-4229-8368-DC7993FC14BE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Mcp.Server.Tests", "src\Nexo.Mcp.Server.Tests\Nexo.Mcp.Server.Tests.csproj", "{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -975,6 +981,42 @@ Global
{0ED1235E-0E34-400E-B6CD-1C22D0560D2E}.Release|x64.Build.0 = Release|Any CPU
{0ED1235E-0E34-400E-B6CD-1C22D0560D2E}.Release|x86.ActiveCfg = Release|Any CPU
{0ED1235E-0E34-400E-B6CD-1C22D0560D2E}.Release|x86.Build.0 = Release|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Debug|x64.ActiveCfg = Debug|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Debug|x64.Build.0 = Debug|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Debug|x86.ActiveCfg = Debug|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Debug|x86.Build.0 = Debug|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Release|Any CPU.Build.0 = Release|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Release|x64.ActiveCfg = Release|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Release|x64.Build.0 = Release|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Release|x86.ActiveCfg = Release|Any CPU
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97}.Release|x86.Build.0 = Release|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Debug|x64.ActiveCfg = Debug|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Debug|x64.Build.0 = Debug|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Debug|x86.ActiveCfg = Debug|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Debug|x86.Build.0 = Debug|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Release|Any CPU.Build.0 = Release|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Release|x64.ActiveCfg = Release|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Release|x64.Build.0 = Release|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Release|x86.ActiveCfg = Release|Any CPU
{97B79E4E-E40C-4229-8368-DC7993FC14BE}.Release|x86.Build.0 = Release|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Debug|x64.ActiveCfg = Debug|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Debug|x64.Build.0 = Debug|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Debug|x86.ActiveCfg = Debug|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Debug|x86.Build.0 = Debug|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Release|Any CPU.Build.0 = Release|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Release|x64.ActiveCfg = Release|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Release|x64.Build.0 = Release|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Release|x86.ActiveCfg = Release|Any CPU
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1050,6 +1092,9 @@ Global
{275010BE-F67F-4538-946D-A5F8BC6BFCB1} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F}
{AF97E930-0777-4223-A249-F4B45672C390} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F}
{0ED1235E-0E34-400E-B6CD-1C22D0560D2E} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F}
{003FFC1F-C2A4-4A5A-BE4C-C3A3818BFA97} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F}
{97B79E4E-E40C-4229-8368-DC7993FC14BE} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F}
{D01611B1-E0C8-4F44-8D9E-768CF3BD7394} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {12345678-1234-1234-1234-123456789ABC}
Expand Down
1 change: 1 addition & 0 deletions docs/DocsIndex.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Documentation index for the Nexo platform. Start here to find what you need.
- `docs/samples/StableSdkHostSample/Program.cs` — reference host integration that only uses stable SDK extension points.
- `docs/runtime/ExecutionRouting.md` — NCR-based generation routing (local, peer network, RunPod), preferences, and resilience behavior.
- `docs/AgentExecutionIsolation.md` — per-agent isolation tiers (in-process through container-per-agent), JSON field, and invocation metadata for transports.
- `docs/architecture/ProtocolIntegration-MCP-A2A.md` — MCP + A2A protocol adapters: MCP server bridge over `ITool` (allowlists, policy gate, stdio host), planned MCP client and A2A server/client phases.
- `docs/runtime/specs/README.md` — runtime spec documents.
- `docs/runtime/benchmarks/README.md` — runtime benchmark goals and notes.
- `apps/runtime-studio/README.md` — **hub** for the Runtime Studio agent-set JSON, CLI vs API-hosted background agents, and how the Director portal fits; anchor [How this fits](../apps/runtime-studio/README.md#how-runtime-studio-fits-with-nexo-api).
Expand Down
116 changes: 116 additions & 0 deletions docs/architecture/ProtocolIntegration-MCP-A2A.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Protocol Integration: MCP and A2A

Nexo speaks the two industry-standard agent protocols through Tier-2 adapter projects:

- **MCP (Model Context Protocol)** — AI clients (Claude, IDEs, other agents) discover and call
tools. Nexo acts as an **MCP server** (exposing allowlisted Nexo tools) and, in a later phase,
as an **MCP client** (consuming external MCP servers' tools as `ITool`s).
- **A2A (Agent2Agent)** — peer agents discover each other via agent cards and delegate tasks.
Planned: Nexo as an **A2A server** (external agents delegate to allowlisted Nexo agents) and as
an **A2A client** (`A2AAgentTransport` behind the existing endpoint routing).

Both protocols use the official SDKs (`ModelContextProtocol` 2.x, stable; `A2A` 1.0 preview) —
the hand-rolled JSON-RPC endpoint in the commercial Game Director vertical predates this layer
and is unaffected.

## Design rules (all phases)

1. **Kernel spine stays clean.** `Nexo.Abstractions`, `Nexo.Core.*`, `Nexo.Contracts`, and
`Nexo.Hosting` never reference protocol SDK packages. Hosts opt in by calling the
`AddNexo*` extensions from the adapter projects (same pattern as
`Nexo.Transport.Grpc.Server.Host`). This also keeps preview packages out of the
`Nexo.Hosting` NuGet pack graph.
2. **Fail-closed everywhere.** Every surface has `Enabled=false` defaults, explicit allowlists
that default to empty, `ValidateOnStart` options validation, and a hard refusal to enable
under `NEXO_DEPLOYMENT_PROFILE=airgapped`.
3. **Existing seams, not parallel plumbing.** Tools flow through `ITool`/`ToolSchema`
(`src/Nexo.Abstractions`), agents will flow through `IAgentTransport` + endpoint routing,
and HTTP exposure lands behind the Nexo API middleware chain under `/api/...`.

## Phase 1 (this phase): Nexo as MCP server

Projects:

| Project | Role |
|---|---|
| `src/Nexo.Mcp.Server/` | Protocol-agnostic bridge: catalog, policy gate, handlers over the official SDK |
| `src/Nexo.Mcp.Server.Host/` | Standalone **stdio** host for local AI clients (Claude Desktop, Claude Code, IDEs) |
| `src/Nexo.Mcp.Server.Tests/` | Unit tests |

### How exposure works

Tools become callable over MCP only when **both** are true:

1. The host registers the tool in DI: `services.AddSingleton<ITool, RepoFsReadTool>();`
2. The operator allowlists its id: `Nexo__Mcp__Server__ExposedToolIds__0=repo.fs.read`

Every `tools/call` then flows through `NexoMcpToolBridge`, which applies in order:

1. **Catalog routing** — only allowlisted tools exist; unknown names are JSON-RPC
`invalid params` protocol errors.
2. **Argument overrides** — operator-pinned arguments replace caller-supplied ones
(`Nexo__Mcp__Server__ArgumentOverrides__repo.fs.read__root=/srv/repo`). Use this for the
`repo.fs.*` tools, whose `root` argument would otherwise let a remote caller point them at an
arbitrary directory.
3. **Policy gate** — `IMcpInvocationGate` (default: every DI-registered `IPolicy` must approve,
evaluated against the canonical tool id, mirroring `AgentHost`'s approve+audit pair, which MCP
traffic otherwise bypasses). Denials come back as `isError` tool results.
4. **Concurrency ceiling** — `MaxConcurrentToolCalls` (default 8); Nexo tools were written for a
single agent loop and are not assumed reentrant.
5. **Audit logging** — caller principal, canonical id, duration, and outcome.

Startup validation (`McpCatalogStartupValidator`) fails the host boot when the surface is enabled
but the catalog is invalid: allowlisted id with no registered tool, an input schema that is not a
JSON object schema, or two ids collapsing to the same sanitized MCP name (`repo.fs.read` →
`repo_fs_read`; MCP-strict clients reject dots).

### Running the stdio host

```bash
dotnet run --project src/Nexo.Mcp.Server.Host -- \
Nexo__Mcp__Server__ExposedToolIds__0=repo.fs.read \
Nexo__Mcp__Server__ExposedToolIds__1=repo.fs.list \
Nexo__Mcp__Server__RepoRoot=/srv/repo
```

(Values can equally come from environment variables or `appsettings.json`; `Enabled` defaults to
`true` in this host only — running the binary is the opt-in — and all logging goes to stderr
because stdout carries the protocol stream.)

Claude Code registration example:

```bash
claude mcp add nexo -- dotnet run --project src/Nexo.Mcp.Server.Host
```

The host pre-registers only the read-only repo tools (`repo.fs.read`, `repo.fs.list`) for
allowlisting; mutating tools (`repo.fs.write`, `repo.git.commit`, `dotnet.run`, …) must be added
by the operator explicitly in their own host.

### HTTP exposure

`AddNexoMcpServer(configuration).WithHttpTransport()` + `MapNexoMcpEndpoint()` (defaults to
`/api/mcp`, streamable HTTP). The Nexo.API wiring lands in the application-layer phase; the
mapping helper deliberately refuses the commercial endpoint's `AllowAnonymous()` pattern and maps
nothing at all while disabled.

## Later phases (planned)

- **MCP client** — external MCP servers' tools proxied as namespaced `ITool`s
(`mcp:{server}:{tool}`) with pinned descriptions, HTTP transports only in v1.
- **A2A server** — agent-card projection of allowlisted `AgentCard`s; task execution through the
gRPC-facade pattern (barrier identity resolution → registry-validated agent name →
`IAgentTransport.SendAsync`); synchronous terminal tasks in v1.
- **A2A client** — `A2AAgentTransport : IAgentTransport`; peers are ordinary
`EndpointDescriptor`s using an `a2a+https://` scheme convention, dispatched by a
scheme-routing composite so gRPC and A2A coexist.
- **Nexo.API wiring** — `/api/mcp` + `/api/a2a/*` behind an explicit all-verbs auth filter and
rate limiting; `IngressCatalog` rows; per-tenant capability allowlists.

## Deliberately deferred

Durable A2A tasks + SSE streaming + push notifications; MCP client stdio child processes
(needs a command allowlist design); dynamic tool-set mutation (restart to pick up remote tool
changes); MEAI `AIFunction` bridging; mesh `ICapabilityAdvertisement`-based A2A peer discovery;
migration of the commercial Game Director MCP endpoint onto this layer (breaking for its
clients; needs consumer sign-off).
1 change: 1 addition & 0 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ High-level maps of how Nexo is structured. For day-to-day commands, see the repo
| [Kernel phase matrix](KernelPhaseMatrix.md) | `NexoKernelRegistrar` phases, module flags, and `make kernel-gate` proof. |
| [.NET SDK and target frameworks](DotnetVersions.md) | Why `global.json` pins SDK 9.x while many libraries target `net8.0`. |
| [Runtime vs application layout](runtime-vs-application.md) | `src/` kernel vs `application/src/` hosts; `Nexo.Runtime.sln`, `Nexo.Runtime.Bundle`, NuGet metapackages, and packing scripts. |
| [Protocol integration: MCP + A2A](ProtocolIntegration-MCP-A2A.md) | MCP server bridge over `ITool` (allowlists, policy gate, stdio host) and the planned MCP client / A2A phases. |
| [Shipping and consumption (all audiences)](../DistributionModels.md) | How Nexo is packaged (NuGet, containers, HTTP); pinning; **distribution-matrix** CI jobs per channel. |
| [Forge map adaptation](forge-map-adaptation.md) | `MapAdaptationPlanner`, dry-run pipeline, engine manifest JSON, and Forge persistence options. |
| [Forge map host integration](forge-map-host-integration.md) | Milestones M1–M6; terrain payload summaries; optional material **`IModel`** augmentation; tile cache; Unity/Godot package layouts. |
Expand Down
22 changes: 22 additions & 0 deletions src/Nexo.Mcp.Server.Host/Nexo.Mcp.Server.Host.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<!-- Delivery vehicle, not a library: never packed. -->
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Nexo.Hosting\Nexo.Hosting.csproj" />
<ProjectReference Include="..\Nexo.Mcp.Server\Nexo.Mcp.Server.csproj" />
<ProjectReference Include="..\Nexo.Tools.Dev\Nexo.Tools.Dev.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
</Project>
49 changes: 49 additions & 0 deletions src/Nexo.Mcp.Server.Host/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Nexo MCP stdio server host.
//
// Boots the Nexo kernel and serves the MCP tool bridge over stdio for local AI clients
// (Claude Desktop / Claude Code / IDEs spawn this binary and speak MCP over stdin/stdout).
// Mirrors Nexo.Transport.Grpc.Server.Host: a thin delivery vehicle over Nexo.Hosting.
//
// Configuration (env vars or appsettings.json):
// Nexo__Mcp__Server__Enabled defaults to true here — running this binary IS the opt-in;
// still overridable to false, and refused under AirGapped.
// Nexo__Mcp__Server__ExposedToolIds__0 allowlist, e.g. "repo.fs.read" — empty exposes zero tools.
// Nexo__Mcp__Server__RepoRoot world-snapshot root (defaults to the working directory).
// Nexo__Mcp__Server__ArgumentOverrides__repo.fs.read__root
// pin caller-visible args, e.g. force 'root' for repo tools.
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Nexo.Abstractions;
using Nexo.Hosting;
using Nexo.Mcp.Server;
using Nexo.Tools.Dev;

var builder = Host.CreateApplicationBuilder(args);

// Lowest-priority defaults: everything (appsettings, env vars, args) overrides them.
builder.Configuration.Sources.Insert(0, new MemoryConfigurationSource
{
InitialData = new Dictionary<string, string?>
{
[$"{NexoMcpServerOptions.SectionPath}:Enabled"] = "true",
},
});

// stdout belongs to the MCP stdio transport — a single stray log line corrupts the JSON-RPC
// stream, so every console log goes to stderr.
builder.Logging.ClearProviders();
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services.AddNexo();

// Read-only repo tools available for allowlisting out of the box. Nothing is exposed until the
// operator lists ids in ExposedToolIds; mutating tools (repo.fs.write, git commit, dotnet run)
// are deliberately not pre-registered — hosts that want them must add them explicitly.
builder.Services.AddSingleton<ITool, RepoFsReadTool>();
builder.Services.AddSingleton<ITool, RepoFsListTool>();

builder.Services.AddNexoMcpServer(builder.Configuration).WithStdioServerTransport();

await builder.Build().RunAsync();
65 changes: 65 additions & 0 deletions src/Nexo.Mcp.Server.Tests/FakeTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Text.Json;
using Nexo.Abstractions;

namespace Nexo.Mcp.Server.Tests;

/// <summary>Configurable ITool double shared by the contributor and bridge tests.</summary>
internal sealed class FakeTool : ITool
{
private readonly Func<ToolCall, WorldSnapshot, CancellationToken, Task<ToolResult>> _invoke;

public FakeTool(
string id,
string? inputJsonSchema = """{"type":"object"}""",
Func<ToolCall, WorldSnapshot, CancellationToken, Task<ToolResult>>? invoke = null,
string description = "fake tool")
{
Id = id;
Schema = new ToolSchema(id, description, inputJsonSchema);
_invoke = invoke ?? ((_, _, _) => Task.FromResult(new ToolResult(new FakeDelta(), "ok")));
}

public string Id { get; }

public ToolSchema Schema { get; }

public ToolCall? LastCall { get; private set; }

public WorldSnapshot? LastSnapshot { get; private set; }

public Task<ToolResult> InvokeAsync(ToolCall toolCall, WorldSnapshot s, CancellationToken ct)
{
LastCall = toolCall;
LastSnapshot = s;
return _invoke(toolCall, s, ct);
}
}

/// <summary>Minimal IActionDelta for tool results in tests.</summary>
internal sealed class FakeDelta : IActionDelta
{
private readonly List<string> _log = new();

public int TickFrom => 0;

public int TickTo => 1;

public IReadOnlyList<byte>? Signature { get; set; }

public IReadOnlyList<string> Log => _log;

public FakeDelta WithLog(string line)
{
_log.Add(line);
return this;
}
}

internal static class Json
{
public static JsonElement Object(string json = "{}")
{
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
}
Loading
Loading