From f1787c4e9261cc1c2bd7331f535221f9aa5e801e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Daba=C5=A1inskas?= Date: Sat, 21 Mar 2026 19:51:12 +0200 Subject: [PATCH 1/2] feat(runtime): add auto-stop for max iterations in non-interactive mode When max iterations are reached in non-interactive mode (e.g., MCP server), the runtime now automatically stops execution instead of blocking indefinitely waiting for user input. This prevents the system from hanging when `ToolsApproved` is true and provides a clear assistant message explaining why execution was stopped. --- pkg/runtime/loop.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/runtime/loop.go b/pkg/runtime/loop.go index ae161a815..2e296af8d 100644 --- a/pkg/runtime/loop.go +++ b/pkg/runtime/loop.go @@ -175,6 +175,24 @@ func (r *LocalRuntime) RunStream(ctx context.Context, sess *session.Session) <-c r.executeNotificationHooks(ctx, a, sess.ID, "warning", maxIterMsg) r.executeOnUserInputHooks(ctx, sess.ID, "max iterations reached") + // In non-interactive mode (e.g. MCP server), auto-stop instead of + // blocking forever waiting for user input. + if sess.ToolsApproved { + slog.Debug("Auto-stopping after max iterations (non-interactive)", "agent", a.Name()) + + assistantMessage := chat.Message{ + Role: chat.MessageRoleAssistant, + Content: fmt.Sprintf( + "Execution stopped after reaching the configured max_iterations limit (%d).", + runtimeMaxIterations, + ), + CreatedAt: time.Now().Format(time.RFC3339), + } + + addAgentMessage(sess, a, &assistantMessage, events) + return + } + // Wait for user decision (resume / reject) select { case req := <-r.resumeChan: From 039447fe7eb3668401e8700c66f95070b38d8c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Daba=C5=A1inskas?= Date: Thu, 26 Mar 2026 08:56:10 +0100 Subject: [PATCH 2/2] feat(session): add non-interactive mode flag to distinguish from tools approval - Add `NonInteractive` field to Session struct to explicitly mark sessions running without user interaction - Update runtime loop to use `NonInteractive` instead of `ToolsApproved` for auto-stopping logic - Set non-interactive mode in A2A adapter, evaluation framework, and MCP server - This separates the concept of approved tools from non-interactive execution context --- pkg/a2a/adapter.go | 1 + pkg/evaluation/save.go | 1 + pkg/mcp/server.go | 1 + pkg/runtime/loop.go | 2 +- pkg/session/session.go | 12 ++++++++++++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/a2a/adapter.go b/pkg/a2a/adapter.go index 969463eaa..871ff7f17 100644 --- a/pkg/a2a/adapter.go +++ b/pkg/a2a/adapter.go @@ -49,6 +49,7 @@ func runDockerAgent(ctx agent.InvocationContext, t *team.Team, agentName string, session.WithMaxConsecutiveToolCalls(a.MaxConsecutiveToolCalls()), session.WithMaxOldToolCallTokens(a.MaxOldToolCallTokens()), session.WithToolsApproved(true), + session.WithNonInteractive(true), ) // Create runtime diff --git a/pkg/evaluation/save.go b/pkg/evaluation/save.go index b93b147c1..d419b3839 100644 --- a/pkg/evaluation/save.go +++ b/pkg/evaluation/save.go @@ -59,6 +59,7 @@ func SessionFromEvents(events []map[string]any, title string, questions []string sess := session.New( session.WithTitle(title), session.WithToolsApproved(true), + session.WithNonInteractive(true), ) // Add user questions as initial messages. diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go index 581fff30c..84d48d5ec 100644 --- a/pkg/mcp/server.go +++ b/pkg/mcp/server.go @@ -164,6 +164,7 @@ func CreateToolHandler(t *team.Team, agentName string) func(context.Context, *mc session.WithMaxOldToolCallTokens(ag.MaxOldToolCallTokens()), session.WithUserMessage(input.Message), session.WithToolsApproved(true), + session.WithNonInteractive(true), ) rt, err := runtime.New(t, diff --git a/pkg/runtime/loop.go b/pkg/runtime/loop.go index 2e296af8d..81a84ec0e 100644 --- a/pkg/runtime/loop.go +++ b/pkg/runtime/loop.go @@ -177,7 +177,7 @@ func (r *LocalRuntime) RunStream(ctx context.Context, sess *session.Session) <-c // In non-interactive mode (e.g. MCP server), auto-stop instead of // blocking forever waiting for user input. - if sess.ToolsApproved { + if sess.NonInteractive { slog.Debug("Auto-stopping after max iterations (non-interactive)", "agent", a.Name()) assistantMessage := chat.Message{ diff --git a/pkg/session/session.go b/pkg/session/session.go index b9740d4ac..28309be69 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -76,6 +76,12 @@ type Session struct { // ToolsApproved is a flag to indicate if the tools have been approved ToolsApproved bool `json:"tools_approved"` + // NonInteractive indicates the session is running in a non-interactive context + // (e.g. MCP server, A2A adapter, evaluation framework) where there is no user + // to provide input. This is distinct from ToolsApproved which can also be set + // in interactive TUI sessions when a user approves all tools. + NonInteractive bool `json:"non_interactive,omitempty"` + // HideToolResults is a flag to indicate if tool results should be hidden HideToolResults bool `json:"hide_tool_results"` @@ -476,6 +482,12 @@ func WithToolsApproved(toolsApproved bool) Opt { } } +func WithNonInteractive(nonInteractive bool) Opt { + return func(s *Session) { + s.NonInteractive = nonInteractive + } +} + func WithHideToolResults(hideToolResults bool) Opt { return func(s *Session) { s.HideToolResults = hideToolResults