[Feature] 沙箱跨 Run 依赖缓存:按项目+lockfile 内容寻址复用 npm/Go/pip 等依赖目录,支持 setup/maintenance 两段式准备

Abierto
#329 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
12/100
Tipo de issue
Nueva funcionalidad
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
docker, go

Línea de trabajo

Start with internal/runtime/sandbox_manager.go (RunWorkspace, DestroyRun), internal/runtime/sandbox_docker.go (ensureStarted, the workspace mount) and internal/runtime/sandbox_environment.go (runEnvironmentInstall, .install-stamp) to see how per-run state is created and removed. This is cross-cutting design work touching the domain model, both sandbox drivers and the remote runner, so it is not a newcomer task. Done means the acceptance checklist in the issue is met, including the cache-isolation and concurrent-fill tests.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

背景与借鉴来源

AI 编码 Agent 每次开工都要先装依赖(npm install、go mod download、pip install……)。在大仓库上这一步往往要几分钟,而且每次下载的内容基本一样。主流平台都把“装依赖”和“依赖缓存”做成了平台能力:

项目 规模 / 活跃度 可借鉴做法
openai/codex(Codex Cloud) ~13.5 万 star,2026-11-11 发布 rust-v0.167.2 环境分为 setup script 和 maintenance script:setup 跑完后容器状态会被缓存(官方文档写最多 12 小时),之后的任务直接用缓存容器,只跑轻量的 maintenance script 刷新依赖;setup 脚本或环境变量改了,缓存就失效。用户也很在意这一点,例如 openai/codex#25086 就是在反馈缓存没生效、每次都重装
GitHub Copilot coding agent(文档) GitHub 官方产品 在 .github/workflows/copilot-setup-steps.yml 里声明环境准备步骤,示例直接用 actions/cache(~5.6k star)缓存依赖目录,key 取 lockfile 的 hash
OpenHands/OpenHands ~9.3 万 star,每天都有提交 仓库根目录放 .openhands/setup.sh,每次开始工作都会执行,用来装依赖(文档)
e2b-dev/E2B ~1.5 万 star,每天都有提交 沙箱模板把装好依赖的状态预先做成快照,从模板启动新沙箱是秒级

它们的共同点是:按 lockfile / 脚本内容算出缓存 key,跨任务复用依赖目录或环境快照,后续任务只做增量刷新。

LoopForge 现状与不足

(以 commit a7c6f15 为准)

  1. 每个 Run 一个工作区,依赖缓存全落在这个工作区里,Run 结束就跟着删掉
    • internal/runtime/sandbox_manager.go 的 RunWorkspace(runID) 给每个 Run 单独分配目录 root/<runID>;DestroyRun 在 Run 结束后整个删掉(internal/engine/run_environment.go:321 调用)。
    • Docker 沙箱只挂载了一个工作区(internal/runtime/sandbox_docker.go:641:-v workspace:/workspace)。Agent 的 HOME 设成 /workspace/.agentflow/private-home/<provider>(internal/runtime/command_binding.go:59),所以 ~/.npm、~/go/pkg/mod、~/.cache/pip 这些缓存也都写在本次 Run 的工作区里。
    • DiscardRunContainer 的注释写着 rerun 时“不需要上次填进去的 installs and caches”。也就是说,重试时依赖缓存同样被扔掉。
  2. 只缓存了镜像层,项目依赖没有缓存:ensureDockerImage / cachedDockerImageName(sandbox_docker.go:766、:481)按 Dockerfile 内容的 hash 复用镜像,removeDockerSandbox 也特意保留了缓存镜像。可项目自身的依赖(由 lockfile 决定)通常不会写进平台级 Dockerfile,每个 Run 都要从网络重新拉。
  3. 现有的“准备”机制只在单个工作区内去重
    • 环境资产的 EnvironmentDefinition.InstallScript(internal/domain/assets.go:142)由 runEnvironmentInstall(internal/runtime/sandbox_environment.go:154)执行。它用 .install-stamp 记录脚本 hash,但 stamp 放在 .agentflow/environment/<id>/,也在本次 Run 的工作区里,换一个 Run 就要重装。
    • 脚本节点的 RunOnWorkspaceRebuild + EnsureWorkspacePreparation(internal/runtime/workspace_preparation.go:268)用 preparationFingerprint 记录已执行的步骤,同样只作用于本工作区和本沙箱,无法跨 Run 复用产物。
  4. 已有 issue 都不涉及这件事:#218 缓存的是“纯产物节点的执行结果”,不是依赖目录;#124/#150 改进的是克隆和构建镜像的进度与超时提示;#117 讲沙箱调度解耦。

结果是:同一个项目连续跑十个 SOP,就要完整装十次依赖。既浪费时间和 Token(Agent 会等依赖、处理安装报错),又反复占用外网带宽或内部镜像源。

建议方案

做一个按项目隔离、按内容寻址的跨 Run 依赖缓存卷,配合 setup / maintenance 两段式准备脚本:

  1. 缓存声明(加在环境资产或沙箱模板上)
    • 给 EnvironmentDefinition(或 SandboxProfile)加一个 Caches []CacheSpec 字段,每项包含:
      • id
      • paths:容器内路径,例如 /workspace/.agentflow/private-home/*/.npm,或者更简单的做法:平台通过环境变量把 npm_config_cache、GOMODCACHE、PIP_CACHE_DIR、CARGO_HOME 等统一指向 /cache/<id>
      • keyFiles:用来算 key 的文件,例如 package-lock.json、go.sum、requirements*.txt
      • restoreKeys:前缀回退,借鉴 actions/cache
    • 内置 npm/pnpm/yarn、Go、pip/uv、Maven/Gradle、Cargo 的预设模板,用户勾选即可。
  2. 挂载与隔离
    • Docker:创建容器时(sandbox_docker.go 的 ensureStarted)对每个缓存额外挂 --mount type=volume,src=lf-cache-<projectID>-<cacheID>-<keyhash>,dst=/cache/<id>。
    • native/bwrap:--bind <cacheRoot>/<project>/<id>/<keyhash>。
    • 卷只在同一项目内共享,不同项目和不同用户互不可见。对 secret 敏感的目录(如 .npmrc、凭据文件)不得放进缓存,可复用 gitwork/exclude.go 中 IsCredentialFile 的判断。
    • 远程 Runner 在本机维护同样结构的缓存目录,任务载荷(RunnerSandboxSpec)里只带缓存声明,不带缓存内容。
  3. 两段式准备
    • 现有 InstallScript 作为 setup 阶段:缓存未命中时执行,执行完把 stamp 写进缓存卷而不是工作区,这样跨 Run 也认得出来。
    • 新增可选的 MaintenanceScript:缓存命中时只跑它(例如 npm ci --prefer-offline、go mod download)。
    • setup 脚本内容、keyFiles hash、镜像 hash 中任意一项变化,缓存就失效。
  4. 并发与回收
    • 同一缓存 key 首次填充时加文件锁,或者先写临时卷再原子改名,避免两个 Run 同时写坏缓存。
    • 后台定期按 LRU 和总容量上限清理(可放进 ReclaimCachedDockerImages 所在的回收循环)。
    • 项目设置页提供“清空缓存”按钮。
  5. 可观测
    • workspace.preparation.* 遥测事件加上 cacheHit/cacheMiss/cacheKey/restoreKey/准备耗时。
    • Run 详情页的“工作区准备”面板显示命中情况和节省的时间。

验收标准

  • 环境资产(或沙箱模板)可以声明缓存项,并提供至少 npm、Go、pip 三种预设;API 和前端都能编辑和校验。
  • 同一项目、lockfile 不变时,第二个 Run 命中缓存:只执行 maintenance 脚本(或什么都不执行),准备耗时较首次明显下降。附 Go + Node 示例仓库的前后耗时对比。
  • 修改 lockfile 或 setup 脚本后 key 变化,按 restoreKeys 回退到最近的缓存再做增量安装。
  • 不同项目之间的缓存互不可见(有测试覆盖),凭据类文件不会写进缓存。
  • 两个 Run 并发填充同一个 key 时不损坏缓存(有并发测试)。
  • 缓存总量超过上限时按 LRU 清理;项目管理员可以手动清空。
  • Docker 驱动和 native 驱动都支持;远程 Runner 在本机缓存,任务载荷不含缓存内容。
  • 准备阶段的遥测和 Run 详情页能看到 hit/miss、key 和耗时。
  • 不声明缓存时行为与现在完全一致(回归测试通过)。
Lenguaje dominante
Go
Estrellas
141
Forks
28
Merge medio
1 d 15 h
PR fusionados (30 d)
113

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de Tencent/LoopForge

Todos los issues de Tencent/LoopForge

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.