Skip to content

Commit 923e652

Browse files
committed
examples: 14 ARCP-primitive samples translated from python-sdk
1 parent c7bb729 commit 923e652

72 files changed

Lines changed: 4177 additions & 84 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCP.sln

Lines changed: 294 additions & 84 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<RootNamespace>ARCP.Samples.Cancellation</RootNamespace>
6+
<AssemblyName>ARCP.Samples.Cancellation</AssemblyName>
7+
<IsPackable>false</IsPackable>
8+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
9+
<NoWarn>$(NoWarn);CS1591;SA0001;CA1303;CA2007;CA1812;CA1822;CS0414;CA1031;CA1308;CA5394;CA1062;CA1002;CA1024;CA1051;CA1716;CA1707;SA1300;SA1633;SA1200;SA1201;SA1202;SA1203;SA1204;SA1402;SA1503;SA1505;SA1508;SA1513;SA1515;SA1623;SA1629;SA1642;SA1649;CA2227;CA1304;CA1310;CA1305;CA1311;CS0162;CA1860;CA1861;CA1859;CS0219;IDE0058;IDE0059;IDE0060;CA1052;CA1822</NoWarn>
10+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
11+
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
12+
<AnalysisMode>None</AnalysisMode>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\src\ARCP\ARCP.csproj" />
21+
</ItemGroup>
22+
23+
</Project>

samples/Cancellation/Program.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Two scenarios over the §10.4 / §10.5 control surface.
2+
using ARCP.Client;
3+
using ARCP.Errors;
4+
using ARCP.Ids;
5+
using ARCP.Messages.Control;
6+
using ARCP.Messages.Execution;
7+
using static ARCP.Samples.Cancellation.ClientStubs;
8+
using Env = ARCP.Envelope.Envelope;
9+
10+
const int CancelDeadlineMs = 5_000;
11+
12+
string which = args.Length > 0 ? args[0] : "cancel";
13+
if (which == "cancel")
14+
{
15+
await ScenarioCancelAsync();
16+
}
17+
else if (which == "interrupt")
18+
{
19+
await ScenarioInterruptAsync();
20+
}
21+
else
22+
{
23+
throw new InvalidOperationException($"unknown scenario: {which}");
24+
}
25+
26+
static async Task<JobId> StartLongJobAsync(ARCPClient client)
27+
{
28+
Env accepted = await Request(
29+
client,
30+
Envelope(client, "tool.invoke", new ToolInvoke(
31+
Tool: "demo.long_running",
32+
Arguments: System.Text.Json.JsonSerializer.SerializeToElement(new { work_seconds = 600 }))),
33+
timeout: TimeSpan.FromSeconds(10));
34+
return ((JobAccepted)accepted.Payload).JobId;
35+
}
36+
37+
static async Task<Env> CancelJobAsync(ARCPClient client, JobId jobId, string reason, int deadlineMs)
38+
{
39+
// Cooperative cancel. Runtime drives target to a clean checkpoint
40+
// inside `deadline_ms` before terminating; escalates to ABORTED on
41+
// timeout (RFC §10.4).
42+
Env reply = await Request(
43+
client,
44+
Envelope(client, "cancel", new Cancel(
45+
Target: CancelTarget.Job,
46+
TargetId: jobId.Value,
47+
Reason: reason,
48+
DeadlineMs: deadlineMs)),
49+
timeout: TimeSpan.FromMilliseconds(deadlineMs + 5_000));
50+
if (reply.Type == "cancel.refused")
51+
{
52+
CancelRefused refused = (CancelRefused)reply.Payload;
53+
throw new ARCPException(ErrorCode.FailedPrecondition, $"cancel refused: {refused.Reason}");
54+
}
55+
return reply;
56+
}
57+
58+
static Task InterruptJobAsync(ARCPClient client, JobId jobId, string prompt) =>
59+
// Distinct from cancel: pauses the job (`blocked`), runtime emits
60+
// `human.input.request`. Job is NOT terminated (RFC §10.5).
61+
Send(client, Envelope(client, "interrupt", new Interrupt(
62+
Target: CancelTarget.Job,
63+
TargetId: jobId.Value,
64+
Prompt: prompt)));
65+
66+
static async Task<Env> AwaitTerminalAsync(ARCPClient client, JobId jobId)
67+
{
68+
await foreach (Env env in Events(client))
69+
{
70+
if (!Equals(env.JobId, jobId)) continue;
71+
if (env.Type is "job.completed" or "job.failed" or "job.cancelled") return env;
72+
}
73+
throw new InvalidOperationException("event stream closed before terminal");
74+
}
75+
76+
static async Task ScenarioCancelAsync()
77+
{
78+
ARCPClient client = null!; // transport, identity, auth elided
79+
await Open(client);
80+
try
81+
{
82+
JobId jobId = await StartLongJobAsync(client);
83+
await Task.Delay(TimeSpan.FromSeconds(2)); // let the job actually start
84+
Env ack = await CancelJobAsync(client, jobId, reason: "user_aborted", deadlineMs: CancelDeadlineMs);
85+
Console.WriteLine($"cancel ack: {ack.Type}");
86+
Env terminal = await AwaitTerminalAsync(client, jobId);
87+
Console.WriteLine($"terminal: {terminal.Type}");
88+
}
89+
finally
90+
{
91+
await client.CloseAsync();
92+
}
93+
}
94+
95+
static async Task ScenarioInterruptAsync()
96+
{
97+
ARCPClient client = null!;
98+
await Open(client);
99+
try
100+
{
101+
JobId jobId = await StartLongJobAsync(client);
102+
await Task.Delay(TimeSpan.FromSeconds(2));
103+
await InterruptJobAsync(client, jobId, prompt: "Pause and ask before touching production tables.");
104+
// Runtime now emits human.input.request; answer via samples/HumanInput.
105+
await foreach (Env env in Events(client))
106+
{
107+
if (env.Type == "human.input.request" && Equals(env.JobId, jobId))
108+
{
109+
ARCP.Messages.Human.HumanInputRequest req = (ARCP.Messages.Human.HumanInputRequest)env.Payload;
110+
Console.WriteLine($"awaiting human: \"{req.Prompt}\"");
111+
return;
112+
}
113+
}
114+
}
115+
finally
116+
{
117+
await client.CloseAsync();
118+
}
119+
}

samples/Cancellation/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Cancellation
2+
3+
Two scenarios that exercise the §10.4–§10.5 control surface that
4+
distinguishes ARCP from "agent over plain HTTP":
5+
6+
- `cancel`: cooperative termination with a deadline.
7+
- `interrupt`: pause the job and route through a human, no
8+
termination.
9+
10+
## Before ARCP
11+
12+
Cancellation usually means closing the socket or trying to kill the
13+
process. The agent's tool was already mid-network call, so it
14+
either completes anyway (silent waste of money) or leaves a
15+
half-applied side effect.
16+
17+
## With ARCP
18+
19+
```csharp
20+
// Stop the job; the runtime drives it to a clean checkpoint
21+
// inside `deadline_ms` before terminating.
22+
Env ack = await CancelJobAsync(client, jobId, reason: "user_aborted",
23+
deadlineMs: 5_000); // cancel.accepted
24+
Env terminal = await AwaitTerminalAsync(client, jobId); // job.cancelled
25+
26+
// Or: pause the job, ask the human, resume.
27+
await InterruptJobAsync(client, jobId,
28+
prompt: "Pause and ask before touching prod.");
29+
// runtime emits human.input.request; answer with the HITL relay.
30+
```
31+
32+
## ARCP primitives
33+
34+
- `cancel` cooperative contract — RFC §10.4 (`cancel.accepted` /
35+
`cancel.refused`, `deadline_ms`, escalation to `ABORTED`).
36+
- `interrupt` (distinct from cancel) — §10.5; emits
37+
`human.input.request`, leaves the job in `blocked`.
38+
- `capabilities.interrupt: false` fallback to `cancel` (advertised
39+
per §10.5).
40+
41+
## File tour
42+
43+
- `Program.cs` — two scenarios driven by `argv[0]` (`cancel` or `interrupt`).
44+
- `Stubs.cs` — elided client helpers.
45+
46+
## Variations
47+
48+
- Pair `interrupt` with [HumanInput](../HumanInput) for a working
49+
pause-and-ask loop.
50+
- Send `cancel` against a `stream_id` instead of a `job_id` to
51+
terminate just one stream.
52+
- Race many peers, cancel the slowest once N succeed.

samples/Cancellation/Stubs.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Elision helpers. Real SDK supplies these via the public API; throwing
2+
// stubs keep the protocol code readable.
3+
using System.Runtime.CompilerServices;
4+
using ARCP.Client;
5+
using ARCP.Messages.Session;
6+
using Env = ARCP.Envelope.Envelope;
7+
using MsgType = ARCP.Envelope.MessageType;
8+
9+
namespace ARCP.Samples.Cancellation;
10+
11+
internal static class ClientStubs
12+
{
13+
public static Task<SessionAccepted> Open(ARCPClient client) =>
14+
throw new NotImplementedException();
15+
16+
public static Task Send(ARCPClient client, Env envelope) =>
17+
throw new NotImplementedException();
18+
19+
public static Task<Env> Request(
20+
ARCPClient client, Env envelope, TimeSpan? timeout = null) =>
21+
throw new NotImplementedException();
22+
23+
#pragma warning disable CS1998
24+
public static async IAsyncEnumerable<Env> Events(
25+
ARCPClient client,
26+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
27+
{
28+
yield break;
29+
}
30+
#pragma warning restore CS1998
31+
32+
public static Env Envelope(
33+
ARCPClient client,
34+
string type,
35+
MsgType payload,
36+
ARCP.Ids.JobId? jobId = null,
37+
ARCP.Ids.StreamId? streamId = null,
38+
ARCP.Ids.SubscriptionId? subscriptionId = null,
39+
ARCP.Ids.TraceId? traceId = null,
40+
ARCP.Ids.MessageId? correlationId = null,
41+
ARCP.Ids.IdempotencyKey? idempotencyKey = null,
42+
IReadOnlyDictionary<string, System.Text.Json.JsonElement>? extensions = null) =>
43+
throw new NotImplementedException();
44+
}
45+
46+
internal static class EnvelopeStubs
47+
{
48+
public static Env FromWire(System.Text.Json.JsonElement wire) =>
49+
throw new NotImplementedException();
50+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<RootNamespace>ARCP.Samples.CapabilityNegotiation</RootNamespace>
6+
<AssemblyName>ARCP.Samples.CapabilityNegotiation</AssemblyName>
7+
<IsPackable>false</IsPackable>
8+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
9+
<NoWarn>$(NoWarn);CS1591;SA0001;CA1303;CA2007;CA1812;CA1822;CS0414;CA1031;CA1308;CA5394;CA1062;CA1002;CA1024;CA1051;CA1716;CA1707;SA1300;SA1633;SA1200;SA1201;SA1202;SA1203;SA1204;SA1402;SA1503;SA1505;SA1508;SA1513;SA1515;SA1623;SA1629;SA1642;SA1649;CA2227;CA1304;CA1310;CA1305;CA1311;CS0162;CA1860;CA1861;CA1859;CS0219;IDE0058;IDE0059;IDE0060;CA1052;CA1822</NoWarn>
10+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
11+
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
12+
<AnalysisMode>None</AnalysisMode>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\src\ARCP\ARCP.csproj" />
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Marketplace profile + per-tenant usage rollup.
2+
namespace ARCP.Samples.CapabilityNegotiation;
3+
4+
public sealed record Profile(double CostPerMtok, int P50LatencyMs, string ModelClass);
5+
6+
public sealed class Usage
7+
{
8+
public long TokensIn { get; set; }
9+
10+
public long TokensOut { get; set; }
11+
12+
public double CostUsd { get; set; }
13+
14+
public Dictionary<string, double> ByPeer { get; } = new();
15+
}

0 commit comments

Comments
 (0)