Skip to content

Commit 4e8d895

Browse files
committed
Add canonical examples and recipes
Add missing canonical Java examples, port the recipe modules, wire Spotless into CI, and move the provisioned credential integration test to the client test module. Closes #9 Closes #10 Closes #12 Closes #13
1 parent 8a08a10 commit 4e8d895

36 files changed

Lines changed: 1620 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ jobs:
5252
- name: Validate Gradle wrapper
5353
uses: gradle/actions/wrapper-validation@v4
5454

55+
- name: Check formatting (Spotless)
56+
run: ./gradlew spotlessCheck --no-daemon
57+
5558
- name: Build
5659
run: ./gradlew build --no-daemon --stacktrace
5760

58-
- name: Run all 10 examples
61+
- name: Run all runnable examples and recipes
5962
run: |
6063
./gradlew --no-daemon --stacktrace \
6164
:examples:submit-and-stream:run \
@@ -67,7 +70,23 @@ jobs:
6770
:examples:list-jobs:run \
6871
:examples:lease-expires-at:run \
6972
:examples:idempotent-retry:run \
70-
:examples:custom-auth:run
73+
:examples:custom-auth:run \
74+
:examples:provisioned-credentials:run \
75+
:examples:ack-backpressure:run \
76+
:examples:delegate:run \
77+
:examples:spring-boot:run \
78+
:examples:jakarta:run \
79+
:examples:lease-violation:run \
80+
:examples:progress:run \
81+
:examples:resume:run \
82+
:examples:stdio:run \
83+
:examples:subscribe:run \
84+
:examples:tracing:run \
85+
:examples:vendor-extensions:run \
86+
:recipes:email-vendor-leases:run \
87+
:recipes:mcp-skill:run \
88+
:recipes:multi-agent-budget:run \
89+
:recipes:stream-resume:run
7190
7291
- name: Upload test reports
7392
if: always()

arcp-client/src/main/java/dev/arcp/client/ArcpClient.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public final class ArcpClient implements AutoCloseable, Flow.Subscriber<Envelope
9494
private volatile @Nullable SessionId sessionId;
9595
private volatile @Nullable Session session;
9696
private volatile boolean closed;
97+
private final @Nullable String resumeToken;
98+
private final @Nullable Long lastEventSeq;
9799

98100
private ArcpClient(Builder b) {
99101
this.transport = Objects.requireNonNull(b.transport, "transport");
@@ -106,6 +108,8 @@ private ArcpClient(Builder b) {
106108
this.scheduler = b.scheduler != null ? b.scheduler
107109
: Executors.newScheduledThreadPool(1, r -> Thread.ofPlatform()
108110
.name("arcp-client-scheduler", 0).daemon(true).unstarted(r));
111+
this.resumeToken = b.resumeToken;
112+
this.lastEventSeq = b.lastEventSeq;
109113
}
110114

111115
public static Builder builder(Transport transport) {
@@ -116,7 +120,7 @@ public static Builder builder(Transport transport) {
116120
public CompletableFuture<Session> connect() {
117121
transport.incoming().subscribe(this);
118122
SessionHello hello = new SessionHello(
119-
info, auth, Capabilities.of(requestedFeatures), null, null);
123+
info, auth, Capabilities.of(requestedFeatures), resumeToken, lastEventSeq);
120124
send(Message.Type.SESSION_HELLO, hello, null, null, null, null);
121125
return sessionFuture;
122126
}
@@ -204,6 +208,20 @@ public void close() {
204208
scheduler.shutdownNow();
205209
}
206210

211+
/** Returns the highest event sequence number seen from the server, or -1 if none. */
212+
public long lastSeenSeq() {
213+
return lastSeenSeq.get();
214+
}
215+
216+
/** Returns the active session after {@link #connect()} completes. */
217+
public Session session() {
218+
Session current = session;
219+
if (current == null) {
220+
throw new IllegalStateException("client is not connected");
221+
}
222+
return current;
223+
}
224+
207225
@Override
208226
public void onSubscribe(Flow.Subscription s) {
209227
this.subscription = s;
@@ -481,6 +499,8 @@ public static final class Builder {
481499
private boolean autoAck = true;
482500
private Duration ackInterval = Duration.ofMillis(200);
483501
private @Nullable ScheduledExecutorService scheduler;
502+
private @Nullable String resumeToken;
503+
private @Nullable Long lastEventSeq;
484504

485505
Builder(Transport transport) {
486506
this.transport = transport;
@@ -526,6 +546,21 @@ public Builder scheduler(ScheduledExecutorService s) {
526546
return this;
527547
}
528548

549+
/** Resume a prior session by supplying the token received in {@link Session#resumeToken()}. */
550+
public Builder resumeToken(String token) {
551+
this.resumeToken = token;
552+
return this;
553+
}
554+
555+
/**
556+
* Resume from a known event sequence number (§6.3). Used together with
557+
* {@link #resumeToken(String)} to re-subscribe to events the client may have missed.
558+
*/
559+
public Builder lastEventSeq(long seq) {
560+
this.lastEventSeq = seq;
561+
return this;
562+
}
563+
529564
public ArcpClient build() {
530565
return new ArcpClient(this);
531566
}

arcp-runtime/src/test/java/dev/arcp/runtime/credentials/CredentialProvisioningIntegrationTest.java renamed to arcp-client/src/test/java/dev/arcp/client/credentials/CredentialProvisioningIntegrationTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package dev.arcp.runtime.credentials;
1+
package dev.arcp.client.credentials;
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -20,6 +20,9 @@
2020
import dev.arcp.core.transport.MemoryTransport;
2121
import dev.arcp.runtime.ArcpRuntime;
2222
import dev.arcp.runtime.agent.JobOutcome;
23+
import dev.arcp.runtime.credentials.CredentialProvisioner;
24+
import dev.arcp.runtime.credentials.InMemoryCredentialRevocationStore;
25+
import dev.arcp.runtime.credentials.IssuedCredential;
2326
import java.time.Duration;
2427
import java.time.Instant;
2528
import java.util.EnumSet;

build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
plugins {
2+
id("com.diffplug.spotless") version "6.25.0" apply false
3+
}
4+
15
allprojects {
26
group = "dev.arcp"
37
version = "1.0.0-SNAPSHOT"
@@ -38,6 +42,13 @@ subprojects {
3842
charSet = "UTF-8"
3943
}
4044
}
45+
apply(plugin = "com.diffplug.spotless")
46+
configure<com.diffplug.gradle.spotless.SpotlessExtension> {
47+
java {
48+
googleJavaFormat()
49+
removeUnusedImports()
50+
}
51+
}
4152
}
4253

4354
// Shared publishing metadata. Per-subproject build files still own the
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins { application }
2+
3+
application {
4+
mainClass.set("dev.arcp.examples.ackbackpressure.Main")
5+
applicationDefaultJvmArgs = listOf("-ea")
6+
}
7+
8+
java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }
9+
10+
tasks.withType<JavaCompile>().configureEach {
11+
options.release.set(21)
12+
options.encoding = "UTF-8"
13+
}
14+
15+
dependencies {
16+
implementation(project(":arcp"))
17+
runtimeOnly(libs.slf4j.simple)
18+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package dev.arcp.examples.ackbackpressure;
2+
3+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
4+
import dev.arcp.client.ArcpClient;
5+
import dev.arcp.client.JobHandle;
6+
import dev.arcp.core.capabilities.Feature;
7+
import dev.arcp.core.events.EventBody;
8+
import dev.arcp.core.events.StatusEvent;
9+
import dev.arcp.core.transport.MemoryTransport;
10+
import dev.arcp.runtime.ArcpRuntime;
11+
import dev.arcp.runtime.agent.JobOutcome;
12+
import java.time.Duration;
13+
import java.util.EnumSet;
14+
import java.util.concurrent.Flow;
15+
import java.util.concurrent.TimeUnit;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
18+
/**
19+
* Demonstrates manual ack/back-pressure: client opts out of auto-ack,
20+
* counts events itself, then sends a single explicit ack at the end.
21+
*/
22+
public final class Main {
23+
public static void main(String[] args) throws Exception {
24+
MemoryTransport[] pair = MemoryTransport.pair();
25+
ArcpRuntime runtime = ArcpRuntime.builder()
26+
.agent("ticker", "1.0.0", (input, ctx) -> {
27+
for (int i = 1; i <= 20; i++) {
28+
ctx.emit(new StatusEvent("tick-" + i, null));
29+
}
30+
return JobOutcome.Success.inline(input.payload());
31+
})
32+
.build();
33+
runtime.accept(pair[0]);
34+
35+
AtomicInteger received = new AtomicInteger();
36+
37+
try (ArcpClient client = ArcpClient.builder(pair[1])
38+
.autoAck(false)
39+
.features(EnumSet.allOf(Feature.class))
40+
.build()) {
41+
client.connect(Duration.ofSeconds(5));
42+
43+
JobHandle handle = client.submit(
44+
ArcpClient.jobSubmit("ticker@1.0.0", JsonNodeFactory.instance.objectNode()));
45+
46+
handle.events().subscribe(new Flow.Subscriber<>() {
47+
@Override
48+
public void onSubscribe(Flow.Subscription s) {
49+
s.request(Long.MAX_VALUE);
50+
}
51+
52+
@Override
53+
public void onNext(EventBody body) {
54+
received.incrementAndGet();
55+
}
56+
57+
@Override
58+
public void onError(Throwable throwable) {}
59+
60+
@Override
61+
public void onComplete() {}
62+
});
63+
64+
handle.result().get(5, TimeUnit.SECONDS);
65+
66+
// Allow event delivery to finish.
67+
Thread.sleep(200);
68+
69+
assert received.get() == 20 : "expected 20 events, got " + received.get();
70+
71+
// Explicit ack after processing all events.
72+
client.ack(client.lastSeenSeq());
73+
System.out.println("OK ack-backpressure");
74+
}
75+
runtime.close();
76+
}
77+
}

examples/bun/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Bun
2+
3+
Bun is not a Java runtime target. The Java SDK covers the same transport and
4+
protocol behavior through the `stdio`, `jakarta`, and `spring-boot` examples.
5+
6+
This directory exists to document the TypeScript SDK's Bun example as not
7+
applicable for the Java SDK.

examples/delegate/build.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins { application }
2+
3+
application {
4+
mainClass.set("dev.arcp.examples.delegate.Main")
5+
applicationDefaultJvmArgs = listOf("-ea")
6+
}
7+
8+
java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }
9+
10+
tasks.withType<JavaCompile>().configureEach {
11+
options.release.set(21)
12+
options.encoding = "UTF-8"
13+
}
14+
15+
dependencies {
16+
implementation(project(":arcp"))
17+
runtimeOnly(libs.slf4j.simple)
18+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package dev.arcp.examples.delegate;
2+
3+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
4+
import dev.arcp.client.ArcpClient;
5+
import dev.arcp.client.JobHandle;
6+
import dev.arcp.core.events.DelegateEvent;
7+
import dev.arcp.core.events.EventBody;
8+
import dev.arcp.core.ids.JobId;
9+
import dev.arcp.core.transport.MemoryTransport;
10+
import dev.arcp.runtime.ArcpRuntime;
11+
import dev.arcp.runtime.agent.JobOutcome;
12+
import java.time.Duration;
13+
import java.util.concurrent.CompletableFuture;
14+
import java.util.concurrent.Flow;
15+
import java.util.concurrent.TimeUnit;
16+
17+
/**
18+
* Demonstrates manual delegation: the parent agent emits a DelegateEvent; the client detects it
19+
* and spawns the child job explicitly (the Java runtime does not auto-spawn children).
20+
*/
21+
public final class Main {
22+
public static void main(String[] args) throws Exception {
23+
MemoryTransport[] pair = MemoryTransport.pair();
24+
ArcpRuntime runtime =
25+
ArcpRuntime.builder()
26+
.agent(
27+
"parent",
28+
"1.0.0",
29+
(input, ctx) -> {
30+
ctx.emit(new DelegateEvent(JobId.generate(), "child@1.0.0"));
31+
return JobOutcome.Success.inline(input.payload());
32+
})
33+
.agent(
34+
"child",
35+
"1.0.0",
36+
(input, ctx) -> JobOutcome.Success.inline(input.payload()))
37+
.build();
38+
runtime.accept(pair[0]);
39+
40+
try (ArcpClient client = ArcpClient.builder(pair[1]).build()) {
41+
client.connect(Duration.ofSeconds(5));
42+
43+
CompletableFuture<JobHandle> childFuture = new CompletableFuture<>();
44+
45+
JobHandle parentHandle =
46+
client.submit(
47+
ArcpClient.jobSubmit(
48+
"parent@1.0.0", JsonNodeFactory.instance.objectNode()));
49+
50+
parentHandle
51+
.events()
52+
.subscribe(
53+
new Flow.Subscriber<>() {
54+
@Override
55+
public void onSubscribe(Flow.Subscription s) {
56+
s.request(Long.MAX_VALUE);
57+
}
58+
59+
@Override
60+
public void onNext(EventBody body) {
61+
if (body instanceof DelegateEvent) {
62+
try {
63+
childFuture.complete(
64+
client.submit(
65+
ArcpClient.jobSubmit(
66+
"child@1.0.0",
67+
JsonNodeFactory.instance
68+
.objectNode())));
69+
} catch (Exception e) {
70+
childFuture.completeExceptionally(e);
71+
}
72+
}
73+
}
74+
75+
@Override
76+
public void onError(Throwable t) {
77+
childFuture.completeExceptionally(t);
78+
}
79+
80+
@Override
81+
public void onComplete() {}
82+
});
83+
84+
parentHandle.result().get(5, TimeUnit.SECONDS);
85+
childFuture.get(5, TimeUnit.SECONDS).result().get(5, TimeUnit.SECONDS);
86+
System.out.println("OK delegate");
87+
}
88+
runtime.close();
89+
}
90+
}

examples/jakarta/build.gradle.kts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins { application }
2+
3+
application {
4+
mainClass.set("dev.arcp.examples.jakarta.Main")
5+
applicationDefaultJvmArgs = listOf("-ea")
6+
}
7+
8+
java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }
9+
10+
tasks.withType<JavaCompile>().configureEach {
11+
options.release.set(21)
12+
options.encoding = "UTF-8"
13+
}
14+
15+
dependencies {
16+
implementation(project(":arcp-middleware-jakarta"))
17+
implementation(project(":arcp-client"))
18+
implementation(libs.jetty.server)
19+
implementation(libs.jetty.ee10.servlet)
20+
implementation(libs.jetty.ee10.websocket.jakarta.server)
21+
runtimeOnly(libs.slf4j.simple)
22+
}

0 commit comments

Comments
 (0)