A job is the unit of work managed by the runtime. The client submits one,
receives a JobHandle, observes events, and eventually reads a terminal
result.
JobHandle handle = await client.SubmitAsync(
agent: "code-refactor@2.0.0",
input: new { repo = "/workspace/app" },
leaseRequest: new Lease(new Dictionary<string, IReadOnlyList<string>>
{
[LeaseNamespaces.FsRead] = new[] { "/workspace/**" },
[LeaseNamespaces.FsWrite] = new[] { "/workspace/src/**" },
[LeaseNamespaces.CostBudget]= new[] { "USD:5.00" },
}),
leaseConstraints: new LeaseConstraints
{
ExpiresAt = DateTimeOffset.UtcNow.AddHours(1),
},
idempotencyKey: "refactor-2026-W19",
maxRuntimeSec: 3600);SubmitAsync returns once job.accepted arrives. The handle is populated:
Console.WriteLine(handle.JobId); // "job_01J..."
Console.WriteLine(handle.Agent); // "code-refactor@2.0.0"
Console.WriteLine(handle.TraceId); // W3C trace ID
// handle.Lease, handle.Budget also availablepending → running → { success | error | cancelled | timed_out }
The terminal envelope is job.result for success and job.error for all
failure modes. JobResult.FinalStatus and JobResult.Success distinguish
them on the client side.
await foreach (var ev in handle.Events(cancellationToken))
{
switch (ev.Kind)
{
case "log": Console.WriteLine(ev.Body.GetProperty("message").GetString()); break;
case "progress": Console.WriteLine($"{ev.Body.GetProperty("current")}/{ev.Body.GetProperty("total")}"); break;
default: Console.WriteLine(ev.Kind); break;
}
}
var result = await handle.Result;
// result.Success == true
// result.FinalStatus == "success"
// result.Result?.Result — the agent's return value deserializedIf you don't need to watch events:
var result = await handle.Result;handle.Result completes when the terminal envelope (job.result or
job.error) arrives.
await handle.CancelAsync(reason: "user-requested");
var result = await handle.Result;
// result.FinalStatus == "cancelled"Only the submitter session can cancel. The runtime signals cancellation by
cancelling the agent's CancellationToken. Agents should propagate it —
the runtime emits job.error { final_status: "cancelled", code: "CANCELLED" }
once the agent unwinds.
Submitting twice with the same idempotencyKey and same principal returns
the same job_id and resolves to the same job:
var h1 = await client.SubmitAsync("echo", new { hi = 1 }, idempotencyKey: "run-42");
var h2 = await client.SubmitAsync("echo", new { hi = 1 }, idempotencyKey: "run-42");
// h1.JobId == h2.JobIdSubmitting the same key with a different agent or input produces
DuplicateKeyException (DUPLICATE_KEY).
TTL is configurable on the server:
new ArcpServerOptions
{
IdempotencyWindowSec = 86400, // 24 h (default)
};Declare a budget ceiling in the lease:
var lease = new Lease(new Dictionary<string, IReadOnlyList<string>>
{
[LeaseNamespaces.CostBudget] = new[] { "USD:5.00", "credits:1000" },
});The agent reports spend via metric events:
await ctx.MetricAsync("cost.inference", 0.0234, unit: "USD", cancellationToken: ct);When any counter reaches zero the next authority-bearing call returns
BUDGET_EXHAUSTED. The runtime surfaces this as a tool_result.error so the
agent can decide whether to continue with non-cost-bearing work; it escalates
to job.error only when exhaustion is fatal.
Child leases on delegation MUST NOT exceed the parent's remaining budget.
LeaseManager.AssertSubset enforces this (see Leases).
For large final results the agent emits a sequence of result_chunk events:
Agent side:
server.RegisterAgent("report", async (ctx, ct) =>
{
var rid = ctx.BeginResultStream();
await ctx.WriteChunkAsync(rid, "Title\n", more: true, ct);
await ctx.WriteChunkAsync(rid, "Section A: ...", more: true, ct);
await ctx.WriteChunkAsync(rid, "End.", more: false, ct);
return "report generated, 3 chunks"; // becomes terminal summary
});Client side:
var handle = await client.SubmitAsync("report");
var assembled = new StringBuilder();
await foreach (var chunk in handle.Chunks(cancellationToken))
assembled.Append(chunk.DecodedString);
var terminal = await handle.Result;
// terminal.Result.ResultId — ULID referencing the assembled content
// terminal.Result.ResultSize — total byte count
// terminal.Result.Summary — the agent's return valueBinary chunks: pass a ReadOnlyMemory<byte> overload; the runtime
base64-encodes it for transport.
Invariants (spec §8.4):
chunk_seqis 0-based monotonic perresult_id.- The final chunk has
more: false. encoding ∈ { "utf8", "base64" }.- A job MUST NOT mix inline
resultandresult_chunk.
- Job events — all 10 reserved event kinds.
- Leases —
cost.budget,model.use,fs.read/fs.write. - Delegation — child-job patterns.
- Errors —
BUDGET_EXHAUSTED,CANCELLED,TIMEOUT.