Version: 0.9.6.dev.260407
后端: 1.execute 正式终止协议补齐(abort / exhausted / completed 统一建模) - 更新model/common_state.go:新增 FlowTerminalStatus / FlowTerminalOutcome;补齐 Abort/Exhaust/ClearTerminalOutcome/IsCompleted 等统一终止语义 - 更新model/execute_contract.go:新增 ExecuteActionAbort 与 AbortIntent;补齐 action 校验互斥规则 - 更新prompt/execute.go:Plan/ReAct 两套 execute contract 升级到 V2,补充 abort 协议与 JSON 示例 2.graph 路由与 deliver 收口统一围绕 terminal outcome - 更新graph/common_graph.go:RoughBuild 改 branch;粗排异常可直接 Deliver;Execute 路由不再按“最后一轮”提前误收口 - 更新node/execute.go:轮次耗尽改写为 Exhaust;接入 handleExecuteActionAbort;abort 不在 execute 直接对用户收口 - 更新node/deliver.go:deliver summary 优先按 abort/exhausted 收口;不再无脑 Done;最终状态文案改为“本轮流程已结束” - 更新node/agent_nodes.go:仅 completed 路径写 schedule preview,aborted/exhausted 跳过 3.提示与状态摘要同步终止语义 - 更新prompt/base.go:state summary 增加 terminal outcome 展示 前端:无 仓库:无
This commit is contained in:
@@ -257,9 +257,13 @@ func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
// 中断(confirm/ask_user)路径不写,避免把中间态暴露给前端。
|
||||
if st.Deps.WriteSchedulePreview != nil && st.ScheduleState != nil {
|
||||
flowState := st.EnsureFlowState()
|
||||
if err := st.Deps.WriteSchedulePreview(ctx, st.ScheduleState, flowState.UserID, flowState.ConversationID, flowState.TaskClassIDs); err != nil {
|
||||
// 写缓存失败不阻断主流程,降级为仅 log。
|
||||
log.Printf("[WARN] deliver: 写入排程预览缓存失败 chat=%s: %v", flowState.ConversationID, err)
|
||||
if flowState != nil && flowState.IsCompleted() {
|
||||
if err := st.Deps.WriteSchedulePreview(ctx, st.ScheduleState, flowState.UserID, flowState.ConversationID, flowState.TaskClassIDs); err != nil {
|
||||
// 写缓存失败不阻断主流程,降级为仅 log。
|
||||
log.Printf("[WARN] deliver: 写入排程预览缓存失败 chat=%s: %v", flowState.ConversationID, err)
|
||||
}
|
||||
} else if flowState != nil {
|
||||
log.Printf("[DEBUG] deliver: skip schedule preview chat=%s terminal_status=%s", flowState.ConversationID, flowState.TerminalStatus())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,12 +85,10 @@ func RunDeliverNode(ctx context.Context, input DeliverNodeInput) error {
|
||||
deliverStatusBlockID,
|
||||
deliverStageName,
|
||||
"done",
|
||||
"任务已完成。",
|
||||
"本轮流程已结束。",
|
||||
true,
|
||||
)
|
||||
|
||||
// 5. 标记流程结束。
|
||||
flowState.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,6 +99,15 @@ func generateDeliverSummary(
|
||||
flowState *newagentmodel.CommonState,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
) string {
|
||||
if flowState != nil {
|
||||
switch {
|
||||
case flowState.IsAborted():
|
||||
return normalizeSpeak(buildAbortSummary(flowState))
|
||||
case flowState.IsExhaustedTerminal():
|
||||
return normalizeSpeak(buildExhaustedSummary(flowState))
|
||||
}
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
return buildMechanicalSummary(flowState)
|
||||
}
|
||||
@@ -125,6 +132,38 @@ func generateDeliverSummary(
|
||||
return normalizeSpeak(result.Text)
|
||||
}
|
||||
|
||||
// buildAbortSummary 生成“流程已终止”的统一交付文案。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 第二轮开始,abort 的用户可见文案由终止方提前写入 CommonState;
|
||||
// 2. deliver 不再重新猜测或改写业务异常,只做最终收口;
|
||||
// 3. 若历史快照缺失 user_message,则回退到一份通用说明,避免前端收到空白结果。
|
||||
func buildAbortSummary(state *newagentmodel.CommonState) string {
|
||||
if state == nil || state.TerminalOutcome == nil {
|
||||
return "本轮流程已终止。"
|
||||
}
|
||||
if msg := strings.TrimSpace(state.TerminalOutcome.UserMessage); msg != "" {
|
||||
return msg
|
||||
}
|
||||
return "本轮流程已终止,请根据当前提示检查后再继续。"
|
||||
}
|
||||
|
||||
// buildExhaustedSummary 生成“轮次耗尽”的统一收口文案。
|
||||
func buildExhaustedSummary(state *newagentmodel.CommonState) string {
|
||||
if state == nil {
|
||||
return "本轮执行已达到安全轮次上限,当前先停止继续操作。"
|
||||
}
|
||||
|
||||
prefix := "本轮执行已达到安全轮次上限,当前先停止继续操作。"
|
||||
if state.TerminalOutcome != nil && strings.TrimSpace(state.TerminalOutcome.UserMessage) != "" {
|
||||
prefix = strings.TrimSpace(state.TerminalOutcome.UserMessage)
|
||||
}
|
||||
if !state.HasPlan() {
|
||||
return prefix
|
||||
}
|
||||
return prefix + "\n\n" + strings.TrimSpace(buildMechanicalSummary(state))
|
||||
}
|
||||
|
||||
// buildMechanicalSummary 在 LLM 不可用时,机械拼接一份最小可用总结。
|
||||
func buildMechanicalSummary(state *newagentmodel.CommonState) string {
|
||||
if state == nil {
|
||||
@@ -138,7 +177,7 @@ func buildMechanicalSummary(state *newagentmodel.CommonState) string {
|
||||
return "任务流程已结束。"
|
||||
}
|
||||
|
||||
if state.Exhausted() {
|
||||
if state.IsExhaustedTerminal() {
|
||||
sb.WriteString(fmt.Sprintf("任务因执行轮次耗尽提前结束,已完成 %d/%d 步。\n", current, total))
|
||||
} else {
|
||||
sb.WriteString("所有计划步骤已执行完毕。\n")
|
||||
@@ -153,7 +192,7 @@ func buildMechanicalSummary(state *newagentmodel.CommonState) string {
|
||||
sb.WriteString(fmt.Sprintf("%s %s\n", marker, strings.TrimSpace(step.Content)))
|
||||
}
|
||||
|
||||
if state.Exhausted() && current < total {
|
||||
if state.IsExhaustedTerminal() && current < total {
|
||||
sb.WriteString("\n如需继续完成剩余步骤,可以告诉我继续。")
|
||||
}
|
||||
|
||||
|
||||
@@ -131,8 +131,14 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
|
||||
// 4. 消耗一轮预算,并检查是否耗尽。
|
||||
if !flowState.NextRound() {
|
||||
// 轮次耗尽,强制进入交付阶段。
|
||||
flowState.Done()
|
||||
// 1. 轮次耗尽属于安全边界触发的被动停止,不应伪装成“正常完成”。
|
||||
// 2. 这里统一写入 exhausted 终止结果,让 deliver 阶段按未完成收口。
|
||||
// 3. 后续 graph 只需围绕 CommonState 的终止结果路由,无需再猜测原因。
|
||||
flowState.Exhaust(
|
||||
executeStageName,
|
||||
"本轮执行已达到安全轮次上限,当前先停止继续操作。如需继续,我可以在你确认后接着处理剩余步骤。",
|
||||
"execute rounds exhausted before task completion",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -232,7 +238,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
conversationContext,
|
||||
rawText,
|
||||
fmt.Sprintf("你的执行决策不合法:%s", err.Error()),
|
||||
"合法的 action 包括:continue(继续当前步骤)、ask_user(追问用户)、confirm(写操作确认)、next_plan(推进到下一步)、done(任务完成)。",
|
||||
"合法的 action 包括:continue(继续当前步骤)、ask_user(追问用户)、confirm(写操作确认)、next_plan(推进到下一步)、done(任务完成)、abort(正式终止本轮流程)。",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -279,8 +285,9 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
if speakText != "" {
|
||||
isConfirmWithCard := decision.Action == newagentmodel.ExecuteActionConfirm && !input.AlwaysExecute
|
||||
isAskUser := decision.Action == newagentmodel.ExecuteActionAskUser
|
||||
isAbort := decision.Action == newagentmodel.ExecuteActionAbort
|
||||
|
||||
if !isConfirmWithCard && !isAskUser {
|
||||
if !isConfirmWithCard && !isAskUser && !isAbort {
|
||||
// 推流给前端
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx,
|
||||
@@ -292,11 +299,15 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
return fmt.Errorf("执行文案推送失败: %w", err)
|
||||
}
|
||||
}
|
||||
// 始终写入历史(confirm 卡片场景下也写,保证上下文连续)
|
||||
conversationContext.AppendHistory(&schema.Message{
|
||||
Role: schema.Assistant,
|
||||
Content: speakText,
|
||||
})
|
||||
// 1. confirm / ask_user 的 speak 仍要写入历史,避免下一轮 LLM 丢失自己的执行上下文。
|
||||
// 2. abort 不在这里写历史,避免先输出中间 speak,再在 deliver 收到第二份终止文案。
|
||||
// 3. ask_user 只是不在这里伪流式推送,真正的对外展示仍由 PendingInteraction.DisplayText 承担。
|
||||
if !isAbort {
|
||||
conversationContext.AppendHistory(&schema.Message{
|
||||
Role: schema.Assistant,
|
||||
Content: speakText,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 按 LLM 决策执行动作,后端信任 LLM 判断,不做语义校验。
|
||||
@@ -347,6 +358,12 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
flowState.Done()
|
||||
return nil
|
||||
|
||||
case newagentmodel.ExecuteActionAbort:
|
||||
// 1. abort 是 execute 层的正式终止协议。
|
||||
// 2. 这里只负责把终止结果写入 CommonState,真正的用户收口统一交给 deliver。
|
||||
// 3. 这样 rough_build / execute / 后续其他 stop 条件都能走同一套图内收口。
|
||||
return handleExecuteActionAbort(decision, flowState)
|
||||
|
||||
default:
|
||||
// 1. LLM 输出了不支持的 action,不应直接报错终止,而应给它修正机会。
|
||||
// 2. 使用通用修正函数追加错误反馈,让 Graph 继续循环。
|
||||
@@ -359,7 +376,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
conversationContext,
|
||||
llmOutput,
|
||||
fmt.Sprintf("你输出的 action \"%s\" 不是合法的执行动作。", decision.Action),
|
||||
"合法的 action 包括:continue(继续当前步骤)、ask_user(追问用户)、next_plan(推进到下一步)、done(任务完成)。",
|
||||
"合法的 action 包括:continue(继续当前步骤)、ask_user(追问用户)、confirm(写操作确认)、next_plan(推进到下一步)、done(任务完成)、abort(正式终止本轮流程)。",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -441,6 +458,37 @@ func handleExecuteActionConfirm(
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleExecuteActionAbort 处理 execute 阶段声明的正式终止请求。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只负责把 abort 协议落到 CommonState;
|
||||
// 2. 不直接向用户发最终文案,避免和 deliver 收口重复;
|
||||
// 3. 若模型未提供 internal_reason,则回退到 decision.Reason 作为排查信息。
|
||||
func handleExecuteActionAbort(
|
||||
decision *newagentmodel.ExecuteDecision,
|
||||
flowState *newagentmodel.CommonState,
|
||||
) error {
|
||||
if decision == nil || decision.Abort == nil {
|
||||
return fmt.Errorf("abort 动作缺少终止信息")
|
||||
}
|
||||
if flowState == nil {
|
||||
return fmt.Errorf("abort 动作缺少流程状态")
|
||||
}
|
||||
|
||||
internalReason := strings.TrimSpace(decision.Abort.InternalReason)
|
||||
if internalReason == "" {
|
||||
internalReason = strings.TrimSpace(decision.Reason)
|
||||
}
|
||||
|
||||
flowState.Abort(
|
||||
executeStageName,
|
||||
decision.Abort.Code,
|
||||
decision.Abort.UserMessage,
|
||||
internalReason,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeToolCall 执行工具调用并记录证据。
|
||||
//
|
||||
// 职责边界:
|
||||
@@ -698,7 +746,7 @@ func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string {
|
||||
|
||||
total := len(state.Tasks)
|
||||
pendingNoSlot := 0
|
||||
pendingWithSlot := 0
|
||||
suggestedTotal := 0
|
||||
existingTotal := 0
|
||||
taskItemWithSlot := 0
|
||||
eventWithSlot := 0
|
||||
@@ -707,14 +755,12 @@ func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string {
|
||||
t := &state.Tasks[i]
|
||||
hasSlot := len(t.Slots) > 0
|
||||
|
||||
switch t.Status {
|
||||
case "pending":
|
||||
if hasSlot {
|
||||
pendingWithSlot++
|
||||
} else {
|
||||
pendingNoSlot++
|
||||
}
|
||||
case "existing":
|
||||
switch {
|
||||
case newagenttools.IsPendingTask(*t):
|
||||
pendingNoSlot++
|
||||
case newagenttools.IsSuggestedTask(*t):
|
||||
suggestedTotal++
|
||||
case newagenttools.IsExistingTask(*t):
|
||||
existingTotal++
|
||||
}
|
||||
|
||||
@@ -729,10 +775,10 @@ func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string {
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"tasks=%d pending_no_slot=%d pending_with_slot=%d existing=%d task_item_with_slot=%d event_with_slot=%d",
|
||||
"tasks=%d pending=%d suggested=%d existing=%d task_item_with_slot=%d event_with_slot=%d",
|
||||
total,
|
||||
pendingNoSlot,
|
||||
pendingWithSlot,
|
||||
suggestedTotal,
|
||||
existingTotal,
|
||||
taskItemWithSlot,
|
||||
eventWithSlot,
|
||||
|
||||
Reference in New Issue
Block a user