Skip to content

Commit d3055d4

Browse files
committed
feat: 对齐 codex-cli 能力面——CLI typed 方法 + app-server 协议扩展
CLI 路线(依据官方 developer-commands 文档): - debug typed:debugModels / debugModelsBundled / debugPromptInput - mcp HTTP 注册 typed:mcpAddUrl / mcpAddUrlWithBearer(--url + --bearer-token-env-var,token 不落 config.toml) - plugin 生命周期 typed:pluginAdd/List/Remove + pluginMarketplace Add/List/Remove/Upgrade - cloud typed:cloudExec(--env + query)/ cloudList(--json) - features typed:featuresEnable/Disable/List - review 自定义指令:reviewPrompt app-server WS 路线(依据官方 app-server 文档 + app-server-protocol v2 形状): - 新增 CodexAppServerRpc:短连接通用 JSON-RPC,含文档要求的容错 initialize 握手(老 daemon 不支持时忽略错误继续) - thread 生命周期:listThreads(limit[,cursor]) / readThread / readThreadRaw / forkThread / archiveThread / unarchiveThread / deleteThread - turn 控制:interruptTurn(threadId,turnId) / steerTurn(threadId, expectedTurnId,prompt)——线程状态在服务端,原 turn 连接流式输出 中亦可用 - turn/started 捕获 turnId:AppServerTurnRequest.onTurnStarted 回调 + AppServerTurnResult.turnId,支撑运行中转向/打断 - 逃生通道 execRpc(method,params) 覆盖未建模方法 - 假服务器同步扩展 + E2E:协议握手、thread 生命周期全链路、turnId 暴露;修复 Rpc 主请求结果未桥接到实例 future 的确定性缺陷 测试 230 个全绿(+3)。README 双语同步。
1 parent bed5797 commit d3055d4

12 files changed

Lines changed: 1118 additions & 0 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ Typical scenarios:
7979
| Auth / MCP / misc | Active development | `login`, `loginWithApiKey`, `loginWithAccessToken`, `loginDeviceAuth`, `loginStatus`, `logout`, `mcpList` / `mcpAdd` / `mcpGet` / `mcpRemove` / `mcpLogin` / `mcpLogout`, `update`, `features`, `completion`, `app` |
8080
| Session admin | Active development | `archiveSession`, `unarchiveSession`, `queue`, `deleteSession`, `deleteSessionForce`, `agents`, `migrateRollouts` |
8181
| App-server WebSocket route | Active development | `CodexAppServerClient.runTurn` / `runTurnAsync`, `thread/start` / `thread/resume`, agent-message deltas, `sessionKey → threadId` LRU (1000) |
82+
| App-server protocol surface | Active development | `thread/list` / `read` / `fork` / `archive` / `unarchive` / `delete`, `turn/interrupt` / `turn/steer`, `model/list`-style escape hatch `execRpc`; `turnId` exposed via `onTurnStarted` + `AppServerTurnResult` |
83+
| CLI typed additions | Active development | `debugModels(Bundled)` / `debugPromptInput`, `mcpAddUrl(WithBearer)`, `pluginAdd/List/Remove` + `pluginMarketplace*`, `cloudExec` / `cloudList`, `featuresEnable/Disable/List`, `reviewPrompt` |
8284
| Config model | Active development | `CodexClientConfig` POJO (plain, Spring-bindable), `CodexAppServerConfig` POJO |
8385

8486
> **Note**: `codex mcp-server` was removed upstream — `CodexClient.mcpServer()`
@@ -296,6 +298,25 @@ try (CodexAppServerClient client = new CodexAppServerClient(config)) {
296298
}
297299
```
298300

301+
### 8.4 App-server protocol operations
302+
303+
```java
304+
try (CodexAppServerClient client = new CodexAppServerClient(config)) {
305+
List<AppServerThread> threads = client.listThreads(20);
306+
AppServerThread forked = client.forkThread("th_123");
307+
client.steerTurn("th_123", "turn_9", "也检查一下测试覆盖率"); // redirect a running turn
308+
client.interruptTurn("th_123", "turn_9"); // cancel a running turn
309+
client.archiveThread("th_123");
310+
String raw = client.execRpc("model/list", Map.of("limit", 10)); // escape hatch
311+
}
312+
```
313+
314+
Lifecycle calls run over a short-lived connection with the documented
315+
tolerant `initialize` handshake; thread state is server-side, so
316+
`steerTurn` / `interruptTurn` work while the original turn connection is
317+
still streaming. Running turns expose their id via
318+
`AppServerTurnRequest.onTurnStarted` and `AppServerTurnResult.getTurnId()`.
319+
299320
The turn maps to `thread/start` (or `thread/resume` when `sessionKey` already
300321
maps to a thread id) → `turn/start``item/completed` (only agent messages
301322
surface) → `turn/completed`. Unknown notifications are logged at debug level

README.zh-CN.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ SDK 覆盖:
7676
| 认证 / MCP / 其他 | 活跃开发 | `login``loginWithApiKey``loginWithAccessToken``loginDeviceAuth``loginStatus``logout``mcpList` / `mcpAdd` / `mcpGet` / `mcpRemove` / `mcpLogin` / `mcpLogout``update``features``completion``app` |
7777
| 会话管理 | 活跃开发 | `archiveSession``unarchiveSession``queue``deleteSession``deleteSessionForce``agents``migrateRollouts` |
7878
| App-server WebSocket 路线 | 活跃开发 | `CodexAppServerClient.runTurn` / `runTurnAsync``thread/start` / `thread/resume`、agent 消息 delta、`sessionKey → threadId` LRU(1000) |
79+
| App-server 协议面 | 活跃开发 | `thread/list` / `read` / `fork` / `archive` / `unarchive` / `delete``turn/interrupt` / `turn/steer`、逃生通道 `execRpc``turnId``onTurnStarted``AppServerTurnResult` 暴露 |
80+
| CLI typed 补齐 | 活跃开发 | `debugModels(Bundled)` / `debugPromptInput``mcpAddUrl(WithBearer)``pluginAdd/List/Remove` + `pluginMarketplace*``cloudExec` / `cloudList``featuresEnable/Disable/List``reviewPrompt` |
7981
| 配置模型 | 活跃开发 | `CodexClientConfig` POJO(纯对象,可绑定 Spring 配置)、`CodexAppServerConfig` POJO |
8082

8183
> **注意**:上游已移除 `codex mcp-server` 子命令——`CodexClient.mcpServer()`
@@ -288,6 +290,24 @@ try (CodexAppServerClient client = new CodexAppServerClient(config)) {
288290
}
289291
```
290292

293+
### 8.4 App-server 协议操作
294+
295+
```java
296+
try (CodexAppServerClient client = new CodexAppServerClient(config)) {
297+
List<AppServerThread> threads = client.listThreads(20);
298+
AppServerThread forked = client.forkThread("th_123");
299+
client.steerTurn("th_123", "turn_9", "也检查一下测试覆盖率"); // 运行中转向
300+
client.interruptTurn("th_123", "turn_9"); // 运行中打断
301+
client.archiveThread("th_123");
302+
String raw = client.execRpc("model/list", Map.of("limit", 10)); // 逃生通道
303+
}
304+
```
305+
306+
生命周期调用走短连接并带文档规定的容错 `initialize` 握手;线程状态在
307+
服务端,因此原 turn 连接仍在流式输出时 `steerTurn` / `interruptTurn`
308+
同样可用。运行中的 turn 经 `AppServerTurnRequest.onTurnStarted`
309+
`AppServerTurnResult.getTurnId()` 暴露其 id。
310+
291311
一个 turn 对应:`thread/start``sessionKey` 已有映射时走 `thread/resume`)→
292312
`turn/start``item/completed`(仅 agent 消息对外呈现)→ `turn/completed`
293313
未知通知只记录 debug 日志,不中断 turn。失败——连接、JSON-RPC 错误、

src/main/java/io/github/easy4j/codex/CodexClient.java

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,172 @@ public CodexCliResult apply(String taskId) {
789789
*/
790790
public CodexCliResult features() { return cli.features(); }
791791

792+
/**
793+
* Runs {@code codex debug models} — the raw model catalog as JSON.
794+
*
795+
* @return the raw CLI invocation result; never {@code null}.
796+
* @since 3.0.0
797+
*/
798+
public CodexCliResult debugModels() { return cli.debugModels(); }
799+
800+
/**
801+
* Runs {@code codex debug models --bundled} — only the bundled catalog.
802+
*
803+
* @return the raw CLI invocation result; never {@code null}.
804+
* @since 3.0.0
805+
*/
806+
public CodexCliResult debugModelsBundled() { return cli.debugModelsBundled(); }
807+
808+
/**
809+
* Runs {@code codex debug prompt-input <prompt>} — the model-visible
810+
* prompt input list as JSON.
811+
*
812+
* @param prompt the prompt to render.
813+
* @return the raw CLI invocation result; never {@code null}.
814+
* @since 3.0.0
815+
*/
816+
public CodexCliResult debugPromptInput(String prompt) { return cli.debugPromptInput(prompt); }
817+
818+
/**
819+
* Runs {@code codex mcp add <name> --url <url>} — registers a streamable
820+
* HTTP MCP server.
821+
*
822+
* @param name the MCP server name.
823+
* @param url the streamable HTTP endpoint URL.
824+
* @return the raw CLI invocation result; never {@code null}.
825+
* @since 3.0.0
826+
*/
827+
public CodexCliResult mcpAddUrl(String name, String url) { return cli.mcpAddUrl(name, url); }
828+
829+
/**
830+
* Runs {@code codex mcp add <name> --url <url> --bearer-token-env-var <env>}
831+
* — registers an HTTP MCP server whose bearer token lives in an env var.
832+
*
833+
* @param name the MCP server name.
834+
* @param url the streamable HTTP endpoint URL.
835+
* @param bearerTokenEnvVar environment variable holding the bearer token.
836+
* @return the raw CLI invocation result; never {@code null}.
837+
* @since 3.0.0
838+
*/
839+
public CodexCliResult mcpAddUrlWithBearer(String name, String url, String bearerTokenEnvVar) {
840+
return cli.mcpAddUrlWithBearer(name, url, bearerTokenEnvVar);
841+
}
842+
843+
/**
844+
* Runs {@code codex plugin add <plugin[@marketplace]>}.
845+
*
846+
* @param pluginRef plugin reference.
847+
* @return the raw CLI invocation result; never {@code null}.
848+
* @since 3.0.0
849+
*/
850+
public CodexCliResult pluginAdd(String pluginRef) { return cli.pluginAdd(pluginRef); }
851+
852+
/**
853+
* Runs {@code codex plugin list}.
854+
*
855+
* @return the raw CLI invocation result; never {@code null}.
856+
* @since 3.0.0
857+
*/
858+
public CodexCliResult pluginList() { return cli.pluginList(); }
859+
860+
/**
861+
* Runs {@code codex plugin remove <plugin[@marketplace]>}.
862+
*
863+
* @param pluginRef plugin reference to remove.
864+
* @return the raw CLI invocation result; never {@code null}.
865+
* @since 3.0.0
866+
*/
867+
public CodexCliResult pluginRemove(String pluginRef) { return cli.pluginRemove(pluginRef); }
868+
869+
/**
870+
* Runs {@code codex plugin marketplace add <source>}.
871+
*
872+
* @param source the marketplace source.
873+
* @return the raw CLI invocation result; never {@code null}.
874+
* @since 3.0.0
875+
*/
876+
public CodexCliResult pluginMarketplaceAdd(String source) { return cli.pluginMarketplaceAdd(source); }
877+
878+
/**
879+
* Runs {@code codex plugin marketplace list}.
880+
*
881+
* @return the raw CLI invocation result; never {@code null}.
882+
* @since 3.0.0
883+
*/
884+
public CodexCliResult pluginMarketplaceList() { return cli.pluginMarketplaceList(); }
885+
886+
/**
887+
* Runs {@code codex plugin marketplace remove <name>}.
888+
*
889+
* @param name the marketplace name.
890+
* @return the raw CLI invocation result; never {@code null}.
891+
* @since 3.0.0
892+
*/
893+
public CodexCliResult pluginMarketplaceRemove(String name) { return cli.pluginMarketplaceRemove(name); }
894+
895+
/**
896+
* Runs {@code codex plugin marketplace upgrade <name>}.
897+
*
898+
* @param name the marketplace name to upgrade.
899+
* @return the raw CLI invocation result; never {@code null}.
900+
* @since 3.0.0
901+
*/
902+
public CodexCliResult pluginMarketplaceUpgrade(String name) { return cli.pluginMarketplaceUpgrade(name); }
903+
904+
/**
905+
* Runs {@code codex cloud exec --env <envId> <query>} — submits a cloud task.
906+
*
907+
* @param envId the cloud environment id.
908+
* @param query the task query.
909+
* @return the raw CLI invocation result; never {@code null}.
910+
* @since 3.0.0
911+
*/
912+
public CodexCliResult cloudExec(String envId, String query) { return cli.cloudExec(envId, query); }
913+
914+
/**
915+
* Runs {@code codex cloud list --env <envId> --json}.
916+
*
917+
* @param envId the cloud environment id.
918+
* @return the raw CLI invocation result; never {@code null}.
919+
* @since 3.0.0
920+
*/
921+
public CodexCliResult cloudList(String envId) { return cli.cloudList(envId); }
922+
923+
/**
924+
* Runs {@code codex features enable <feature>}.
925+
*
926+
* @param feature the feature flag name.
927+
* @return the raw CLI invocation result; never {@code null}.
928+
* @since 3.0.0
929+
*/
930+
public CodexCliResult featuresEnable(String feature) { return cli.featuresEnable(feature); }
931+
932+
/**
933+
* Runs {@code codex features disable <feature>}.
934+
*
935+
* @param feature the feature flag name.
936+
* @return the raw CLI invocation result; never {@code null}.
937+
* @since 3.0.0
938+
*/
939+
public CodexCliResult featuresDisable(String feature) { return cli.featuresDisable(feature); }
940+
941+
/**
942+
* Runs {@code codex features list}.
943+
*
944+
* @return the raw CLI invocation result; never {@code null}.
945+
* @since 3.0.0
946+
*/
947+
public CodexCliResult featuresList() { return cli.featuresList(); }
948+
949+
/**
950+
* Runs {@code codex review <prompt>} — a custom-instruction review.
951+
*
952+
* @param prompt the review instruction.
953+
* @return the raw CLI invocation result; never {@code null}.
954+
* @since 3.0.0
955+
*/
956+
public CodexCliResult reviewPrompt(String prompt) { return cli.reviewPrompt(prompt); }
957+
792958
/**
793959
* Runs {@code codex mcp-server}.
794960
*
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.easy4j.codex.appserver;
17+
18+
import lombok.Builder;
19+
import lombok.Data;
20+
21+
/**
22+
* Tolerant summary of a Codex thread as returned by the app-server
23+
* {@code thread/list}, {@code thread/read} and {@code thread/fork} methods.
24+
*
25+
* <p>The SDK accepts both camelCase and snake_case field spellings when
26+
* populating this model, staying forward-compatible with protocol revisions.</p>
27+
*
28+
* @author <a href="https://github.com/loong10k">Loong Wan</a>
29+
* @since 3.0.0
30+
* @see CodexAppServerClient#listThreads(int)
31+
*/
32+
@Data
33+
@Builder
34+
public class AppServerThread {
35+
36+
/** Stable opaque thread identifier used by resume/fork/interrupt operations. */
37+
private String id;
38+
39+
/** Display name (often a prompt summary); may be {@code null}. */
40+
private String name;
41+
42+
/** Working directory captured when the thread was created; may be {@code null}. */
43+
private String cwd;
44+
45+
/** Creation timestamp as reported by the server; may be {@code null}. */
46+
private String createdAt;
47+
48+
/** Last-mutation timestamp as reported by the server; may be {@code null}. */
49+
private String updatedAt;
50+
51+
/** {@code true} when the thread has been archived. */
52+
private boolean archived;
53+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ public class AppServerTurnRequest {
5050
*/
5151
private Consumer<String> onDelta;
5252

53+
/**
54+
* Optional callback invoked when the server reports {@code turn/started}
55+
* with the active turn id. Use it together with
56+
* {@link CodexAppServerClient#steerTurn(String, String, String)} or
57+
* {@link CodexAppServerClient#interruptTurn(String, String)} to act on a
58+
* turn while it is still running.
59+
*/
60+
private Consumer<String> onTurnStarted;
61+
5362
/**
5463
* Returns the session key in a comparable form, or {@code null} when unset.
5564
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public class AppServerTurnResult {
3232
/** Codex thread id the turn ran on; also the value stored in the session mapping. */
3333
private String threadId;
3434

35+
/** Turn id reported by {@code turn/started}; usable for steer/interrupt while running. */
36+
private String turnId;
37+
3538
/**
3639
* Concatenated text of every agent-message item completed during the turn;
3740
* falls back to the {@code message} carried by {@code turn/completed} when

0 commit comments

Comments
 (0)