Skip to content

Commit 8019cc4

Browse files
committed
feat(appserver): align 3.x protocol runtime with 2.x
1 parent ac6b111 commit 8019cc4

10 files changed

Lines changed: 360 additions & 31 deletions

src/main/java/io/github/easy4j/codex/appserver/AppServerTurnRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public class AppServerTurnRequest {
5959
*/
6060
private Consumer<String> onTurnStarted;
6161

62+
/**
63+
* Optional structured listener for turn lifecycle and streaming events.
64+
* Existing callbacks remain supported for source compatibility.
65+
*/
66+
private CodexAppServerListener listener;
67+
6268
/**
6369
* Returns the session key in a comparable form, or {@code null} when unset.
6470
*

src/main/java/io/github/easy4j/codex/appserver/CodexAppServerClient.java

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public class CodexAppServerClient implements AutoCloseable {
7777
private final CodexAppServerConfig config;
7878
private final ObjectMapper objectMapper =
7979
JsonMapper.builder().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
80-
private final ThreadMappingCache threadBySession;
80+
private final ThreadMappingStore threadBySession;
81+
private final SessionExecutionCoordinator sessionCoordinator = new SessionExecutionCoordinator();
8182
private final Object httpClientLock = new Object();
8283
private final ExecutorService clientExecutor = Executors.newCachedThreadPool(r -> {
8384
Thread thread = new Thread(r, "codex-app-server-client");
@@ -95,8 +96,15 @@ public class CodexAppServerClient implements AutoCloseable {
9596
* @throws NullPointerException if {@code config} is {@code null}.
9697
*/
9798
public CodexAppServerClient(CodexAppServerConfig config) {
99+
this(config, new ThreadMappingCache(
100+
Objects.requireNonNull(config, "config").getMaxSessionMappings()));
101+
}
102+
103+
public CodexAppServerClient(
104+
CodexAppServerConfig config,
105+
ThreadMappingStore threadMappingStore) {
98106
this.config = Objects.requireNonNull(config, "config");
99-
this.threadBySession = new ThreadMappingCache(config.getMaxSessionMappings());
107+
this.threadBySession = Objects.requireNonNull(threadMappingStore, "threadMappingStore");
100108
}
101109

102110
/**
@@ -142,6 +150,19 @@ public CompletableFuture<AppServerTurnResult> runTurnAsync(AppServerTurnRequest
142150
if (Objects.isNull(request.getPrompt()) || request.getPrompt().trim().isEmpty()) {
143151
throw new IllegalArgumentException("Codex prompt must not be blank");
144152
}
153+
String sessionKey = request.normalizedSessionKey();
154+
if (Objects.isNull(sessionKey)) {
155+
return startTurn(request);
156+
}
157+
return sessionCoordinator.submit(sessionKey, () -> startTurn(request));
158+
}
159+
160+
private CompletableFuture<AppServerTurnResult> startTurn(AppServerTurnRequest request) {
161+
if (closed) {
162+
CompletableFuture<AppServerTurnResult> failed = new CompletableFuture<>();
163+
failed.completeExceptionally(new IllegalStateException("Codex app-server client is closed"));
164+
return failed;
165+
}
145166
CodexAppServerTurn turn =
146167
new CodexAppServerTurn(request, config, objectMapper, threadBySession, httpClient());
147168
return turn.start();
@@ -187,7 +208,7 @@ public List<AppServerThread> listThreads(int limit, String cursor) {
187208
if (limit > 0) {
188209
params.put("limit", limit);
189210
}
190-
JsonNode result = execRpcNode("thread/list", params);
211+
JsonNode result = execRpcNode(CodexAppServerProtocol.THREAD_LIST, params);
191212
List<AppServerThread> threads = new ArrayList<>();
192213
for (JsonNode node : result.path("threads")) {
193214
threads.add(parseThread(node));
@@ -205,7 +226,7 @@ public List<AppServerThread> listThreads(int limit, String cursor) {
205226
public AppServerThread readThread(String threadId) {
206227
Map<String, Object> params = new LinkedHashMap<>();
207228
params.put("threadId", Objects.requireNonNull(threadId, "threadId").trim());
208-
JsonNode result = execRpcNode("thread/read", params);
229+
JsonNode result = execRpcNode(CodexAppServerProtocol.THREAD_READ, params);
209230
return parseThread(result.path("thread"));
210231
}
211232

@@ -234,26 +255,26 @@ public String readThreadRaw(String threadId) {
234255
public AppServerThread forkThread(String threadId) {
235256
Map<String, Object> params = new LinkedHashMap<>();
236257
params.put("threadId", Objects.requireNonNull(threadId, "threadId").trim());
237-
JsonNode result = execRpcNode("thread/fork", params);
258+
JsonNode result = execRpcNode(CodexAppServerProtocol.THREAD_FORK, params);
238259
return parseThread(result.path("thread"));
239260
}
240261

241262
/** Archives a thread via {@code thread/archive}. */
242263
public void archiveThread(String threadId) {
243-
simpleThreadCall("thread/archive", threadId);
264+
simpleThreadCall(CodexAppServerProtocol.THREAD_ARCHIVE, threadId);
244265
}
245266

246267
/** Unarchives a thread via {@code thread/unarchive}. */
247268
public void unarchiveThread(String threadId) {
248-
simpleThreadCall("thread/unarchive", threadId);
269+
simpleThreadCall(CodexAppServerProtocol.THREAD_UNARCHIVE, threadId);
249270
}
250271

251272
/**
252273
* Permanently deletes a thread and its spawned descendants via
253274
* {@code thread/delete}. Ephemeral roots cannot be deleted.
254275
*/
255276
public void deleteThread(String threadId) {
256-
simpleThreadCall("thread/delete", threadId);
277+
simpleThreadCall(CodexAppServerProtocol.THREAD_DELETE, threadId);
257278
}
258279

259280
/**
@@ -269,7 +290,7 @@ public void interruptTurn(String threadId, String turnId) {
269290
Map<String, Object> params = new LinkedHashMap<>();
270291
params.put("threadId", Objects.requireNonNull(threadId, "threadId").trim());
271292
params.put("turnId", Objects.requireNonNull(turnId, "turnId").trim());
272-
execRpcNode("turn/interrupt", params);
293+
execRpcNode(CodexAppServerProtocol.TURN_INTERRUPT, params);
273294
}
274295

275296
/**
@@ -290,7 +311,7 @@ public void steerTurn(String threadId, String expectedTurnId, String prompt) {
290311
params.put("threadId", Objects.requireNonNull(threadId, "threadId").trim());
291312
params.put("expectedTurnId", Objects.requireNonNull(expectedTurnId, "expectedTurnId").trim());
292313
params.put("input", List.of(input));
293-
execRpcNode("turn/steer", params);
314+
execRpcNode(CodexAppServerProtocol.TURN_STEER, params);
294315
}
295316

296317
/**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2018-present, easy-4-java (https://github.com/easy-4-java).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
*/
7+
package io.github.easy4j.codex.appserver;
8+
9+
/**
10+
* Listener for high-value Codex app-server turn events.
11+
*
12+
* <p>All methods are optional. Implementations should return quickly because
13+
* callbacks are invoked on the WebSocket event path.</p>
14+
*/
15+
public interface CodexAppServerListener {
16+
17+
default void onTurnStarted(String turnId) {
18+
}
19+
20+
default void onTextDelta(String delta) {
21+
}
22+
23+
default void onItemCompleted(String itemType, String content) {
24+
}
25+
26+
default void onTokenUsage(String rawJson) {
27+
}
28+
29+
default void onWarning(String rawJson) {
30+
}
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2018-present, easy-4-java (https://github.com/easy-4-java).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
*/
7+
package io.github.easy4j.codex.appserver;
8+
9+
/**
10+
* Canonical method and notification names used by the Codex app-server
11+
* integration. Keeping protocol strings in one place prevents the turn and
12+
* generic-RPC transports from drifting apart.
13+
*/
14+
final class CodexAppServerProtocol {
15+
16+
static final String INITIALIZE = "initialize";
17+
static final String INITIALIZED = "notifications/initialized";
18+
19+
static final String THREAD_START = "thread/start";
20+
static final String THREAD_RESUME = "thread/resume";
21+
static final String THREAD_LIST = "thread/list";
22+
static final String THREAD_READ = "thread/read";
23+
static final String THREAD_FORK = "thread/fork";
24+
static final String THREAD_ARCHIVE = "thread/archive";
25+
static final String THREAD_UNARCHIVE = "thread/unarchive";
26+
static final String THREAD_DELETE = "thread/delete";
27+
28+
static final String TURN_START = "turn/start";
29+
static final String TURN_STEER = "turn/steer";
30+
static final String TURN_INTERRUPT = "turn/interrupt";
31+
32+
static final String TURN_STARTED = "turn/started";
33+
static final String TURN_COMPLETED = "turn/completed";
34+
static final String TURN_FAILED = "turn/failed";
35+
static final String ITEM_STARTED = "item/started";
36+
static final String ITEM_COMPLETED = "item/completed";
37+
static final String AGENT_MESSAGE_DELTA = "item/agentMessage/delta";
38+
static final String TOKEN_USAGE_UPDATED = "thread/tokenUsage/updated";
39+
static final String ERROR = "error";
40+
41+
private CodexAppServerProtocol() {
42+
}
43+
}

src/main/java/io/github/easy4j/codex/appserver/CodexAppServerRpc.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void onOpen(WebSocket socket) {
125125
clientInfo.put("version", "3.0.x");
126126
Map<String, Object> initParams = new java.util.LinkedHashMap<>();
127127
initParams.put("clientInfo", clientInfo);
128-
sendRpc(INIT_REQUEST_ID, "initialize", initParams);
128+
sendRpc(INIT_REQUEST_ID, CodexAppServerProtocol.INITIALIZE, initParams);
129129
}
130130

131131
@Override
@@ -236,7 +236,7 @@ private void handleInitializeSettled() {
236236
// both back-to-back from inside the onText callback.
237237
Map<String, Object> payload = new java.util.LinkedHashMap<>();
238238
payload.put("jsonrpc", "2.0");
239-
payload.put("method", "initialized");
239+
payload.put("method", CodexAppServerProtocol.INITIALIZED);
240240
payload.put("params", java.util.Collections.emptyMap());
241241
socket.sendText(toJson(payload), true).whenComplete((w, error) -> {
242242
if (Objects.nonNull(error)) {

0 commit comments

Comments
 (0)