Files
artex/agent/terminalreason.go
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

162 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"context"
"fmt"
"strings"
"time"
"github.com/Autumn-27/norma/harness"
)
// runTrace retains the latest tool call so an interrupted run can identify the
// operation that was still in flight.
type runTrace struct {
startedAt time.Time
id string
name string
input string
at time.Time
pending bool
}
func (t *runTrace) start(id, name, input string) {
t.id, t.name, t.input, t.at, t.pending = id, name, input, time.Now(), true
}
func (t *runTrace) done(id string) {
if id == t.id {
t.pending = false
}
}
var reasonHint = map[harness.TerminalReason]string{
harness.ReasonCompleted: "模型正常结束了本轮,但没有留下文字总结;事实和资产以本轮工具调用记录为准",
harness.ReasonMaxTurns: "达到步数上限(MaxTurns):SDK 已执行收尾并写回事实和资产,意图会标记为 exhausted,供规划者换方向继续,而不是作为失败处理",
harness.ReasonTimeout: "达到单次运行的墙钟预算(MaxDuration):到点会打断在跑的工具并就地进收尾,把已识别的事实和资产写回,意图会标记为 exhausted",
harness.ReasonModelError: "模型或 API 调用失败(网络、鉴权、限流、供应商 5xx 等),重试用尽后意图标记为 blocked——传输层故障导致这条意图基本没真正探成;查其执行过程(get_worker_trace)后再决定重派或换法",
harness.ReasonBlockingLimit: "上下文长度达到硬上限,请求在发出前被拦截;应收窄意图粒度或压缩工具返回",
harness.ReasonPromptTooLong: "提示词过长且上下文压缩重试已经用尽,无法继续执行",
harness.ReasonImageError: "当前模型不支持本轮多模态内容;请切换支持视觉的模型或避免工具返回图片",
harness.ReasonStopHookPrevented: "Stop 钩子阻止本轮结束,随后未能继续;请检查任务 Guard 规则是否过严",
harness.ReasonHookStopped: "工具或钩子主动停止继续执行,例如越界目标或禁用命令;请检查最后一条 tool_result 的拦截说明",
harness.ReasonAbortedStreaming: "运行在模型输出流式生成阶段被取消",
harness.ReasonAbortedTools: "运行在工具执行阶段被取消",
}
// terminalText renders a terminal event with no final text into a compact summary
// and a Markdown detail block.
func terminalText(ctx context.Context, term *harness.Terminal, tr *runTrace) (string, string) {
reason := term.Reason
aborted := reason == harness.ReasonAbortedStreaming || reason == harness.ReasonAbortedTools
// Prompt may return ctx.Err directly without a terminal event. Preserve the
// cancellation cause instead of falling back to an empty/unknown terminal reason.
if reason == "" && ctx.Err() != nil {
aborted = true
}
var sum string
if aborted {
_, short, _, ok := AbortReason(ctx)
if !ok {
short = "未能取得取消原因"
}
stage := "执行过程中"
switch reason {
case harness.ReasonAbortedStreaming:
stage = "模型输出阶段"
case harness.ReasonAbortedTools:
stage = "工具执行阶段"
}
sum = "(运行被中断:" + short + ";停在" + stage + progressSuffix(term, tr) + ",未完成)"
} else if reason == harness.ReasonMaxTurns || reason == harness.ReasonTimeout {
sum = "(达到运行预算上限(" + string(reason) + "),已收尾写回事实" + progressSuffix(term, tr) + ";本次无文字总结)"
} else {
hint := terminalReasonHint(reason)
sum = "(无文字总结,终态 " + terminalReasonLabel(reason) + ":" + firstLine(hint, 80) + ")"
}
var b strings.Builder
b.WriteString(sum)
b.WriteString("\n\n")
displayReason := terminalReasonLabel(reason)
fmt.Fprintf(&b, "- **终态**: `%s` - %s\n", displayReason, terminalReasonHint(reason))
if aborted {
code, _, why, ok := AbortReason(ctx)
if ok {
fmt.Fprintf(&b, "- **中断原因** (`%s`): %s\n", code, why)
} else {
b.WriteString("- **中断原因**: 无法取得;取消方可能没有通过 context.WithCancelCause 附加具名原因\n")
}
}
if term.Err != nil {
fmt.Fprintf(&b, "- **底层错误**: `%v`\n", term.Err)
}
if aborted && strings.TrimSpace(term.Text) != "" {
b.WriteString("- **取消前已生成的部分输出**:\n\n")
b.WriteString(term.Text)
b.WriteString("\n\n")
}
if term.Turns > 0 {
fmt.Fprintf(&b, "- **已执行**: %d 轮模型回合\n", term.Turns)
}
if !tr.startedAt.IsZero() {
fmt.Fprintf(&b, "- **本次运行耗时**: %s\n", roundDur(time.Since(tr.startedAt)))
}
if u := term.Usage; u.InputTokens+u.OutputTokens+u.CacheReadTokens+u.CacheWriteTokens > 0 {
fmt.Fprintf(&b, "- **累计 token**: 输入 %d / 输出 %d / 缓存读 %d / 缓存写 %d\n",
u.InputTokens, u.OutputTokens, u.CacheReadTokens, u.CacheWriteTokens)
}
if tr.name == "" {
b.WriteString("- **工具调用**: 本次运行还没有发出工具调用就结束了\n")
} else if tr.pending {
fmt.Fprintf(&b, "- **中断时正在执行的工具**: `%s`(已运行 %s,**未返回结果**)\n\n ```json\n %s\n ```\n",
tr.name, roundDur(time.Since(tr.at)), firstLine(tr.input, 300))
} else {
fmt.Fprintf(&b, "- **中断前最后一个工具**: `%s`(已正常返回)\n", tr.name)
}
return sum, b.String()
}
func terminalReasonLabel(reason harness.TerminalReason) string {
if reason == "" {
return "context_canceled"
}
return string(reason)
}
func terminalReasonHint(reason harness.TerminalReason) string {
if hint := reasonHint[reason]; hint != "" {
return hint
}
if reason == "" {
return "运行的 context 已取消,但底层没有产生 Terminal 事件"
}
return "未知终态;harness 可能新增了 TerminalReason,请补充 reasonHint"
}
func progressSuffix(term *harness.Terminal, tr *runTrace) string {
var parts []string
if term.Turns > 0 {
parts = append(parts, fmt.Sprintf("%d 轮", term.Turns))
}
if !tr.startedAt.IsZero() {
parts = append(parts, roundDur(time.Since(tr.startedAt)))
}
if len(parts) == 0 {
return ""
}
return ",已运行 " + strings.Join(parts, " / ")
}
func roundDur(d time.Duration) string {
switch {
case d < time.Minute:
return d.Round(100 * time.Millisecond).String()
case d < time.Hour:
return d.Round(time.Second).String()
default:
return d.Round(time.Minute).String()
}
}