82 lines
2.5 KiB
Go
82 lines
2.5 KiB
Go
package loop
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// buildRequest assembles a model.Request from the agent's public surface,
|
|
// mirroring agent.buildRequest so the streaming path behaves identically to
|
|
// RunStep (leading instruction message, tool schemas, model routing).
|
|
func (l *Loop) buildRequest(messages []model.Message) model.Request {
|
|
req := model.Request{
|
|
Model: l.agent.Model(),
|
|
Messages: l.assembleMessages(messages),
|
|
Temperature: l.temperature(),
|
|
MaxTokens: l.agent.MaxTokens(),
|
|
Purpose: l.agent.Purpose(),
|
|
Tools: l.toolSchemas(),
|
|
}
|
|
if l.agent.Router() != nil {
|
|
if m, _ := l.agent.Router().RouteRequest(req); m != "" {
|
|
req.Model = m
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
|
|
// assembleMessages prepends the agent's instructions as a leading system message
|
|
// when they are non-empty, otherwise returns the input slice unchanged.
|
|
func (l *Loop) assembleMessages(messages []model.Message) []model.Message {
|
|
instr := l.agent.Instructions()
|
|
if instr == "" {
|
|
return messages
|
|
}
|
|
out := make([]model.Message, 0, len(messages)+1)
|
|
out = append(out, model.TextMessage(model.RoleSystem, instr))
|
|
out = append(out, messages...)
|
|
return out
|
|
}
|
|
|
|
// temperature returns a pointer to the configured temperature, mapping zero to
|
|
// nil so the provider default applies (matching the agent package).
|
|
func (l *Loop) temperature() *float64 {
|
|
t := l.agent.Temperature()
|
|
if t == 0 {
|
|
return nil
|
|
}
|
|
return &t
|
|
}
|
|
|
|
// toolSchemas advertises the registered tool schemas to the model, or nil when no
|
|
// dispatcher is configured.
|
|
func (l *Loop) toolSchemas() []model.ToolSpec {
|
|
d := l.dispatcher()
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
return d.ToolSchemas()
|
|
}
|
|
|
|
// decodeToolCalls decodes tool calls from a model.Response using the same two
|
|
// conventions as the agent package: the normalized RoleTool placeholder, then a
|
|
// JSON envelope of the form {"tool_calls":[...]} in Content. It reports ok=true
|
|
// (with a non-empty slice) when a call is decoded.
|
|
func decodeToolCalls(resp model.Response) ([]agent.ToolCall, bool) {
|
|
if resp.Message.Role == model.RoleAssistant && resp.Message.ToolCallName != "" {
|
|
return []agent.ToolCall{{Name: resp.Message.ToolCallName, Args: map[string]any{}}}, true
|
|
}
|
|
var payload struct {
|
|
ToolCalls []agent.ToolCall `json:"tool_calls"`
|
|
}
|
|
if err := json.Unmarshal([]byte(resp.Content), &payload); err != nil {
|
|
return nil, false
|
|
}
|
|
if len(payload.ToolCalls) == 0 {
|
|
return nil, false
|
|
}
|
|
return payload.ToolCalls, true
|
|
}
|