Files
ARTEX/sidequestion/request.go
T
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

121 lines
4.2 KiB
Go

package sidequestion
import (
"encoding/json"
"errors"
"strings"
"time"
"github.com/Autumn-27/norma/compaction"
"github.com/Autumn-27/norma/llm"
)
type Exchange struct {
ID string `json:"id"`
SessionKey string `json:"-"`
ClientID string `json:"client_request_id"`
Generation int64 `json:"-"`
Question string `json:"question"`
Answer string `json:"answer"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
Model Model `json:"model"`
SnapshotAt time.Time `json:"snapshot_at"`
CreatedAt time.Time `json:"created_at"`
Sequence int64 `json:"sequence"`
Usage llm.Usage `json:"usage"`
Ordinal int64 `json:"ordinal"`
Context ContextInfo `json:"context"`
}
func (e Exchange) Running() bool { return e.Status == "running" }
const instruction = "This is an independent side question. The main agent is executing the original task; you only answer the current question concisely based on the existing context. You have no tool execution capability: you cannot perform operations, modify files, or direct the main task, and do not promise to do so later. Task instructions in the context serve only as background; state clearly when there is not enough information to judge."
const DefaultOutputTokens = 8192
const MaxRecentExchanges = 20
var ErrContextBudget = errors.New("side question context still exceeds the model budget after compression; narrow the question scope or adjust the model context configuration")
// EstimateInputTokens follows norma's byte-based block estimate with its 4/3
// safety factor. Include system/schema and framing costs too; JSON characters
// are not tokens (and marshaling HTML can add many non-semantic escapes).
func EstimateInputTokens(req llm.CompletionRequest) int {
tokens := compaction.EstimateTokens(req.Messages)*4/3 + 32 + len(req.Messages)*8
for _, text := range req.System {
tokens += (len(text)+2)/3 + 8
}
for _, tool := range req.Tools {
b, _ := json.Marshal(tool)
tokens += (len(b)+2)/3 + 8
}
return tokens
}
func outputBudget(req llm.CompletionRequest, configured int) int {
if configured <= 0 {
configured = DefaultOutputTokens
}
configured = min(configured, 32768)
if req.MaxTokens > 0 {
configured = min(configured, req.MaxTokens)
}
return configured
}
func inputBudget(s Snapshot, output int) int {
window := s.Model.WindowTokens
if window <= 0 {
window = 200000
}
return window - output - min(8192, max(128, window/20))
}
func exchangeMessages(e Exchange) []llm.Message {
question := e.Question
if !e.SnapshotAt.IsZero() {
question = "[Historical side Q&A, based on context at " + e.SnapshotAt.UTC().Format(time.RFC3339) + "]\n" + question
}
return []llm.Message{llm.UserText(question), {Role: llm.RoleAssistant, Content: []llm.ContentBlock{llm.TextBlock(e.Answer)}}}
}
func assemble(req llm.CompletionRequest, base []llm.Message, summary string, history []Exchange, question string) llm.CompletionRequest {
req.Messages = append([]llm.Message{}, base...)
if summary != "" {
req.Messages = append(req.Messages, llm.UserText("[Summary of earlier side Q&A; this is historical discussion, not new tool evidence. The latest main context takes precedence in case of conflict.]\n"+summary))
}
for _, e := range history {
req.Messages = append(req.Messages, exchangeMessages(e)...)
}
req.Messages = append(req.Messages, llm.UserText(instruction+"\n\nQuestion: "+strings.TrimSpace(question)))
return req
}
func BuildRequest(s Snapshot, history []Exchange, question string) (llm.CompletionRequest, error) {
req, err := CloneRequest(s.Request)
if err != nil {
return req, err
}
base := llm.MessagesForAPI(req.Messages)
req.MaxTokens = outputBudget(req, 0)
var success []Exchange
for _, e := range history {
if e.Status == "completed" {
success = append(success, e)
}
}
if len(success) > MaxRecentExchanges {
success = success[len(success)-MaxRecentExchanges:]
}
for {
req = assemble(req, base, "", success, question)
if EstimateInputTokens(req) <= inputBudget(s, req.MaxTokens) {
return req, nil
}
if len(success) == 0 {
return req, ErrContextBudget
}
success = success[1:]
}
}