feat(ai): session persist + continue HTTP/UI, checkpoints, queue

落地 docs/16 P1:会话落盘与真续跑(prior tool 轨迹)、incomplete「加预算继续」、
awaiting_confirm outcome、同参拒+第4/6轮检查点、侧栏忙时单槽排队。
Ask 真 token 流式记缺口(三协议 SSE 成本高)。
This commit is contained in:
logan committed 2026-10-15 14:18:18 +08:00
1 parent 1513f4bc2d
commit d543f3b924
10 files changed
+783 -104

No files matched your search

+23 -12
View File
@@ -59,6 +59,8 @@ type AgentRes struct {
IncompleteReason string `json:"incompleteReason,omitempty"`
Resumable bool `json:"resumable,omitempty"`
StepsBudget int `json:"stepsBudget,omitempty"`
// Trajectory 含 system/user/assistant.tool_calls/tool 的完整对话(供落盘续跑;JSON 默认省略大字段可选)
Trajectory []Message `json:"trajectory,omitempty"`
}
// RunAgent 多轮:LLM tool_call → aitools.Runtime.Call → 回灌。
@@ -90,6 +92,11 @@ func runAgent(ctx context.Context, llm ToolChat, rt *aitools.Runtime, req AgentR
if runID == "" {
runID = NewRunID()
}
sessionID := strings.TrimSpace(req.SessionID)
if sessionID == "" {
sessionID = NewSessionID()
req.SessionID = sessionID
}
rt.RunID = runID
// Agent 路径禁止模型自行带确认令牌
rt.RejectCallerConfirm = true
@@ -119,13 +126,14 @@ func runAgent(ctx context.Context, llm ToolChat, rt *aitools.Runtime, req AgentR
}
}
baseMeta := func(steps []AgentStep, rounds int) AgentRes {
finish := func(steps []AgentStep, rounds int, msgs []Message) AgentRes {
return AgentRes{
Steps: steps,
Rounds: rounds,
RunID: runID,
SessionID: req.SessionID,
SessionID: sessionID,
StepsBudget: maxRounds,
Trajectory: append([]Message{}, msgs...),
}
}
@@ -134,19 +142,22 @@ func runAgent(ctx context.Context, llm ToolChat, rt *aitools.Runtime, req AgentR
rounds := 0
for round := 0; round < maxRounds; round++ {
if err := ctx.Err(); err != nil {
res := baseMeta(steps, rounds)
res := finish(steps, rounds, msgs)
res.Content = "已取消"
res.Outcome = OutcomeCancelled
res.Resumable = len(steps) > 0
emitEvent(emit, AgentEvent{Type: AgentEventError, Error: "cancelled", Steps: steps, Rounds: rounds, RunID: runID, Outcome: OutcomeCancelled})
emitEvent(emit, AgentEvent{Type: AgentEventError, Error: "cancelled", Steps: steps, Rounds: rounds, RunID: runID, SessionID: sessionID, Outcome: OutcomeCancelled})
return res, err
}
rounds = round + 1
if tip := CheckpointHint(rounds); tip != "" {
msgs = append(msgs, Message{Role: "system", Content: tip})
}
turn, err := llm.ChatWithTools(ctx, req.Model, msgs, tools)
if err != nil {
res := baseMeta(steps, rounds)
res := finish(steps, rounds, msgs)
res.Outcome = OutcomeFailed
emitEvent(emit, AgentEvent{Type: AgentEventError, Error: err.Error(), Steps: steps, Rounds: rounds, RunID: runID, Outcome: OutcomeFailed})
emitEvent(emit, AgentEvent{Type: AgentEventError, Error: err.Error(), Steps: steps, Rounds: rounds, RunID: runID, SessionID: sessionID, Outcome: OutcomeFailed})
return res, err
}
if len(turn.ToolCalls) == 0 {
@@ -157,13 +168,13 @@ func runAgent(ctx context.Context, llm ToolChat, rt *aitools.Runtime, req AgentR
for _, part := range chunkDelta(content, 36) {
emitEvent(emit, AgentEvent{Type: AgentEventDelta, Delta: part})
}
res := baseMeta(steps, rounds)
res := finish(steps, rounds, msgs)
res.Content = content
res.SQL = extractSQL(content)
res.Outcome = OutcomeCompleted
emitEvent(emit, AgentEvent{
Type: AgentEventDone, Content: res.Content, SQL: res.SQL, Steps: steps, Rounds: rounds,
RunID: runID, Outcome: OutcomeCompleted, StepsBudget: maxRounds,
RunID: runID, SessionID: sessionID, Outcome: OutcomeCompleted, StepsBudget: maxRounds,
})
return res, nil
}
@@ -217,13 +228,13 @@ func runAgent(ctx context.Context, llm ToolChat, rt *aitools.Runtime, req AgentR
}
if awaiting {
content := "需要确认写操作后才能继续。"
res := baseMeta(steps, rounds)
res := finish(steps, rounds, msgs)
res.Content = content
res.Outcome = OutcomeAwaitingConfirm
res.Resumable = true
emitEvent(emit, AgentEvent{
Type: AgentEventDone, Content: content, Steps: steps, Rounds: rounds,
RunID: runID, Outcome: OutcomeAwaitingConfirm, Resumable: true, StepsBudget: maxRounds,
RunID: runID, SessionID: sessionID, Outcome: OutcomeAwaitingConfirm, Resumable: true, StepsBudget: maxRounds,
})
return res, nil
}
@@ -234,14 +245,14 @@ func runAgent(ctx context.Context, llm ToolChat, rt *aitools.Runtime, req AgentR
for _, part := range chunkDelta(content, 36) {
emitEvent(emit, AgentEvent{Type: AgentEventDelta, Delta: part})
}
res := baseMeta(steps, rounds)
res := finish(steps, rounds, msgs)
res.Content = content
res.Outcome = OutcomeIncomplete
res.IncompleteReason = ReasonMaxSteps
res.Resumable = true
emitEvent(emit, AgentEvent{
Type: AgentEventIncomplete, Content: content, Steps: steps, Rounds: rounds,
RunID: runID, Outcome: OutcomeIncomplete, IncompleteReason: ReasonMaxSteps,
RunID: runID, SessionID: sessionID, Outcome: OutcomeIncomplete, IncompleteReason: ReasonMaxSteps,
Resumable: true, StepsBudget: maxRounds,
})
return res, nil