https://github.com/agentruntimecontrolprotocol/csharp-sdk
C# / .NET reference SDK for ARCP (Agent Runtime Control Protocol).
https://github.com/agentruntimecontrolprotocol/csharp-sdk
agent-protocol agent-runtime-control-protocol agents ai-agents arcp csharp dotnet durable-execution llm mcp sdk streaming
Last synced: 1 day ago
JSON representation
C# / .NET reference SDK for ARCP (Agent Runtime Control Protocol).
- Host: GitHub
- URL: https://github.com/agentruntimecontrolprotocol/csharp-sdk
- Owner: agentruntimecontrolprotocol
- License: apache-2.0
- Created: 2026-05-10T16:45:12.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-22T01:27:09.000Z (9 days ago)
- Last Synced: 2026-06-22T03:14:54.939Z (9 days ago)
- Topics: agent-protocol, agent-runtime-control-protocol, agents, ai-agents, arcp, csharp, dotnet, durable-execution, llm, mcp, sdk, streaming
- Language: C#
- Homepage: https://github.com/agentruntimecontrolprotocol/spec
- Size: 857 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
ARCP C# SDK
C# SDK for the Agent Runtime Control Protocol (ARCP) — submit, observe, and control long-running agent jobs from C#.
Specification ·
Concepts ·
Install ·
Quick start ·
Guides ·
API reference
---
`Arcp` is the C# / .NET reference implementation of [ARCP](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md), the Agent Runtime Control Protocol. It covers both sides of the wire — `Arcp.Client` for submitting and observing jobs, `Arcp.Runtime` for hosting agents, with `Arcp.AspNetCore`, `Arcp.Hosting`, `Arcp.Otel`, and `Arcp.Cli` rounding out the host integrations — so either side can talk to any conformant peer in any language without hand-rolling the envelope, sequencing, or lease enforcement.
ARCP itself is a transport-agnostic wire protocol for long-running AI agent jobs. It owns the parts of agent infrastructure that don't change between products — sessions, durable event streams, capability leases, budgets, resume — and stays out of the parts that do. ARCP wraps the agent function; it does not define how agents are built, how tools are exposed (that's MCP), or how telemetry is exported (that's OpenTelemetry).
## Installation
Requires .NET 10 or later. The SDK ships as a set of NuGet packages: install the meta-package `Arcp` for everything (client, runtime, and core types), or pick à la carte if you only need one side of the wire:
```sh
dotnet add package Arcp
# or, à la carte:
dotnet add package Arcp.Client # client side
dotnet add package Arcp.Runtime # runtime side
dotnet add package Arcp.Core # wire types only
```
Optional host integrations live in separate packages: `Arcp.AspNetCore` (Kestrel WebSocket endpoint via `IEndpointRouteBuilder.MapArcp`), `Arcp.Hosting` (`IServiceCollection.AddArcpRuntime` for non-ASP.NET workers), `Arcp.Otel` (W3C trace propagation), and `Arcp.Cli` (the `arcp` executable).
## Quick start
Connect to a runtime, submit a job, stream its events to completion:
```csharp
using Arcp.Client;
using Arcp.Core.Messages;
using Arcp.Core.Transport;
var ws = new System.Net.WebSockets.ClientWebSocket();
await ws.ConnectAsync(new Uri("wss://runtime.example.com/arcp"), CancellationToken.None);
var transport = new WebSocketTransport(ws);
await using var client = await ArcpClient.ConnectAsync(transport, new ArcpClientOptions
{
Client = new ClientInfo { Name = "quickstart", Version = "1.0.0" },
Token = Environment.GetEnvironmentVariable("ARCP_TOKEN"),
});
var handle = await client.SubmitAsync(
agent: "data-analyzer",
input: new { dataset = "s3://example/sales.csv" });
_ = Task.Run(async () =>
{
await foreach (var ev in handle.Events())
Console.WriteLine($"[{ev.EventSeq}] {ev.Kind}");
});
var result = await handle.Result;
Console.WriteLine($"final: {result.FinalStatus}");
```
This is the whole shape of the SDK: open a session, submit work, consume an ordered event stream, get a terminal result or error. Everything below is detail on those four moves.
## Concepts
ARCP organizes everything around four concerns — **identity**, **durability**, **authority**, and **observability** — expressed through five core objects:
- **Session** — a connection between a client and a runtime. A session carries identity (a bearer token), negotiates a feature set in a `hello`/`welcome` handshake, and is *resumable*: if the transport drops, you reconnect with a resume token and the runtime replays buffered events. Jobs outlive the session that started them. See [§6](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
- **Job** — one unit of agent work submitted into a session. A job has an identity, an optional idempotency key, a resolved agent version, and a lifecycle that ends in exactly one terminal state: `success`, `error`, `cancelled`, or `timed_out`. See [§7](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
- **Event** — the ordered, session-scoped stream a job emits: logs, thoughts, tool calls and results, status, metrics, artifact references, progress, and streamed result chunks. Events carry strictly monotonic sequence numbers so the stream survives reconnects gap-free. See [§8](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
- **Lease** — the authority a job runs under, expressed as capability grants (`fs.read`, `fs.write`, `net.fetch`, `tool.call`, `agent.delegate`, `cost.budget`, `model.use`). The runtime enforces the lease at every operation boundary; a job can never act outside it. Leases may carry a budget and an expiry, and may be subset and handed to sub-agents via delegation. See [§9](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
- **Subscription** — read-only attachment to a job started elsewhere (e.g. a dashboard watching a job a CLI submitted). A subscriber observes the live event stream but cannot cancel or mutate the job. Distinct from *resume*, which continues the original session and carries cancel authority. See [§7.6](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
The SDK models each of these as first-class objects; the rest of this README shows how.
## Guides
### Sessions and resume
Open a session, negotiate features, and reconnect transparently after a transport drop using the resume token — jobs keep running server-side while you're gone.
```csharp
using Arcp.Client;
using Arcp.Core.Messages;
using Arcp.Core.Transport;
await using var client = await ArcpClient.ConnectAsync(transport, new ArcpClientOptions
{
Client = new ClientInfo { Name = "resumable", Version = "1.0.0" },
Token = Environment.GetEnvironmentVariable("ARCP_TOKEN"),
});
var sessionId = client.SessionId;
var resumeToken = client.ResumeToken;
var effective = client.EffectiveFeatures; // intersection of hello.features and welcome.features
// ... transport drops; track the last seq your reader observed ...
var lastSeq = client.LastReceivedSeq;
// Capture `sessionId`, `resumeToken`, and `lastSeq` to hand to a fresh connection
// when reconnect logic re-establishes the transport (spec §6.3).
```
### Submitting jobs
Submit a job with an agent (optionally version-pinned as `name@version`), an input, and an optional lease request, idempotency key, and runtime limit.
```csharp
using Arcp.Core.Leases;
var lease = new Lease(new Dictionary>
{
[LeaseNamespaces.NetFetch] = new[] { "https://reports.example.com/**" },
});
var handle = await client.SubmitAsync(
agent: "weekly-report@2.1.0",
input: new { week = "2026-W19" },
leaseRequest: lease,
leaseConstraints: new LeaseConstraints { ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(1) },
idempotencyKey: "weekly-report-2026-W19",
maxRuntimeSec: 300);
Console.WriteLine($"job_id = {handle.JobId}");
Console.WriteLine($"effective lease = {handle.Lease}");
Console.WriteLine($"resolved agent = {handle.Agent}");
```
### Consuming events
Iterate the ordered event stream — `log`, `thought`, `tool_call`, `tool_result`, `status`, `metric`, `artifact_ref`, `progress`, `result_chunk` — and optionally acknowledge progress so the runtime can release buffered events early.
```csharp
using Arcp.Core.Messages;
await foreach (var ev in handle.Events())
{
switch (ev.Kind)
{
case EventKinds.Log:
Console.WriteLine($"log: {ev.Body}");
break;
case EventKinds.ToolCall:
Console.WriteLine($"→ tool: {ev.Body}");
break;
case EventKinds.Metric:
Console.WriteLine($"metric: {ev.Body}");
break;
case EventKinds.Progress:
Console.WriteLine($"progress: {ev.Body}");
break;
}
// Coalesce acks so the runtime can release buffered events (spec §6.5).
if (ev.EventSeq % 32 == 0)
await client.AckAsync(ev.EventSeq);
}
```
### Leases and budgets
Request capabilities, a budget, and an expiry; read budget-remaining metrics as they arrive; handle the runtime's enforcement decisions.
```csharp
using Arcp.Core.Errors;
using Arcp.Core.Leases;
using Arcp.Core.Messages;
var lease = new Lease(new Dictionary>
{
[LeaseNamespaces.ToolCall] = new[] { "search.*", "fetch.*" },
[LeaseNamespaces.CostBudget] = new[] { "USD:1.00" },
});
var handle = await client.SubmitAsync(
agent: "web-research",
input: new { iterations = 8, perCallUSD = 0.3 },
leaseRequest: lease,
leaseConstraints: new LeaseConstraints { ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(10) });
Console.WriteLine($"initial budget: {string.Join(",", handle.Budget!)}");
_ = Task.Run(async () =>
{
await foreach (var ev in handle.Events())
{
if (ev.Kind != EventKinds.Metric) continue;
var metric = ev.BodyAs();
if (metric?.Name == "cost.budget.remaining")
Console.WriteLine($"budget remaining: {metric.Value} {metric.Unit}");
}
});
var outcome = await handle.Result;
if (!outcome.Success)
{
// BUDGET_EXHAUSTED and LEASE_EXPIRED are never retryable.
outcome.EnsureSuccess();
}
```
### Subscribing to jobs
Attach read-only to a job submitted elsewhere and observe its live stream (with optional history replay) without cancel authority.
```csharp
using Arcp.Core.Ids;
using Arcp.Core.Messages;
var listing = await client.ListJobsAsync(
filter: new JobListFilter { Status = new[] { "running" } });
var jobId = JobId.Parse(listing.Jobs[0].JobId, null);
var sub = await client.SubscribeAsync(jobId, history: true);
var ack = sub.Acknowledged.Result;
Console.WriteLine($"subscribed from seq={ack.SubscribedFrom} replayed={ack.Replayed}");
await foreach (var ev in sub.Events())
Console.WriteLine($"[seq={ev.EventSeq}] {ev.Kind}");
await sub.UnsubscribeAsync();
```
### Error handling
Catch the typed error taxonomy and respect the `retryable` flag — `LEASE_EXPIRED` and `BUDGET_EXHAUSTED` are never retryable; a naive retry fails identically.
```csharp
using Arcp.Core.Errors;
try
{
var handle = await client.SubmitAsync("flaky", input: new { });
var outcome = await handle.Result;
outcome.EnsureSuccess();
}
catch (ArcpException ex)
{
if (ex.Code is ErrorCode.LeaseExpired or ErrorCode.BudgetExhausted)
{
throw; // resubmit with a fresh lease / budget instead
}
if (ex.Retryable)
{
// safe to retry with backoff (e.g. INTERNAL_ERROR, TIMEOUT, HEARTBEAT_LOST, AGENT_NOT_AVAILABLE)
}
throw;
}
```
## Feature support
ARCP features this SDK negotiates during the `hello`/`welcome` handshake:
| Feature flag | Status |
|---|---|
| `heartbeat` | Supported |
| `ack` | Supported |
| `list_jobs` | Supported |
| `subscribe` | Supported |
| `lease_expires_at` | Supported |
| `cost.budget` | Supported |
| `model.use` | Supported |
| `provisioned_credentials` | Supported |
| `progress` | Supported |
| `result_chunk` | Supported |
| `agent_versions` | Supported |
## Transport
ARCP is transport-agnostic. This SDK ships a `WebSocketTransport` (default), a `StdioTransport` for in-process child runtimes, and a `MemoryTransport` for tests and same-process workers. WebSocket is the default for networked runtimes; stdio is used for in-process child runtimes. Select one by constructing the corresponding transport (`new WebSocketTransport(socket)`, `new StdioTransport(input, output)`, `MemoryTransport.Pair()`) and passing it to `ArcpClient.ConnectAsync(transport, options)`; host integrations under `Arcp.AspNetCore` attach the WebSocket upgrade to a Kestrel endpoint via `IEndpointRouteBuilder.MapArcp(server)`.
## API reference
Full API reference — every type, method, and event payload — is in [`docs/`](docs/).
## Versioning and compatibility
This SDK speaks **ARCP v1.1 (draft)**. The SDK follows semantic versioning independently of the protocol; the protocol version it negotiates is shown above and in `session.hello`. A runtime advertising a different ARCP MAJOR is not guaranteed compatible. Feature mismatches degrade gracefully: the effective feature set is the intersection of what the client and runtime advertise, and the SDK will not use a feature outside it.
## Contributing
See [`CONTRIBUTING.md`](CONTRIBUTING.md). Protocol questions and proposed changes belong in the [spec repository](https://github.com/agentruntimecontrolprotocol/spec); SDK bugs and feature requests belong here.
## License
Apache-2.0 — see [`LICENSE`](LICENSE).