agent: honor cli sessions and use configured llm options
This commit is contained in:
parent
089d1299f5
commit
6466fe011b
3 changed files with 164 additions and 8 deletions
|
|
@ -22,6 +22,7 @@ type AgentInstance struct {
|
|||
Workspace string
|
||||
MaxIterations int
|
||||
ContextWindow int
|
||||
Temperature float64
|
||||
Provider providers.LLMProvider
|
||||
Sessions *session.SessionManager
|
||||
ContextBuilder *ContextBuilder
|
||||
|
|
@ -91,6 +92,7 @@ func NewAgentInstance(
|
|||
Workspace: workspace,
|
||||
MaxIterations: maxIter,
|
||||
ContextWindow: defaults.MaxTokens,
|
||||
Temperature: defaults.Temperature,
|
||||
Provider: provider,
|
||||
Sessions: sessionsManager,
|
||||
ContextBuilder: contextBuilder,
|
||||
|
|
|
|||
|
|
@ -286,8 +286,12 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
|||
|
||||
// Use routed session key, but honor pre-set agent-scoped keys (for ProcessDirect/cron)
|
||||
sessionKey := route.SessionKey
|
||||
if msg.SessionKey != "" && strings.HasPrefix(msg.SessionKey, "agent:") {
|
||||
sessionKey = msg.SessionKey
|
||||
if msg.SessionKey != "" {
|
||||
// Direct CLI calls should honor explicit session keys.
|
||||
// Agent-scoped keys are always honored for other channels.
|
||||
if strings.HasPrefix(msg.SessionKey, "agent:") || msg.Channel == "cli" {
|
||||
sessionKey = msg.SessionKey
|
||||
}
|
||||
}
|
||||
|
||||
logger.InfoCF("agent", "Routed message",
|
||||
|
|
@ -448,6 +452,11 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) {
|
||||
iteration := 0
|
||||
var finalContent string
|
||||
llmMaxTokens := agent.ContextWindow
|
||||
if llmMaxTokens <= 0 {
|
||||
llmMaxTokens = 8192
|
||||
}
|
||||
llmTemperature := agent.Temperature
|
||||
|
||||
for iteration < agent.MaxIterations {
|
||||
iteration++
|
||||
|
|
@ -470,8 +479,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
"model": agent.Model,
|
||||
"messages_count": len(messages),
|
||||
"tools_count": len(providerToolDefs),
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": llmMaxTokens,
|
||||
"temperature": llmTemperature,
|
||||
"system_prompt_len": len(messages[0].Content),
|
||||
})
|
||||
|
||||
|
|
@ -492,8 +501,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": llmMaxTokens,
|
||||
"temperature": llmTemperature,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
@ -508,8 +517,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
return fbResult.Response, nil
|
||||
}
|
||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": llmMaxTokens,
|
||||
"temperature": llmTemperature,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -526,6 +535,10 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
strings.Contains(errMsg, "context") ||
|
||||
strings.Contains(errMsg, "invalidparameter") ||
|
||||
strings.Contains(errMsg, "length")
|
||||
isTransientNetworkError := strings.Contains(errMsg, "eof") ||
|
||||
strings.Contains(errMsg, "connection reset") ||
|
||||
strings.Contains(errMsg, "broken pipe") ||
|
||||
strings.Contains(errMsg, "server closed idle connection")
|
||||
|
||||
if isContextError && retry < maxRetries {
|
||||
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]interface{}{
|
||||
|
|
@ -550,6 +563,17 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if isTransientNetworkError && retry < maxRetries {
|
||||
backoff := time.Duration(retry+1) * 2 * time.Second
|
||||
logger.WarnCF("agent", "Transient network error, retrying LLM call", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"retry": retry,
|
||||
"backoff_sec": backoff.Seconds(),
|
||||
})
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -535,10 +535,12 @@ type failFirstMockProvider struct {
|
|||
currentCall int
|
||||
failError error
|
||||
successResp string
|
||||
lastOpts map[string]interface{}
|
||||
}
|
||||
|
||||
func (m *failFirstMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
m.currentCall++
|
||||
m.lastOpts = opts
|
||||
if m.currentCall <= m.failures {
|
||||
return nil, m.failError
|
||||
}
|
||||
|
|
@ -628,3 +630,131 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
|
|||
t.Errorf("Expected history to be compressed (len < 8), got %d", len(finalHistory))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentLoop_TransientEOFRetry verifies retry on transient network EOF errors.
|
||||
func TestAgentLoop_TransientEOFRetry(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 2048,
|
||||
Temperature: 0.2,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &failFirstMockProvider{
|
||||
failures: 1,
|
||||
failError: fmt.Errorf("failed to send request: EOF"),
|
||||
successResp: "Recovered from transient error",
|
||||
}
|
||||
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
response, err := al.ProcessDirectWithChannel(context.Background(), "Ping", "eof-retry-session", "test", "chat")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected success after transient retry, got error: %v", err)
|
||||
}
|
||||
if response != "Recovered from transient error" {
|
||||
t.Errorf("Expected 'Recovered from transient error', got '%s'", response)
|
||||
}
|
||||
if provider.currentCall != 2 {
|
||||
t.Errorf("Expected 2 calls (1 fail + 1 success), got %d", provider.currentCall)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProcessDirectWithChannel_HonorsCLISessionKey verifies explicit CLI session keys are used.
|
||||
func TestProcessDirectWithChannel_HonorsCLISessionKey(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 1024,
|
||||
MaxToolIterations: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &mockProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
sessionKey := "my-cli-session"
|
||||
_, err = al.ProcessDirectWithChannel(context.Background(), "hello", sessionKey, "cli", "direct")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
||||
defaultAgent := al.registry.GetDefaultAgent()
|
||||
if defaultAgent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
history := defaultAgent.Sessions.GetHistory(sessionKey)
|
||||
if len(history) == 0 {
|
||||
t.Fatalf("Expected history under explicit CLI session key %q, got none", sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentLoop_UsesConfiguredLLMOptions verifies max_tokens and temperature are sourced from config.
|
||||
func TestAgentLoop_UsesConfiguredLLMOptions(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 1536,
|
||||
Temperature: 0.25,
|
||||
MaxToolIterations: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &failFirstMockProvider{
|
||||
successResp: "ok",
|
||||
}
|
||||
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
_, err = al.ProcessDirectWithChannel(context.Background(), "hello", "opts-session", "cli", "direct")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
||||
gotMax, ok := provider.lastOpts["max_tokens"].(int)
|
||||
if !ok {
|
||||
t.Fatalf("Expected max_tokens int option, got %#v", provider.lastOpts["max_tokens"])
|
||||
}
|
||||
if gotMax != 1536 {
|
||||
t.Fatalf("max_tokens = %d, want 1536", gotMax)
|
||||
}
|
||||
|
||||
gotTemp, ok := provider.lastOpts["temperature"].(float64)
|
||||
if !ok {
|
||||
t.Fatalf("Expected temperature float64 option, got %#v", provider.lastOpts["temperature"])
|
||||
}
|
||||
if gotTemp != 0.25 {
|
||||
t.Fatalf("temperature = %v, want 0.25", gotTemp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue