From 184d02542400e1642abe2d840daa08f93cc120cc Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:44:12 +0000 Subject: [PATCH] feat(a2a): Remove task goes idle feature This removes the confusing "task goes idle" functionality where agents would stop polling for task results after a timeout or when reaching max poll intervals. Key changes: - Removed `idle_timeout_sec` configuration option from A2ATaskConfig - Removed `immediate_idle` polling strategy support - Removed `WentIdle` and `IdleTimeout` fields from A2ASubmitTaskResult - Removed `handleImmediateIdle()` and `checkIdleConditions()` methods - Updated UI to remove idle status handling - Updated documentation and default configs Tasks will now continue polling indefinitely until they reach a terminal state (completed, failed, or input_required), ensuring agents don't stop listening for results while tasks are still running. Fixes #197 Co-authored-by: Eden Reich --- .infer/config.yaml | 1 - CONFIG.md | 1 - cmd/root.go | 1 - config/config.go | 2 - internal/services/tools/a2a_task.go | 110 ++---------------- .../tools/a2a_task_error_handling_test.go | 2 - internal/ui/components/tool_call_renderer.go | 3 - 7 files changed, 11 insertions(+), 109 deletions(-) diff --git a/.infer/config.yaml b/.infer/config.yaml index 2e68489a..a33f652b 100644 --- a/.infer/config.yaml +++ b/.infer/config.yaml @@ -243,7 +243,6 @@ a2a: ttl: 300 task: status_poll_seconds: 5 - idle_timeout_sec: 60 polling_strategy: exponential initial_poll_interval_sec: 2 max_poll_interval_sec: 60 diff --git a/CONFIG.md b/CONFIG.md index e48c99c5..0bf48704 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -159,7 +159,6 @@ a2a: # Task monitoring configuration task: status_poll_seconds: 5 - idle_timeout_sec: 60 polling_strategy: "exponential" initial_poll_interval_sec: 2 max_poll_interval_sec: 60 diff --git a/cmd/root.go b/cmd/root.go index a5ea0aef..098f63d3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -68,7 +68,6 @@ func initConfig() { v.SetDefault("a2a.cache.enabled", defaults.A2A.Cache.Enabled) v.SetDefault("a2a.cache.ttl", defaults.A2A.Cache.TTL) v.SetDefault("a2a.task.status_poll_seconds", defaults.A2A.Task.StatusPollSeconds) - v.SetDefault("a2a.task.idle_timeout_sec", defaults.A2A.Task.IdleTimeoutSec) v.SetDefault("a2a.task.polling_strategy", defaults.A2A.Task.PollingStrategy) v.SetDefault("a2a.task.initial_poll_interval_sec", defaults.A2A.Task.InitialPollIntervalSec) v.SetDefault("a2a.task.max_poll_interval_sec", defaults.A2A.Task.MaxPollIntervalSec) diff --git a/config/config.go b/config/config.go index e53f08de..a29b5059 100644 --- a/config/config.go +++ b/config/config.go @@ -360,7 +360,6 @@ type A2AAgentInfo struct { // A2ATaskConfig contains configuration for A2A task processing type A2ATaskConfig struct { StatusPollSeconds int `yaml:"status_poll_seconds" mapstructure:"status_poll_seconds"` - IdleTimeoutSec int `yaml:"idle_timeout_sec" mapstructure:"idle_timeout_sec"` PollingStrategy string `yaml:"polling_strategy" mapstructure:"polling_strategy"` InitialPollIntervalSec int `yaml:"initial_poll_interval_sec" mapstructure:"initial_poll_interval_sec"` MaxPollIntervalSec int `yaml:"max_poll_interval_sec" mapstructure:"max_poll_interval_sec"` @@ -643,7 +642,6 @@ Respond with ONLY the title, no quotes or explanation.`, }, Task: A2ATaskConfig{ StatusPollSeconds: 5, - IdleTimeoutSec: 60, PollingStrategy: "exponential", InitialPollIntervalSec: 2, MaxPollIntervalSec: 60, diff --git a/internal/services/tools/a2a_task.go b/internal/services/tools/a2a_task.go index f586e708..8f55a6d1 100644 --- a/internal/services/tools/a2a_task.go +++ b/internal/services/tools/a2a_task.go @@ -24,16 +24,14 @@ type A2ASubmitTaskTool struct { // A2ASubmitTaskResult represents the result of an A2A task operation type A2ASubmitTaskResult struct { - TaskID string `json:"task_id"` - ContextID string `json:"context_id,omitempty"` - AgentURL string `json:"agent_url"` - State string `json:"state"` - Message string `json:"message"` - TaskResult string `json:"task_result,omitempty"` - Success bool `json:"success"` - WentIdle bool `json:"went_idle,omitempty"` - IdleTimeout time.Duration `json:"idle_timeout,omitempty"` - Task *adk.Task `json:"task,omitempty"` + TaskID string `json:"task_id"` + ContextID string `json:"context_id,omitempty"` + AgentURL string `json:"agent_url"` + State string `json:"state"` + Message string `json:"message"` + TaskResult string `json:"task_result,omitempty"` + Success bool `json:"success"` + Task *adk.Task `json:"task,omitempty"` } // NewA2ASubmitTaskTool creates a new A2A task tool @@ -291,13 +289,8 @@ func (t *A2ASubmitTaskTool) pollTaskInBackground( adkClient := t.getOrCreateClient(agentURL) strategy := t.config.A2A.Task.PollingStrategy - if strategy == "immediate_idle" { - t.handleImmediateIdle(agentURL, taskID, state) - t.stopPolling(taskID) - return - } - currentInterval, deadline := t.initializePollingStrategy(agentURL, taskID, strategy) + currentInterval := t.initializePollingStrategy(agentURL, taskID, strategy) state.CurrentInterval = currentInterval state.NextPollTime = time.Now().Add(currentInterval) @@ -321,16 +314,6 @@ func (t *A2ASubmitTaskTool) pollTaskInBackground( pollingDetails.WriteString(fmt.Sprintf("Poll #%d: interval=%v, elapsed=%v\n", pollAttempt, currentInterval, time.Since(state.StartedAt))) - shouldStop, stopResult := t.checkIdleConditions(agentURL, taskID, strategy, currentInterval, deadline, pollAttempt, state, pollingDetails.String()) - if shouldStop { - if stopResult != nil && state.ResultChan != nil { - state.ResultChan <- stopResult - time.Sleep(100 * time.Millisecond) - } - t.stopPolling(taskID) - return - } - state.LastPollAt = time.Now() currentTask, err := t.queryTask(ctx, adkClient, taskID) @@ -378,28 +361,7 @@ func (t *A2ASubmitTaskTool) getOrCreateClient(agentURL string) client.A2AClient return client.NewClient(agentURL) } -func (t *A2ASubmitTaskTool) handleImmediateIdle(agentURL, taskID string, state *domain.TaskPollingState) { - idleTimeout := time.Duration(t.config.A2A.Task.IdleTimeoutSec) * time.Second - result := &domain.ToolExecutionResult{ - ToolName: "A2A_SubmitTask", - Success: true, - Duration: time.Since(state.StartedAt), - Data: A2ASubmitTaskResult{ - TaskID: taskID, - AgentURL: agentURL, - State: string(adk.TaskStateWorking), - Success: true, - Message: "Task delegated and went idle immediately", - WentIdle: true, - IdleTimeout: idleTimeout, - }, - } - if state.ResultChan != nil { - state.ResultChan <- result - } -} - -func (t *A2ASubmitTaskTool) initializePollingStrategy(agentURL, taskID, strategy string) (time.Duration, time.Time) { +func (t *A2ASubmitTaskTool) initializePollingStrategy(agentURL, taskID, strategy string) time.Duration { var currentInterval time.Duration if strategy == "exponential" { @@ -408,53 +370,7 @@ func (t *A2ASubmitTaskTool) initializePollingStrategy(agentURL, taskID, strategy currentInterval = time.Duration(t.config.A2A.Task.StatusPollSeconds) * time.Second } - idleTimeout := time.Duration(t.config.A2A.Task.IdleTimeoutSec) * time.Second - deadline := time.Now().Add(idleTimeout) - - return currentInterval, deadline -} - -func (t *A2ASubmitTaskTool) checkIdleConditions(agentURL, taskID, strategy string, currentInterval time.Duration, deadline time.Time, pollAttempt int, state *domain.TaskPollingState, pollingDetails string) (bool, *domain.ToolExecutionResult) { - idleTimeout := time.Duration(t.config.A2A.Task.IdleTimeoutSec) * time.Second - - if strategy == "exponential" { - maxInterval := time.Duration(t.config.A2A.Task.MaxPollIntervalSec) * time.Second - if currentInterval >= maxInterval { - result := &domain.ToolExecutionResult{ - ToolName: "A2A_SubmitTask", - Success: true, - Duration: time.Since(state.StartedAt), - Data: A2ASubmitTaskResult{ - TaskID: taskID, - AgentURL: agentURL, - State: string(adk.TaskStateWorking), - Success: true, - Message: fmt.Sprintf("Task went idle after reaching max poll interval of %v", maxInterval), - WentIdle: true, - IdleTimeout: idleTimeout, - }, - } - return true, result - } - } else if time.Now().After(deadline) { - result := &domain.ToolExecutionResult{ - ToolName: "A2A_SubmitTask", - Success: true, - Duration: time.Since(state.StartedAt), - Data: A2ASubmitTaskResult{ - TaskID: taskID, - AgentURL: agentURL, - State: string(adk.TaskStateWorking), - Success: true, - Message: fmt.Sprintf("Task went idle after %v", idleTimeout), - WentIdle: true, - IdleTimeout: idleTimeout, - }, - } - return true, result - } - - return false, nil + return currentInterval } func (t *A2ASubmitTaskTool) queryTask(ctx context.Context, adkClient client.A2AClient, taskID string) (*adk.Task, error) { @@ -690,10 +606,6 @@ func (t *A2ASubmitTaskTool) FormatPreview(result *domain.ToolExecutionResult) st preview = data.Message } - if data.WentIdle { - return fmt.Sprintf("%s (went idle)", preview) - } - return preview } diff --git a/internal/services/tools/a2a_task_error_handling_test.go b/internal/services/tools/a2a_task_error_handling_test.go index 382edde4..eb71d75f 100644 --- a/internal/services/tools/a2a_task_error_handling_test.go +++ b/internal/services/tools/a2a_task_error_handling_test.go @@ -342,7 +342,6 @@ func TestA2ASubmitTaskTool_MultipleAgents(t *testing.T) { Enabled: true, Task: config.A2ATaskConfig{ StatusPollSeconds: 1, - IdleTimeoutSec: 5, }, Tools: config.A2AToolsConfig{ SubmitTask: config.SubmitTaskToolConfig{ @@ -427,7 +426,6 @@ func TestA2ASubmitTaskTool_NoExistingTask(t *testing.T) { Enabled: true, Task: config.A2ATaskConfig{ StatusPollSeconds: 1, - IdleTimeoutSec: 5, }, Tools: config.A2AToolsConfig{ SubmitTask: config.SubmitTaskToolConfig{ diff --git a/internal/ui/components/tool_call_renderer.go b/internal/ui/components/tool_call_renderer.go index c8147e4b..0799cb9a 100644 --- a/internal/ui/components/tool_call_renderer.go +++ b/internal/ui/components/tool_call_renderer.go @@ -295,9 +295,6 @@ func (r *ToolCallRenderer) renderToolCallContent(toolInfo ToolInfo, arguments, s case "executed", "completed", "complete": statusIcon = icons.CheckMark statusText = status - case "idle": - statusIcon = icons.CheckMark - statusText = "delegated" case "error", "failed": statusIcon = icons.CrossMark statusText = status