Skip to content
Merged
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
7 changes: 4 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,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。
推进 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。

## 路线图分层

Expand All @@ -26,15 +26,15 @@

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

## 当前活跃方向

- 前端:从 Mock 数据过渡到真实 REST / WebSocket client,承接 UI 同学设计。
- 后端:实现 Hub Server、Edge-Hub 通信、账号/群聊/同步/中继能力。
- 客户端: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。
- 客户端: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。

## 验收门槛

Expand All @@ -53,6 +53,7 @@
- [x] 增加 Edge 本地进程 executor 边界,支持 stdout/stderr、成功、失败和取消事件映射。
- [x] 为 Edge 本地进程 executor 增加工作目录配置边界;这只是本地进程 workdir 能力,不是完整真实 Runner adapter。
- [x] 为 Edge 本地进程 executor 增加 generic adapter profile / 命令模板最小层,支持从 Run 上下文展开 args/env。
- [x] 为 Edge 启动入口增加 `--runner-profile agenthub-runner-mock` preset,默认使用仓库自带 Runner mock CLI。
- [ ] 将 Runner 真正接入 Edge Run lifecycle,替换 handler 内置 mock flow。
- [ ] M2 完成后归档或更新 `docs/client-roadmap.md`,避免路线图重复。
- [ ] 为 Runner 真实 CLI adapter 规划最小测试夹具。
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# feat/client-runner-profile-preset-delicious233 路线图

最后更新:2026-05-23

## 当前目标

- [x] 为 Edge 启动入口增加可测试的 `agenthub-runner-mock` Runner profile preset。

## 写入范围

- `edge-server/cmd/agenthub-edge/`
- `runner/README.md`
- `docs/roadmap.md`
- `docs/roadmaps/client.md`
- `docs/roadmaps/branches/feat-client-runner-profile-preset-delicious233.md`

## 已完成

- [x] 新增 `--runner-profile` CLI 参数,默认空值保持原有内置 MockExecutor 行为。
- [x] 支持 `agenthub-runner-mock` preset,默认生成 `agenthub-runner --mock`。
- [x] 支持 `--runner-command` 覆盖 command,同时保留 preset 默认参数。
- [x] 保留用户追加的 `--runner-arg`、`--runner-env` 和 `--runner-workdir`。
- [x] 对未知 profile 返回清晰配置错误。
- [x] 修复 review 反馈:明确 `--runner-command` 只接受单个可执行入口,并补 profile + env 未知占位符回归测试。

## 下一步

- [ ] 后续分支继续接入真实 Runner adapter,不在本分支接 Claude Code / Codex / OpenCode。

## 验收

- [x] `git diff --check`
- [x] `python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('api/openapi.yaml').read_text(encoding='utf-8')); print('yaml ok')"`
- [x] `cd edge-server; go test -count=1 ./...`
- [x] `cd runner; go test -count=1 ./...`
3 changes: 2 additions & 1 deletion docs/roadmaps/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

## 当前目标

推进 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。
推进 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。

## 近期任务

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

Expand Down
26 changes: 25 additions & 1 deletion edge-server/cmd/agenthub-edge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
type config struct {
Addr string
StoreFile string
RunnerProfile string
RunnerCommand string
RunnerArgs repeatedString
RunnerEnv repeatedString
Expand All @@ -23,6 +24,8 @@ type config struct {

type repeatedString []string

const runnerProfileAgentHubMock = "agenthub-runner-mock"

func (v *repeatedString) String() string {
return fmt.Sprint([]string(*v))
}
Expand Down Expand Up @@ -72,13 +75,17 @@ func buildConfig(args []string) (config, error) {
cfg := config{}
fs.StringVar(&cfg.Addr, "addr", "127.0.0.1:3210", "listen address")
fs.StringVar(&cfg.StoreFile, "store-file", "", "JSON store snapshot file path")
fs.StringVar(&cfg.RunnerCommand, "runner-command", "", "local process command to execute for each run; empty uses the mock executor")
fs.StringVar(&cfg.RunnerProfile, "runner-profile", "", "runner profile preset; supported: "+runnerProfileAgentHubMock)
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")
fs.StringVar(&cfg.RunnerWorkDir, "runner-workdir", "", "working directory for --runner-command; empty inherits the edge process working directory")
fs.Var(&cfg.RunnerArgs, "runner-arg", "argument passed to --runner-command; may be repeated")
fs.Var(&cfg.RunnerEnv, "runner-env", "environment variable passed to --runner-command as KEY=VALUE; may be repeated")
if err := fs.Parse(args); err != nil {
return config{}, err
}
if err := applyRunnerProfile(&cfg); err != nil {
return config{}, err
}
cfg.RunnerCommand = strings.TrimSpace(cfg.RunnerCommand)
if cfg.RunnerCommand == "" && len(cfg.RunnerArgs) > 0 {
return config{}, fmt.Errorf("--runner-arg requires --runner-command")
Expand All @@ -98,6 +105,23 @@ func buildConfig(args []string) (config, error) {
return cfg, nil
}

func applyRunnerProfile(cfg *config) error {
cfg.RunnerProfile = strings.TrimSpace(cfg.RunnerProfile)
if cfg.RunnerProfile == "" {
return nil
}
switch cfg.RunnerProfile {
case runnerProfileAgentHubMock:
if strings.TrimSpace(cfg.RunnerCommand) == "" {
cfg.RunnerCommand = "agenthub-runner"
}
cfg.RunnerArgs = append(repeatedString{"--mock"}, cfg.RunnerArgs...)
default:
return fmt.Errorf("unknown --runner-profile %q; supported values: agenthub-runner-mock", cfg.RunnerProfile)
}
return nil
}

func newStoreFromConfig(cfg config) (store.Repository, error) {
if cfg.StoreFile == "" {
return store.New(), nil
Expand Down
82 changes: 82 additions & 0 deletions edge-server/cmd/agenthub-edge/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func TestBuildConfigDefaultsToMemoryStore(t *testing.T) {
if cfg.StoreFile != "" {
t.Fatalf("StoreFile = %q, want empty", cfg.StoreFile)
}
if cfg.RunnerProfile != "" {
t.Fatalf("RunnerProfile = %q, want empty", cfg.RunnerProfile)
}
if cfg.RunnerCommand != "" {
t.Fatalf("RunnerCommand = %q, want empty", cfg.RunnerCommand)
}
Expand Down Expand Up @@ -70,6 +73,85 @@ func TestBuildConfigParsesStoreFile(t *testing.T) {
}
}

func TestBuildConfigAppliesRunnerProfilePreset(t *testing.T) {
cfg, err := buildConfig([]string{"--runner-profile", "agenthub-runner-mock"})
if err != nil {
t.Fatalf("buildConfig returned error: %v", err)
}

if cfg.RunnerProfile != "agenthub-runner-mock" {
t.Fatalf("RunnerProfile = %q, want preset name", cfg.RunnerProfile)
}
if cfg.RunnerCommand != "agenthub-runner" {
t.Fatalf("RunnerCommand = %q, want preset command", cfg.RunnerCommand)
}
if got, want := []string(cfg.RunnerArgs), []string{"--mock"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
t.Fatalf("RunnerArgs = %#v, want %#v", got, want)
}
}

func TestBuildConfigRunnerProfileAllowsCommandOverride(t *testing.T) {
cfg, err := buildConfig([]string{
"--runner-profile", "agenthub-runner-mock",
"--runner-command", "custom-runner",
})
if err != nil {
t.Fatalf("buildConfig returned error: %v", err)
}

if cfg.RunnerCommand != "custom-runner" {
t.Fatalf("RunnerCommand = %q, want custom command", cfg.RunnerCommand)
}
if got, want := []string(cfg.RunnerArgs), []string{"--mock"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
t.Fatalf("RunnerArgs = %#v, want %#v", got, want)
}
}

func TestBuildConfigRunnerProfilePreservesUserArgOrder(t *testing.T) {
cfg, err := buildConfig([]string{
"--runner-profile", "agenthub-runner-mock",
"--runner-arg", "--addr=127.0.0.1:0",
})
if err != nil {
t.Fatalf("buildConfig returned error: %v", err)
}

if got, want := []string(cfg.RunnerArgs), []string{"--mock", "--addr=127.0.0.1:0"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
t.Fatalf("RunnerArgs = %#v, want %#v", got, want)
}
}

func TestBuildConfigRunnerProfileValidatesUserEnvTemplate(t *testing.T) {
cfg, err := buildConfig([]string{
"--runner-profile", "agenthub-runner-mock",
"--runner-env", "PROFILE_RUN={{run.id}}",
})
if err != nil {
t.Fatalf("buildConfig returned error: %v", err)
}

if got, want := []string(cfg.RunnerEnv), []string{"PROFILE_RUN={{run.id}}"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") {
t.Fatalf("RunnerEnv = %#v, want %#v", got, want)
}
}

func TestBuildConfigRunnerProfileRejectsInvalidUserEnvTemplate(t *testing.T) {
_, err := buildConfig([]string{
"--runner-profile", "agenthub-runner-mock",
"--runner-env", "BAD={{unknown}}",
})
if err == nil || !strings.Contains(err.Error(), "--runner-env") || !strings.Contains(err.Error(), "unknown placeholder") {
t.Fatalf("buildConfig error = %v, want runner env unknown placeholder error", err)
}
}

func TestBuildConfigRejectsUnknownRunnerProfile(t *testing.T) {
_, err := buildConfig([]string{"--runner-profile", "missing-profile"})
if err == nil || !strings.Contains(err.Error(), "unknown --runner-profile") {
t.Fatalf("buildConfig error = %v, want unknown runner profile error", err)
}
}

func TestBuildConfigRejectsUnexpectedArguments(t *testing.T) {
_, err := buildConfig([]string{"unexpected"})
if err == nil || !strings.Contains(err.Error(), "unexpected positional arguments") {
Expand Down
16 changes: 16 additions & 0 deletions runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ go build ./cmd/agenthub-runner
go run ./cmd/agenthub-runner --mock
```

### 通过 Edge Runner profile 启动

开发 Edge 本地进程接入时,可以用仓库自带 mock Runner preset:

```powershell
agenthub-edge --runner-profile agenthub-runner-mock
```

需要指定本地构建产物或包装脚本时,可以覆盖 command;preset 的 `--mock` 参数仍会保留,后续 `--runner-arg` 会继续追加:

```powershell
agenthub-edge --runner-profile agenthub-runner-mock --runner-command agenthub-runner --runner-arg --addr=127.0.0.1:0
```

`--runner-command` 只能填写单个可执行文件名或可执行文件路径,不能填写 `go run ./cmd/agenthub-runner` 这类整串 shell 命令。需要走 `go run` 时,后续应先补 command + args 分离能力,或临时使用脚本 / 包装可执行文件。

### 可用参数

| 参数 | 默认值 | 说明 |
Expand Down
Loading