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:
+89
-13
@@ -153,20 +153,51 @@ type TurnResult struct {
|
||||
StopReason string
|
||||
}
|
||||
|
||||
// turn runs the bounded LLM loop: request, execute tool calls, repeat until
|
||||
// the model replies with plain content or the round bound is hit.
|
||||
// turn builds the single-task message shape and runs the shared bounded
|
||||
// turn (the `once`/`loop` path: system prompt + task prompt, tools on).
|
||||
func (c *Conductor) turn(ctx context.Context, client *llm.Client, t task.Task, model string) (*TurnResult, error) {
|
||||
msgs := []llm.Message{
|
||||
{Role: "system", Content: c.systemPrompt()},
|
||||
{Role: "user", Content: t.Prompt},
|
||||
}
|
||||
var palette []llm.Tool
|
||||
if c.bash != nil {
|
||||
palette = append(palette, c.bash.Definition())
|
||||
return c.runTurn(ctx, client, turnParams{
|
||||
msgs: []llm.Message{
|
||||
{Role: "system", Content: c.systemPrompt()},
|
||||
{Role: "user", Content: t.Prompt},
|
||||
},
|
||||
model: model,
|
||||
palette: c.toolPalette(),
|
||||
})
|
||||
}
|
||||
|
||||
// turnParams is one bounded-turn invocation, shared by the task path
|
||||
// (`once`/`loop`) and the serve front door.
|
||||
type turnParams struct {
|
||||
msgs []llm.Message
|
||||
model string
|
||||
palette []llm.Tool // tools offered to the model; nil = tools OFF
|
||||
maxTokens int
|
||||
temperature *float64
|
||||
}
|
||||
|
||||
func (c *Conductor) toolPalette() []llm.Tool {
|
||||
if c.bash == nil {
|
||||
return nil
|
||||
}
|
||||
return []llm.Tool{c.bash.Definition()}
|
||||
}
|
||||
|
||||
// runTurn is the bounded LLM loop: request, execute tool calls, repeat until
|
||||
// the model replies with plain content or the round bound is hit. With a nil
|
||||
// palette the turn is pure chat: a tool call the model produces anyway is
|
||||
// refused with a tool result, never executed.
|
||||
func (c *Conductor) runTurn(ctx context.Context, client *llm.Client, p turnParams) (*TurnResult, error) {
|
||||
msgs := p.msgs
|
||||
res := &TurnResult{}
|
||||
for round := 1; round <= c.cfg.Loop.MaxRounds; round++ {
|
||||
resp, err := client.Chat(ctx, llm.ChatRequest{Model: model, Messages: msgs, Tools: palette})
|
||||
resp, err := client.Chat(ctx, llm.ChatRequest{
|
||||
Model: p.model,
|
||||
Messages: msgs,
|
||||
Tools: p.palette,
|
||||
MaxTokens: p.maxTokens,
|
||||
Temperature: p.temperature,
|
||||
})
|
||||
if err != nil {
|
||||
res.StopReason = "llm_error"
|
||||
return res, err
|
||||
@@ -186,11 +217,18 @@ func (c *Conductor) turn(ctx context.Context, client *llm.Client, t task.Task, m
|
||||
}
|
||||
msgs = append(msgs, m)
|
||||
for _, tc := range m.ToolCalls {
|
||||
out, derr := c.execTool(ctx, tc)
|
||||
res.ToolCalls++
|
||||
if derr != nil {
|
||||
var out string
|
||||
if len(p.palette) == 0 {
|
||||
out = "error: tools are disabled on this endpoint"
|
||||
res.Denied++
|
||||
} else {
|
||||
var derr error
|
||||
out, derr = c.execTool(ctx, tc)
|
||||
if derr != nil {
|
||||
res.Denied++
|
||||
}
|
||||
}
|
||||
res.ToolCalls++
|
||||
msgs = append(msgs, llm.Message{Role: "tool", ToolCallID: tc.ID, Content: out})
|
||||
}
|
||||
}
|
||||
@@ -198,6 +236,34 @@ func (c *Conductor) turn(ctx context.Context, client *llm.Client, t task.Task, m
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ServeTurnOpts carries the OpenAI-compatible request knobs the serve front
|
||||
// door forwards to the upstream model.
|
||||
type ServeTurnOpts struct {
|
||||
MaxTokens int
|
||||
Temperature *float64
|
||||
}
|
||||
|
||||
// ServeTurn runs one bounded conductor turn over a pre-assembled conversation
|
||||
// history — the `harness serve` path (stateless: the client sends the whole
|
||||
// conversation each call, tools are OFF). If the history carries no leading
|
||||
// system message, a minimal one identifying the vertical is prepended; the
|
||||
// client's own system message always wins.
|
||||
func (c *Conductor) ServeTurn(ctx context.Context, msgs []llm.Message, model string, opts ServeTurnOpts) (*TurnResult, error) {
|
||||
if len(msgs) == 0 || msgs[0].Role != "system" {
|
||||
msgs = append([]llm.Message{{Role: "system", Content: c.serveSystemPrompt()}}, msgs...)
|
||||
}
|
||||
client, err := c.llmClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.runTurn(ctx, client, turnParams{
|
||||
msgs: msgs,
|
||||
model: model,
|
||||
maxTokens: opts.MaxTokens,
|
||||
temperature: opts.Temperature,
|
||||
})
|
||||
}
|
||||
|
||||
// execTool dispatches one tool call; the returned string is the model-facing
|
||||
// result. The returned error is non-nil only for gate denials (counted).
|
||||
func (c *Conductor) execTool(ctx context.Context, call llm.ToolCall) (string, error) {
|
||||
@@ -315,6 +381,16 @@ match the configured allow-list (unmatched or denied commands fail).`,
|
||||
c.cfg.Vertical, c.cfg.WorkRoot)
|
||||
}
|
||||
|
||||
func (c *Conductor) serveSystemPrompt() string {
|
||||
return fmt.Sprintf(`You are %s, the interactive front door of the MOPAC harness (served to
|
||||
OpenWebUI, stateless turns, tools off). Answer directly and completely; your
|
||||
reply is returned verbatim to the user.`, c.cfg.Vertical)
|
||||
}
|
||||
|
||||
// Router exposes the model router so `harness serve` can build its servable
|
||||
// model catalog from the same class -> tier -> model mapping `once` uses.
|
||||
func (c *Conductor) Router() *models.Router { return c.router }
|
||||
|
||||
// DispatchEvent is the events -> conductor wiring point: the `harness
|
||||
// events` receiver hands every stored, actionable webhook to the conductor
|
||||
// here. Phase 2b stub: it records what a real dispatch would do; the
|
||||
|
||||
Reference in New Issue
Block a user