Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
# Non-blocking: a Codecov outage must not break CI.
- name: Upload coverage to Codecov
# codecov/codecov-action v6.0.1
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
fail_ci_if_error: false
flags: unittests
Expand Down
2 changes: 1 addition & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Status legend: Implemented ✅ · Partial 🚧 · Not implemented ⛔.
| ---- | ----------- | ------ | ----- |
| §10 | `delegate` event kind on parent's `job.event` stream | ✅ | `JobContext.DelegateAsync` |
| §11 | `trace_id` propagation; OTel span attrs | ✅ | `TraceAttributes`, `ArcpTracing.WithTracing` |
| §11 (v1.1) | Span attrs `arcp.lease.expires_at`, `arcp.budget.remaining` | | `TraceAttributes` |
| §11 (v1.1) | Span attrs `arcp.lease.expires_at`, `arcp.budget.remaining` | | Not emitted by `ArcpTracing` |
| §12 | 15 canonical error codes with retryable booleans | ✅ | `ErrorCode.All`, `ErrorCode.IsRetryable` |

## Test cross-reference
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<ItemGroup Label="Runtime">
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.0" />
<PackageVersion Include="Ulid" Version="1.4.1" />
Expand All @@ -23,7 +23,7 @@
</ItemGroup>

<ItemGroup Label="Test">
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="FluentAssertions" Version="8.10.0" />
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ await using var client = await ArcpClient.ConnectAsync(transport, new ArcpClient

var sessionId = client.SessionId;
var resumeToken = client.ResumeToken;
var effective = client.EffectiveFeatures; // intersection of client/runtime hello.features
var effective = client.EffectiveFeatures; // intersection of hello.features and welcome.features

// ... transport drops; track the last seq your reader observed ...
var lastSeq = client.LastReceivedSeq;
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
| `Arcp.Runtime` | `ArcpServer`, `JobManager`, `LeaseManager`, `SessionState` — the side that runs them. |
| `Arcp.AspNetCore` | Mounts a runtime on Kestrel via `IEndpointRouteBuilder.MapArcp("/arcp")`. |
| `Arcp.Otel` | Wraps `ITransport` with `ActivitySource`-based OTel instrumentation. |
| `Arcp.Hosting` | Registers a runtime in `IHostedService` for non-ASP.NET workers. |
| `Arcp.Hosting` | Registers `ArcpServer` in DI via `AddArcpRuntime` for non-ASP.NET workers. |
| `Arcp.Cli` | `arcp serve` / `arcp submit` / `arcp version` executable. |
| `Arcp` | Umbrella meta-package — `dotnet add package Arcp` pulls Core + Client + Runtime. |

Expand Down Expand Up @@ -37,12 +37,12 @@ Every message is a JSON object envelope:

Unknown top-level fields are preserved verbatim in
`Envelope.Extensions` (`Dictionary<string, JsonElement>`), so
vendor-extension hints round-trip without loss (spec §5.1).
vendor-extension hints round-trip without loss (spec §5).

## Versioning

The SDK follows SemVer strictly. The `arcp` wire version field
(`"1.1"`) is fixed in `Arcp.Core.WireVersion.Current`. Adding a
defaults to `"1.1"` on `Envelope.Arcp` (also exposed as `Arcp.ArcpInfo.ProtocolVersion`). Adding a
public member is a minor bump; changing a signature is a major bump.
One minor deprecation cycle (`[Obsolete]`) before removal. See the
[style guide](./style-guide.md#14-versioning--compatibility).
Expand Down
2 changes: 1 addition & 1 deletion docs/conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Opt out of features on either peer:
```csharp
new ArcpClientOptions
{
Features = new FeatureSet(["heartbeat", "ack"]), // drop the rest
Features = new[] { FeatureFlags.Heartbeat, FeatureFlags.Ack }, // drop the rest
};
```

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Agents can produce errors by throwing:
```csharp
server.RegisterAgent("strict", async (ctx, ct) =>
{
if (!ctx.Lease.Contains(LeaseNamespaces.FsRead))
if (ctx.Lease.Get(LeaseNamespaces.FsRead).Count == 0)
throw new PermissionDeniedException("fs.read required");

// ...
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/job-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ server.RegisterAgent("researcher", async (ctx, ct) =>
await ctx.StatusAsync("starting", "Fetching data...", ct);

await ctx.ToolCallAsync("fetch", callId: "c1",
args: new { url = "https://api.example.com/data" }, ct);
args: new { url = "https://api.example.com/data" }, cancellationToken: ct);
var data = /* ... */ "";
await ctx.ToolResultAsync("c1", result: data, ct);
await ctx.ToolResultAsync("c1", result: data, cancellationToken: ct);

await ctx.ProgressAsync(current: 1, total: 3, message: "fetched", ct);
await ctx.ProgressAsync(current: 1, total: 3, message: "fetched", cancellationToken: ct);

await ctx.LogAsync("info", "Processing ...", ct);
await ctx.MetricAsync("cost.inference", 0.012, unit: "USD", cancellationToken: ct);
Expand All @@ -40,7 +40,7 @@ server.RegisterAgent("researcher", async (ctx, ct) =>
uri: "s3://bucket/report.pdf",
contentType: "application/pdf",
byteSize: 42_000,
ct: ct);
cancellationToken: ct);

return new { status = "done" };
});
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/leases.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ server.RegisterAgent("file-writer", async (ctx, ct) =>
ctx.Lease,
ctx.LeaseConstraints,
LeaseNamespaces.FsWrite,
path: "/workspace/src/output.cs");
pattern: "/workspace/src/output.cs");
}
catch (PermissionDeniedException ex)
{
Expand Down
6 changes: 1 addition & 5 deletions docs/guides/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ constants.
| Name | Purpose |
| ----------------- | -------------------------------------------------- |
| `Arcp.Transport` | One span per envelope (send and receive). |
| `Arcp.Runtime` | Runtime-internal spans (dispatch, agent run). |
| `Arcp.Runtime` | Application spans you start manually (e.g. delegation). |

## Span shape

Expand All @@ -66,10 +66,6 @@ For each envelope, the wrapper:
| `arcp.job_id` | envelope `job_id` |
| `arcp.trace_id` | envelope `trace_id` |
| `arcp.event_seq` | envelope `event_seq` |
| `arcp.agent` | `payload.agent` (on submit / accept) |
| `arcp.lease.capabilities` | comma-joined lease keys |
| `arcp.lease.expires_at` | ISO 8601 string (v1.1) |
| `arcp.budget.remaining` | JSON-stringified currency map (v1.1) |

## Use with ASP.NET Core

Expand Down
10 changes: 8 additions & 2 deletions docs/guides/vendor-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ Write on the send side by populating `Extensions` before calling
`ITransport.SendAsync`:

```csharp
var env = new Envelope { /* ... */ };
env.Extensions["x-vendor.acme.priority"] = JsonSerializer.SerializeToElement("high");
var env = new Envelope
{
/* ... */
Extensions = new Dictionary<string, JsonElement>
{
["x-vendor.acme.priority"] = JsonSerializer.SerializeToElement("high"),
},
};
await transport.SendAsync(env, ct);
```

Expand Down
2 changes: 1 addition & 1 deletion docs/projects/Arcp.AspNetCore.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@ app.MapArcp(server, o => { o.Path = "/arcp-external"; o.AllowedHosts = new[] {

- [Arcp.Runtime](./Arcp.Runtime.md) — `ArcpServer` configuration.
- [Arcp.Otel](./Arcp.Otel.md) — transport instrumentation.
- [Arcp.Hosting](./Arcp.Hosting.md) — `IHostedService` / DI integration.
- [Arcp.Hosting](./Arcp.Hosting.md) — `AddArcpRuntime` DI registration.
- [Troubleshooting — 403 Forbidden](../troubleshooting.md#websocket-upgrade-returns-403-forbidden) — allowed-host failures.
- [Troubleshooting — HEARTBEAT_LOST](../troubleshooting.md#job-is-cancelled-with-heartbeat_lost) — keepalive collision.
6 changes: 3 additions & 3 deletions docs/projects/Arcp.Core.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dotnet add package Arcp.Core
The JSON container that wraps every ARCP message on the wire (spec §5):

```csharp
public sealed class Envelope
public sealed record Envelope
{
public string Arcp { get; init; } // wire version, e.g. "1.1"
public string Id { get; init; } // random message id
Expand All @@ -25,10 +25,10 @@ public sealed class Envelope
public string? JobId { get; init; }
public string? TraceId { get; init; }
public long? EventSeq { get; init; }
public JsonElement? Payload { get; init; }
public object? Payload { get; init; }

// Unknown top-level fields round-trip here (§15):
public Dictionary<string, JsonElement> Extensions { get; }
public IDictionary<string, JsonElement>? Extensions { get; init; }
}
```

Expand Down
4 changes: 0 additions & 4 deletions docs/projects/Arcp.Otel.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ For each envelope the wrapper:
| `arcp.job_id` | envelope `job_id` |
| `arcp.trace_id` | envelope `trace_id` |
| `arcp.event_seq` | envelope `event_seq` |
| `arcp.agent` | `payload.agent` (on submit / accept) |
| `arcp.lease.capabilities` | comma-joined lease keys |
| `arcp.lease.expires_at` | ISO 8601 string (v1.1) |
| `arcp.budget.remaining` | JSON-stringified currency map (v1.1) |

## Propagating trace IDs to child jobs

Expand Down
2 changes: 1 addition & 1 deletion docs/projects/Arcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Optional add-ons are **not** bundled and must be referenced explicitly:
| ------------------ | ----------------------------------------------------- |
| `Arcp.AspNetCore` | Kestrel / ASP.NET Core hosting (`MapArcp`). |
| `Arcp.Otel` | OpenTelemetry transport instrumentation. |
| `Arcp.Hosting` | `IHostedService` + `IHostApplicationLifetime` wiring. |
| `Arcp.Hosting` | `IServiceCollection.AddArcpRuntime` DI registration. |
| `Arcp.Cli` | `arcp` CLI tool — serve and submit from a terminal. |

## Typical project file
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ intentionally drops the WebSocket connection mid-stream and reconnects using
its `resume_token`. The runtime replays the missed chunks gap-free.

Concepts: `ctx.BeginResultStream`, `ctx.WriteChunkAsync`, `handle.Chunks`,
`ArcpClientOptions.ResumeToken`, `RESUME_WINDOW_EXPIRED`.
`ArcpClient.ResumeToken`, `RESUME_WINDOW_EXPIRED`.

→ [`recipes/stream-resume/`](../recipes/stream-resume/)

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ trace ID). See

An incoming envelope has a malformed payload. The most common cause is a
version mismatch between the client and server SDK. Verify both sides use
compatible `arcp` wire versions (`client.WireVersion` / `server.WireVersion`).
compatible `arcp` wire versions (the envelope `arcp` field, default `"1.1"`).

### Vendor extension fields disappear

Expand Down
2 changes: 2 additions & 0 deletions recipes/multi-agent-budget/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ await ctx.LogAsync("warn",
var plannerLease = new Lease(new Dictionary<string, IReadOnlyList<string>>
{
["cost.budget"] = new[] { "USD:5.00" },
// Spec §9.3 deny-by-default: the planner must hold agent.delegate to delegate to workers.
["agent.delegate"] = new[] { "*" },
});

var handle = await client.SubmitAsync(
Expand Down
2 changes: 2 additions & 0 deletions samples/CostBudget/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
var lease = new Lease(new Dictionary<string, IReadOnlyList<string>>
{
["cost.budget"] = new[] { "USD:1.00" },
// Spec §9.3 deny-by-default: tool.call must be covered by the lease for the agent to emit it.
["tool.call"] = new[] { "*" },
});
var handle = await client.SubmitAsync("research", leaseRequest: lease);
Console.WriteLine($"initial budget: {string.Join(",", handle.Budget!)}");
Expand Down
8 changes: 7 additions & 1 deletion samples/Delegate/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// samples/Delegate: parent agent submits a child job and emits a `delegate` event linking them.
// Spec: §10, §13.2.
using Arcp.Client;
using Arcp.Core.Leases;
using Arcp.Core.Messages;
using Arcp.Core.Transport;
using Arcp.Runtime;
Expand All @@ -24,7 +25,12 @@
{
Client = new ClientInfo { Name = "delegate-client", Version = "1.0.0" },
});
var handle = await client.SubmitAsync("parent");
// Spec §9.3 deny-by-default: agent.delegate must be covered by the lease for the parent to delegate.
var lease = new Lease(new Dictionary<string, IReadOnlyList<string>>
{
["agent.delegate"] = new[] { "*" },
});
var handle = await client.SubmitAsync("parent", leaseRequest: lease);
var res = await handle.Result;
Console.WriteLine($"parent: {res.FinalStatus}");
return 0;
41 changes: 33 additions & 8 deletions src/Arcp.Client/ArcpClient.Dispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ private async Task ReaderLoop(CancellationToken cancellationToken)
{
await foreach (var env in _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false))
{
if (env.EventSeq is { } seq) Interlocked.Exchange(ref _lastReceivedSeq, seq);
if (env.EventSeq is { } seq)
{
// Spec §8.3: event_seq is strictly monotonic and gap-free. If the new seq skips
// the expected successor, surface a detectable broken-session signal instead of
// silently accepting the gap.
var prev = Interlocked.Read(ref _lastReceivedSeq);
if (prev > 0 && seq > prev + 1) OnEventSeqGap(prev + 1, seq);
if (seq > prev) Interlocked.Exchange(ref _lastReceivedSeq, seq);
}
await DispatchAsync(env, cancellationToken).ConfigureAwait(false);
}
}
Expand Down Expand Up @@ -130,15 +138,32 @@ private ValueTask RespondToPingAsync(SessionPingPayload p, CancellationToken can

private void PropagateSessionError(SessionErrorPayload err)
{
var jobError = new JobErrorPayload
{
Code = err.Code,
Message = err.Message,
Retryable = err.Retryable,
Detail = err.Detail,
};

foreach (var h in _handles.Values)
{
h.OnError(new JobErrorPayload
{
Code = err.Code,
Message = err.Message,
Retryable = err.Retryable,
Detail = err.Detail,
});
h.OnError(jobError);
}

// A submission rejected before acceptance lives in _pendingSubmits, not _handles, and a
// list_jobs request lives in _listJobsRequests. session.error is not correlated to a
// specific request id, so the safe contract is to fault every outstanding request — leaving
// them pending would hang SubmitAsync/ListJobsAsync until the caller's token fires.
while (_pendingSubmits.TryDequeue(out var pending))
{
pending.OnError(jobError);
}

foreach (var key in _listJobsRequests.Keys)
{
if (_listJobsRequests.TryRemove(key, out var tcs))
tcs.TrySetException(JobHandle.ToException(err.Code, err.Message, err.Detail));
}
Comment on lines 149 to 167

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t fail every accepted job on a request-level session.error.

Now that session.error is the rejection path for pending job.submit and session.list_jobs, this method will also call OnError(...) on every already-accepted handle in _handles. One bad submit or list request can therefore terminally fail unrelated running jobs even if the server keeps streaming their events/results. Split request-scoped rejection handling from truly session-fatal errors, or mark the session broken and stop further dispatch before you fail accepted handles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Arcp.Client/ArcpClient.Dispatch.cs` around lines 149 - 167, The current
handler calls OnError on every accepted job handle (_handles) for any
session.error; change it so session.error is distinguished between
request-scoped rejections (for job.submit and session.list_jobs) and truly
session-fatal errors: when the error is request-scoped only dequeue and call
OnError on _pendingSubmits and fail entries in _listJobsRequests (using
JobHandle.ToException), but do NOT iterate _handles; when it is a session-fatal
error, set a session-failure flag (e.g. _sessionBroken) to stop further dispatch
and then iterate _handles and call OnError; update the dispatch logic to check
_sessionBroken before processing incoming events so already-accepted jobs are
only failed on actual session-level fatal errors.

}
}
16 changes: 16 additions & 0 deletions src/Arcp.Client/ArcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ public sealed partial class ArcpClient : IAsyncDisposable
/// <summary>Gets the last received seq.</summary>
public long LastReceivedSeq => Interlocked.Read(ref _lastReceivedSeq);

/// <summary>True once an inbound <c>event_seq</c> has skipped the expected next value, indicating
/// the session stream has a gap and SHOULD be treated as broken (and resumed once resume is
/// wired) per spec §8.3.</summary>
public bool IsSessionBroken { get; private set; }

/// <summary>Raised when an inbound <c>event_seq</c> skips the expected successor (spec §8.3). The
/// arguments are <c>(expectedSeq, receivedSeq)</c>. Handlers run on the reader loop; keep them
/// fast and non-throwing.</summary>
public event Action<long, long>? EventSeqGapDetected;

private void OnEventSeqGap(long expected, long received)
{
IsSessionBroken = true;
EventSeqGapDetected?.Invoke(expected, received);
}

/// <summary>Initializes a new instance of the <see cref="ArcpClient"/> class.</summary>
public ArcpClient(ITransport transport, ArcpClientOptions options)
{
Expand Down
25 changes: 25 additions & 0 deletions src/Arcp.Client/JobHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,35 @@ internal void OnResult(JobResultPayload payload)

internal void OnError(JobErrorPayload payload)
{
// If the job was rejected before acceptance (e.g. a server session.error for a duplicate
// key or unavailable agent), the awaiter on Accepted must fault rather than hang forever.
// For a post-acceptance terminal error, Accepted is already resolved so this is a no-op.
_accepted.TrySetException(ToException(payload.Code, payload.Message, payload.Detail));
_terminal.TrySetResult(new JobResult(false, null, payload));
_events.Writer.TryComplete();
}

/// <summary>Map a wire error code to the most specific <see cref="ArcpException"/> subtype so
/// callers can <c>catch</c> on the concrete type (e.g. <see cref="DuplicateKeyException"/>).</summary>
internal static ArcpException ToException(string code, string message, string? detail) => code switch
{
ErrorCode.DuplicateKey => new DuplicateKeyException(message, detail),
ErrorCode.AgentNotAvailable => new AgentNotAvailableException(message, detail),
ErrorCode.AgentVersionNotAvailable => new AgentVersionNotAvailableException(message, detail),
ErrorCode.LeaseSubsetViolation => new LeaseSubsetViolationException(message, detail),
ErrorCode.PermissionDenied => new PermissionDeniedException(message, detail),
ErrorCode.JobNotFound => new JobNotFoundException(message, detail),
ErrorCode.InvalidRequest => new InvalidRequestException(message, detail),
ErrorCode.Unauthenticated => new UnauthenticatedException(message, detail),
ErrorCode.BudgetExhausted => new BudgetExhaustedException(message, detail),
ErrorCode.LeaseExpired => new LeaseExpiredException(message, detail),
ErrorCode.ResumeWindowExpired => new ResumeWindowExpiredException(message, detail),
ErrorCode.HeartbeatLost => new HeartbeatLostException(message, detail),
ErrorCode.Timeout => new Arcp.Core.Errors.TimeoutException(message, detail),
ErrorCode.Cancelled => new CancelledException(message, detail),
_ => new ArcpException(code, message, detail),
};
Comment on lines +92 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Map INTERNAL_ERROR to InternalErrorException.

ToException(...) is now the central wire-to-client exception map, but ErrorCode.InternalError still falls through to plain ArcpException. That loses the concrete SDK type on the newly added INTERNAL_ERROR path.

Suggested fix
         ErrorCode.ResumeWindowExpired => new ResumeWindowExpiredException(message, detail),
         ErrorCode.HeartbeatLost => new HeartbeatLostException(message, detail),
+        ErrorCode.InternalError => new InternalErrorException(message, detail),
         ErrorCode.Timeout => new Arcp.Core.Errors.TimeoutException(message, detail),
         ErrorCode.Cancelled => new CancelledException(message, detail),
         _ => new ArcpException(code, message, detail),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <summary>Map a wire error code to the most specific <see cref="ArcpException"/> subtype so
/// callers can <c>catch</c> on the concrete type (e.g. <see cref="DuplicateKeyException"/>).</summary>
internal static ArcpException ToException(string code, string message, string? detail) => code switch
{
ErrorCode.DuplicateKey => new DuplicateKeyException(message, detail),
ErrorCode.AgentNotAvailable => new AgentNotAvailableException(message, detail),
ErrorCode.AgentVersionNotAvailable => new AgentVersionNotAvailableException(message, detail),
ErrorCode.LeaseSubsetViolation => new LeaseSubsetViolationException(message, detail),
ErrorCode.PermissionDenied => new PermissionDeniedException(message, detail),
ErrorCode.JobNotFound => new JobNotFoundException(message, detail),
ErrorCode.InvalidRequest => new InvalidRequestException(message, detail),
ErrorCode.Unauthenticated => new UnauthenticatedException(message, detail),
ErrorCode.BudgetExhausted => new BudgetExhaustedException(message, detail),
ErrorCode.LeaseExpired => new LeaseExpiredException(message, detail),
ErrorCode.ResumeWindowExpired => new ResumeWindowExpiredException(message, detail),
ErrorCode.HeartbeatLost => new HeartbeatLostException(message, detail),
ErrorCode.Timeout => new Arcp.Core.Errors.TimeoutException(message, detail),
ErrorCode.Cancelled => new CancelledException(message, detail),
_ => new ArcpException(code, message, detail),
};
/// <summary>Map a wire error code to the most specific <see cref="ArcpException"/> subtype so
/// callers can <c>catch</c> on the concrete type (e.g. <see cref="DuplicateKeyException"/>).</summary>
internal static ArcpException ToException(string code, string message, string? detail) => code switch
{
ErrorCode.DuplicateKey => new DuplicateKeyException(message, detail),
ErrorCode.AgentNotAvailable => new AgentNotAvailableException(message, detail),
ErrorCode.AgentVersionNotAvailable => new AgentVersionNotAvailableException(message, detail),
ErrorCode.LeaseSubsetViolation => new LeaseSubsetViolationException(message, detail),
ErrorCode.PermissionDenied => new PermissionDeniedException(message, detail),
ErrorCode.JobNotFound => new JobNotFoundException(message, detail),
ErrorCode.InvalidRequest => new InvalidRequestException(message, detail),
ErrorCode.Unauthenticated => new UnauthenticatedException(message, detail),
ErrorCode.BudgetExhausted => new BudgetExhaustedException(message, detail),
ErrorCode.LeaseExpired => new LeaseExpiredException(message, detail),
ErrorCode.ResumeWindowExpired => new ResumeWindowExpiredException(message, detail),
ErrorCode.HeartbeatLost => new HeartbeatLostException(message, detail),
ErrorCode.InternalError => new InternalErrorException(message, detail),
ErrorCode.Timeout => new Arcp.Core.Errors.TimeoutException(message, detail),
ErrorCode.Cancelled => new CancelledException(message, detail),
_ => new ArcpException(code, message, detail),
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Arcp.Client/JobHandle.cs` around lines 92 - 111, The ToException method
currently doesn't handle ErrorCode.InternalError and falls back to
ArcpException; update the switch in JobHandle.ToException to include
ErrorCode.InternalError => new InternalErrorException(message, detail) so
INTERNAL_ERROR maps to the concrete InternalErrorException type (ensure
InternalErrorException is referenced/imported where used).


/// <summary>Events.</summary>
public async IAsyncEnumerable<JobEvent> Events([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Arcp.Client/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ Arcp.Client.ArcpClient.CancelJobAsync(Arcp.Core.Ids.JobId jobId, string? reason
Arcp.Client.ArcpClient.ConnectAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Arcp.Client.ArcpClient.DisposeAsync() -> System.Threading.Tasks.ValueTask
Arcp.Client.ArcpClient.EffectiveFeatures.get -> System.Collections.Generic.IReadOnlyList<string!>!
Arcp.Client.ArcpClient.EventSeqGapDetected -> System.Action<long, long>?
Arcp.Client.ArcpClient.HeartbeatIntervalSec.get -> int?
Arcp.Client.ArcpClient.IsSessionBroken.get -> bool
Arcp.Client.ArcpClient.LastReceivedSeq.get -> long
Arcp.Client.ArcpClient.ListJobsAsync(Arcp.Core.Messages.JobListFilter? filter = null, int? limit = null, string? cursor = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Arcp.Core.Messages.SessionJobsPayload!>!
Arcp.Client.ArcpClient.ResumeToken.get -> string?
Expand Down
Loading
Loading