@@ -12,6 +12,7 @@ import (
1212 "context"
1313 _ "embed"
1414 "encoding/base64"
15+ "encoding/json"
1516 "errors"
1617 "fmt"
1718 "log/slog"
@@ -780,6 +781,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
780781
781782 var stepMessages []fantasy.Message
782783 var shouldSummarize bool
784+ sanitizedToolCalls := make(map[string]bool)
783785 // Don't send MaxOutputTokens if 0 โ some providers (e.g. LM Studio) reject it
784786 var maxOutputTokens *int64
785787 if call.MaxOutputTokens > 0 {
@@ -923,10 +925,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
923925 slog.Warn("Provider request failed, retrying", providerRetryLogFields(err, delay)...)
924926 },
925927 OnToolCall: func(tc fantasy.ToolCallContent) error {
928+ input, wasSanitized := sanitizeToolInput(tc.ToolName, tc.ToolCallID, tc.Input)
929+ if wasSanitized {
930+ sanitizedToolCalls[tc.ToolCallID] = true
931+ }
926932 toolCall := message.ToolCall{
927933 ID: tc.ToolCallID,
928934 Name: tc.ToolName,
929โ Input: tc.Input,
935+ Input: input,
930936 ProviderExecuted: false,
931937 Finished: true,
932938 }
@@ -937,6 +943,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
937943 },
938944 OnToolResult: func(result fantasy.ToolResultContent) error {
939945 toolResult := a.convertToToolResult(result)
946+ if sanitizedToolCalls[result.ToolCallID] {
947+ toolResult.Content = "Tool call failed: arguments were not valid JSON. Please check your tool call format and try again."
948+ toolResult.IsError = true
949+ }
940950 // Use parent ctx instead of genCtx to ensure the message is created
941951 // even if the request is canceled mid-stream
942952 _, createMsgErr := a.messages.Create(ctx, currentAssistant.SessionID, message.CreateMessageParams{
@@ -2180,3 +2190,19 @@ func providerRetryLogFields(err *fantasy.ProviderError, delay time.Duration) []a
21802190 }
21812191 return fields
21822192}
2193+
2194+// sanitizeToolInput validates tool call JSON from the provider.
2195+// Malformed input is replaced with an empty object to prevent
2196+// stuck conversations from truncated or malformed model output.
2197+// The second return value indicates whether sanitization occurred.
2198+func sanitizeToolInput(toolName, toolCallID, input string) (string, bool) {
2199+ if !json.Valid([]byte(input)) {
2200+ slog.Warn("Malformed tool call JSON from provider, replacing with empty object",
2201+ "tool", toolName,
2202+ "id", toolCallID,
2203+ "input_len", len(input),
2204+ )
2205+ return "{}", true
2206+ }
2207+ return input, false
2208+}