diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index add7a84..5f85a9e 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -76,6 +76,8 @@ func run(ctx context.Context, cfg agentConfig, state state) error { var roundsWithoutPlan int // consecutive tool rounds without a PLAN: block var lastAction string // previous round's LastAction for stall detection var stallRounds int // consecutive rounds with unchanged LastAction + var lastErrorSig string // signature of previous round's error(s) for repeat detection + var repeatErrorCount int // consecutive rounds with identical error signature // resultCache stores outputs of read-safe tool calls keyed by name+args. // Only tools classified as parallel-safe (immutable reads) are cached. // sync.Map is required because execOne may run in concurrent goroutines. @@ -488,6 +490,36 @@ func run(ctx context.Context, cfg agentConfig, state state) error { }, nil) } + // Repeated identical error detection: if the same error occurs 3 times + // in a row, the model is stuck on a format/argument issue. Inject the + // error content as a corrective nudge immediately rather than waiting + // for the generic soft limit. + if allErrors && len(results) > 0 { + var errSig strings.Builder + for _, r := range results { + if r.IsError { + errSig.WriteString(r.Content) + errSig.WriteByte('\n') + } + } + sig := errSig.String() + if sig == lastErrorSig { + repeatErrorCount++ + } else { + repeatErrorCount = 1 + lastErrorSig = sig + } + if repeatErrorCount == 3 { + state.update(backend.Message{ + Role: "user", + Content: "[system: you have made the same malformed tool call 3 times in a row. Read the error message carefully and fix the arguments. The error was: " + strings.TrimSpace(results[0].Content) + "]", + }, nil) + } + } else { + repeatErrorCount = 0 + lastErrorSig = "" + } + // Replan gate: if the model has been calling tools without emitting a // PLAN: block, force it to replan before continuing. if strings.Contains(msg.Content, "PLAN:") { diff --git a/pkg/backend/codewhisperer.go b/pkg/backend/codewhisperer.go index bbfed8f..951409d 100644 --- a/pkg/backend/codewhisperer.go +++ b/pkg/backend/codewhisperer.go @@ -646,14 +646,20 @@ type kiroAuthSource interface { // staticKiroAuth holds a pre-issued bearer token. type staticKiroAuth struct { - token string + token string + profileARN string } func (s *staticKiroAuth) DefaultEndpoint(_ context.Context) (string, error) { + if s.profileARN != "" { + return kiroDefaultEndpoint(s.profileARN) + } return "", fmt.Errorf("base URL not configured and no profile ARN available with static auth") } -func (s *staticKiroAuth) ProfileARN(_ context.Context) (string, error) { return "", nil } +func (s *staticKiroAuth) ProfileARN(_ context.Context) (string, error) { + return s.profileARN, nil +} func (s *staticKiroAuth) AccessToken(_ context.Context) (string, error) { t := strings.TrimSpace(s.token) @@ -712,10 +718,6 @@ func (s *sqliteKiroAuth) ProfileARN(ctx context.Context) (string, error) { if err != nil { return "", err } - // Personal/social accounts don't send profileArn in requests. - if state.IsSocial { - return "", nil - } return strings.TrimSpace(state.ProfileARN), nil } @@ -1377,7 +1379,10 @@ func kiroUpdateSSOCacheToken(path, accessToken, refreshToken, expiresAt string) func newKiroAuthSource(apiKey string) (kiroAuthSource, error) { apiKey = strings.TrimSpace(apiKey) if !strings.HasPrefix(strings.ToLower(apiKey), "sqlite://") { - return &staticKiroAuth{token: apiKey}, nil + return &staticKiroAuth{ + token: apiKey, + profileARN: strings.TrimSpace(os.Getenv("OLLIE_KIRO_PROFILE_ARN")), + }, nil } // Parse sqlite:// URI parsed, err := url.Parse(apiKey) diff --git a/pkg/tools/execute/code.go b/pkg/tools/execute/code.go index 6bf590a..340e157 100644 --- a/pkg/tools/execute/code.go +++ b/pkg/tools/execute/code.go @@ -193,7 +193,7 @@ func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) ( return "", fmt.Errorf("execute_code: bad args: %w", err) } if len(steps) == 0 { - return "", fmt.Errorf("execute_code: steps is required") + return "", fmt.Errorf("execute_code: steps is required. Format: {\"steps\": [{\"code\": \"your code here\"}]}") } return e.dispatchSteps(ctx, steps, timeout, sandboxName) } @@ -203,10 +203,10 @@ func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) ( func dispatchCallTool(ctx context.Context, e *Server, args json.RawMessage) (string, error) { calls, timeout, sandboxName, err := execCallToolArgs(args) if err != nil { - return "", fmt.Errorf("call_tool: bad args: %w", err) + return "", fmt.Errorf("call_tool: bad args: %w. Expected format: {\"calls\": [{\"tool\": \"tool_name\", \"args\": [\"arg1\", ...]}]}", err) } if len(calls) == 0 { - return "", fmt.Errorf("call_tool: calls is required") + return "", fmt.Errorf("call_tool: calls is required. Format: {\"calls\": [{\"tool\": \"tool_name\", \"args\": [\"arg1\", ...]}]}") } // Validate: every top-level call must name a tool (or be a parallel group of tools). for i, c := range calls { @@ -238,7 +238,7 @@ func dispatchPipeCall(ctx context.Context, e *Server, args json.RawMessage) (str return "", fmt.Errorf("pipe: bad args: %w", err) } if len(stages) == 0 { - return "", fmt.Errorf("pipe: stages is required") + return "", fmt.Errorf("pipe: stages is required. Format: {\"stages\": [{\"code\": \"...\"} or {\"tool\": \"name\", \"args\": [...]}]}") } return e.dispatchPipe(ctx, stages, timeout, sandboxName) }