agent: honor cli sessions and use configured llm options

This commit is contained in:
Hussain 2026-02-19 04:38:11 +08:00
parent 089d1299f5
commit 6466fe011b
3 changed files with 164 additions and 8 deletions

View file

@ -22,6 +22,7 @@ type AgentInstance struct {
Workspace string Workspace string
MaxIterations int MaxIterations int
ContextWindow int ContextWindow int
Temperature float64
Provider providers.LLMProvider Provider providers.LLMProvider
Sessions *session.SessionManager Sessions *session.SessionManager
ContextBuilder *ContextBuilder ContextBuilder *ContextBuilder
@ -91,6 +92,7 @@ func NewAgentInstance(
Workspace: workspace, Workspace: workspace,
MaxIterations: maxIter, MaxIterations: maxIter,
ContextWindow: defaults.MaxTokens, ContextWindow: defaults.MaxTokens,
Temperature: defaults.Temperature,
Provider: provider, Provider: provider,
Sessions: sessionsManager, Sessions: sessionsManager,
ContextBuilder: contextBuilder, ContextBuilder: contextBuilder,

View file

@ -286,9 +286,13 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
// Use routed session key, but honor pre-set agent-scoped keys (for ProcessDirect/cron) // Use routed session key, but honor pre-set agent-scoped keys (for ProcessDirect/cron)
sessionKey := route.SessionKey sessionKey := route.SessionKey
if msg.SessionKey != "" && strings.HasPrefix(msg.SessionKey, "agent:") { 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 sessionKey = msg.SessionKey
} }
}
logger.InfoCF("agent", "Routed message", logger.InfoCF("agent", "Routed message",
map[string]interface{}{ map[string]interface{}{
@ -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) { func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) {
iteration := 0 iteration := 0
var finalContent string var finalContent string
llmMaxTokens := agent.ContextWindow
if llmMaxTokens <= 0 {
llmMaxTokens = 8192
}
llmTemperature := agent.Temperature
for iteration < agent.MaxIterations { for iteration < agent.MaxIterations {
iteration++ iteration++
@ -470,8 +479,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
"model": agent.Model, "model": agent.Model,
"messages_count": len(messages), "messages_count": len(messages),
"tools_count": len(providerToolDefs), "tools_count": len(providerToolDefs),
"max_tokens": 8192, "max_tokens": llmMaxTokens,
"temperature": 0.7, "temperature": llmTemperature,
"system_prompt_len": len(messages[0].Content), "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, fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) { func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{ return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
"max_tokens": 8192, "max_tokens": llmMaxTokens,
"temperature": 0.7, "temperature": llmTemperature,
}) })
}, },
) )
@ -508,8 +517,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
return fbResult.Response, nil return fbResult.Response, nil
} }
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{ return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
"max_tokens": 8192, "max_tokens": llmMaxTokens,
"temperature": 0.7, "temperature": llmTemperature,
}) })
} }
@ -526,6 +535,10 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
strings.Contains(errMsg, "context") || strings.Contains(errMsg, "context") ||
strings.Contains(errMsg, "invalidparameter") || strings.Contains(errMsg, "invalidparameter") ||
strings.Contains(errMsg, "length") 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 { if isContextError && retry < maxRetries {
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]interface{}{ 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 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 break
} }

View file

@ -535,10 +535,12 @@ type failFirstMockProvider struct {
currentCall int currentCall int
failError error failError error
successResp string 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) { 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.currentCall++
m.lastOpts = opts
if m.currentCall <= m.failures { if m.currentCall <= m.failures {
return nil, m.failError 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)) 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)
}
}