feat(agent): 诊断 run 取消/终止原因,取代笼统的中断提示

- 新增 AbortCause(cancelcause.go):各取消点用 WithCancelCause/WithTimeoutCause
  附上原因(暂停/删除/kill_work/目标达成/drain 兜底/聊天停止/进程关闭/硬超时…),
  captureRun 用 context.Cause 还原出到底是谁、为何取消。
- 新增 terminalreason.go:把终端状态转成可读的一行摘要 + 多行详情(取消原因、
  终止 reason、轮数、卡住的工具),替换原来固定那句猜测式提示。
- engine/worker/planner/capture 及 conversations/orchestration/server 接入;
  cmd/artex/main.go、前端 llm/main-content 相应调整。
- 配套测试 terminalreason_test.go / engine_cancelcause_test.go。
This commit is contained in:
Autumn-27 committed 2026-08-19 09:20:10 -04:00
1 parent 8e9b126972
commit a6bd18050c
16 files changed
+624 -60

No files matched your search

+7 -7
View File
@@ -60,7 +60,7 @@ type Server struct {
// convBusyKey), so a manual stop can abort JUST that session's agent run. Set/
// cleared alongside chatBusy under chatMu. Aborting a run does NOT touch the P3
// trigger queue — the drain goroutine simply proceeds to the next queued fire.
chatCancel map[string]context.CancelFunc
chatCancel map[string]context.CancelCauseFunc
// triggerQ buffers P3 trigger fires PER AGENT. A per-agent "pump" launches runs up
// to a concurrency limit derived from the agent's策略: serial → limit 1 (+ optional
@@ -123,7 +123,7 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
log.Fatalf("[auth] JWT key: %v", err)
}
s := &Server{m: m, engine: NewEngine(m), ctx: ctx, skillDir: skillDir, jwtKey: key, chatBusy: map[string]bool{},
chatCancel: map[string]context.CancelFunc{}, triggerQ: map[string][]triggeredRun{},
chatCancel: map[string]context.CancelCauseFunc{}, triggerQ: map[string][]triggeredRun{},
triggerActive: map[string]int{}, triggerCfg: map[string]triggerBehavior{},
profAgents: map[int64]*profBundle{}, profChatAgents: map[int64]*agent.ChatAgent{},
provByProfile: map[int64]*provEntry{}}
@@ -217,7 +217,7 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
log.Printf("[engine] task %s 重置 %d 个残留 running 意图为 open", t.ID, n)
}
if t.Paused {
s.engine.Pause(t.ID)
s.engine.Pause(t.ID, agent.AbortPausedOnReload)
}
// 任务级超时:为每个未终态、带 timeout 的任务起 deadline 协调器,独立于 planner/worker
// loop——非活跃任务重启后也能在到点后被收尾(deadline 已过则立即走收尾时序)。
@@ -902,7 +902,7 @@ func (s *Server) control(w http.ResponseWriter, r *http.Request) {
}
switch req.Action {
case "pause":
s.engine.Pause(t.ID)
s.engine.Pause(t.ID, agent.AbortPausedByUser)
case "resume":
s.engine.Run(s.ctx, t) // ensure loops are alive, then un-pause + nudge
s.engine.Resume(t)
@@ -2330,7 +2330,7 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) {
}
// Cancellable context so stopChat can abort this turn without affecting
// the server's root context (mirrors the conversation stop pattern).
ctx, cancel := context.WithCancel(s.ctx)
ctx, cancel := context.WithCancelCause(s.ctx)
s.chatBusy[t.ID] = true
s.chatCancel[t.ID] = cancel
s.chatMu.Unlock()
@@ -2342,7 +2342,7 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) {
// handler returns immediately and the browser never needs to hold the request.
go func() {
defer func() {
cancel()
cancel(agent.AbortChatTurnFinished)
s.chatMu.Lock()
delete(s.chatBusy, t.ID)
delete(s.chatCancel, t.ID)
@@ -2386,7 +2386,7 @@ func (s *Server) stopChat(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"status": "idle"})
return
}
cancel()
cancel(agent.AbortChatStoppedByUser)
writeJSON(w, 200, map[string]any{"status": "stopping"})
}