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
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,

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)
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
}
}
logger.InfoCF("agent", "Routed message",
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) {
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
}

View file

@ -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)
}
}