Skip to content

Commit 120dab5

Browse files
feat(client): 增加 Runner profile preset
合入客户端 Runner profile preset:Edge CLI 支持 agenthub-runner-mock,保留手工 runner command 模式,并更新路线图。
1 parent b0d83cd commit 120dab5

6 files changed

Lines changed: 164 additions & 5 deletions

File tree

docs/roadmap.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## 当前总目标
66

7-
推进 M2 Edge 本地数据层,让前端、后端、客户端三条线能围绕稳定的 Project / Thread / Run / Item / Event 模型并行开发。当前客户端 PR #30 已提供内存态最小实现,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Runner lifecycle 边界,`feat/client-store-boundary-delicious233` 已抽象 Edge store 接口边界,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已接入 Edge 启动参数 `--store-file``feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,下一步重点是真实 Runner adapter。
7+
推进 M2 Edge 本地数据层,让前端、后端、客户端三条线能围绕稳定的 Project / Thread / Run / Item / Event 模型并行开发。当前客户端 PR #30 已提供内存态最小实现,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Runner lifecycle 边界,`feat/client-store-boundary-delicious233` 已抽象 Edge store 接口边界,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已接入 Edge 启动参数 `--store-file`,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补 `agenthub-runner-mock` preset 入口,下一步重点是真实 Runner adapter。
88

99
## 路线图分层
1010

@@ -26,15 +26,15 @@
2626

2727
- [x] M1 客户端本地链路:Desktop Shell + Local Edge + Mock Runner + smoke test。
2828
- [ ] M2 Edge 本地数据层:Project / Thread / Run / Item / EventStore。最小内存实现已在 PR #30,message/item 写入链路、Runner lifecycle 边界、store 接口边界、轻量 JSON 文件持久化实现和 `--store-file` 启动参数已补齐,SQLite 仍是后续可选评估项。
29-
- [ ] M3 真实 Runner:CLI Agent 进程、取消、日志、错误映射。本地进程 executor、本地进程工作目录边界和 generic adapter profile / 命令模板最小层已补齐,但还不是 Claude Code / Codex / OpenCode 的完整 adapter。
29+
- [ ] M3 真实 Runner:CLI Agent 进程、取消、日志、错误映射。本地进程 executor、本地进程工作目录边界、generic adapter profile / 命令模板最小层和仓库自带 mock Runner preset 已补齐,但还不是 Claude Code / Codex / OpenCode 的完整 adapter。
3030
- [ ] M4 Workspace 能力:worktree、diff、preview、artifact、approval。
3131
- [ ] M5 Hub 协作链路:Edge-Hub sync、远程查看、远程审批。
3232

3333
## 当前活跃方向
3434

3535
- 前端:从 Mock 数据过渡到真实 REST / WebSocket client,承接 UI 同学设计。
3636
- 后端:实现 Hub Server、Edge-Hub 通信、账号/群聊/同步/中继能力。
37-
- 客户端:PR #30 推进 Edge 本地数据层,`feat/client-thread-messages-delicious233` 补齐 message/item 写入链路,`feat/client-run-lifecycle-delicious233``feat/client-store-boundary-delicious233` 分别补齐 lifecycle/store 可替换边界,`feat/client-store-persistence-delicious233` 增加轻量 JSON 文件持久化 store,`feat/client-edge-store-file-flag-delicious233` 将文件 store 接入 Edge 启动入口,`feat/client-runner-process-adapter-delicious233` 增加可测试的本地进程 executor,`feat/client-runner-workdir-delicious233` 增加本地进程工作目录配置,`feat/client-runner-adapter-profile-delicious233` 增加可测试的 generic adapter profile / 命令模板最小层,后续继续做真实 Runner adapter。
37+
- 客户端:PR #30 推进 Edge 本地数据层,`feat/client-thread-messages-delicious233` 补齐 message/item 写入链路,`feat/client-run-lifecycle-delicious233``feat/client-store-boundary-delicious233` 分别补齐 lifecycle/store 可替换边界,`feat/client-store-persistence-delicious233` 增加轻量 JSON 文件持久化 store,`feat/client-edge-store-file-flag-delicious233` 将文件 store 接入 Edge 启动入口,`feat/client-runner-process-adapter-delicious233` 增加可测试的本地进程 executor,`feat/client-runner-workdir-delicious233` 增加本地进程工作目录配置,`feat/client-runner-adapter-profile-delicious233` 增加可测试的 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 增加可测试的 `agenthub-runner-mock` preset 入口,后续继续做真实 Runner adapter。
3838

3939
## 验收门槛
4040

@@ -53,6 +53,7 @@
5353
- [x] 增加 Edge 本地进程 executor 边界,支持 stdout/stderr、成功、失败和取消事件映射。
5454
- [x] 为 Edge 本地进程 executor 增加工作目录配置边界;这只是本地进程 workdir 能力,不是完整真实 Runner adapter。
5555
- [x] 为 Edge 本地进程 executor 增加 generic adapter profile / 命令模板最小层,支持从 Run 上下文展开 args/env。
56+
- [x] 为 Edge 启动入口增加 `--runner-profile agenthub-runner-mock` preset,默认使用仓库自带 Runner mock CLI。
5657
- [ ] 将 Runner 真正接入 Edge Run lifecycle,替换 handler 内置 mock flow。
5758
- [ ] M2 完成后归档或更新 `docs/client-roadmap.md`,避免路线图重复。
5859
- [ ] 为 Runner 真实 CLI adapter 规划最小测试夹具。
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# feat/client-runner-profile-preset-delicious233 路线图
2+
3+
最后更新:2026-05-23
4+
5+
## 当前目标
6+
7+
- [x] 为 Edge 启动入口增加可测试的 `agenthub-runner-mock` Runner profile preset。
8+
9+
## 写入范围
10+
11+
- `edge-server/cmd/agenthub-edge/`
12+
- `runner/README.md`
13+
- `docs/roadmap.md`
14+
- `docs/roadmaps/client.md`
15+
- `docs/roadmaps/branches/feat-client-runner-profile-preset-delicious233.md`
16+
17+
## 已完成
18+
19+
- [x] 新增 `--runner-profile` CLI 参数,默认空值保持原有内置 MockExecutor 行为。
20+
- [x] 支持 `agenthub-runner-mock` preset,默认生成 `agenthub-runner --mock`
21+
- [x] 支持 `--runner-command` 覆盖 command,同时保留 preset 默认参数。
22+
- [x] 保留用户追加的 `--runner-arg``--runner-env``--runner-workdir`
23+
- [x] 对未知 profile 返回清晰配置错误。
24+
- [x] 修复 review 反馈:明确 `--runner-command` 只接受单个可执行入口,并补 profile + env 未知占位符回归测试。
25+
26+
## 下一步
27+
28+
- [ ] 后续分支继续接入真实 Runner adapter,不在本分支接 Claude Code / Codex / OpenCode。
29+
30+
## 验收
31+
32+
- [x] `git diff --check`
33+
- [x] `python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('api/openapi.yaml').read_text(encoding='utf-8')); print('yaml ok')"`
34+
- [x] `cd edge-server; go test -count=1 ./...`
35+
- [x] `cd runner; go test -count=1 ./...`

docs/roadmaps/client.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## 当前目标
1414

15-
推进 M2 Edge 本地数据层,把 M1 的内存事件流升级为 Project / Thread / Run / Item / Event 模型。当前 PR #30 已完成内存态最小模型,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Edge Run lifecycle executor 边界,`feat/client-store-boundary-delicious233` 已抽象可替换 store 接口,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已将文件 store 接入 Edge 启动参数,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录配置边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,后续继续补真实 Runner adapter。
15+
推进 M2 Edge 本地数据层,把 M1 的内存事件流升级为 Project / Thread / Run / Item / Event 模型。当前 PR #30 已完成内存态最小模型,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Edge Run lifecycle executor 边界,`feat/client-store-boundary-delicious233` 已抽象可替换 store 接口,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已将文件 store 接入 Edge 启动参数,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录配置边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补仓库自带 mock Runner preset 入口,后续继续补真实 Runner adapter。
1616

1717
## 近期任务
1818

@@ -31,6 +31,7 @@
3131
- [x] 增加可测试的本地进程 executor,覆盖 stdout/stderr 输出、正常退出、非零退出、取消和重复启动。
3232
- [x] 增加本地进程工作目录配置,覆盖构造期目录验证和子进程实际运行目录;这不是完整 Claude Code / Codex / OpenCode adapter。
3333
- [x] 增加 generic adapter profile / 命令模板最小层,覆盖 args/env 的 Run 占位符展开、未知占位符错误、固定 args 兼容和 workdir 不回退。
34+
- [x] 增加 `--runner-profile agenthub-runner-mock`,覆盖默认 command、command override、用户 args/env 追加和未知 profile 错误。
3435
- [ ] 将真实 Runner adapter 接入 Edge Run lifecycle。
3536
- [ ] 细化 Project / Thread / Item 的 OpenAPI 响应 schema。
3637

edge-server/cmd/agenthub-edge/main.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
type config struct {
1616
Addr string
1717
StoreFile string
18+
RunnerProfile string
1819
RunnerCommand string
1920
RunnerArgs repeatedString
2021
RunnerEnv repeatedString
@@ -23,6 +24,8 @@ type config struct {
2324

2425
type repeatedString []string
2526

27+
const runnerProfileAgentHubMock = "agenthub-runner-mock"
28+
2629
func (v *repeatedString) String() string {
2730
return fmt.Sprint([]string(*v))
2831
}
@@ -72,13 +75,17 @@ func buildConfig(args []string) (config, error) {
7275
cfg := config{}
7376
fs.StringVar(&cfg.Addr, "addr", "127.0.0.1:3210", "listen address")
7477
fs.StringVar(&cfg.StoreFile, "store-file", "", "JSON store snapshot file path")
75-
fs.StringVar(&cfg.RunnerCommand, "runner-command", "", "local process command to execute for each run; empty uses the mock executor")
78+
fs.StringVar(&cfg.RunnerProfile, "runner-profile", "", "runner profile preset; supported: "+runnerProfileAgentHubMock)
79+
fs.StringVar(&cfg.RunnerCommand, "runner-command", "", "local process executable to run for each run; empty uses the built-in mock executor only when --runner-profile is empty")
7680
fs.StringVar(&cfg.RunnerWorkDir, "runner-workdir", "", "working directory for --runner-command; empty inherits the edge process working directory")
7781
fs.Var(&cfg.RunnerArgs, "runner-arg", "argument passed to --runner-command; may be repeated")
7882
fs.Var(&cfg.RunnerEnv, "runner-env", "environment variable passed to --runner-command as KEY=VALUE; may be repeated")
7983
if err := fs.Parse(args); err != nil {
8084
return config{}, err
8185
}
86+
if err := applyRunnerProfile(&cfg); err != nil {
87+
return config{}, err
88+
}
8289
cfg.RunnerCommand = strings.TrimSpace(cfg.RunnerCommand)
8390
if cfg.RunnerCommand == "" && len(cfg.RunnerArgs) > 0 {
8491
return config{}, fmt.Errorf("--runner-arg requires --runner-command")
@@ -98,6 +105,23 @@ func buildConfig(args []string) (config, error) {
98105
return cfg, nil
99106
}
100107

108+
func applyRunnerProfile(cfg *config) error {
109+
cfg.RunnerProfile = strings.TrimSpace(cfg.RunnerProfile)
110+
if cfg.RunnerProfile == "" {
111+
return nil
112+
}
113+
switch cfg.RunnerProfile {
114+
case runnerProfileAgentHubMock:
115+
if strings.TrimSpace(cfg.RunnerCommand) == "" {
116+
cfg.RunnerCommand = "agenthub-runner"
117+
}
118+
cfg.RunnerArgs = append(repeatedString{"--mock"}, cfg.RunnerArgs...)
119+
default:
120+
return fmt.Errorf("unknown --runner-profile %q; supported values: agenthub-runner-mock", cfg.RunnerProfile)
121+
}
122+
return nil
123+
}
124+
101125
func newStoreFromConfig(cfg config) (store.Repository, error) {
102126
if cfg.StoreFile == "" {
103127
return store.New(), nil

edge-server/cmd/agenthub-edge/main_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ func TestBuildConfigDefaultsToMemoryStore(t *testing.T) {
2121
if cfg.StoreFile != "" {
2222
t.Fatalf("StoreFile = %q, want empty", cfg.StoreFile)
2323
}
24+
if cfg.RunnerProfile != "" {
25+
t.Fatalf("RunnerProfile = %q, want empty", cfg.RunnerProfile)
26+
}
2427
if cfg.RunnerCommand != "" {
2528
t.Fatalf("RunnerCommand = %q, want empty", cfg.RunnerCommand)
2629
}
@@ -70,6 +73,85 @@ func TestBuildConfigParsesStoreFile(t *testing.T) {
7073
}
7174
}
7275

76+
func TestBuildConfigAppliesRunnerProfilePreset(t *testing.T) {
77+
cfg, err := buildConfig([]string{"--runner-profile", "agenthub-runner-mock"})
78+
if err != nil {
79+
t.Fatalf("buildConfig returned error: %v", err)
80+
}
81+
82+
if cfg.RunnerProfile != "agenthub-runner-mock" {
83+
t.Fatalf("RunnerProfile = %q, want preset name", cfg.RunnerProfile)
84+
}
85+
if cfg.RunnerCommand != "agenthub-runner" {
86+
t.Fatalf("RunnerCommand = %q, want preset command", cfg.RunnerCommand)
87+
}
88+
if got, want := []string(cfg.RunnerArgs), []string{"--mock"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
89+
t.Fatalf("RunnerArgs = %#v, want %#v", got, want)
90+
}
91+
}
92+
93+
func TestBuildConfigRunnerProfileAllowsCommandOverride(t *testing.T) {
94+
cfg, err := buildConfig([]string{
95+
"--runner-profile", "agenthub-runner-mock",
96+
"--runner-command", "custom-runner",
97+
})
98+
if err != nil {
99+
t.Fatalf("buildConfig returned error: %v", err)
100+
}
101+
102+
if cfg.RunnerCommand != "custom-runner" {
103+
t.Fatalf("RunnerCommand = %q, want custom command", cfg.RunnerCommand)
104+
}
105+
if got, want := []string(cfg.RunnerArgs), []string{"--mock"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
106+
t.Fatalf("RunnerArgs = %#v, want %#v", got, want)
107+
}
108+
}
109+
110+
func TestBuildConfigRunnerProfilePreservesUserArgOrder(t *testing.T) {
111+
cfg, err := buildConfig([]string{
112+
"--runner-profile", "agenthub-runner-mock",
113+
"--runner-arg", "--addr=127.0.0.1:0",
114+
})
115+
if err != nil {
116+
t.Fatalf("buildConfig returned error: %v", err)
117+
}
118+
119+
if got, want := []string(cfg.RunnerArgs), []string{"--mock", "--addr=127.0.0.1:0"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
120+
t.Fatalf("RunnerArgs = %#v, want %#v", got, want)
121+
}
122+
}
123+
124+
func TestBuildConfigRunnerProfileValidatesUserEnvTemplate(t *testing.T) {
125+
cfg, err := buildConfig([]string{
126+
"--runner-profile", "agenthub-runner-mock",
127+
"--runner-env", "PROFILE_RUN={{run.id}}",
128+
})
129+
if err != nil {
130+
t.Fatalf("buildConfig returned error: %v", err)
131+
}
132+
133+
if got, want := []string(cfg.RunnerEnv), []string{"PROFILE_RUN={{run.id}}"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
134+
t.Fatalf("RunnerEnv = %#v, want %#v", got, want)
135+
}
136+
}
137+
138+
func TestBuildConfigRunnerProfileRejectsInvalidUserEnvTemplate(t *testing.T) {
139+
_, err := buildConfig([]string{
140+
"--runner-profile", "agenthub-runner-mock",
141+
"--runner-env", "BAD={{unknown}}",
142+
})
143+
if err == nil || !strings.Contains(err.Error(), "--runner-env") || !strings.Contains(err.Error(), "unknown placeholder") {
144+
t.Fatalf("buildConfig error = %v, want runner env unknown placeholder error", err)
145+
}
146+
}
147+
148+
func TestBuildConfigRejectsUnknownRunnerProfile(t *testing.T) {
149+
_, err := buildConfig([]string{"--runner-profile", "missing-profile"})
150+
if err == nil || !strings.Contains(err.Error(), "unknown --runner-profile") {
151+
t.Fatalf("buildConfig error = %v, want unknown runner profile error", err)
152+
}
153+
}
154+
73155
func TestBuildConfigRejectsUnexpectedArguments(t *testing.T) {
74156
_, err := buildConfig([]string{"unexpected"})
75157
if err == nil || !strings.Contains(err.Error(), "unexpected positional arguments") {

runner/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ go build ./cmd/agenthub-runner
1919
go run ./cmd/agenthub-runner --mock
2020
```
2121

22+
### 通过 Edge Runner profile 启动
23+
24+
开发 Edge 本地进程接入时,可以用仓库自带 mock Runner preset:
25+
26+
```powershell
27+
agenthub-edge --runner-profile agenthub-runner-mock
28+
```
29+
30+
需要指定本地构建产物或包装脚本时,可以覆盖 command;preset 的 `--mock` 参数仍会保留,后续 `--runner-arg` 会继续追加:
31+
32+
```powershell
33+
agenthub-edge --runner-profile agenthub-runner-mock --runner-command agenthub-runner --runner-arg --addr=127.0.0.1:0
34+
```
35+
36+
`--runner-command` 只能填写单个可执行文件名或可执行文件路径,不能填写 `go run ./cmd/agenthub-runner` 这类整串 shell 命令。需要走 `go run` 时,后续应先补 command + args 分离能力,或临时使用脚本 / 包装可执行文件。
37+
2238
### 可用参数
2339

2440
| 参数 | 默认值 | 说明 |

0 commit comments

Comments
 (0)