[Feature] SOP 运行中向 Agent 节点插话:不打断地注入纠偏指令,支持排队到下一次尝试与一步打断续接

Aberta
#410 0 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
5/5
Tempo estimado
Mais de uma semana
Facilidade para iniciantes
25/100
Tipo de issue
Funcionalidade
Clareza
Razoavelmente clara
Status de atividade
Ativa
Stack de tecnologia
go, javascript, sql

Direção de pesquisa

Start by reading the existing assistant steer flow in internal/domain/assistant_generation.go, internal/repo/assistant_generations.go, and internal/httpapi/assistant_generation.go, then compare it with run pause/resume in internal/engine/run_ops.go and node execution in internal/engine/node_execute.go. The issue calls for run-level steer and queue behavior across APIs, persistence, hooks, retries, and the UI; done means the listed delivery modes, status transitions, authorization and node-type checks, tests, and acceptance criteria all work.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

背景与借鉴来源

主流 AI coding agent 都把「Agent 正在干活时,人可以随时插一句话纠偏」当成基础交互,而且区分了两种投递方式:

  • openai/codex(https://github.com/openai/codex ,约 13.5 万 star,今天仍有提交):运行中发的消息分 Steer(在当前轮内尽快注入,不丢弃已做的工作)和 Queue(排队到当前轮结束后作为下一轮输入)。社区还在讨论「Background note / Soft steer」(openai/codex#55647),即只作为背景提示、不打断当前步骤。
  • anthropics/claude-code(https://github.com/anthropics/claude-code ,约 15.5 万 star,今天仍有提交):Agent 工作中输入的消息会在下一个工具调用边界注入;hooks 的 PostToolUse / UserPromptSubmit 可以通过 hookSpecificOutput.additionalContext 把额外上下文送进模型。社区诉求见 #98493(排队而不只是 steer)、#73661(排队消息按 FIFO 逐条处理,而不是合并成一条)。
  • OpenHands/OpenHands(https://github.com/OpenHands/OpenHands ,约 9.3 万 star,活跃)、cline/cline(约 7.2 万 star,活跃):Agent 运行中的用户消息进入事件流,在下一步推理前被读到,不需要先停止任务。

这些项目的共同点:插话不等于暂停。插话不杀进程、不丢上下文,在安全边界(工具调用之间或一轮结束时)投递;投递状态(待投递 / 已送达 / 已被 Agent 确认)对人可见。

LoopForge 现状与不足

LoopForge 已经在助手会话里实现了 steer,但 SOP 运行里的 Agent 节点没有对应能力:

  1. 助手会话已有 steer:internal/domain/assistant_generation.go 的 AssistantSteer,internal/repo/assistant_generations.go 的 SteerAssistantGeneration,internal/httpapi/assistant_generation.go 的 followSteer,前端在 web/workbench.js 的 steer()。
  2. SOP 运行只有「暂停 → 补充上下文 → 继续」:POST /api/runs/:id/pause → Engine.Pause(internal/engine/run_ops.go)会 cancelRunnerTasksForRun 并 Interrupt 取消本地调度,直接杀掉正在跑的 Agent CLI;之后要再调 POST /api/runs/:id/resume,由 resumePaused 写入 recordHumanContext、用 ResumeSessionID 加 --resume(internal/runtime/provider_claude.go / provider_codex.go),提示词变成「请从刚才中断的位置继续执行」(internal/engine/node_execute.go)。
    • 想纠正方向必须先打断、等节点停稳、再点一次继续,至少两步。
    • 打断时 CLI 可能正改到一半文件、正跑一半测试,还要重新起沙箱进程、重建 MCP,长任务成本很高。
    • 子 SOP 要级联暂停和恢复(childRunIDs),一句提示的代价很大。
  3. 人工上下文只在节点启动时读一次:writeHumanContextFile 只在 node_execute.go 准备沙箱时写 human-context.md;节点运行期间人补充的信息没有通道送进正在跑的 CLI。
  4. 没有排队语义:节点运行中,人不能先留言、等这一轮完成后再作为下一轮(返工 / 下一次尝试)的输入;只能等节点失败或进入门禁再写。
  5. 平台已经给各 CLI 装了 hooks(internal/observability/hooks.go 的 PostToolUse/UserPromptSubmit,internal/runtime/hooks.go 的 writeHookSettings/mergeHookSettings),但现在只用来做观测落盘(hooks.ndjson),没有用来回写上下文。

建议方案

1. 数据模型与 API
  • 新增运行级消息队列(归属 RunsRepo,新建 run_steers 表或 Run 的子资源),字段:id, run_id, node_id(可空=当前活跃 Agent 节点), mode(steer|queue), content, actor_id, status(pending|delivered|acknowledged|expired|cancelled), delivered_attempt, delivered_via(hook|resume|next_attempt), created_at, delivered_at。按 AGENTS.md 约定,migrations/ 下补一份 SQL 脚本。
  • POST /api/runs/:id/steer(runsHandlers,复用 project_write_access.go 的写权限校验),请求体 {nodeId?, mode, content};DELETE /api/runs/:id/steer/:steerId 撤回未投递的消息;GET 在运行详情里返回队列。
  • 写入运行事件(e.event(..., "run.steer.queued" / "run.steer.delivered"))和 appendControlTelemetry,审计里能看到谁在哪个节点第几次尝试插了什么话。
2. 投递(按执行器能力分级,从无损到有损)
  • A. 工具调用边界注入(首选,不打断):对支持 hooks 回写上下文的 CLI(Claude Code / CodeBuddy 的 PostToolUse 与 UserPromptSubmit),在 hook.py 旁加一个轻量命令:读取工作区下 .agentflow/steer/inbox.jsonl 中 pending 的消息,以 hookSpecificOutput.additionalContext 输出,并把已投递的 id 写到 outbox。引擎(本地调度或 runner daemon)负责把队列同步进 inbox,并从 outbox/hooks.ndjson 回收投递状态。
  • B. 回合结束后续接:不支持 A 的执行器(Codex exec --json、OpenCode 等),mode=steer 退化为「当前 CLI 进程结束后立刻以 --resume <sessionID> 再起一轮,提示词只包含插话内容」,节点保持 running,不经过 waiting_approval,也不计入 MaxAttempts。
  • C. 立即打断(显式选择):前端提供「打断并插话」,内部等价于 Pause + ResumePaused(humanContext) 一步完成,复用现有 ResumeSessionID 逻辑,不需要人点两次。
  • mode=queue:不在当前尝试里投递,挂到该节点下一次尝试(门禁 rework、targeted_fix 重试、或人工 Retry)的 Feedback 中,追加在现有 FeedbackPrompt 后面;多条按 FIFO 逐条列出,不合并改写。
3. 前端
  • 运行详情页的活跃 Agent 节点卡片加输入框,支持「插话 / 排队 / 打断并插话」三种发送方式(交互参考 web/workbench.js 现有 steer());消息气泡显示投递状态,pending 状态可撤回。
4. 边界
  • 只对 type=agent 节点生效;script / approval 节点直接拒绝(400);子 SOP 节点按 ChildRunID 转给子运行当前活跃的 Agent 节点。
  • 单条长度沿用 maxHumanContextBytes(8000);同一节点 pending 上限(例如 20 条)。
  • 节点在投递前结束(成功 / 失败 / 进入门禁):steer 自动转为 queue 留给下一次尝试,或标为 expired 并提示,不静默丢弃。
  • 插话内容同样写入 human-context 产物,保证事后可追溯;gitwork/exclude.go 中排除 .agentflow/steer/,不进入交付分支。

验收标准

  1. 在 Claude Code / CodeBuddy 执行器的 Agent 节点运行期间调用 POST /api/runs/:id/steer(mode=steer),CLI 进程不重启(同一 invocation id、无 run.paused/run.resumed 事件),下一次工具调用后模型输出能体现插话内容;消息状态从 pending → delivered,运行事件里有 run.steer.delivered。
  2. 在 Codex 执行器上同样操作,当前轮结束后自动 --resume 续接,节点不进入 waiting_approval,Attempt 不加 1。
  3. mode=queue 的多条消息在节点下一次尝试时按 FIFO 逐条出现在提示词里,当前尝试中不投递。
  4. 「打断并插话」一步完成暂停加继续,行为等同于现在先 pause 再 resume 带 context 的组合,子 SOP 级联正确。
  5. 节点在消息投递前结束时,消息被转为 queue 或标为 expired,界面可见,不会静默丢失;pending 消息可撤回。
  6. 对 script / approval 节点插话返回 400;无项目写权限返回 403。
  7. 单元测试覆盖:队列状态机、hook 注入命令的读取与回执、三种降级路径、节点结束时的转存逻辑;e2e 覆盖运行详情页发送插话并看到状态变化。
  8. .agentflow/steer/ 不出现在交付分支的 diff 中。
Linguagem predominante
Go
Estrelas
141
Forks
28
Merge médio
1d 15h
PRs com merge (30d)
113

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de Tencent/LoopForge

Todas as issues de Tencent/LoopForge

Issues semelhantes

Mais issues de Go

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.