Files
ARTEX/agent/terminalreason.go
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

162 lines
6.6 KiB
Go

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: "the model ended this turn normally but left no text summary; facts and assets are as recorded by this turn's tool calls",
harness.ReasonMaxTurns: "reached the turn cap (MaxTurns): the SDK ran the wrap-up and wrote back facts and assets; the intent is marked exhausted so the planner can continue in a different direction, rather than being treated as failed",
harness.ReasonTimeout: "reached this run's wall-clock budget (MaxDuration): at the deadline the in-flight tool is interrupted and the run settles in place, writing back the identified facts and assets; the intent is marked exhausted",
harness.ReasonModelError: "the model or API call failed (network, auth, rate limiting, provider 5xx, etc.) and retries were exhausted, so the intent is marked blocked — a transport-layer failure means this intent was barely explored; check its execution (get_worker_trace) before deciding whether to redispatch or switch tactics",
harness.ReasonBlockingLimit: "context length hit the hard cap and the request was intercepted before being sent; narrow the intent's granularity or compress tool output",
harness.ReasonPromptTooLong: "the prompt was too long and context-compaction retries were exhausted; cannot continue",
harness.ReasonImageError: "the current model does not support this turn's multimodal content; switch to a vision-capable model or avoid returning images from tools",
harness.ReasonStopHookPrevented: "a Stop hook prevented this turn from ending and the run could not continue afterwards; check whether the task's Guard rules are too strict",
harness.ReasonHookStopped: "a tool or hook actively stopped execution, e.g. an out-of-scope target or a banned command; check the interception note on the last tool_result",
harness.ReasonAbortedStreaming: "the run was cancelled while the model was streaming its output",
harness.ReasonAbortedTools: "the run was cancelled during tool execution",
}
// 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 = "cancellation reason unavailable"
}
stage := "mid-execution"
switch reason {
case harness.ReasonAbortedStreaming:
stage = "the model output stage"
case harness.ReasonAbortedTools:
stage = "the tool execution stage"
}
sum = "(run interrupted: " + short + "; stopped during " + stage + progressSuffix(term, tr) + ", unfinished)"
} else if reason == harness.ReasonMaxTurns || reason == harness.ReasonTimeout {
sum = "(run budget cap reached (" + string(reason) + "); wrap-up wrote back facts" + progressSuffix(term, tr) + "; no text summary this run)"
} else {
hint := terminalReasonHint(reason)
sum = "(no text summary; terminal state " + terminalReasonLabel(reason) + ": " + firstLine(hint, 80) + ")"
}
var b strings.Builder
b.WriteString(sum)
b.WriteString("\n\n")
displayReason := terminalReasonLabel(reason)
fmt.Fprintf(&b, "- **Terminal state**: `%s` - %s\n", displayReason, terminalReasonHint(reason))
if aborted {
code, _, why, ok := AbortReason(ctx)
if ok {
fmt.Fprintf(&b, "- **Interruption cause** (`%s`): %s\n", code, why)
} else {
b.WriteString("- **Interruption cause**: unavailable; the cancelling side may not have attached a named cause via context.WithCancelCause\n")
}
}
if term.Err != nil {
fmt.Fprintf(&b, "- **Underlying error**: `%v`\n", term.Err)
}
if aborted && strings.TrimSpace(term.Text) != "" {
b.WriteString("- **Partial output generated before cancellation**:\n\n")
b.WriteString(term.Text)
b.WriteString("\n\n")
}
if term.Turns > 0 {
fmt.Fprintf(&b, "- **Executed**: %d model turns\n", term.Turns)
}
if !tr.startedAt.IsZero() {
fmt.Fprintf(&b, "- **Run duration**: %s\n", roundDur(time.Since(tr.startedAt)))
}
if u := term.Usage; u.InputTokens+u.OutputTokens+u.CacheReadTokens+u.CacheWriteTokens > 0 {
fmt.Fprintf(&b, "- **Total tokens**: input %d / output %d / cache read %d / cache write %d\n",
u.InputTokens, u.OutputTokens, u.CacheReadTokens, u.CacheWriteTokens)
}
if tr.name == "" {
b.WriteString("- **Tool calls**: the run ended before any tool call was made\n")
} else if tr.pending {
fmt.Fprintf(&b, "- **Tool in flight at interruption**: `%s` (ran for %s, **no result returned**)\n\n ```json\n %s\n ```\n",
tr.name, roundDur(time.Since(tr.at)), firstLine(tr.input, 300))
} else {
fmt.Fprintf(&b, "- **Last tool before interruption**: `%s` (returned normally)\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 "the run's context was cancelled, but no Terminal event was produced underneath"
}
return "unknown terminal state; harness may have added a TerminalReason — please add it to reasonHint"
}
func progressSuffix(term *harness.Terminal, tr *runTrace) string {
var parts []string
if term.Turns > 0 {
parts = append(parts, fmt.Sprintf("%d turns", term.Turns))
}
if !tr.startedAt.IsZero() {
parts = append(parts, roundDur(time.Since(tr.startedAt)))
}
if len(parts) == 0 {
return ""
}
return ", ran for " + 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()
}
}