added C-c handling
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -628,7 +629,8 @@ func isInvalidAssistantErr(err error) bool {
|
|||||||
return strings.Contains(s, "Invalid assistant message") || strings.Contains(s, "content or tool_calls must be set")
|
return strings.Contains(s, "Invalid assistant message") || strings.Contains(s, "content or tool_calls must be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
||||||
|
if err := ctx.Err(); err != nil { return Message{}, err }
|
||||||
sanitizeMessages(msgs)
|
sanitizeMessages(msgs)
|
||||||
p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": msgs, "stream": cfg.Stream}
|
p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": msgs, "stream": cfg.Stream}
|
||||||
if tools != nil { p["tools"] = tools }
|
if tools != nil { p["tools"] = tools }
|
||||||
@@ -650,13 +652,18 @@ func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
|||||||
DialContext: (&net.Dialer{Timeout: time.Duration(cfg.Timeout) * time.Second}).DialContext,
|
DialContext: (&net.Dialer{Timeout: time.Duration(cfg.Timeout) * time.Second}).DialContext,
|
||||||
ResponseHeaderTimeout: time.Duration(cfg.Timeout) * time.Second,
|
ResponseHeaderTimeout: time.Duration(cfg.Timeout) * time.Second,
|
||||||
}}
|
}}
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
fib := []int{1, 1, 2, 3, 5, 8, 13, 21, 34}
|
fib := []int{1, 1, 2, 3, 5, 8, 13, 21, 34}
|
||||||
pend := c("...requesting...", 1, 2)
|
pend := c("...requesting...", 1, 2)
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i <= len(fib); i++ {
|
for i := 0; i <= len(fib); i++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
if COL { fmt.Print("\r\033[K") }
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
if COL { fmt.Print("\r" + pend) } else { fmt.Println(pend) }
|
if COL { fmt.Print("\r" + pend) } else { fmt.Println(pend) }
|
||||||
req, _ := http.NewRequest("POST", strings.TrimRight(cfg.Endpoint, "/")+"/chat/completions", bytes.NewReader(body))
|
req, _ := http.NewRequestWithContext(ctx, "POST", strings.TrimRight(cfg.Endpoint, "/")+"/chat/completions", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Bantam/1.0)")
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Bantam/1.0)")
|
||||||
if cfg.APIKey != "" && cfg.APIKey != "-" { req.Header.Set("Authorization", "Bearer "+cfg.APIKey) }
|
if cfg.APIKey != "" && cfg.APIKey != "-" { req.Header.Set("Authorization", "Bearer "+cfg.APIKey) }
|
||||||
@@ -673,12 +680,19 @@ func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
|||||||
}
|
}
|
||||||
if err == nil { break }
|
if err == nil { break }
|
||||||
if COL { fmt.Print("\r\033[K") }
|
if COL { fmt.Print("\r\033[K") }
|
||||||
|
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
|
||||||
|
return Message{}, ctx.Err()
|
||||||
|
}
|
||||||
if is4xxClientErr {
|
if is4xxClientErr {
|
||||||
return Message{}, err
|
return Message{}, err
|
||||||
}
|
}
|
||||||
if i < len(fib) {
|
if i < len(fib) {
|
||||||
fmt.Println(c(fmt.Sprintf("[network error: %v, retrying in %ds...]", err, fib[i]), 31))
|
fmt.Println(c(fmt.Sprintf("[network error: %v, retrying in %ds...]", err, fib[i]), 31))
|
||||||
time.Sleep(time.Duration(fib[i]) * time.Second)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return Message{}, ctx.Err()
|
||||||
|
case <-time.After(time.Duration(fib[i]) * time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -696,16 +710,19 @@ func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
|||||||
} `json:"message"`
|
} `json:"message"`
|
||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&cr); err != nil { return Message{}, err }
|
if err := json.NewDecoder(resp.Body).Decode(&cr); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || ctx.Err() != nil { return Message{}, ctx.Err() }
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
if len(cr.Choices) == 0 { return Message{}, errors.New("empty choices in LLM response") }
|
if len(cr.Choices) == 0 { return Message{}, errors.New("empty choices in LLM response") }
|
||||||
m := cr.Choices[0].Message.Message
|
m := cr.Choices[0].Message.Message
|
||||||
if m.ReasoningContent == "" { m.ReasoningContent = cr.Choices[0].Message.Reasoning }
|
if m.ReasoningContent == "" { m.ReasoningContent = cr.Choices[0].Message.Reasoning }
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return parseStream(resp.Body)
|
return parseStream(ctx, resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseStream(r io.Reader) (Message, error) {
|
func parseStream(ctx context.Context, r io.Reader) (Message, error) {
|
||||||
var content, reas string
|
var content, reas string
|
||||||
var rh, ch bool
|
var rh, ch bool
|
||||||
var lineBuf string
|
var lineBuf string
|
||||||
@@ -724,6 +741,7 @@ func parseStream(r io.Reader) (Message, error) {
|
|||||||
sc := bufio.NewScanner(r)
|
sc := bufio.NewScanner(r)
|
||||||
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
|
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
|
||||||
for sc.Scan() {
|
for sc.Scan() {
|
||||||
|
if err := ctx.Err(); err != nil { return Message{}, err }
|
||||||
ln := strings.TrimSpace(sc.Text())
|
ln := strings.TrimSpace(sc.Text())
|
||||||
if !strings.HasPrefix(ln, "data:") { continue }
|
if !strings.HasPrefix(ln, "data:") { continue }
|
||||||
data := strings.TrimSpace(ln[5:])
|
data := strings.TrimSpace(ln[5:])
|
||||||
@@ -774,6 +792,7 @@ func parseStream(r io.Reader) (Message, error) {
|
|||||||
if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments }
|
if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil { return Message{}, err }
|
||||||
flushTable()
|
flushTable()
|
||||||
if lineBuf != "" {
|
if lineBuf != "" {
|
||||||
if !mdSt.inCode && isTableLine(lineBuf) {
|
if !mdSt.inCode && isTableLine(lineBuf) {
|
||||||
@@ -796,12 +815,17 @@ func parseStream(r io.Reader) (Message, error) {
|
|||||||
return m, sc.Err()
|
return m, sc.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func shell(cmd string, timeout int) string {
|
func shell(ctx context.Context, cmd string, timeout int) string {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
cmdCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput()
|
c := exec.CommandContext(cmdCtx, "sh", "-c", cmd)
|
||||||
|
c.WaitDelay = 100 * time.Millisecond
|
||||||
|
out, err := c.CombinedOutput()
|
||||||
res := strings.TrimSpace(string(out))
|
res := strings.TrimSpace(string(out))
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
if ctx.Err() != nil {
|
||||||
|
return "[interrupted]\n\nexit: -1"
|
||||||
|
}
|
||||||
|
if cmdCtx.Err() == context.DeadlineExceeded {
|
||||||
return fmt.Sprintf("%s\n\n[shell timeout after %ds]\nexit: -1", res, timeout)
|
return fmt.Sprintf("%s\n\n[shell timeout after %ds]\nexit: -1", res, timeout)
|
||||||
}
|
}
|
||||||
code := 0
|
code := 0
|
||||||
@@ -824,11 +848,15 @@ func last(msgs []Message) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func AL(cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
|
func AL(ctx context.Context, cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
|
||||||
done := false
|
done := false
|
||||||
for i := 0; i < cfg.MaxALIterations && !done; i++ {
|
for i := 0; i < cfg.MaxALIterations && !done; i++ {
|
||||||
m, err := llm(cfg, msgs, TOOLS)
|
if err := ctx.Err(); err != nil { return msgs, err }
|
||||||
|
m, err := llm(ctx, cfg, msgs, TOOLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
|
||||||
|
return msgs, err
|
||||||
|
}
|
||||||
if isInvalidAssistantErr(err) {
|
if isInvalidAssistantErr(err) {
|
||||||
stripped := false
|
stripped := false
|
||||||
for j := len(msgs) - 1; j >= 0; j-- {
|
for j := len(msgs) - 1; j >= 0; j-- {
|
||||||
@@ -861,6 +889,7 @@ func AL(cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
|
|||||||
}
|
}
|
||||||
if len(m.ToolCalls) == 0 { done = true; break }
|
if len(m.ToolCalls) == 0 { done = true; break }
|
||||||
for _, tc := range m.ToolCalls {
|
for _, tc := range m.ToolCalls {
|
||||||
|
if err := ctx.Err(); err != nil { return msgs, err }
|
||||||
fn, astr := tc.Function.Name, tc.Function.Arguments
|
fn, astr := tc.Function.Name, tc.Function.Arguments
|
||||||
fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33))
|
fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33))
|
||||||
res, sty := "", 2
|
res, sty := "", 2
|
||||||
@@ -871,18 +900,28 @@ func AL(cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
|
|||||||
switch fn {
|
switch fn {
|
||||||
case "shell_exec":
|
case "shell_exec":
|
||||||
cmd, _ := a["command"].(string)
|
cmd, _ := a["command"].(string)
|
||||||
res = shell(cmd, cfg.ShellTimeout)
|
res = shell(ctx, cmd, cfg.ShellTimeout)
|
||||||
|
if err := ctx.Err(); err != nil { return msgs, err }
|
||||||
case "run_subagent":
|
case "run_subagent":
|
||||||
pr, _ := a["prompt"].(string)
|
pr, _ := a["prompt"].(string)
|
||||||
if depth >= MAX_DEPTH {
|
if depth >= MAX_DEPTH {
|
||||||
res, sty = fmt.Sprintf("[subagent depth limit (%d) reached, child not spawned]", MAX_DEPTH), 31
|
res, sty = fmt.Sprintf("[subagent depth limit (%d) reached, child not spawned]", MAX_DEPTH), 31
|
||||||
} else if subr, err := AL(cfg, []Message{{Role: "system", Content: strp(sp + "\n\nImportant: this is a child agent")}, {Role: "user", Content: strp(pr)}}, sp, depth+1); err != nil {
|
|
||||||
res, sty = "[subagent error: "+err.Error()+"]", 31
|
|
||||||
} else {
|
} else {
|
||||||
res, sty = last(subr), 2
|
subMsgs := []Message{
|
||||||
|
{Role: "system", Content: strp(sp + "\n\nImportant: this is a child agent")},
|
||||||
|
{Role: "user", Content: strp(pr)},
|
||||||
|
}
|
||||||
|
if subr, err := AL(ctx, cfg, subMsgs, sp, depth+1); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
|
||||||
|
return msgs, err
|
||||||
|
}
|
||||||
|
res, sty = "[subagent error: "+err.Error()+"]", 31
|
||||||
|
} else {
|
||||||
|
res, sty = last(subr), 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
res, sty = "Unknown tool: "+fn, 31
|
res, sty = "Unknown tool: " + fn, 31
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println(c("[tool result: "+fn+"]", 32) + "\n" + c(res, sty) + "\n")
|
fmt.Println(c("[tool result: "+fn+"]", 32) + "\n" + c(res, sty) + "\n")
|
||||||
@@ -989,7 +1028,7 @@ func autosave(msgs []Message) {
|
|||||||
os.WriteFile(filepath.Join(sdir(), "autosave.json"), b, 0644)
|
os.WriteFile(filepath.Join(sdir(), "autosave.json"), b, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func summarize(cfg *Cfg, msgs []Message) (string, error) {
|
func summarize(ctx context.Context, cfg *Cfg, msgs []Message) (string, error) {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, m := range msgs {
|
for _, m := range msgs {
|
||||||
if m.Role == "system" { continue }
|
if m.Role == "system" { continue }
|
||||||
@@ -1013,7 +1052,7 @@ func summarize(cfg *Cfg, msgs []Message) (string, error) {
|
|||||||
sys := "You are a conversation summarizer for an AI agent's context window. Summarize concisely but completely, preserving all important facts, decisions, code, errors, and the current task state, so the agent can continue the work without the original messages. Output only the summary."
|
sys := "You are a conversation summarizer for an AI agent's context window. Summarize concisely but completely, preserving all important facts, decisions, code, errors, and the current task state, so the agent can continue the work without the original messages. Output only the summary."
|
||||||
cc := *cfg
|
cc := *cfg
|
||||||
cc.Stream = false
|
cc.Stream = false
|
||||||
m, err := llm(&cc, []Message{{Role: "system", Content: strp(sys)}, {Role: "user", Content: strp("Summarize this conversation:\n\n" + joined)}}, nil)
|
m, err := llm(ctx, &cc, []Message{{Role: "system", Content: strp(sys)}, {Role: "user", Content: strp("Summarize this conversation:\n\n" + joined)}}, nil)
|
||||||
if err != nil { return "", err }
|
if err != nil { return "", err }
|
||||||
s := ""
|
s := ""
|
||||||
if m.Content != nil { s = *m.Content }
|
if m.Content != nil { s = *m.Content }
|
||||||
@@ -1022,11 +1061,11 @@ func summarize(cfg *Cfg, msgs []Message) (string, error) {
|
|||||||
return strings.TrimSpace(s), nil
|
return strings.TrimSpace(s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compact(cfg *Cfg, msgs []Message) ([]Message, string, error) {
|
func compact(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, string, error) {
|
||||||
if len(msgs) == 0 || msgs[0].Role != "system" {
|
if len(msgs) == 0 || msgs[0].Role != "system" {
|
||||||
return msgs, "", errors.New("session has no system message")
|
return msgs, "", errors.New("session has no system message")
|
||||||
}
|
}
|
||||||
s, err := summarize(cfg, msgs)
|
s, err := summarize(ctx, cfg, msgs)
|
||||||
if err != nil { return msgs, "", err }
|
if err != nil { return msgs, "", err }
|
||||||
return []Message{{Role: "system", Content: msgs[0].Content}, {Role: "user", Content: strp("Summary of the previous conversation:\n" + s + "\n\nPlease continue from here.")}}, s, nil
|
return []Message{{Role: "system", Content: msgs[0].Content}, {Role: "user", Content: strp("Summary of the previous conversation:\n" + s + "\n\nPlease continue from here.")}}, s, nil
|
||||||
}
|
}
|
||||||
@@ -1189,8 +1228,8 @@ func readLine(prompt string) (string, bool) {
|
|||||||
e.buf[e.pos] = '\n'
|
e.buf[e.pos] = '\n'
|
||||||
e.pos++
|
e.pos++
|
||||||
case 0x03:
|
case 0x03:
|
||||||
fmt.Print("\r\n")
|
fmt.Print("^C\r\n")
|
||||||
return "", false
|
return "", true
|
||||||
case 0x04:
|
case 0x04:
|
||||||
if len(e.buf) == 0 {
|
if len(e.buf) == 0 {
|
||||||
fmt.Print("\r\n")
|
fmt.Print("\r\n")
|
||||||
@@ -1247,14 +1286,23 @@ func main() {
|
|||||||
if cmd != "" {
|
if cmd != "" {
|
||||||
astr, _ := json.Marshal(map[string]string{"command": cmd})
|
astr, _ := json.Marshal(map[string]string{"command": cmd})
|
||||||
fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33))
|
fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33))
|
||||||
res := shell(cmd, cfg.ShellTimeout)
|
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
res := shell(sigCtx, cmd, cfg.ShellTimeout)
|
||||||
|
cancel()
|
||||||
fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2))
|
fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msgs = append(msgs, Message{Role: "user", Content: strp(u)})
|
msgs = append(msgs, Message{Role: "user", Content: strp(u)})
|
||||||
if msgs, err = AL(&cfg, msgs, sp, 0); err != nil {
|
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
msgs, err = AL(sigCtx, &cfg, msgs, sp, 0)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || sigCtx.Err() != nil {
|
||||||
|
fmt.Println(c("\n[interrupted]", 33))
|
||||||
|
} else {
|
||||||
|
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
||||||
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
autosave(msgs)
|
autosave(msgs)
|
||||||
@@ -1317,9 +1365,15 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Println(c("[compacting conversation...]", 33))
|
fmt.Println(c("[compacting conversation...]", 33))
|
||||||
nm, sm, err := compact(&cfg, msgs)
|
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
nm, sm, err := compact(sigCtx, &cfg, msgs)
|
||||||
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(c("[compact failed: "+err.Error()+"]", 31))
|
if errors.Is(err, context.Canceled) || sigCtx.Err() != nil {
|
||||||
|
fmt.Println(c("\n[interrupted]", 33))
|
||||||
|
} else {
|
||||||
|
fmt.Println(c("[compact failed: "+err.Error()+"]", 31))
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msgs = nm
|
msgs = nm
|
||||||
@@ -1354,7 +1408,9 @@ func main() {
|
|||||||
if cmd != "" {
|
if cmd != "" {
|
||||||
astr, _ := json.Marshal(map[string]string{"command": cmd})
|
astr, _ := json.Marshal(map[string]string{"command": cmd})
|
||||||
fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33))
|
fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33))
|
||||||
res := shell(cmd, cfg.ShellTimeout)
|
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
res := shell(sigCtx, cmd, cfg.ShellTimeout)
|
||||||
|
cancel()
|
||||||
fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2))
|
fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -1365,11 +1421,20 @@ func main() {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msgs = append(msgs, Message{Role: "user", Content: strp(u)})
|
turnMsgs := append([]Message{}, msgs...)
|
||||||
var err error
|
turnMsgs = append(turnMsgs, Message{Role: "user", Content: strp(u)})
|
||||||
if msgs, err = AL(&cfg, msgs, sp, 0); err != nil {
|
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
resMsgs, err := AL(sigCtx, &cfg, turnMsgs, sp, 0)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || sigCtx.Err() != nil {
|
||||||
|
fmt.Println(c("\n[interrupted]", 33))
|
||||||
|
} else {
|
||||||
|
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
msgs = resMsgs
|
||||||
autosave(msgs)
|
autosave(msgs)
|
||||||
}
|
}
|
||||||
done:
|
done:
|
||||||
|
|||||||
+118
-31
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -58,7 +59,7 @@ func TestParseStreamReasoningNoDuplication(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ func TestParseStreamReasoningAlias(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ func TestParseStreamContentOnly(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -129,7 +130,7 @@ func TestParseStreamReasoningOnly(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ func TestParseStreamReasoningOnly(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseStreamEmpty(t *testing.T) {
|
func TestParseStreamEmpty(t *testing.T) {
|
||||||
for _, in := range []string{"", "\n\n", "event: message\n\n"} {
|
for _, in := range []string{"", "\n\n", "event: message\n\n"} {
|
||||||
msg, err := parseStream(strings.NewReader(in))
|
msg, err := parseStream(context.Background(), strings.NewReader(in))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error for input %q: %v", in, err)
|
t.Fatalf("unexpected parseStream error for input %q: %v", in, err)
|
||||||
}
|
}
|
||||||
@@ -161,7 +162,7 @@ func TestParseStreamToolCallSplitAcrossChunks(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -187,7 +188,7 @@ func TestParseStreamMultipleToolCallsKeepFirstAppearanceOrder(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -209,7 +210,7 @@ func TestParseStreamJunkAndNoChoicesIgnored(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -225,7 +226,7 @@ func TestParseStreamReasoningAfterContent(t *testing.T) {
|
|||||||
`data: [DONE]`,
|
`data: [DONE]`,
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected parseStream error: %v", err)
|
t.Fatalf("unexpected parseStream error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -475,33 +476,45 @@ func TestIsInvalidAssistantErr(t *testing.T) {
|
|||||||
// ---------- shell ----------
|
// ---------- shell ----------
|
||||||
|
|
||||||
func TestShellBasic(t *testing.T) {
|
func TestShellBasic(t *testing.T) {
|
||||||
res := shell("echo hi", 10)
|
res := shell(context.Background(), "echo hi", 10)
|
||||||
if !strings.Contains(res, "hi") || !strings.HasSuffix(res, "exit: 0") {
|
if !strings.Contains(res, "hi") || !strings.HasSuffix(res, "exit: 0") {
|
||||||
t.Errorf("shell(echo hi) = %q", res)
|
t.Errorf("shell(echo hi) = %q", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShellExitCodeAndStderr(t *testing.T) {
|
func TestShellExitCodeAndStderr(t *testing.T) {
|
||||||
res := shell("echo out; echo err >&2; exit 7", 10)
|
res := shell(context.Background(), "echo out; echo err >&2; exit 7", 10)
|
||||||
if !strings.Contains(res, "out") || !strings.Contains(res, "err") || !strings.HasSuffix(res, "exit: 7") {
|
if !strings.Contains(res, "out") || !strings.Contains(res, "err") || !strings.HasSuffix(res, "exit: 7") {
|
||||||
t.Errorf("shell multi = %q", res)
|
t.Errorf("shell multi = %q", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShellUnknownCommand(t *testing.T) {
|
func TestShellUnknownCommand(t *testing.T) {
|
||||||
res := shell("definitely_not_a_command_xyz", 10)
|
res := shell(context.Background(), "definitely_not_a_command_xyz", 10)
|
||||||
if !strings.Contains(res, "exit: 127") {
|
if !strings.Contains(res, "exit: 127") {
|
||||||
t.Errorf("expected exit 127, got %q", res)
|
t.Errorf("expected exit 127, got %q", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShellTimeout(t *testing.T) {
|
func TestShellTimeout(t *testing.T) {
|
||||||
res := shell("sleep 5", 1)
|
res := shell(context.Background(), "sleep 5", 1)
|
||||||
if !strings.Contains(res, "[shell timeout after 1s]") || !strings.HasSuffix(res, "exit: -1") {
|
if !strings.Contains(res, "[shell timeout after 1s]") || !strings.HasSuffix(res, "exit: -1") {
|
||||||
t.Errorf("expected timeout marker and exit -1, got %q", res)
|
t.Errorf("expected timeout marker and exit -1, got %q", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShellContextCancellation(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
res := shell(ctx, "sleep 5", 10)
|
||||||
|
if !strings.Contains(res, "[interrupted]") || !strings.HasSuffix(res, "exit: -1") {
|
||||||
|
t.Errorf("expected interrupted result on cancellation, got %q", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- last / summary ----------
|
// ---------- last / summary ----------
|
||||||
|
|
||||||
func TestLast(t *testing.T) {
|
func TestLast(t *testing.T) {
|
||||||
@@ -694,23 +707,23 @@ func TestAutosave(t *testing.T) {
|
|||||||
// ---------- summarize / compact (error paths only, no network) ----------
|
// ---------- summarize / compact (error paths only, no network) ----------
|
||||||
|
|
||||||
func TestSummarizeEmpty(t *testing.T) {
|
func TestSummarizeEmpty(t *testing.T) {
|
||||||
if _, err := summarize(&Cfg{}, nil); err == nil || !strings.Contains(err.Error(), "no conversation") {
|
if _, err := summarize(context.Background(), &Cfg{}, nil); err == nil || !strings.Contains(err.Error(), "no conversation") {
|
||||||
t.Errorf("expected no-conversation error, got %v", err)
|
t.Errorf("expected no-conversation error, got %v", err)
|
||||||
}
|
}
|
||||||
if _, err := summarize(&Cfg{}, []Message{{Role: "system", Content: strp("sys")}}); err == nil {
|
if _, err := summarize(context.Background(), &Cfg{}, []Message{{Role: "system", Content: strp("sys")}}); err == nil {
|
||||||
t.Errorf("expected error for system-only conversation")
|
t.Errorf("expected error for system-only conversation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompactNoSystem(t *testing.T) {
|
func TestCompactNoSystem(t *testing.T) {
|
||||||
msgs, _, err := compact(&Cfg{}, nil)
|
msgs, _, err := compact(context.Background(), &Cfg{}, nil)
|
||||||
if err == nil || !strings.Contains(err.Error(), "no system message") {
|
if err == nil || !strings.Contains(err.Error(), "no system message") {
|
||||||
t.Errorf("expected no-system error, got %v", err)
|
t.Errorf("expected no-system error, got %v", err)
|
||||||
}
|
}
|
||||||
if msgs != nil {
|
if msgs != nil {
|
||||||
t.Errorf("expected original messages on error")
|
t.Errorf("expected original messages on error")
|
||||||
}
|
}
|
||||||
msgs2, _, err2 := compact(&Cfg{}, []Message{{Role: "user", Content: strp("x")}})
|
msgs2, _, err2 := compact(context.Background(), &Cfg{}, []Message{{Role: "user", Content: strp("x")}})
|
||||||
if err2 == nil {
|
if err2 == nil {
|
||||||
t.Errorf("expected error when first message is not system")
|
t.Errorf("expected error when first message is not system")
|
||||||
}
|
}
|
||||||
@@ -930,7 +943,7 @@ func TestLLMNonStreamingAndHeaders(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "secret"
|
cfg.APIKey = "secret"
|
||||||
m, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, TOOLS)
|
m, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, TOOLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("llm: %v", err)
|
t.Fatalf("llm: %v", err)
|
||||||
}
|
}
|
||||||
@@ -960,7 +973,7 @@ func TestLLMNoAuthHeaderWhenNoKey(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
if _, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err != nil {
|
if _, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err != nil {
|
||||||
t.Fatalf("llm: %v", err)
|
t.Fatalf("llm: %v", err)
|
||||||
}
|
}
|
||||||
if gotAuth != "" {
|
if gotAuth != "" {
|
||||||
@@ -981,7 +994,7 @@ func TestLLMStreaming(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = true
|
cfg.Stream = true
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
m, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
m, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("llm: %v", err)
|
t.Fatalf("llm: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1005,7 +1018,7 @@ func TestLLM4xxReturnsImmediately(t *testing.T) {
|
|||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
_, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
_, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||||
if err == nil || !strings.Contains(err.Error(), "Invalid assistant message") {
|
if err == nil || !strings.Contains(err.Error(), "Invalid assistant message") {
|
||||||
t.Fatalf("expected 400 error, got %v", err)
|
t.Fatalf("expected 400 error, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -1031,7 +1044,7 @@ func TestLLMRetriesOn5xxThenSucceeds(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
m, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
m, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("llm after retries: %v", err)
|
t.Fatalf("llm after retries: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1053,7 +1066,7 @@ func TestLLMEmptyChoices(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
if _, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err == nil || !strings.Contains(err.Error(), "empty choices") {
|
if _, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err == nil || !strings.Contains(err.Error(), "empty choices") {
|
||||||
t.Errorf("expected empty choices error, got %v", err)
|
t.Errorf("expected empty choices error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1080,7 +1093,7 @@ func TestALToolLoop(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
msgs, err := AL(&cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("run")}}, "sys", 0)
|
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("run")}}, "sys", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("AL: %v", err)
|
t.Fatalf("AL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1142,7 +1155,7 @@ func TestALRunSubagent(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
msgs, err := AL(&cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("parent task")}}, "sys", 0)
|
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("parent task")}}, "sys", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("AL: %v", err)
|
t.Fatalf("AL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1198,7 @@ func TestALSubagentDepthLimit(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
msgs, err := AL(&cfg, []Message{{Role: "user", Content: strp("go")}}, "sys", MAX_DEPTH)
|
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "user", Content: strp("go")}}, "sys", MAX_DEPTH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("AL: %v", err)
|
t.Fatalf("AL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1232,7 +1245,7 @@ func TestALStripsInvalidAssistantAndRetries(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
msgs, err := AL(&cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("go")}}, "sys", 0)
|
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("go")}}, "sys", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("AL: %v", err)
|
t.Fatalf("AL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1256,7 +1269,7 @@ func TestSummarizeHappyPath(t *testing.T) {
|
|||||||
cfg.Endpoint = srv.URL
|
cfg.Endpoint = srv.URL
|
||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
s, err := summarize(&cfg, []Message{{Role: "user", Content: strp("hello world")}})
|
s, err := summarize(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hello world")}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("summarize: %v", err)
|
t.Fatalf("summarize: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1276,7 +1289,7 @@ func TestCompactHappyPath(t *testing.T) {
|
|||||||
cfg.Stream = false
|
cfg.Stream = false
|
||||||
cfg.APIKey = "-"
|
cfg.APIKey = "-"
|
||||||
orig := []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("hello world")}}
|
orig := []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("hello world")}}
|
||||||
msgs, sm, err := compact(&cfg, orig)
|
msgs, sm, err := compact(context.Background(), &cfg, orig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("compact: %v", err)
|
t.Fatalf("compact: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1339,7 +1352,7 @@ func TestLLMForwardsRelevantParameters(t *testing.T) {
|
|||||||
}, "\n"))
|
}, "\n"))
|
||||||
|
|
||||||
cfg := getCfg(cfgFile)
|
cfg := getCfg(cfgFile)
|
||||||
_, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
_, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("llm: %v", err)
|
t.Fatalf("llm: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1591,7 +1604,7 @@ func TestRenderMDNoColorFallback(t *testing.T) {
|
|||||||
|
|
||||||
func TestDirectShellExecution(t *testing.T) {
|
func TestDirectShellExecution(t *testing.T) {
|
||||||
cmd := "echo direct_exec_test"
|
cmd := "echo direct_exec_test"
|
||||||
res := shell(cmd, 10)
|
res := shell(context.Background(), cmd, 10)
|
||||||
if !strings.HasPrefix(res, "direct_exec_test") || !strings.Contains(res, "exit: 0") {
|
if !strings.HasPrefix(res, "direct_exec_test") || !strings.Contains(res, "exit: 0") {
|
||||||
t.Errorf("direct shell exec failed, got %q", res)
|
t.Errorf("direct shell exec failed, got %q", res)
|
||||||
}
|
}
|
||||||
@@ -1604,6 +1617,80 @@ func TestDirectShellExecution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestALContextCancellation(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer func() {
|
||||||
|
close(done)
|
||||||
|
srv.CloseClientConnections()
|
||||||
|
srv.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg := defCfg
|
||||||
|
cfg.Endpoint = srv.URL
|
||||||
|
cfg.Stream = false
|
||||||
|
cfg.APIKey = "-"
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
origMsgs := []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("hello")}}
|
||||||
|
inputMsgs := append([]Message{}, origMsgs...)
|
||||||
|
msgs, err := AL(ctx, &cfg, inputMsgs, "sys", 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected context cancellation error, got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
|
||||||
|
t.Errorf("expected context.Canceled, got %v", err)
|
||||||
|
}
|
||||||
|
// Verify that input messages slice was not mutated with partial assistant messages
|
||||||
|
if len(msgs) != len(origMsgs) {
|
||||||
|
t.Errorf("expected %d messages after cancellation, got %d: %+v", len(origMsgs), len(msgs), msgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLLMContextCancellation(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer func() {
|
||||||
|
close(done)
|
||||||
|
srv.CloseClientConnections()
|
||||||
|
srv.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg := defCfg
|
||||||
|
cfg.Endpoint = srv.URL
|
||||||
|
cfg.Stream = false
|
||||||
|
cfg.APIKey = "-"
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err := llm(ctx, &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected context cancellation error, got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
|
||||||
|
t.Errorf("expected context.Canceled, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user