Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ jobs:
THRESHOLDS["internal/worker/claudecode"]=53
THRESHOLDS["internal/worker/opencodeserver"]=9
THRESHOLDS["internal/worker/noop"]=80
THRESHOLDS["internal/worker/acp"]=40
THRESHOLDS["internal/messaging/feishu"]=48
THRESHOLDS["internal/worker/codexcli"]=51
THRESHOLDS["internal/worker/pi"]=0
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ hotplex cron history <id|name> [--json]
- 无 `api/` 目录(使用 JSON over WebSocket)
- PostgreSQL 支持已实现(`db.driver: "postgres"`),SQLite 仍为默认
- OpenCode CLI 适配器已移除(由 OCS 替代)
- ACPX 适配器仅存在类型常量(无实现
- ACP 适配器已实现(JSON-RPC 2.0 over stdio,原 ACPX 已移除
- Windows 自更新不支持(exe 运行时被锁,使用 `scripts/install.ps1` 替代)

### 跨平台支持
Expand Down
2 changes: 1 addition & 1 deletion client/examples/07_multi_worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
var workerTypes = []string{
"claude_code",
"opencode_server",
"acpx",
"acp",
}

func main() {
Expand Down
2 changes: 1 addition & 1 deletion client/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ go run ./01_quickstart
| `HOTPLEX_GATEWAY_URL` | `ws://localhost:8888/ws` | Gateway WebSocket URL |
| `HOTPLEX_API_KEY` | — | API Key for authentication |
| `HOTPLEX_SIGNING_KEY` | — | ES256 signing key (PEM/hex/base64) for JWT auth |
| `HOTPLEX_WORKER_TYPE` | `claude_code` | Worker type (claude_code, opencode_server, acpx) |
| `HOTPLEX_WORKER_TYPE` | `claude_code` | Worker type (claude_code, opencode_server, acp) |
| `HOTPLEX_SESSION_ID` | — | Existing session ID (for resume) |
| `HOTPLEX_TASK` | *(varies)* | Task prompt to send |
| `HOTPLEX_AUTO_APPROVE` | — | Set to `1` to auto-approve all permissions |
Expand Down
7 changes: 7 additions & 0 deletions cmd/hotplex/gateway_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/hrygo/hotplex/internal/sqlutil"
"github.com/hrygo/hotplex/internal/tracing"
"github.com/hrygo/hotplex/internal/webchat"
"github.com/hrygo/hotplex/internal/worker/acp"
"github.com/hrygo/hotplex/internal/worker/claudecode"
"github.com/hrygo/hotplex/internal/worker/codexcli"
"github.com/hrygo/hotplex/internal/worker/opencodeserver"
Expand Down Expand Up @@ -280,6 +281,7 @@ func runGateway(configPath string, devMode bool, stopCh <-chan struct{}) (err er

opencodeserver.InitSingleton(log, cfg.Worker.OpenCodeServer)
claudecode.InitConfig(cfg.Worker.ClaudeCode)
acp.InitConfig(cfg.Worker.ACP)
if cfg.Worker.CodexCLI.UseAppServer {
codexcli.InitSingleton(log, cfg.Worker.CodexCLI)
} else {
Expand Down Expand Up @@ -319,6 +321,11 @@ func runGateway(configPath string, devMode bool, stopCh <-chan struct{}) (err er
codexcli.InitConfig(next.Worker.CodexCLI)
}
})
cfgStore.RegisterFunc(func(prev, next *config.Config) {
if !reflect.DeepEqual(prev.Worker.ACP, next.Worker.ACP) {
acp.InitConfig(next.Worker.ACP)
}
})
cfgStore.RegisterFunc(func(prev, next *config.Config) {
if !reflect.DeepEqual(prev.Worker.ClaudeCode.MCPServers, next.Worker.ClaudeCode.MCPServers) {
bridge.UpdateMCPConfig(buildMCPConfigJSON(next))
Expand Down
8 changes: 4 additions & 4 deletions docs/architecture/Platform-Messaging-Architecture-Diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Worker Registry + Adapters (ACPX 未实现) │ │
│ │ ClaudeCode │ OpenCodeSrv │ ~~ACPX~~ │ │
│ │ (stdio) │ (HTTP/SSE) │ (—) │ │
│ │ Worker Registry + Adapters │ │
│ │ ClaudeCode │ OpenCodeSrv │ ACP │ │
│ │ (stdio) │ (HTTP/SSE) │ (stdio/RPC) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -101,7 +101,7 @@ internal/
worker.go Worker 接口
registry.go 自注册工厂
base/ 共享生命周期基座
claudecode/ opencodesrv/ pimon/ (acpx/ — ⚠️ 未实现)
claudecode/ opencodesrv/ pimon/ acp/

messaging/ ★ NEW (~650 行, 零核心文件改动)
├── platform_conn.go PlatformConn 接口 (WriteCtx + Close)
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/Worker-Gateway-Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ Lifecycle: persistent | ephemeral | managed
| ------------------- | ---------- | ----------- | ---------- | ------------------------------------------- |
| Claude Code | stdio | stream-json | persistent | turn 间进程不退出,热复用 |
| **OpenCode Server** | HTTP + SSE | SSE/JSON | managed | `opencode serve`,单进程多 session |
| ACPX | — | — | — | ⚠️ **未实现**(`internal/worker/acpx/` 为空目录) |
| **ACP** | stdio | JSON-RPC 2.0 | per-session | 通用 ACP 协议适配器(`internal/worker/acp/`) |

> **Hot-Multiplexing**:persistent Worker 在 turn 结束后**不退出进程**,保持 `idle` 状态等待下一轮 stdin 输入,实现零冷启动。ephemeral Worker 每次执行完毕退出。managed Worker 由外部进程管理生命周期。

Expand Down
26 changes: 25 additions & 1 deletion docs/reference/aep-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,30 @@ message.start → message.delta* → message.end

Autonomous 模式下为**通知性质**,Worker 内部执行,Client 无需回传结果。

### tool_update(工具调用中间状态)

```json
{ "type": "tool_update", "data": { "id": "call_123", "name": "read_file", "status": "in_progress" } }
```

ACP 专用:映射 `tool_call_update`,报告工具调用的中间状态(`pending` / `in_progress`)。

### plan(计划更新)

```json
{ "type": "plan", "data": { "entries": [{"id": "1", "text": "Read config file", "status": "completed"}] } }
```

ACP 专用:映射 `AgentPlanUpdate`,Agent 的计划/任务列表变更通知。

### mode_update(模式切换)

```json
{ "type": "mode_update", "data": { "mode_id": "code", "name": "Code Mode" } }
```

ACP 专用:映射 `CurrentModeUpdate`,Agent 执行模式切换通知。

### state(状态变更)

```json
Expand Down Expand Up @@ -424,4 +448,4 @@ Heartbeat: ping ←→ pong

**必须支持**:`init`、`input`、`control`、`ping`、`init_ack`、`message.delta`、`state`、`error`、`done`、`pong`

**可选扩展**:`message.start/end`、`message`、`tool_call/result`、`reasoning`、`step`、`raw`、`permission_*`、`question_*`、`elicitation_*`、`context_usage`、`mcp_status`、`worker_command`
**可选扩展**:`message.start/end`、`message`、`tool_call/result`、`tool_update`、`plan`、`mode_update`、`reasoning`、`step`、`raw`、`permission_*`、`question_*`、`elicitation_*`、`context_usage`、`mcp_status`、`worker_command`
2 changes: 1 addition & 1 deletion docs/specs/ACP-Worker-Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ worker:

```go
// internal/worker/acp/worker.go — 注册(与现有 Worker 一致,放在 worker.go 的 init() 中)
const TypeACP worker.WorkerType = "acp" // 新常量,与 TypeACPX = "acpx" 共存
const TypeACP worker.WorkerType = "acp" // 替代原 TypeACPX = "acpx"

func init() {
worker.Register(TypeACP, func() (worker.Worker, error) {
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/Codex-CLI-Worker-Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const (
TypeClaudeCode WorkerType = "claude_code"
TypeOpenCodeSrv WorkerType = "opencode_server"
TypeCodexCLI WorkerType = "codex_cli" // 新增
TypeACPX WorkerType = "acpx"
TypeACP WorkerType = "acp"
TypeUnknown WorkerType = "unknown"
)
```
Expand Down
2 changes: 1 addition & 1 deletion e2e/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,5 +441,5 @@ var allWorkerTypes = []struct {
}{
{"claude_code", string(worker.TypeClaudeCode)},
{"opencode_server", string(worker.TypeOpenCodeSrv)},
{"acpx", string(worker.TypeACPX)},
{"acp", string(worker.TypeACP)},
}
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ type WorkerConfig struct {
OpenCodeServer OpenCodeServerConfig `mapstructure:"opencode_server"`
ClaudeCode ClaudeCodeConfig `mapstructure:"claude_code"`
CodexCLI CodexCLIConfig `mapstructure:"codex_cli"`
ACP ACPConfig `mapstructure:"acp"`
Environment []string `mapstructure:"environment"`
}

Expand Down Expand Up @@ -598,6 +599,13 @@ type OpenCodeServerConfig struct {
HTTPTimeout time.Duration `mapstructure:"http_timeout"`
}

// ACPConfig holds ACP (Agent Client Protocol) worker settings.
// ACP is a universal worker type that connects to any ACP-compatible agent via stdio.
type ACPConfig struct {
Command string `mapstructure:"command" json:"command"` // ACP agent binary (e.g. "hermes-acp")
AutoApprove bool `mapstructure:"auto_approve,omitempty" json:"auto_approve,omitempty"` // auto-approve permission requests
}

// AutoRetryConfig controls automatic retry behavior when LLM provider returns
// temporary errors (429 rate limit, 529 overload, 400 bad request, etc.).
type AutoRetryConfig struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/gateway/conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func TestSessionStateForWorker(t *testing.T) {
t.Parallel()
require.Equal(t, events.StateCreated, SessionStateForWorker(worker.TypeClaudeCode))
require.Equal(t, events.StateCreated, SessionStateForWorker(worker.TypeOpenCodeSrv))
require.Equal(t, events.StateCreated, SessionStateForWorker(worker.TypeACPX))
require.Equal(t, events.StateCreated, SessionStateForWorker(worker.TypeACP))
}

func TestDefaultServerCaps(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions internal/session/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestDeriveSessionKey_UUIDv5Format(t *testing.T) {
}{
{"u1", worker.TypeClaudeCode, "s1", "/tmp/hotplex/workspace"},
{"user_long_id", worker.TypeOpenCodeSrv, "my-session-123", "/tmp/hotplex/projects/app"},
{"", worker.TypeACPX, "", ""},
{"", worker.TypeACP, "", ""},
{"owner", worker.TypeOpenCodeSrv, "session-with-dashes", "/var/hotplex/projects"},
}

Expand Down Expand Up @@ -80,7 +80,7 @@ func TestDeriveSessionKey_AllWorkerTypes(t *testing.T) {
for _, wt := range []worker.WorkerType{
worker.TypeClaudeCode,
worker.TypeOpenCodeSrv,
worker.TypeACPX,
worker.TypeACP,
worker.TypeUnknown,
} {
wt := wt
Expand Down Expand Up @@ -221,7 +221,7 @@ func TestDerivePlatformSessionKey_AllWorkerTypes(t *testing.T) {
for _, wt := range []worker.WorkerType{
worker.TypeClaudeCode,
worker.TypeOpenCodeSrv,
worker.TypeACPX,
worker.TypeACP,
worker.TypeUnknown,
} {
wt := wt
Expand Down
11 changes: 5 additions & 6 deletions internal/worker/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Worker Adapter Package

## OVERVIEW
Go worker adapter package with 2 runtime adapters (ClaudeCode, OpenCodeSrv) + 1 noop reference implementation + shared process lifecycle management. ACPX type constant exists but has no implementation.
Go worker adapter package with 3 runtime adapters (ClaudeCode, OpenCodeSrv, ACP) + 1 noop reference implementation + shared process lifecycle management.

## STRUCTURE
```
Expand All @@ -11,7 +11,7 @@ internal/worker/
noop/ # Reference implementation (compile-time assertions)
claudecode/ # Claude Code adapter (claude --print --session-id, 631 lines)
opencodeserver/ # OpenCode Server adapter (HTTP+SSE, 952 lines)
acpx/ # EMPTY — only TypeACPX constant exists in worker.go
acp/ # ACP (Agent Client Protocol) adapter (JSON-RPC 2.0 over stdio)
base/
worker.go # BaseWorker shared lifecycle: Terminate/Kill/Wait/Health/LastIO
conn.go # stdin SessionConn: NDJSON over stdio, WriteAll, InputRecoverer
Expand All @@ -25,7 +25,7 @@ internal/worker/
|------|----------|-------|
| Add new Worker adapter | `internal/worker/<name>/` | Implement `Worker` + `SessionConn` + `Capabilities`, register via `init()` |
| Core adapter interfaces | `worker.go` | SessionConn (line 19), Capabilities (line 40), Worker (line 84) |
| Worker type constants | `worker.go:70` | TypeClaudeCode, TypeOpenCodeSrv, TypeACPX, TypeUnknown |
| Worker type constants | `worker.go:70` | TypeClaudeCode, TypeOpenCodeSrv, TypeACP, TypeUnknown |
| Process lifecycle | `proc/manager.go` | Start/Terminate/Kill/Wait/ReadLine |
| Worker registration | `registry.go` | `Register(t WorkerType, b Builder)`, blank import in main.go |
| Compile-time interface checks | `noop/worker.go` | `var _ worker.Worker = (*Worker)(nil)` assertions |
Expand Down Expand Up @@ -57,11 +57,10 @@ func NewWorker(t WorkerType) (Worker, error)
| ClaudeCode | stdio (`claude --print --session-id`) | `--resume` flag | External (gateway) |
| OpenCodeSrv | HTTP+SSE (`opencode serve`) | Process managed | Via HTTP API |
| Noop | N/A | N/A | Testing only |
| ACPX | N/A | N/A | Type constant only, no implementation |
| ACP | stdio (JSON-RPC 2.0 over NDJSON) | NewSession/LoadSession | Via Initialize handshake |

## ANTI-PATTERNS
- Do NOT use `math/rand` for crypto — use `crypto/rand` for JTI, tokens
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 P2: 反模式条目重复

L63-66 与 L67-70 内容重复(仅 L70 多了 hermes-acp)。删除 L63-66,保留含 hermes-acp 的版本。

- Do NOT skip `Setpgid:true` — child process cleanup depends on PGID isolation
- Do NOT skip graceful shutdown — always attempt SIGTERM before SIGKILL
- Do NOT use shell execution — only call `claude`/`opencode` binaries directly
- Do NOT register ACPX adapter — directory is empty, only TypeACPX constant exists
- Do NOT use shell execution — only call `claude`/`opencode`/`hermes-acp` binaries directly
Loading