loop: shared bounded-turn core accepts history + knobs (ServeTurn)

Turn machinery refactored so the upcoming serve front door reuses it
instead of copying it: the round loop is now runTurn over arbitrary
message history with an optional tool palette and forwarded max_tokens/
temperature. The serve path (ServeTurn) is stateless chat over client
history with tools hard-off: a tool call the model produces anyway is
refused as a tool result, never executed. Also exposes the model router
(so serve can build its catalog) and a sorted class list; llm requests
can now carry temperature.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-29 01:07:47 -05:00
parent 2901bb8cae
commit 8614827d44
5 changed files with 242 additions and 13 deletions
+129
View File
@@ -317,3 +317,132 @@ func TestOnceUnknownClassIsRoutingError(t *testing.T) {
t.Fatalf("err = %v, want unknown-class error", err)
}
}
func TestServeTurnPrependsSystemPromptWhenMissing(t *testing.T) {
f := newFakeLLM(t, textMsg("front door reply"))
cfg := testConfig(t, f.srv.URL)
cond, err := New(cfg, os.Stdout)
if err != nil {
t.Fatal(err)
}
turn, err := cond.ServeTurn(context.Background(),
[]llm.Message{{Role: "user", Content: "hello there"}}, "glm-5.3", ServeTurnOpts{})
if err != nil {
t.Fatalf("ServeTurn: %v", err)
}
if turn.Content != "front door reply" || turn.StopReason != "complete" {
t.Errorf("turn = %+v", turn)
}
req := f.request(0)
if req.Model != "glm-5.3" {
t.Errorf("request model = %q, want glm-5.3", req.Model)
}
if len(req.Messages) != 2 || req.Messages[0].Role != "system" {
t.Fatalf("messages = %+v, want [system, user]", req.Messages)
}
if !strings.Contains(req.Messages[0].Content, "teststack") || !strings.Contains(req.Messages[0].Content, "OpenWebUI") {
t.Errorf("prepended system prompt = %q, want vertical identity", req.Messages[0].Content)
}
if req.Messages[1].Role != "user" || req.Messages[1].Content != "hello there" {
t.Errorf("user message = %+v", req.Messages[1])
}
}
func TestServeTurnPreservesClientSystemMessage(t *testing.T) {
f := newFakeLLM(t, textMsg("aye"))
cfg := testConfig(t, f.srv.URL)
cond, err := New(cfg, os.Stdout)
if err != nil {
t.Fatal(err)
}
history := []llm.Message{
{Role: "system", Content: "You are a pirate."},
{Role: "user", Content: "greet me"},
{Role: "assistant", Content: "ahoy"},
{Role: "user", Content: "again"},
}
if _, err := cond.ServeTurn(context.Background(), history, "glm-5.3", ServeTurnOpts{}); err != nil {
t.Fatalf("ServeTurn: %v", err)
}
got := f.request(0).Messages
if len(got) != len(history) {
t.Fatalf("history length = %d, want %d (client system message wins, no prepend)", len(got), len(history))
}
for i, m := range history {
if got[i].Role != m.Role || got[i].Content != m.Content {
t.Errorf("message[%d] = %+v, want %+v", i, got[i], m)
}
}
}
func TestServeTurnToolsOffNeverExecutes(t *testing.T) {
// pwd IS on the allow list: if the palette leaked into the serve path
// the command would run and its output would come back as the tool
// result. Tools are off, so the model must instead see the disabled
// refusal and the turn must count a denial.
f := newFakeLLM(t,
toolCallMsg("call-1", "bash", `{"command":"pwd"}`),
textMsg("No tools then, here is the answer."),
)
cfg := testConfig(t, f.srv.URL)
cond, err := New(cfg, os.Stdout)
if err != nil {
t.Fatal(err)
}
turn, err := cond.ServeTurn(context.Background(),
[]llm.Message{{Role: "user", Content: "run pwd"}}, "glm-5.3", ServeTurnOpts{})
if err != nil {
t.Fatalf("ServeTurn: %v", err)
}
if f.requestCount() != 2 {
t.Fatalf("LLM calls = %d, want 2 (refused tool call fed back)", f.requestCount())
}
if turn.ToolCalls != 1 || turn.Denied != 1 {
t.Errorf("turn = %+v, want 1 refused tool call", turn)
}
toolMsg := f.request(1).Messages[len(f.request(1).Messages)-1]
if toolMsg.Role != "tool" || !strings.Contains(toolMsg.Content, "tools are disabled") {
t.Errorf("tool result = %+v, want the disabled refusal", toolMsg)
}
if turn.Content != "No tools then, here is the answer." {
t.Errorf("turn content = %q", turn.Content)
}
}
func TestServeTurnForwardsKnobs(t *testing.T) {
f := newFakeLLM(t, textMsg("ok"))
cfg := testConfig(t, f.srv.URL)
cond, err := New(cfg, os.Stdout)
if err != nil {
t.Fatal(err)
}
temp := 0.7
if _, err := cond.ServeTurn(context.Background(),
[]llm.Message{{Role: "user", Content: "hi"}}, "glm-5.3",
ServeTurnOpts{MaxTokens: 512, Temperature: &temp}); err != nil {
t.Fatalf("ServeTurn: %v", err)
}
req := f.request(0)
if req.MaxTokens != 512 {
t.Errorf("max_tokens = %d, want 512 forwarded", req.MaxTokens)
}
if req.Temperature == nil || *req.Temperature != 0.7 {
t.Errorf("temperature = %v, want 0.7 forwarded", req.Temperature)
}
if len(req.Tools) != 0 {
t.Errorf("tools = %+v, want none on the serve path", req.Tools)
}
}
func TestRouterAccessor(t *testing.T) {
cfg := testConfig(t, "http://unused")
cond, err := New(cfg, os.Stdout)
if err != nil {
t.Fatal(err)
}
classes := cond.Router().Classes()
want := []string{"code", "primary", "study"}
if fmt.Sprint(classes) != fmt.Sprint(want) {
t.Errorf("Router().Classes() = %v, want %v", classes, want)
}
}