mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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
162 lines
6.6 KiB
Go
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()
|
|
}
|
|
}
|