function obj = run(obj, domain, partitioning, timestepIndex, index, agents)
    arguments (Input)
        obj (1, 1) {mustBeA(obj, "agent")};
        domain (1, 1) {mustBeGeometry};
        partitioning (:, :) double;
        timestepIndex (1, 1) double;
        index (1, 1) double;
        agents (:, 1) {mustBeA(agents, "cell")};
    end
    arguments (Output)
        obj (1, 1) {mustBeA(obj, "agent")};
    end

    % Collect objective function values across partition
    partitionMask = partitioning == index;
    if ~any(partitionMask(:))
        % This agent has no partition, maintain current state
        return;
    end
    objectiveValues = domain.objective.values(partitionMask); % f(omega) on W_n

    % Compute sensor performance on partition
    maskedX = domain.objective.X(partitionMask);
    maskedY = domain.objective.Y(partitionMask);

    % Compute agent performance at the current position and each delta position +/- X, Y, Z
    delta = domain.objective.discretizationStep; % smallest possible step size that gets different results
    deltaApplicator = [0, 0, 0; 1, 0, 0; -1, 0, 0; 0, 1, 0; 0, -1, 0; 0, 0, 1; 0, 0, -1]; % none, +X, -X, +Y, -Y, +Z, -Z
    C_delta = NaN(7, 1); % agent performance at delta steps in each direction
    for ii = 1:7
        % Apply delta to position
        pos = obj.pos + delta * deltaApplicator(ii, 1:3);
        
        % Compute performance values on partition
        if ii < 6
            % Compute sensing performance
            sensorValues = obj.sensorModel.sensorPerformance(pos, [maskedX, maskedY, zeros(size(maskedX))]); % S_n(omega, P_n) on W_n
            % Objective performance does not change for 0, +/- X, +/- Y steps.
            % Those values are computed once before the loop and are only
            % recomputed when +/- Z steps are applied
        else
            % Redo partitioning for Z stepping only
            partitioning = obj.partition(agents, domain.objective);

            % Recompute partiton-derived performance values for objective
            partitionMask = partitioning == index;
            objectiveValues = domain.objective.values(partitionMask); % f(omega) on W_n

            % Recompute partiton-derived performance values for sensing
            maskedX = domain.objective.X(partitionMask);
            maskedY = domain.objective.Y(partitionMask);
            sensorValues = obj.sensorModel.sensorPerformance(pos, [maskedX, maskedY, zeros(size(maskedX))]); % S_n(omega, P_n) on W_n
        end

        % Rearrange data into image arrays
        F = NaN(size(partitionMask));
        F(partitionMask) = objectiveValues;
        S = NaN(size(partitionMask));
        S(partitionMask) = sensorValues;

        % Compute agent performance
        C = S .* F;
        C_delta(ii) = sum(C(~isnan(C)));
    end

    % Store agent performance at current time and place
    if coder.target('MATLAB')
        obj.performance(timestepIndex + 1) = C_delta(1);
    end

    % Compute gradient by finite central differences
    gradC = [(C_delta(2)-C_delta(3))/(2*delta), (C_delta(4)-C_delta(5))/(2*delta), (C_delta(6)-C_delta(7))/(2*delta)];

    % Compute scaling factor
    targetRate = obj.initialStepSize - obj.stepDecayRate * timestepIndex; % slow down as you get closer
    gradNorm = norm(gradC);

    % Compute unconstrained next position.
    % Guard against near-zero gradient: when sensor performance is saturated
    % or near-zero across the whole partition, rateFactor -> Inf and pNext
    % explodes. Stay put instead.
    if gradNorm < 1e-100
        pNext = obj.pos;
    else
        pNext = obj.pos + (targetRate / gradNorm) * gradC;
    end

    % Move to next position
    obj.lastPos = obj.pos;
    obj.pos = pNext;

    % Reinitialize collision geometry in the new position
    d = obj.pos - obj.collisionGeometry.center;
    if isa(obj.collisionGeometry, "rectangularPrism")
        obj.collisionGeometry = obj.collisionGeometry.initialize([obj.collisionGeometry.minCorner; obj.collisionGeometry.maxCorner] + d, obj.collisionGeometry.tag, obj.collisionGeometry.label);
    elseif isa(obj.collisionGeometry, "spherical")
        obj.collisionGeometry = obj.collisionGeometry.initialize(obj.collisionGeometry.center + d, obj.collisionGeometry.radius, obj.collisionGeometry.tag, obj.collisionGeometry.label);
    else
        error("?");
    end
end